Javascript  |  358行  |  11.33 KB

// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview CFInstall.js provides a set of utilities for managing
 * the Chrome Frame detection and installation process.
 * @author slightlyoff@google.com (Alex Russell)
 */

(function(scope) {
  // bail if we'd be over-writing an existing CFInstall object
  if (scope['CFInstall']) {
    return;
  }

  /**
   * returns an item based on DOM ID. Optionally a document may be provided to
   * specify the scope to search in. If a node is passed, it's returned as-is.
   * @param {string|Node} id The ID of the node to be located or a node
   * @param {Node} doc Optional A document to search for id.
   * @return {Node}
   */
  var byId = function(id, doc) {
    return (typeof id == 'string') ? (doc || document).getElementById(id) : id;
  };

  /////////////////////////////////////////////////////////////////////////////
  // Plugin Detection
  /////////////////////////////////////////////////////////////////////////////

  /**
   * Checks to find out if ChromeFrame is available as a plugin
   * @return {Boolean}
   */
  var isAvailable = function() {
    // For testing purposes.
    if (scope.CFInstall._force) {
      return scope.CFInstall._forceValue;
    }

    // Look for CF in the User Agent before trying more expensive checks
    var ua = navigator.userAgent.toLowerCase();
    if (ua.indexOf("chromeframe") >= 0) {
      return true;
    }

    if (typeof window['ActiveXObject'] != 'undefined') {
      try {
        var obj = new ActiveXObject('ChromeTab.ChromeFrame');
        if (obj) {
          obj.registerBhoIfNeeded();
          return true;
        }
      } catch(e) {
        // squelch
      }
    }
    return false;
  };

  /**
   * Creates a style sheet in the document containing the passed rules.
   */
  var injectStyleSheet = function(rules) {
    try {
      var ss = document.createElement('style');
      ss.setAttribute('type', 'text/css');
      if (ss.styleSheet) {
        ss.styleSheet.cssText = rules;
      } else {
        ss.appendChild(document.createTextNode(rules));
      }
      var h = document.getElementsByTagName('head')[0];
      var firstChild = h.firstChild;
      h.insertBefore(ss, firstChild);
    } catch (e) {
      // squelch
    }
  };

  /** @type {boolean} */
  var cfStyleTagInjected = false;
  /** @type {boolean} */
  var cfHiddenInjected = false;

  /**
   * Injects style rules into the document to handle formatting of Chrome Frame
   * prompt. Multiple calls have no effect.
   */
  var injectCFStyleTag = function() {
    if (cfStyleTagInjected) {
      // Once and only once
      return;
    }
    var rules = '.chromeFrameInstallDefaultStyle {' +
                   'width: 800px;' +
                   'height: 600px;' +
                   'position: absolute;' +
                   'left: 50%;' +
                   'top: 50%;' +
                   'margin-left: -400px;' +
                   'margin-top: -300px;' +
                 '}' +
                 '.chromeFrameOverlayContent {' +
                   'position: absolute;' +
                   'margin-left: -400px;' +
                   'margin-top: -300px;' +
                   'left: 50%;' +
                   'top: 50%;' +
                   'border: 1px solid #93B4D9;' +
                   'background-color: white;' +
                   'z-index: 2001;' +
                 '}' +
                 '.chromeFrameOverlayContent iframe {' +
                   'width: 800px;' +
                   'height: 600px;' +
                   'border: none;' +
                 '}' +
                 '.chromeFrameOverlayCloseBar {' +
                   'height: 1em;' +
                   'text-align: right;' +
                   'background-color: #CADEF4;' +
                 '}' +
                 '.chromeFrameOverlayUnderlay {' +
                   'position: absolute;' +
                   'width: 100%;' +
                   'height: 100%;' +
                   'background-color: white;' +
                   'opacity: 0.5;' +
                   '-moz-opacity: 0.5;' +
                   '-webkit-opacity: 0.5;' +
                   '-ms-filter: ' +
                      '"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";' +
                   'filter: alpha(opacity=50);' +
                   'z-index: 2000;' +
                 '}';
    injectStyleSheet(rules);
    cfStyleTagInjected = true;
  };

  /**
   * Injects style rules to hide the overlay version of the GCF prompt.
   * Multiple calls have no effect.
   */
  var closeOverlay = function() {
    // IE has a limit to the # of <style> tags allowed, so we avoid
    // tempting the fates.
    if (cfHiddenInjected) {
      return;
    }
    var rules = '.chromeFrameOverlayContent { display: none; }' +
                '.chromeFrameOverlayUnderlay { display: none; }';
    injectStyleSheet(rules);
    // Hide the dialog for a year (or until cookies are deleted).
    var age = 365 * 24 * 60 * 60 * 1000;
    document.cookie = "disableGCFCheck=1;path=/;max-age="+age;
    cfHiddenInjected = true;
  };

  /**
   * Plucks properties from the passed arguments and sets them on the passed
   * DOM node
   * @param {Node} node The node to set properties on
   * @param {Object} args A map of user-specified properties to set
   */
  var setProperties = function(node, args) {

    var srcNode = byId(args['node']);

    node.id = args['id'] || (srcNode ? srcNode['id'] || getUid(srcNode) : '');

    // TODO(slightlyoff): Opera compat? need to test there
    var cssText = args['cssText'] || '';
    node.style.cssText = ' ' + cssText;

    var classText = args['className'] || '';
    node.className = classText;

    // default if the browser doesn't so we don't show sad-tab
    var src = args['src'] || 'about:blank';

    node.src = src;

    if (srcNode) {
      srcNode.parentNode.replaceChild(node, srcNode);
    }
  };

  /**
   * Creates an iframe.
   * @param {Object} args A bag of configuration properties, including values
   *    like 'node', 'cssText', 'className', 'id', 'src', etc.
   * @return {Node}
   */
  var makeIframe = function(args) {
    var el = document.createElement('iframe');
    el.setAttribute('frameborder', '0');
    el.setAttribute('border', '0');
    setProperties(el, args);
    return el;
  };

  /**
   * Adds an unadorned iframe into the page, taking arguments to customize it.
   * @param {Object} args A map of user-specified properties to set
   */
  var makeInlinePrompt = function(args) {
    args.className = 'chromeFrameInstallDefaultStyle ' +
                        (args.className || '');
    var ifr = makeIframe(args);
    // TODO(slightlyoff): handle placement more elegantly!
    if (!ifr.parentNode) {
      var firstChild = document.body.firstChild;
      document.body.insertBefore(ifr, firstChild);
    }
  };

  /**
   * Adds a styled, closable iframe into the page with a background that
   * emulates a modal dialog.
   * @param {Object} args A map of user-specified properties to set
   */
  var makeOverlayPrompt = function(args) {
    if (byId('chromeFrameOverlayContent')) {
      return; // Was previously created. Bail.
    }

    var n = document.createElement('span');
    n.innerHTML = '<div class="chromeFrameOverlayUnderlay"></div>' +
      '<table class="chromeFrameOverlayContent"' +
             'id="chromeFrameOverlayContent"' +
             'cellpadding="0" cellspacing="0">' +
        '<tr class="chromeFrameOverlayCloseBar">' +
          '<td>' +
            // TODO(slightlyoff): i18n
            '<button id="chromeFrameCloseButton">close</button>' +
          '</td>' +
        '</tr>' +
        '<tr>' +
          '<td id="chromeFrameIframeHolder"></td>' +
        '</tr>' +
      '</table>';

    var b = document.body;
    // Insert underlay nodes into the document in the right order.
    while (n.firstChild) {
      b.insertBefore(n.lastChild, b.firstChild);
    }
    var ifr = makeIframe(args);
    byId('chromeFrameIframeHolder').appendChild(ifr);
    byId('chromeFrameCloseButton').onclick = closeOverlay;
  };

  var CFInstall = {};

  /**
   * Checks to see if Chrome Frame is available, if not, prompts the user to
   * install. Once installation is begun, a background timer starts,
   * checkinging for a successful install every 2 seconds. Upon detection of
   * successful installation, the current page is reloaded, or if a
   * 'destination' parameter is passed, the page navigates there instead.
   * @param {Object} args A bag of configuration properties. Respected
   *    properties are: 'mode', 'url', 'destination', 'node', 'onmissing',
   *    'preventPrompt', 'oninstall', 'preventInstallDetection', 'cssText', and
   *    'className'.
   * @public
   */
  CFInstall.check = function(args) {
    args = args || {};

    // We currently only support CF in IE
    // TODO(slightlyoff): Update this should we support other browsers!
    var ua = navigator.userAgent;
    var ieRe = /MSIE (\S+); Windows NT/;
    var bail = false;
    if (ieRe.test(ua)) {
      // We also only support Win2003/XPSP2 or better. See:
      //  http://msdn.microsoft.com/en-us/library/ms537503%28VS.85%29.aspx
      if (parseFloat(ieRe.exec(ua)[1]) < 6 &&
          // 'SV1' indicates SP2, only bail if not SP2 or Win2K3
          ua.indexOf('SV1') < 0) {
        bail = true;
      }
    } else {
      // Not IE
      bail = true;
    }
    if (bail) {
      return;
    }

    // Inject the default styles
    injectCFStyleTag();

    if (document.cookie.indexOf("disableGCFCheck=1") >=0) {
      // If we're supposed to hide the overlay prompt, add the rules to do it.
      closeOverlay();
    }

    // When loaded in an alternate protocol (e.g., "file:"), still call out to
    // the right location.
    var currentProtocol = document.location.protocol;
    var protocol = (currentProtocol == 'https:') ? 'https:' : 'http:';
    // TODO(slightlyoff): Update this URL when a mini-installer page is
    //   available.
    var installUrl = protocol + '//www.google.com/chromeframe';
    if (!isAvailable()) {
      if (args.onmissing) {
        args.onmissing();
      }

      args.src = args.url || installUrl;
      var mode = args.mode || 'inline';
      var preventPrompt = args.preventPrompt || false;

      if (!preventPrompt) {
        if (mode == 'inline') {
          makeInlinePrompt(args);
        } else if (mode == 'overlay') {
          makeOverlayPrompt(args);
        } else {
          window.open(args.src);
        }
      }

      if (args.preventInstallDetection) {
        return;
      }

      // Begin polling for install success.
      var installTimer = setInterval(function() {
          // every 2 seconds, look to see if CF is available, if so, proceed on
          // to our destination
          if (isAvailable()) {
            if (args.oninstall) {
              args.oninstall();
            }

            clearInterval(installTimer);
            // TODO(slightlyoff): add a way to prevent navigation or make it
            //    contingent on oninstall?
            window.location = args.destination || window.location;
          }
      }, 2000);
    }
  };

  CFInstall._force = false;
  CFInstall._forceValue = false;
  CFInstall.isAvailable = isAvailable;

  // expose CFInstall to the external scope. We've already checked to make
  // sure we're not going to blow existing objects away.
  scope.CFInstall = CFInstall;

})(this['ChromeFrameInstallScope'] || this);