chat/modules/imSmileys.jsm
author Ben Bucksch <ben.bucksch@beonex.com>
Sat, 15 Feb 2020 12:54:00 +0200
changeset 37355 f66ebccec17986b2a453c42a40e1c53f0042518d
parent 35696 e5737872508567abf8b0444f43eb4ee4ce211c75
child 37651 8580290d78331f918b6385943e868cd45968acbc
permissions -rw-r--r--
Bug 1615137 - Remove duplicate include of lightning-toolbar.dtd file. r=pmorris DONTBUILD

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
  return new TextDecoder();
});

ChromeUtils.defineModuleGetter(
  this,
  "NetUtil",
  "resource://gre/modules/NetUtil.jsm"
);

this.EXPORTED_SYMBOLS = [
  "smileImMarkup", // used to add smile:// img tags into IM markup.
  "smileTextNode", // used to add smile:// img tags to the content of a textnode
  "smileString", // used to add smile:// img tags into a string without parsing it as HTML. Be sure the string doesn't contain HTML tags.
  "getSmileRealURI", // used to retrieve the chrome URI for a smile:// URI
  "getSmileyList", // used to display a list of smileys in the UI
];

var kEmoticonsThemePref = "messenger.options.emoticonsTheme";
var kThemeFile = "theme.json";

Object.defineProperty(this, "gTheme", {
  configurable: true,
  enumerable: true,

  get() {
    delete this.gTheme;
    gPrefObserver.init();
    return (this.gTheme = getTheme());
  },
});

var gPrefObserver = {
  init() {
    Services.prefs.addObserver(kEmoticonsThemePref, gPrefObserver);
  },

  observe(aObject, aTopic, aMsg) {
    if (aTopic != "nsPref:changed" || aMsg != kEmoticonsThemePref) {
      throw new Error("bad notification");
    }

    gTheme = getTheme();
  },
};

function getSmileRealURI(aSmile) {
  aSmile = Services.textToSubURI.unEscapeURIForUI("UTF-8", aSmile);
  if (aSmile in gTheme.iconsHash) {
    return gTheme.baseUri + gTheme.iconsHash[aSmile].filename;
  }

  throw new Error("Invalid smile!");
}

function getSmileyList(aThemeName) {
  let theme = aThemeName == gTheme.name ? gTheme : getTheme(aThemeName);
  if (!theme.json) {
    return null;
  }

  let addAbsoluteUrls = function(aSmiley) {
    return {
      filename: aSmiley.filename,
      src: theme.baseUri + aSmiley.filename,
      textCodes: aSmiley.textCodes,
    };
  };
  return theme.json.smileys.map(addAbsoluteUrls);
}

function getTheme(aName) {
  let name = aName || Services.prefs.getCharPref(kEmoticonsThemePref);

  let theme = {
    name,
    iconsHash: null,
    json: null,
    regExp: null,
  };

  if (name == "none") {
    return theme;
  }

  if (name == "default") {
    theme.baseUri = "chrome://instantbird-emoticons/skin/";
  } else {
    theme.baseUri = "chrome://" + theme.name + "/skin/";
  }
  try {
    let channel = Services.io.newChannel(
      theme.baseUri + kThemeFile,
      null,
      null,
      null,
      Services.scriptSecurityManager.getSystemPrincipal(),
      null,
      Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
      Ci.nsIContentPolicy.TYPE_IMAGE
    );
    let stream = channel.open();
    let bytes = NetUtil.readInputStream(stream, stream.available());
    theme.json = JSON.parse(gTextDecoder.decode(bytes));
    stream.close();
    theme.iconsHash = {};
    for (let smiley of theme.json.smileys) {
      for (let textCode of smiley.textCodes) {
        theme.iconsHash[textCode] = smiley;
      }
    }
  } catch (e) {
    Cu.reportError(e);
  }
  return theme;
}

function getRegexp() {
  if (gTheme.regExp) {
    gTheme.regExp.lastIndex = 0;
    return gTheme.regExp;
  }

  // return null if smileys are disabled
  if (!gTheme.iconsHash) {
    return null;
  }

  if ("" in gTheme.iconsHash) {
    Cu.reportError(
      "Emoticon " + gTheme.iconsHash[""].filename + " matches the empty string!"
    );
    delete gTheme.iconsHash[""];
  }

  let emoticonList = [];
  for (let emoticon in gTheme.iconsHash) {
    emoticonList.push(emoticon);
  }

  let exp = /[[\]{}()*+?.\\^$|]/g;
  emoticonList = emoticonList
    .sort()
    .reverse()
    .map(x => x.replace(exp, "\\$&"));

  if (!emoticonList.length) {
    // the theme contains no valid emoticon, make sure we will return
    // early next time
    gTheme.iconsHash = null;
    return null;
  }

  gTheme.regExp = new RegExp(emoticonList.join("|"), "g");
  return gTheme.regExp;
}

// unused. May be useful later to process a string instead of an HTML node
function smileString(aString) {
  const kSmileFormat =
    '<img class="ib-img-smile" src="smile://$&" alt="$&" title="$&"/>';

  let exp = getRegexp();
  return exp ? aString.replace(exp, kSmileFormat) : aString;
}

function smileTextNode(aNode) {
  /*
   * Skip text nodes that contain the href in the child text node.
   * We must check both the testNode.textContent and the aNode.data since they
   * cover different cases:
   *   textContent: The URL is split over multiple nodes for some reason
   *   data: The URL is not the only content in the link, skip only the one node
   * Check the class name to skip any autolinked nodes from mozTXTToHTMLConv.
   */
  let testNode = aNode;
  while ((testNode = testNode.parentNode)) {
    if (
      testNode.nodeName.toLowerCase() == "a" &&
      (testNode.getAttribute("href") == testNode.textContent.trim() ||
        testNode.getAttribute("href") == aNode.data.trim() ||
        testNode.className.includes("moz-txt-link-"))
    ) {
      return 0;
    }
  }

  let result = 0;
  let exp = getRegexp();
  if (!exp) {
    return result;
  }

  let match;
  while ((match = exp.exec(aNode.data))) {
    let smileNode = aNode.splitText(match.index);
    aNode = smileNode.splitText(exp.lastIndex - match.index);
    // at this point, smileNode is a text node with only the text
    // of the smiley and aNode is a text node with the text after
    // the smiley. The text in aNode hasn't been processed yet.
    let smile = smileNode.data;
    let elt = aNode.ownerDocument.createElement("img");
    elt.setAttribute("src", "smile://" + smile);
    elt.setAttribute("title", smile);
    elt.setAttribute("alt", smile);
    elt.setAttribute("class", "ib-img-smile");
    smileNode.parentNode.replaceChild(elt, smileNode);
    result += 2;
    exp.lastIndex = 0;
  }
  return result;
}

function smileNode(aNode) {
  for (let i = 0; i < aNode.childNodes.length; ++i) {
    let node = aNode.childNodes[i];
    if (
      node.nodeType == node.ELEMENT_NODE &&
      node.namespaceURI == "http://www.w3.org/1999/xhtml"
    ) {
      // we are on a tag, recurse to process its children
      smileNode(node);
    } else if (
      node.nodeType == node.TEXT_NODE ||
      node.nodeType == node.CDATA_SECTION_NODE
    ) {
      // we are on a text node, process it
      smileTextNode(node);
    }
  }
}

function smileImMarkup(aDocument, aText) {
  if (!aDocument) {
    throw new Error("providing an HTML document is required");
  }

  // return early if smileys are disabled
  if (!gTheme.iconsHash) {
    return aText;
  }

  let div = aDocument.createElement("div");
  // eslint-disable-next-line no-unsanitized/property
  div.innerHTML = aText;
  smileNode(div);
  return div.innerHTML;
}