// $Id: drupal.js,v 1.44 2008/05/19 19:42:18 dries Exp $

var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'themes': {}, 'locale': {} };

/**
 * Set the variable that indicates if JavaScript behaviors should be applied
 */
Drupal.jsEnabled = document.getElementsByTagName && document.createElement && document.createTextNode && document.documentElement && document.getElementById;

/**
 * Set the array of allowed HTML tags.
 */
Drupal.allowedTags = {'a': 0, 'em': 1, 'strong': 2, 'cite': 3, 'blockquote': 4, 'code': 5, 'ul': 6, 'ol': 7, 'li': 8, 'dl': 9, 'dt': 10, 'dd': 11};

/**
 * Attach all registered behaviors to a page element.
 *
 * Behaviors are event-triggered actions that attach to page elements, enhancing
 * default non-Javascript UIs. Behaviors are registered in the Drupal.behaviors
 * object as follows:
 * @code
 *    Drupal.behaviors.behaviorName = function () {
 *      ...
 *    };
 * @endcode
 *
 * Drupal.attachBehaviors is added below to the jQuery ready event and so
 * runs on initial page load. Developers implementing AHAH/AJAX in their
 * solutions should also call this function after new page content has been
 * loaded, feeding in an element to be processed, in order to attach all
 * behaviors to the new content.
 *
 * Behaviors should use a class in the form behaviorName-processed to ensure
 * the behavior is attached only once to a given element. (Doing so enables
 * the reprocessing of given elements, which may be needed on occasion despite
 * the ability to limit behavior attachment to a particular element.)
 *
 * @param context
 *   An element to attach behaviors to. If none is given, the document element
 *   is used.
 */
Drupal.attachBehaviors = function(context) {
  context = context || document;
  // Execute all of them.
  jQuery.each(Drupal.behaviors, function() {
    this(context);
  });
};

/**
 * Checks whether a string is valid UTF-8.
 */
Drupal.validateUTF8 = function (text) {
  if (text.length === 0) {
    return true;
  }
  var regex = new RegExp("^.", "us");
  var result = text.match(regex);
  return result.length;
}

/**
 * Filters XSS.
 *
 * See the documentation of the server-side filter_xss() function for further details.
 */
Drupal.filterXSS = function (str, allowedTags) {
  // Setup default values if none supplied.
  if (typeof(allowedTags) != 'undefined') {
    var key, tmp_ar = {};
    for (key in allowedTags) {
      tmp_ar[allowedTags[key]] = key;
    }
    Drupal.allowedTags = tmp_ar;
  }

  str = String(str);
  var regex = '';
  var replace = {
    '\\0': '', // Remove NUL characters (ignored by some browsers).
    '&\\s*\\{[^}]*(\\}\\s*;?|$)': '', // Remove Netscape 4 JS entities.
    '&': '&amp;', // Defuse all HTML entities.
    '&amp;([A-Za-z][A-Za-z0-9]*;)': function (match, set1) { // Change back only well-formed entities in our whitelist Named entities.
      return '&' + set1;
    },
    '&amp;#([0-9]+;)': function (match, set1) { // Decimal numeric entities.
      return '&#' + set1;
    },
    '&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)': function (match, set1) { // Hexadecimal numeric entities.
      return '&#x' + set1;
    }
  };
  for (var character in replace) {
    regex = new RegExp(character, 'g');
    str = str.replace(regex, replace[character]);
  }

  str = str.replace(/(<(?=[^a-zA-Z!\/])|<[^>]*(>|$)|>)/g, Drupal.filterXSSSplit);
  return str;
}

/**
 * Processes an HTML tag.
 */
Drupal.filterXSSSplit = function (m, key) {
  var slash = elem = attrlist = xhtmlSlash = attr2 = xssAttributesList = '';
  var str = m;
  var matches = [];

  if (str.substring(0, 1) != '<') {
    // We matched a lone ">" character
    return '&gt;';
  }
  else if (str.length === 1) {
    // We matched a lone "<" character
    return '&lt;';
  }

  matches = str.match(/^<\s*(\/\s*)?([a-zA-Z0-9]+)([^>]*)>?$/);
  if (typeof(matches) == 'undefined' || matches === null || !matches.length) {
    // Seriously malformed
    return '';
  }

  slash = jQuery.trim(matches[1]);
  elem = matches[2];
  attrlist = matches[3];

  if (typeof(Drupal.allowedTags[elem.toLowerCase()]) == 'undefined' || Drupal.allowedTags[elem.toLowerCase()] === null) {
    // Disallowed HTML element
    return '';
  }

  if (slash !== '') {
    return "</" + elem + ">";
  }

  // Is there a closing XHTML slash at the end of the attributes?
  // In PHP 5.1.0+ we could count the changes, currently we need a separate match
  xhtmlSlash = attrlist.match(/\s?\/\s*$/) ? ' /' : '';
  attrlist = attrlist.replace(/(\s?)\/\s*$/, function (match, set1) {
    return set1;
  });

  // Clean up attributes
  xssAttributesList = Drupal.filterXSSAttributes(attrlist);
  attr2 = xssAttributesList.join(' ');
  attr2 = attr2.replace(/[<>]/, '');
  attr2 = attr2.length ? ' ' + attr2 : '';

  return "<" + elem + attr2 + xhtmlSlash +">";
}

/**
 * Processes a string of HTML attributes.
 */
Drupal.filterXSSAttributes = function (attr) {
  var matches = attrarr = [];
  var mode = 0;
  var attrname = '';
  var skip = false;

  while (attr.length !== 0) {
    // Was the last operation successful?
    working = 0;

    switch (mode) {
      case 0:
        // Attribute name, href for instance
        matches = attr.match(/^([-a-zA-Z]+)/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          attrname = matches[1].toLowerCase();
          skip = (attrname == 'style' || attrname.substring(0, 2) == 'on');
          working = mode = 1;
          attr = attr.replace(/^[-a-zA-Z]+/, '');
        }

        break;

      case 1:
        // Equals sign or valueless ("selected")
        matches = attr.match(/^\s*=\s*/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          working = 1; mode = 2;
          attr = attr.replace(/^\s*=\s*/, '');
          break;
        }

        matches = attr.match(/^\s+/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          working = 1; mode = 0;
          if (!skip) {
            attrarr[attrarr.length++] = attrname;
          }
          attr = attr.replace(/^\s+/, '');
        }

        break;

      case 2:
        // Attribute value, a URL after href= for instance
        matches = attr.match(/^"([^"]*)"(\s+|$)/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          thisval = Drupal.filterXSSBadProtocol(matches[1]);

          if (!skip) {
            attrarr[attrarr.length++] = attrname + '="' + thisval + '"';
          }
          working = 1;
          mode = 0;
          attr = attr.replace(/^"[^"]*"(\s+|$)/, '');
          break;
        }

        matches = attr.match(/^'([^']*)'(\s+|$)/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          thisval = Drupal.filterXSSBadProtocol(matches[1]);

          if (!skip) {
            attrarr[attrarr.length++] = attrname + '="' + thisval + '"';
          }
          working = 1; mode = 0;
          attr = attr.replace(/^'[^']*'(\s+|$)/, '');
          break;
        }

        matches = attr.match(/^([^\s\"']+)(\s+|$)/);
        if (typeof(matches) != 'undefined' && matches !== null && matches.length) {
          thisval = Drupal.filterXSSBadProtocol(matches[1]);

          if (!skip) {
            attrarr[attrarr.length++] = attrname + '="' + thisval + '"';
          }
          working = 1; mode = 0;
          attr = attr.replace(/^[^\s\"']+(\s+|$)/, '');
        }

        break;
    }

    if (working === 0) {
      // not well formed, remove and try again
      attr = attr.replace(/^("[^"]*("|$)|\'[^\']*(\'|$)||\S)*\s*/, '');
      mode = 0;
    }
  }

  // the attribute list ends with a valueless attribute like "selected"
  if (mode === 1) {
    attrarr[attrarr.length++] = attrname;
  }
  return attrarr;
}

/**
 * Processes an HTML attribute value and ensures it does not contain an URL with
 * a disallowed protocol (e.g. javascript:).
 */
Drupal.filterXSSBadProtocol = function (str) {
  allowedProtocols = ['http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'];

  // Get the plain text representation of the attribute value (i.e. its meaning).
  str = Drupal.htmlEntityDecode(str);

  // Iteratively remove any invalid protocol found.

  do {
    before = str;
    colonpos = str.indexOf(':');
    if (colonpos > 0) {
      // We found a colon, possibly a protocol. Verify.
      protocol = str.substring(0, colonpos);
      // If a colon is preceded by a slash, question mark or hash, it cannot
      // possibly be part of the URL scheme. This must be a relative URL,
      // which inherits the (safe) protocol of the base document.
      if (protocol.match(/![\/?#]!/)) {
        break;
      }
      // Per RFC2616, section 3.2.3 (URI Comparison) scheme comparison must be case-insensitive
      // Check if this is a disallowed protocol.
      if (!allowedProtocols[protocol.toLowerCase()]) {
        str = str.substring(colonpos + 1);
      }
    }
  } while (before != str);

  return Drupal.checkPlain(str);
}

/**
 * Decode all HTML entities (including numerical ones) to regular UTF-8 bytes.
 *
 * Double-escaped entities will only be decoded once ("&amp;lt;" becomes "&lt;",
 * not "<").
 */
Drupal.htmlEntityDecode = function (str) {

  var histogram = {}, histogramR = {}, code = 0;
  var entity = chr = '';

  histogram['34'] = 'quot';
  histogram['38'] = 'amp';
  histogram['60'] = 'lt';
  histogram['62'] = 'gt';
  histogram['160'] = 'nbsp';
  histogram['161'] = 'iexcl';
  histogram['162'] = 'cent';
  histogram['163'] = 'pound';
  histogram['164'] = 'curren';
  histogram['165'] = 'yen';
  histogram['166'] = 'brvbar';
  histogram['167'] = 'sect';
  histogram['168'] = 'uml';
  histogram['169'] = 'copy';
  histogram['170'] = 'ordf';
  histogram['171'] = 'laquo';
  histogram['172'] = 'not';
  histogram['173'] = 'shy';
  histogram['174'] = 'reg';
  histogram['175'] = 'macr';
  histogram['176'] = 'deg';
  histogram['177'] = 'plusmn';
  histogram['178'] = 'sup2';
  histogram['179'] = 'sup3';
  histogram['180'] = 'acute';
  histogram['181'] = 'micro';
  histogram['182'] = 'para';
  histogram['183'] = 'middot';
  histogram['184'] = 'cedil';
  histogram['185'] = 'sup1';
  histogram['186'] = 'ordm';
  histogram['187'] = 'raquo';
  histogram['188'] = 'frac14';
  histogram['189'] = 'frac12';
  histogram['190'] = 'frac34';
  histogram['191'] = 'iquest';
  histogram['192'] = 'Agrave';
  histogram['193'] = 'Aacute';
  histogram['194'] = 'Acirc';
  histogram['195'] = 'Atilde';
  histogram['196'] = 'Auml';
  histogram['197'] = 'Aring';
  histogram['198'] = 'AElig';
  histogram['199'] = 'Ccedil';
  histogram['200'] = 'Egrave';
  histogram['201'] = 'Eacute';
  histogram['202'] = 'Ecirc';
  histogram['203'] = 'Euml';
  histogram['204'] = 'Igrave';
  histogram['205'] = 'Iacute';
  histogram['206'] = 'Icirc';
  histogram['207'] = 'Iuml';
  histogram['208'] = 'ETH';
  histogram['209'] = 'Ntilde';
  histogram['210'] = 'Ograve';
  histogram['211'] = 'Oacute';
  histogram['212'] = 'Ocirc';
  histogram['213'] = 'Otilde';
  histogram['214'] = 'Ouml';
  histogram['215'] = 'times';
  histogram['216'] = 'Oslash';
  histogram['217'] = 'Ugrave';
  histogram['218'] = 'Uacute';
  histogram['219'] = 'Ucirc';
  histogram['220'] = 'Uuml';
  histogram['221'] = 'Yacute';
  histogram['222'] = 'THORN';
  histogram['223'] = 'szlig';
  histogram['224'] = 'agrave';
  histogram['225'] = 'aacute';
  histogram['226'] = 'acirc';
  histogram['227'] = 'atilde';
  histogram['228'] = 'auml';
  histogram['229'] = 'aring';
  histogram['230'] = 'aelig';
  histogram['231'] = 'ccedil';
  histogram['232'] = 'egrave';
  histogram['233'] = 'eacute';
  histogram['234'] = 'ecirc';
  histogram['235'] = 'euml';
  histogram['236'] = 'igrave';
  histogram['237'] = 'iacute';
  histogram['238'] = 'icirc';
  histogram['239'] = 'iuml';
  histogram['240'] = 'eth';
  histogram['241'] = 'ntilde';
  histogram['242'] = 'ograve';
  histogram['243'] = 'oacute';
  histogram['244'] = 'ocirc';
  histogram['245'] = 'otilde';
  histogram['246'] = 'ouml';
  histogram['247'] = 'divide';
  histogram['248'] = 'oslash';
  histogram['249'] = 'ugrave';
  histogram['250'] = 'uacute';
  histogram['251'] = 'ucirc';
  histogram['252'] = 'uuml';
  histogram['253'] = 'yacute';
  histogram['254'] = 'thorn';
  histogram['255'] = 'yuml';

  // Reverse table. Cause for maintainability purposes, the histogram is
  // identical to the one in htmlentities.
  for (code in histogram) {
    entity = histogram[code];
    histogramR[entity] = code;
  }

  return str.replace(/(\&([a-zA-Z]+)\;)/g, function(full, m1, m2){
    if (m2 in histogramR) {
      return str.fromCharCode(histogramR[m2]);
    } else {
      return m2;
    }
  });
}

/**
 * Encode special characters in a plain-text string for display as HTML.
 */
Drupal.checkPlain = function(str) {
  str = String(str);
  var replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
  for (var character in replace) {
    var regex = new RegExp(character, 'g');
    str = str.replace(regex, replace[character]);
  }
  return str;
};

/**
 * Translate strings to the page language or a given language.
 *
 * See the documentation of the server-side t() function for further details.
 *
 * @param str
 *   A string containing the English string to translate.
 * @param args
 *   An object of replacements pairs to make after translation. Incidences
 *   of any key in this array are replaced with the corresponding value.
 *   Based on the first character of the key, the value is escaped and/or themed:
 *    - !variable: inserted as is
 *    - @variable: escape plain text to HTML (Drupal.checkPlain)
 *    - %variable: escape text and theme as a placeholder for user-submitted
 *      content (checkPlain + Drupal.theme('placeholder'))
 * @return
 *   The translated string.
 */
Drupal.t = function(str, args) {
  // Fetch the localized version of the string.
  if (Drupal.locale.strings && Drupal.locale.strings[str]) {
    str = Drupal.locale.strings[str];
  }

  if (args) {
    // Transform arguments before inserting them
    for (var key in args) {
      switch (key.charAt(0)) {
        // Escaped only
        case '@':
          args[key] = Drupal.checkPlain(args[key]);
        break;
        // Pass-through
        case '!':
          break;
        // Escaped and placeholder
        case '%':
        default:
          args[key] = Drupal.theme('placeholder', args[key]);
          break;
      }
      str = str.replace(key, args[key]);
    }
  }
  return str;
};

/**
 * Format a string containing a count of items.
 *
 * This function ensures that the string is pluralized correctly. Since Drupal.t() is
 * called by this function, make sure not to pass already-localized strings to it.
 *
 * See the documentation of the server-side format_plural() function for further details.
 *
 * @param count
 *   The item count to display.
 * @param singular
 *   The string for the singular case. Please make sure it is clear this is
 *   singular, to ease translation (e.g. use "1 new comment" instead of "1 new").
 *   Do not use @count in the singular string.
 * @param plural
 *   The string for the plural case. Please make sure it is clear this is plural,
 *   to ease translation. Use @count in place of the item count, as in "@count
 *   new comments".
 * @param args
 *   An object of replacements pairs to make after translation. Incidences
 *   of any key in this array are replaced with the corresponding value.
 *   Based on the first character of the key, the value is escaped and/or themed:
 *    - !variable: inserted as is
 *    - @variable: escape plain text to HTML (Drupal.checkPlain)
 *    - %variable: escape text and theme as a placeholder for user-submitted
 *      content (checkPlain + Drupal.theme('placeholder'))
 *   Note that you do not need to include @count in this array.
 *   This replacement is done automatically for the plural case.
 * @return
 *   A translated string.
 */
Drupal.formatPlural = function(count, singular, plural, args) {
  var args = args || {};
  args['@count'] = count;
  // Determine the index of the plural form.
  var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] === 1) ? 0 : 1);

  if (index === 0) {
    return Drupal.t(singular, args);
  }
  else if (index === 1) {
    return Drupal.t(plural, args);
  }
  else {
    args['@count['+ index +']'] = args['@count'];
    delete args['@count'];
    return Drupal.t(plural.replace('@count', '@count['+ index +']'));
  }
};

/**
 * Generate the themed representation of a Drupal object.
 *
 * All requests for themed output must go through this function. It examines
 * the request and routes it to the appropriate theme function. If the current
 * theme does not provide an override function, the generic theme function is
 * called.
 *
 * For example, to retrieve the HTML that is output by theme_placeholder(text),
 * call Drupal.theme('placeholder', text).
 *
 * @param func
 *   The name of the theme function to call.
 * @param ...
 *   Additional arguments to pass along to the theme function.
 * @return
 *   Any data the theme function returns. This could be a plain HTML string,
 *   but also a complex object.
 */
Drupal.theme = function(func) {
  for (var i = 1, args = []; i < arguments.length; i++) {
    args.push(arguments[i]);
  }

  return (Drupal.theme[func] || Drupal.theme.prototype[func]).apply(this, args);
};

/**
 * Parse a JSON response.
 *
 * The result is either the JSON object, or an object with 'status' 0 and 'data' an error message.
 */
Drupal.parseJson = function (data) {
  if ((data.substring(0, 1) != '{') && (data.substring(0, 1) != '[')) {
    return { status: 0, data: data.length ? data : Drupal.t('Unspecified error') };
  }
  return eval('(' + data + ');');
};

/**
 * Freeze the current body height (as minimum height). Used to prevent
 * unnecessary upwards scrolling when doing DOM manipulations.
 */
Drupal.freezeHeight = function () {
  Drupal.unfreezeHeight();
  var div = document.createElement('div');
  $(div).css({
    position: 'absolute',
    top: '0px',
    left: '0px',
    width: '1px',
    height: $('body').css('height')
  }).attr('id', 'freeze-height');
  $('body').append(div);
};

/**
 * Unfreeze the body height
 */
Drupal.unfreezeHeight = function () {
  $('#freeze-height').remove();
};

/**
 * Wrapper to address the mod_rewrite url encoding bug
 * (equivalent of drupal_urlencode() in PHP).
 */
Drupal.encodeURIComponent = function (item, uri) {
  uri = uri || location.href;
  item = encodeURIComponent(item).replace(/%2F/g, '/');
  return (uri.indexOf('?q=') != -1) ? item : item.replace(/%26/g, '%2526').replace(/%23/g, '%2523').replace(/\/\//g, '/%252F');
};

/**
 * Get the text selection in a textarea.
 */
Drupal.getSelection = function (element) {
  if (typeof(element.selectionStart) != 'number' && document.selection) {
    // The current selection
    var range1 = document.selection.createRange();
    var range2 = range1.duplicate();
    // Select all text.
    range2.moveToElementText(element);
    // Now move 'dummy' end point to end point of original range.
    range2.setEndPoint('EndToEnd', range1);
    // Now we can calculate start and end points.
    var start = range2.text.length - range1.text.length;
    var end = start + range1.text.length;
    return { 'start': start, 'end': end };
  }
  return { 'start': element.selectionStart, 'end': element.selectionEnd };
};

/**
 * Build an error message from ahah response.
 */
Drupal.ahahError = function(xmlhttp, uri) {
  if (xmlhttp.status == 200) {
    if (jQuery.trim($(xmlhttp.responseText).text())) {
      var message = Drupal.t("An error occurred. \n@uri\n@text", {'@uri': uri, '@text': xmlhttp.responseText });
    }
    else {
      var message = Drupal.t("An error occurred. \n@uri\n(no information available).", {'@uri': uri, '@text': xmlhttp.responseText });
    }
  }
  else {
    var message = Drupal.t("An HTTP error @status occurred. \n@uri", {'@uri': uri, '@status': xmlhttp.status });
  }
  return message;
}

// Global Killswitch on the <html> element
if (Drupal.jsEnabled) {
  // Global Killswitch on the <html> element
  $(document.documentElement).addClass('js');
  // 'js enabled' cookie
  document.cookie = 'has_js=1; path=/';
  // Attach all behaviors.
  $(document).ready(function() {
    Drupal.attachBehaviors(this);
  });
}

/**
 * The default themes.
 */
Drupal.theme.prototype = {

  /**
   * Formats text for emphasized display in a placeholder inside a sentence.
   *
   * @param str
   *   The text to format (plain-text).
   * @return
   *   The formatted text (html).
   */
  placeholder: function(str) {
    return '<em>' + Drupal.checkPlain(str) + '</em>';
  }
};

