Fix breakages in https://codereview.chromium.org/1155713003/
[chromium-blink-merge.git] / ios / web / web_state / js / resources / common.js
blob3781416274724745915213d3d83da8b841b84a44
1 // Copyright 2013 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 provides common methods that can be shared by other JavaScripts.
7 goog.provide('__crWeb.common');
9 goog.require('__crWeb.base');
12 /**
13  * Namespace for this file. It depends on |__gCrWeb| having already been
14  * injected. String 'common' is used in |__gCrWeb['common']| as it needs to be
15  * accessed in Objective-C code.
16  */
17 __gCrWeb['common'] = {};
19 /* Beginning of anonymous object. */
20 new function() {
21   // JSON safe object to protect against custom implementation of Object.toJSON
22   // in host pages.
23   __gCrWeb['common'].JSONSafeObject = function JSONSafeObject() {
24   };
26   /**
27    * Protect against custom implementation of Object.toJSON in host pages.
28    */
29   __gCrWeb['common'].JSONSafeObject.prototype.toJSON = null;
31   /**
32    * Retain the original JSON.stringify method where possible to reduce the
33    * impact of sites overriding it
34    */
35   __gCrWeb.common.JSONStringify = JSON.stringify;
37   /**
38    * Prefix used in references to form elements that have no 'id' or 'name'
39    */
40   __gCrWeb.common.kNamelessFormIDPrefix = 'gChrome~';
42   /**
43    * Tests an element's visiblity. This test is expensive so should be used
44    * sparingly.
45    * @param {Element} element A DOM element.
46    * @return {boolean} true if the |element| is currently part of the visible
47    * DOM.
48    */
49   __gCrWeb.common.isElementVisible = function(element) {
50     /** @type {Node} */
51     var node = element;
52     while (node && node !== document) {
53       if (node.nodeType === document.ELEMENT_NODE) {
54         var style = window.getComputedStyle(/** @type {Element} */(node));
55         if (style.display === 'none' || style.visibility === 'hidden') {
56           return false;
57         }
58       }
59       // Move up the tree and test again.
60       node = node.parentNode;
61     }
62     // Test reached the top of the DOM without finding a concealed ancestor.
63     return true;
64   };
66   /**
67    * Based on Element::isFormControlElement() (WebKit)
68    * @param {Element} element A DOM element.
69    * @return {boolean} true if the |element| is a form control element.
70    */
71   __gCrWeb.common.isFormControlElement = function(element) {
72     var tagName = element.tagName;
73     return (tagName === 'INPUT' ||
74             tagName === 'SELECT' ||
75             tagName === 'TEXTAREA');
76   };
78   /**
79    * Detects focusable elements.
80    * @param {Element} element A DOM element.
81    * @return {boolean} true if the |element| is focusable.
82    */
83   __gCrWeb.common.isFocusable = function(element) {
84     // When the disabled or hidden attributes are present, controls do not
85     // receive focus.
86     if (element.hasAttribute('disabled') || element.hasAttribute('hidden'))
87       return false;
88     return __gCrWeb.common.isFormControlElement(element);
89   };
91   /**
92    * Returns an array of control elements in a form.
93    *
94    * This method is based on the logic in method
95    *     void WebFormElement::getFormControlElements(
96    *         WebVector<WebFormControlElement>&) const
97    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
98    * WebFormElement.cpp.
99    *
100    * @param {Element} form A form element for which the control elements are
101    *   returned.
102    * @return {Array<Element>}
103    */
104   __gCrWeb.common.getFormControlElements = function(form) {
105     if (!form) {
106       return [];
107     }
108     var results = [];
109     // Get input and select elements from form.elements.
110     // TODO(chenyu): according to
111     // http://www.w3.org/TR/2011/WD-html5-20110525/forms.html, form.elements are
112     // the "listed elements whose form owner is the form element, with the
113     // exception of input elements whose type attribute is in the Image Button
114     // state, which must, for historical reasons, be excluded from this
115     // particular collection." In WebFormElement.cpp, this method is implemented
116     // by returning elements in form's associated elements that have tag 'INPUT'
117     // or 'SELECT'. Check if input Image Buttons are excluded in that
118     // implementation. Note for Autofill, as input Image Button is not
119     // considered as autofillable elements, there is no impact on Autofill
120     // feature.
121     var elements = form.elements;
122     for (var i = 0; i < elements.length; i++) {
123       if (__gCrWeb.common.isFormControlElement(elements[i])) {
124         results.push(elements[i]);
125       }
126     }
127     return results;
128   };
130   /**
131    * Returns true if an element can be autocompleted.
132    *
133    * This method aims to provide the same logic as method
134    *     bool autoComplete() const
135    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
136    * WebFormElement.cpp.
137    *
138    * @param {Element} element An element to check if it can be autocompleted.
139    * @return {boolean} true if element can be autocompleted.
140    */
141   __gCrWeb.common.autoComplete = function(element) {
142     if (!element) {
143       return false;
144     }
145     if (__gCrWeb.common.getLowerCaseAttribute(
146         element, 'autocomplete') == 'off') {
147       return false;
148     }
149     if (__gCrWeb.common.getLowerCaseAttribute(
150             element.form, 'autocomplete') == 'off') {
151       return false;
152     }
153     return true;
154   };
156   /**
157    * Returns if an element is a text field.
158    * This returns true for all of textfield-looking types such as text,
159    * password, search, email, url, and number.
160    *
161    * This method aims to provide the same logic as method
162    *     bool WebInputElement::isTextField() const
163    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
164    * WebInputElement.cpp, where this information is from
165    *     bool HTMLInputElement::isTextField() const
166    *     {
167    *       return m_inputType->isTextField();
168    *     }
169    * (chromium/src/third_party/WebKit/Source/WebCore/html/HTMLInputElement.cpp)
170    *
171    * The implementation here is based on the following:
172    *
173    * - Method bool InputType::isTextField() defaults to be false and it is
174    *   override to return true only in HTMLInputElement's subclass
175    *   TextFieldInputType (chromium/src/third_party/WebKit/Source/WebCore/html/
176    *   TextFieldInputType.h).
177    *
178    * - The implementation here considers all the subclasses of
179    *   TextFieldInputType: NumberInputType and BaseTextInputType, which has
180    *   subclasses EmailInputType, PasswordInputType, SearchInputType,
181    *   TelephoneInputType, TextInputType, URLInputType. (All these classes are
182    *   defined in chromium/src/third_party/WebKit/Source/WebCore/html/)
183    *
184    * @param {Element} element An element to examine if it is a text field.
185    * @return {boolean} true if element has type=text.
186    */
187   __gCrWeb.common.isTextField = function(element) {
188     if (!element) {
189       return false;
190     }
191     if (element.type === 'hidden') {
192       return false;
193     }
194     return element.type === 'text' ||
195            element.type === 'email' ||
196            element.type === 'password' ||
197            element.type === 'search' ||
198            element.type === 'tel' ||
199            element.type === 'url' ||
200            element.type === 'number';
201   };
203   /**
204    * Sets the checked value of an input and dispatches an change event if
205    * |shouldSendChangeEvent|.
206    *
207    * This is a simplified version of the implementation of
208    *
209    *     void setChecked(bool nowChecked, TextFieldEventBehavior eventBehavior)
210    *
211    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
212    * WebInputElement.cpp, which calls
213    *     void HTMLInputElement::setChecked(
214    *         bool nowChecked, TextFieldEventBehavior eventBehavior)
215    * in chromium/src/third_party/WebKit/Source/core/html/HTMLInputElement.cpp.
216    *
217    * @param {boolean} nowChecked The new checked value of the input element.
218    * @param {Element} input The input element of which the value is set.
219    * @param {boolean} shouldSendChangeEvent Whether a change event should be
220    *     dispatched.
221    */
222   __gCrWeb.common.setInputElementChecked = function(
223       nowChecked, input, shouldSendChangeEvent) {
224     var checkedChanged = input.checked !== nowChecked;
225     input.checked = nowChecked;
226     if (checkedChanged) {
227       __gCrWeb.common.createAndDispatchHTMLEvent(input, 'change', true, false);
228     }
229   };
231   /**
232    * Sets the value of an input and dispatches an change event if
233    * |shouldSendChangeEvent|.
234    *
235    * It is based on the logic in
236    *
237    *     void setValue(const WebString&, bool sendChangeEvent = false)
238    *
239    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/src/
240    * WebInputElement.cpp, which calls
241    *    void setValue(const String& value, TextFieldEventBehavior eventBehavior)
242    * in chromium/src/third_party/WebKit/Source/core/html/HTMLInputElement.cpp.
243    *
244    * @param {string} value The value the input element will be set.
245    * @param {Element} input The input element of which the value is set.
246    * @param {boolean} shouldSendChangeEvent Whether a change event should be
247    *     dispatched.
248    */
249   __gCrWeb.common.setInputElementValue = function(
250       value, input, shouldSendChangeEvent) {
251     // In HTMLInputElement.cpp there is a check on canSetValue(value), which
252     // returns false only for file input. As file input is not relevant for
253     // autofill and this method is only used for autofill for now, there is no
254     // such check in this implementation.
255     var sanitizedValue = __gCrWeb.common.sanitizeValueForInputElement(
256         value, input);
257     var valueChanged = sanitizedValue !== input.value;
258     input.value = sanitizedValue;
259     if (valueChanged) {
260       __gCrWeb.common.createAndDispatchHTMLEvent(input, 'change', true, false);
261     }
262   };
264   /**
265    * Returns a sanitized value of proposedValue for a given input element type.
266    * The logic is based on
267    *
268    *      String sanitizeValue(const String&) const
269    *
270    * in chromium/src/third_party/WebKit/Source/core/html/InputType.h
271    *
272    * @param {string} proposedValue The proposed value.
273    * @param {Element} element The element for which the proposedValue is to be
274    *     sanitized.
275    * @return {string} The sanitized value.
276    */
277    __gCrWeb.common.sanitizeValueForInputElement = function(
278        proposedValue, element) {
279     if (!proposedValue) {
280       return '';
281     }
283     // Method HTMLInputElement::sanitizeValue() calls InputType::sanitizeValue()
284     // (chromium/src/third_party/WebKit/Source/core/html/InputType.cpp) for
285     // non-null proposedValue. InputType::sanitizeValue() returns the original
286     // proposedValue by default and it is overridden in classes
287     // BaseDateAndTimeInputType, ColorInputType, RangeInputType and
288     // TextFieldInputType (all are in
289     // chromium/src/third_party/WebKit/Source/core/html/). Currently only
290     // TextFieldInputType is relevant and sanitizeValue() for other types of
291     // input elements has not been implemented.
292     if (__gCrWeb.common.isTextField(element)) {
293       return __gCrWeb.common.sanitizeValueForTextFieldInputType(
294           proposedValue, element);
295     }
296     return proposedValue;
297    };
299   /**
300    * Returns a sanitized value for a text field.
301    *
302    * The logic is based on |String sanitizeValue(const String&)|
303    * in chromium/src/third_party/WebKit/Source/core/html/TextFieldInputType.h
304    * Note this method is overridden in EmailInputType and NumberInputType.
305    *
306    * @param {string} proposedValue The proposed value.
307    * @param {Element} element The element for which the proposedValue is to be
308    *     sanitized.
309    * @return {string} The sanitized value.
310    */
311   __gCrWeb.common.sanitizeValueForTextFieldInputType = function(
312       proposedValue, element) {
313     var textFieldElementType = element.type;
314     if (textFieldElementType === 'email') {
315       return __gCrWeb.common.sanitizeValueForEmailInputType(
316           proposedValue, element);
317     } else if (textFieldElementType === 'number') {
318       return __gCrWeb.common.sanitizeValueForNumberInputType(proposedValue);
319     }
320     var valueWithLineBreakRemoved = proposedValue.replace(/(\r\n|\n|\r)/gm, '');
321     // TODO(chenyu): Should we also implement numCharactersInGraphemeClusters()
322     // in chromium/src/third_party/WebKit/Source/core/platform/text/
323     // TextBreakIterator.cpp and call it here when computing newLength?
324     // Different from the implementation in TextFieldInputType.h, where a limit
325     // on the text length is considered due to
326     // https://bugs.webkit.org/show_bug.cgi?id=14536, no such limit is
327     // considered here for now.
328     var newLength = valueWithLineBreakRemoved.length;
329     // This logic is from method String limitLength() in TextFieldInputType.h
330     for (var i = 0; i < newLength; ++i) {
331       var current = valueWithLineBreakRemoved[i];
332       if (current < ' ' && current != '\t') {
333         newLength = i;
334         break;
335       }
336     }
337     return valueWithLineBreakRemoved.substring(0, newLength);
338   };
340   /**
341    * Returns the sanitized value for an email input.
342    *
343    * The logic is based on
344    *
345    *     String EmailInputType::sanitizeValue(const String& proposedValue) const
346    *
347    * in chromium/src/third_party/WebKit/Source/core/html/EmailInputType.cpp
348    *
349    * @param {string} proposedValue The proposed value.
350    * @param {Element} element The element for which the proposedValue is to be
351    *     sanitized.
352    * @return {string} The sanitized value.
353    */
354   __gCrWeb.common.sanitizeValueForEmailInputType = function(
355       proposedValue, element) {
356     var valueWithLineBreakRemoved = proposedValue.replace(/(\r\n|\n\r)/gm, '');
358     if (!element.multiple) {
359       return __gCrWeb.common.trim(proposedValue);
360     }
361     var addresses = valueWithLineBreakRemoved.split(',');
362     for (var i = 0; i < addresses.length; ++i) {
363       addresses[i] = __gCrWeb.common.trim(addresses[i]);
364     }
365     return addresses.join(',');
366   };
368   /**
369    * Returns the sanitized value of a proposed value for a number input.
370    *
371    * The logic is based on
372    *
373    *     String NumberInputType::sanitizeValue(const String& proposedValue)
374    *         const
375    *
376    * in chromium/src/third_party/WebKit/Source/core/html/NumberInputType.cpp
377    *
378    * Note in this implementation method Number() is used in the place of method
379    * parseToDoubleForNumberType() called in NumberInputType.cpp.
380    *
381    * @param {string} proposedValue The proposed value.
382    * @return {string} The sanitized value.
383    */
384   __gCrWeb.common.sanitizeValueForNumberInputType = function(proposedValue) {
385     var sanitizedValue = Number(proposedValue);
386     if (isNaN(sanitizedValue)) {
387       return '';
388     }
389     return sanitizedValue.toString();
390   };
392   /**
393    * Trims any whitespace from the start and end of a string.
394    * Used in preference to String.prototype.trim as this can be overridden by
395    * sites.
396    *
397    * @param {string} str The string to be trimmed.
398    * @return {string} The string after trimming.
399    */
400   __gCrWeb.common.trim = function(str) {
401     return str.replace(/^\s+|\s+$/g, '');
402   };
404   /**
405    * Returns the name that should be used for the specified |element| when
406    * storing Autofill data. Various attributes are used to attempt to identify
407    * the element, beginning with 'name' and 'id' attributes. Providing a
408    * uniquely reversible identifier for any element is a non-trivial problem;
409    * this solution attempts to satisfy the majority of cases.
410    *
411    * It aims to provide the logic in
412    *     WebString nameForAutofill() const;
413    * in chromium/src/third_party/WebKit/Source/WebKit/chromium/public/
414    *  WebFormControlElement.h
415    *
416    * @param {Element} element An element of which the name for Autofill will be
417    *     returned.
418    * @return {string} the name for Autofill.
419    */
420   __gCrWeb.common.nameForAutofill = function(element) {
421     if (!element) {
422       return '';
423     }
424     var trimmedName = element.name;
425     if (trimmedName) {
426       trimmedName = __gCrWeb.common.trim(trimmedName);
427       if (trimmedName.length > 0) {
428         return trimmedName;
429       }
430     }
431     trimmedName = element.getAttribute('id');
432     if (trimmedName) {
433       return __gCrWeb.common.trim(trimmedName);
434     }
435     return '';
436   };
438   /**
439    * Acquires the specified DOM |attribute| from the DOM |element| and returns
440    * its lower-case value, or null if not present.
441    * @param {Element} element A DOM element.
442    * @param {string} attribute An attribute name.
443    * @return {?string} Lowercase value of DOM element or null if not present.
444    */
445   __gCrWeb.common.getLowerCaseAttribute = function(element, attribute) {
446     if (!element) {
447       return null;
448     }
449     var value = element.getAttribute(attribute);
450     if (value) {
451       return value.toLowerCase();
452     }
453     return null;
454   };
456   /**
457    * Converts a relative URL into an absolute URL.
458    * @param {Object} doc Document.
459    * @param {string} relativeURL Relative URL.
460    * @return {string} Absolute URL.
461    */
462   __gCrWeb.common.absoluteURL = function(doc, relativeURL) {
463     // In the case of data: URL-based pages, relativeURL === absoluteURL.
464     if (doc.location.protocol === 'data:') {
465       return doc.location.href;
466     }
467     var urlNormalizer = doc['__gCrWebURLNormalizer'];
468     if (!urlNormalizer) {
469       urlNormalizer = doc.createElement('a');
470       doc['__gCrWebURLNormalizer'] = urlNormalizer;
471     }
473     // Use the magical quality of the <a> element. It automatically converts
474     // relative URLs into absolute ones.
475     urlNormalizer.href = relativeURL;
476     return urlNormalizer.href;
477   };
479   /**
480    * Extracts the webpage URL from the given URL by removing the query
481    * and the reference (aka fragment) from the URL.
482    * @param {string} url Web page URL.
483    * @return {string} Web page URL with query and reference removed.
484    */
485   __gCrWeb.common.removeQueryAndReferenceFromURL = function(url) {
486     var queryIndex = url.indexOf('?');
487     if (queryIndex != -1) {
488       return url.substring(0, queryIndex);
489     }
491     var hashIndex = url.indexOf('#');
492     if (hashIndex != -1) {
493       return url.substring(0, hashIndex);
494     }
495     return url;
496   };
498   /**
499    * Returns the form's |name| attribute if non-empty; otherwise the form's |id|
500    * attribute, or the index of the form (with prefix) in document.forms.
501    *
502    * It is partially based on the logic in
503    *     const string16 GetFormIdentifier(const blink::WebFormElement& form)
504    * in chromium/src/components/autofill/renderer/form_autofill_util.h.
505    *
506    * @param {Element} form An element for which the identifier is returned.
507    * @return {string} a string that represents the element's identifier.
508    */
509   __gCrWeb.common.getFormIdentifier = function(form) {
510     if (!form)
511       return '';
512     var name = form.getAttribute('name');
513     if (name && name.length != 0) {
514       return name;
515     }
516     name = form.getAttribute('id');
517     if (name) {
518       return name;
519     }
520     // A form name must be supplied, because the element will later need to be
521     // identified from the name. A last resort is to take the index number of
522     // the form in document.forms. ids are not supposed to begin with digits (by
523     // HTML 4 spec) so this is unlikely to match a true id.
524     for (var idx = 0; idx != document.forms.length; idx++) {
525       if (document.forms[idx] == form) {
526         return __gCrWeb.common.kNamelessFormIDPrefix + idx;
527       }
528     }
529     return '';
530   };
532   /**
533    * Returns the form element from an ID obtained from getFormIdentifier.
534    *
535    * This works on a 'best effort' basis since DOM changes can always change the
536    * actual element that the ID refers to.
537    *
538    * @param {string} name An ID string obtained via getFormIdentifier.
539    * @return {Element} The original form element, if it can be determined.
540    */
541   __gCrWeb.common.getFormElementFromIdentifier = function(name) {
542     // First attempt is from the name / id supplied.
543     var form = document.forms.namedItem(name);
544     if (form) {
545       return form;
546     }
547     // Second attempt is from the prefixed index position of the form in
548     // document.forms.
549     if (name.indexOf(__gCrWeb.common.kNamelessFormIDPrefix) == 0) {
550       var nameAsInteger = 0 |
551           name.substring(__gCrWeb.common.kNamelessFormIDPrefix.length);
552       if (__gCrWeb.common.kNamelessFormIDPrefix + nameAsInteger == name &&
553           nameAsInteger < document.forms.length) {
554         return document.forms[nameAsInteger];
555       }
556     }
557     return null;
558   };
560   /**
561    * Creates and dispatches an HTML event.
562    *
563    * @param {Element} element The element for which an event is created.
564    * @param {string} type The type of the event.
565    * @param {boolean} bubbles A boolean indicating whether the event should
566    *     bubble up through the event chain or not.
567    * @param {boolean} cancelable A boolean indicating whether the event can be
568    *     canceled.
569    */
570   __gCrWeb.common.createAndDispatchHTMLEvent = function(
571       element, type, bubbles, cancelable) {
572     var changeEvent = element.ownerDocument.createEvent('HTMLEvents');
573     changeEvent.initEvent(type, bubbles, cancelable);
575     // A timer is used to avoid reentering JavaScript evaluation.
576     window.setTimeout(function() {
577       element.dispatchEvent(changeEvent);
578     }, 0);
579   };
581   /**
582    * Retrieves favicon information.
583    *
584    * @return {Object} Object containing favicon data.
585    */
586   __gCrWeb.common.getFavicons = function() {
587     var favicons = [];
588     var hasFavicon = false;
589     favicons.toJSON = null;  // Never inherit Array.prototype.toJSON.
590     var links = document.getElementsByTagName('link');
591     var linkCount = links.length;
592     for (var i = 0; i < linkCount; ++i) {
593       if (links[i].rel) {
594         var rel = links[i].rel.toLowerCase();
595         if (rel == 'shortcut icon' ||
596             rel == 'icon' ||
597             rel == 'apple-touch-icon' ||
598             rel == 'apple-touch-icon-precomposed') {
599           var favicon = {
600             rel: links[i].rel.toLowerCase(),
601             href: links[i].href
602           };
603           favicons.push(favicon);
604           if (rel == 'icon' || rel == 'shortcut icon') {
605             hasFavicon = true;
606           }
607         }
608       }
609     }
610     if (!hasFavicon) {
611       // If an HTTP(S)? webpage does not reference a "favicon" then search
612       // for a file named "favicon.ico" at the root of the website (legacy).
613       // http://en.wikipedia.org/wiki/Favicon
614       var location = document.location;
615       if (location.protocol == 'http:' || location.protocol == 'https:') {
616         var favicon = {
617           rel: 'icon',
618           href: location.origin + '/favicon.ico'
619         };
620         favicons.push(favicon);
621       }
622     }
623     return favicons;
624   };
626   /**
627    * Checks whether an <object> node is plugin content (as <object> can also be
628    * used to embed images).
629    * @param {HTMLElement} node The <object> node to check.
630    * @return {boolean} Whether the node appears to be a plugin.
631    * @private
632    */
633   var objectNodeIsPlugin_ = function(node) {
634     return node.hasAttribute('classid') ||
635            (node.hasAttribute('type') && node.type.indexOf('image/') != 0);
636   };
638   /**
639    * Checks whether plugin a node has fallback content.
640    * @param {HTMLElement} node The node to check.
641    * @return {boolean} Whether the node has fallback.
642    * @private
643    */
644   var pluginHasFallbackContent_ = function(node) {
645     return node.textContent.trim().length > 0 ||
646            node.getElementsByTagName('img').length > 0;
647   };
649   /**
650    * Returns a list of plugin elements in the document that have no fallback
651    * content. For nested plugins, only the innermost plugin element is returned.
652    * @return {Array} A list of plugin elements.
653    * @private
654    */
655   var findPluginNodesWithoutFallback_ = function() {
656     var pluginNodes = [];
657     var objects = document.getElementsByTagName('object');
658     var objectCount = objects.length;
659     for (var i = 0; i < objectCount; i++) {
660       var object = objects[i];
661       if (objectNodeIsPlugin_(object) &&
662           !pluginHasFallbackContent_(object)) {
663         pluginNodes.push(object);
664       }
665     }
666     var applets = document.getElementsByTagName('applet');
667     var appletsCount = applets.length;
668     for (var i = 0; i < appletsCount; i++) {
669       var applet = applets[i];
670       if (!pluginHasFallbackContent_(applet)) {
671         pluginNodes.push(applet);
672       }
673     }
674     return pluginNodes;
675   };
677   /**
678    * Finds and stores any plugins that don't have placeholders.
679    * Returns true if any plugins without placeholders are found.
680    */
681   __gCrWeb.common.updatePluginPlaceholders = function() {
682     var plugins = findPluginNodesWithoutFallback_();
683     if (plugins.length > 0) {
684       // Store the list of plugins in a known place for the replacement script
685       // to use, then trigger it.
686       __gCrWeb['placeholderTargetPlugins'] = plugins;
687       return true;
688     }
689     return false;
690   };
691 }  // End of anonymous object