FEATURE: allows html tooltips (#6665)

FEATURE: allows html tooltips (#6665)

From 3453707784243c88bdd3c7437f940cd90bd938b1 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Date: Mon, 26 Nov 2018 11:15:23 +0100
Subject: [PATCH] FEATURE: allows html tooltips (#6665)


diff --git a/app/assets/javascripts/discourse/lib/tooltip.js.es6 b/app/assets/javascripts/discourse/lib/tooltip.js.es6
index 6028a87..227e0f6 100644
--- a/app/assets/javascripts/discourse/lib/tooltip.js.es6
+++ b/app/assets/javascripts/discourse/lib/tooltip.js.es6
@@ -3,10 +3,13 @@ import { escapeExpression } from "discourse/lib/utilities";
 const fadeSpeed = 300;
 const tooltipID = "#discourse-tooltip";
 
-export function showTooltip() {
-  const $this = $(this);
+export function showTooltip($this) {
   const $parent = $this.offsetParent();
-  const content = escapeExpression($this.attr("data-tooltip"));
+  // html tooltip are risky try your best to sanitize anything
+  // displayed as html to avoid XSS attacks
+  const content = $this.attr("data-tooltip")
+    ? escapeExpression($this.attr("data-tooltip"))
+    : $this.attr("data-html-tooltip") || "";
   const retina =
     window.devicePixelRatio && window.devicePixelRatio > 1
       ? "class='retina'"
@@ -19,7 +22,7 @@ export function showTooltip() {
 
   hideTooltip(tooltipID);
 
-  $(this).after(`
+  $this.after(`
     <div id="discourse-tooltip" ${retina}>
       <div class="tooltip-pointer"></div>
       <div class="tooltip-content">${content}</div>
@@ -74,7 +77,9 @@ export function hideTooltip() {
 
 export function registerTooltip(jqueryContext) {
   if (jqueryContext.length) {
-    jqueryContext.off("click").on("click", showTooltip);
+    jqueryContext
+      .off("click")
+      .on("click", event => showTooltip($(event.currentTarget)));
   }
 }
 
diff --git a/test/javascripts/lib/tooltip-test.js.es6 b/test/javascripts/lib/tooltip-test.js.es6
index f65d326..ece7b09 100644
--- a/test/javascripts/lib/tooltip-test.js.es6
+++ b/test/javascripts/lib/tooltip-test.js.es6
@@ -4,22 +4,56 @@ import { registerTooltip } from "discourse/lib/tooltip";
 QUnit.module("lib:tooltip", {
   beforeEach() {
     fixture().html(
-      "<a class='test-link' data-tooltip='XSS<s onmouseover\=alert(document.domain)>XSS'>test</a>"
+      `
+        <a class='test-text-link' data-tooltip='XSS<s onmouseover\=alert(document.domain)>XSS'>test</a>
+        <a class='test-html-link' data-html-tooltip='<p>test</p>'>test</a>
+      `
     );
   }
 });
 
-QUnit.test("it prevents XSS injection", assert => {
-  const $testLink = fixture(".test-link");
-  registerTooltip($testLink);
-  $testLink.click();
-
-  andThen(() => {
-    assert.equal(
-      fixture(".tooltip-content")
-        .html()
-        .trim(),
-      "XSS&lt;s onmouseover=alert(document.domain)&gt;XSS"
-    );
-  });
+QUnit.test("text support", async assert => {
+  const $testTextLink = fixture(".test-text-link");
+  registerTooltip($testTextLink);
+
+  await $testTextLink.click();
+
+  assert.equal(
+    fixture(".tooltip-content")
+      .html()
+      .trim(),
+    "XSS&lt;s onmouseover=alert(document.domain)&gt;XSS",
+    "it prevents XSS injection"
+  );
+
+  assert.equal(
+    fixture(".tooltip-content")
+      .text()
+      .trim(),
+    "XSS<s onmouseover=alert(document.domain)>XSS",
+    "it returns content as plain text"
+  );
+});
+
+QUnit.test("html support", async assert => {
+  const $testHtmlLink = fixture(".test-html-link");
+  registerTooltip($testHtmlLink);
+
+  await $testHtmlLink.click();
+
+  assert.equal(
+    fixture(".tooltip-content")
+      .html()
+      .trim(),
+    "<p>test</p>",
+    "it doesn’t escape HTML"
+  );
+
+  assert.equal(
+    fixture(".tooltip-content")
+      .text()
+      .trim(),
+    "test",
+    "it returns content as plain text"
+  );
 });

GitHub

2 Likes