Bug 1139496 - Allow server provided explanation / ad group name to be displayed on Suggested Tiles [r=adw, f=bsmedberg]
authorMarina Samuel <msamuel@mozilla.com>
Tue, 19 May 2015 12:42:08 -0700
changeset 244748 bafcb71527681b2612b859520f274fd73944ca42
parent 244747 6c8613e718778d645c673edb9a34bf767b5f4ece
child 244749 a93f59b2e2221f273ffccedcfc5ce5218e7bb4b4
push id13052
push useredilee@gmail.com
push dateWed, 20 May 2015 19:10:12 +0000
treeherderfx-team@c20e5947d8dd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1139496
milestone41.0a1
Bug 1139496 - Allow server provided explanation / ad group name to be displayed on Suggested Tiles [r=adw, f=bsmedberg]
browser/base/content/newtab/sites.js
browser/base/content/test/newtab/browser_newtab_enhanced.js
browser/docs/DirectoryLinksProvider.rst
browser/modules/DirectoryLinksProvider.jsm
browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -111,16 +111,36 @@ Site.prototype = {
       this.node.setAttribute("pinned", true);
       control.setAttribute("title", newTabString("unpin"));
     } else {
       this.node.removeAttribute("pinned");
       control.setAttribute("title", newTabString("pin"));
     }
   },
 
+  _newTabString: function(str, substrArr) {
+    let regExp = /%[0-9]\$S/g;
+    let matches;
+    while (matches = regExp.exec(str)) {
+      let match = matches[0];
+      let index = match.charAt(1); // Get the digit in the regExp.
+      str = str.replace(match, substrArr[index - 1]);
+    }
+    return str;
+  },
+
+  _getSuggestedTileExplanation: function() {
+    let targetedName = `<strong> ${this.link.targetedName} </strong>`;
+    let targetedSite = `<strong> ${this.link.targetedSite} </strong>`;
+    if (this.link.explanation) {
+      return this._newTabString(this.link.explanation, [targetedName, targetedSite]);
+    }
+    return newTabString("suggested.button", [targetedName]);
+  },
+
   /**
    * Renders the site's data (fills the HTML fragment).
    */
   _render: function Site_render() {
     let enhanced = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link);
     let url = this.url;
     let title = enhanced && enhanced.title ? enhanced.title :
                 this.link.type == "history" ? this.link.baseDomain :
@@ -135,19 +155,19 @@ Site.prototype = {
 
     if (this.link.targetedSite) {
       if (this.node.getAttribute("type") != "sponsored") {
         this._querySelector(".newtab-sponsored").textContent =
           newTabString("suggested.tag");
       }
 
       this.node.setAttribute("suggested", true);
-      let targetedSite = `<strong> ${this.link.targetedName} </strong>`;
+      let explanation = this._getSuggestedTileExplanation();
       this._querySelector(".newtab-suggested").innerHTML =
-        `<div class='newtab-suggested-bounds'> ${newTabString("suggested.button", [targetedSite])} </div>`;
+        `<div class='newtab-suggested-bounds'> ${explanation} </div>`;
     }
 
     if (this.isPinned())
       this._updateAttributes(true);
     // Capture the page if the thumbnail is missing, which will cause page.js
     // to be notified and call our refreshThumbnail() method.
     this.captureIfMissing();
     // but still display whatever thumbnail might be available now.
--- a/browser/base/content/test/newtab/browser_newtab_enhanced.js
+++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js
@@ -1,110 +1,116 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const PRELOAD_PREF = "browser.newtab.preload";
 
+let suggestedLink = {
+  url: "http://example1.com/2",
+  imageURI: "data:image/png;base64,helloWORLD3",
+  title: "title2",
+  type: "affiliate",
+  frecent_sites: ["classroom.google.com", "codecademy.com", "elearning.ut.ac.id", "khanacademy.org", "learn.jquery.com", "teamtreehouse.com", "tutorialspoint.com", "udacity.com", "w3cschool.cc", "w3schools.com"]
+};
+
 gDirectorySource = "data:application/json," + JSON.stringify({
   "enhanced": [{
     url: "http://example.com/",
     enhancedImageURI: "data:image/png;base64,helloWORLD",
     title: "title",
     type: "organic",
   }],
   "directory": [{
     url: "http://example1.com/",
     enhancedImageURI: "data:image/png;base64,helloWORLD2",
     title: "title1",
     type: "organic"
   }],
-  "suggested": [{
-    url: "http://example1.com/2",
-    imageURI: "data:image/png;base64,helloWORLD3",
-    title: "title2",
-    type: "affiliate",
-    frecent_sites: ["test.com"]
-  }]
+  "suggested": [suggestedLink]
 });
 
 function runTests() {
-  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
-  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origEnhanced = NewTabUtils.allPages.enhanced;
   registerCleanupFunction(() => {
-    DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
     Services.prefs.clearUserPref(PRELOAD_PREF);
     NewTabUtils.allPages.enhanced = origEnhanced;
   });
 
   Services.prefs.setBoolPref(PRELOAD_PREF, false);
 
   function getData(cellNum) {
     let cell = getCell(cellNum);
     if (!cell.site)
       return null;
     let siteNode = cell.site.node;
     return {
       type: siteNode.getAttribute("type"),
       enhanced: siteNode.querySelector(".enhanced-content").style.backgroundImage,
       title: siteNode.querySelector(".newtab-title").textContent,
+      suggested: siteNode.querySelector(".newtab-suggested").innerHTML
     };
   }
 
   // Make the page have a directory link, enhanced link, and history link
   yield setLinks("-1");
 
   // Test with enhanced = false
   yield addNewTabPageTab();
   yield customizeNewTabPage("classic");
   yield customizeNewTabPage("enhanced"); // Toggle enhanced off
-  let {type, enhanced, title} = getData(0);
+  let {type, enhanced, title, suggested} = getData(0);
   isnot(type, "enhanced", "history link is not enhanced");
   is(enhanced, "", "history link has no enhanced image");
   is(title, "example.com");
+  is(suggested, "", "There is no suggested explanation");
 
   is(getData(1), null, "there is only one link and it's a history link");
 
   // Test with enhanced = true
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced"); // Toggle enhanced on
-  ({type, enhanced, title} = getData(0));
+  ({type, enhanced, title, suggested} = getData(0));
   is(type, "organic", "directory link is organic");
   isnot(enhanced, "", "directory link has enhanced image");
   is(title, "title1");
+  is(suggested, "", "There is no suggested explanation");
 
-  ({type, enhanced, title} = getData(1));
+  ({type, enhanced, title, suggested} = getData(1));
   is(type, "enhanced", "history link is enhanced");
   isnot(enhanced, "", "history link has enhanced image");
   is(title, "title");
+  is(suggested, "", "There is no suggested explanation");
 
   is(getData(2), null, "there are only 2 links, directory and enhanced history");
 
   // Test with a pinned link
   setPinnedLinks("-1");
   yield addNewTabPageTab();
-  ({type, enhanced, title} = getData(0));
+  ({type, enhanced, title, suggested} = getData(0));
   is(type, "enhanced", "pinned history link is enhanced");
   isnot(enhanced, "", "pinned history link has enhanced image");
   is(title, "title");
+  is(suggested, "", "There is no suggested explanation");
 
-  ({type, enhanced, title} = getData(1));
+  ({type, enhanced, title, suggested} = getData(1));
   is(type, "organic", "directory link is organic");
   isnot(enhanced, "", "directory link has enhanced image");
   is(title, "title1");
+  is(suggested, "", "There is no suggested explanation");
 
   is(getData(2), null, "directory link pushed out by pinned history link");
 
   // Test pinned link with enhanced = false
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced"); // Toggle enhanced off
-  ({type, enhanced, title} = getData(0));
+  ({type, enhanced, title, suggested} = getData(0));
   isnot(type, "enhanced", "history link is not enhanced");
   is(enhanced, "", "history link has no enhanced image");
   is(title, "example.com");
+  is(suggested, "", "There is no suggested explanation");
 
   is(getData(1), null, "directory link still pushed out by pinned history link");
 
   ok(getContentDocument().getElementById("newtab-intro-what"),
      "'What is this page?' link exists");
 
   yield unpinCell(0);
 
@@ -112,35 +118,72 @@ function runTests() {
 
   // Test that a suggested tile is not enhanced by a directory tile
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
   yield setLinks("-1,2,3,4,5,6,7,8");
 
   // Test with enhanced = false
   yield addNewTabPageTab();
-  ({type, enhanced, title} = getData(0));
+  ({type, enhanced, title, suggested} = getData(0));
   isnot(type, "enhanced", "history link is not enhanced");
   is(enhanced, "", "history link has no enhanced image");
   is(title, "example.com");
+  is(suggested, "", "There is no suggested explanation");
 
   isnot(getData(7), null, "there are 8 history links");
   is(getData(8), null, "there are 8 history links");
 
 
   // Test with enhanced = true
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced");
 
   // Suggested link was not enhanced by directory link with same domain
-  ({type, enhanced, title} = getData(0));
+  ({type, enhanced, title, suggested} = getData(0));
   is(type, "affiliate", "suggested link is affiliate");
   is(enhanced, "", "suggested link has no enhanced image");
   is(title, "title2");
+  ok(suggested.indexOf("Suggested for <strong> Web Education </strong> visitors") > -1, "Suggested for 'Web Education'");
 
   // Enhanced history link shows up second
-  ({type, enhanced, title} = getData(1));
+  ({type, enhanced, title, suggested} = getData(1));
   is(type, "enhanced", "pinned history link is enhanced");
   isnot(enhanced, "", "pinned history link has enhanced image");
   is(title, "title");
+  is(suggested, "", "There is no suggested explanation");
 
   is(getData(9), null, "there is a suggested link followed by an enhanced history link and the remaining history links");
+
+
+
+  // Test override category/adgroup name.
+  suggestedLink.adgroup_name = "Technology";
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
+    "data:application/json," + JSON.stringify({"suggested": [suggestedLink]}));
+
+  yield addNewTabPageTab();
+  ({type, enhanced, title, suggested} = getData(0));
+  Cu.reportError("SUGGEST " + suggested);
+  ok(suggested.indexOf("Suggested for <strong> Technology </strong> visitors") > -1, "Suggested for 'Technology'");
+
+
+  // Test server provided explanation string.
+  suggestedLink.explanation = "Suggested for %1$S enthusiasts who visit sites like %2$S";
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
+    "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]})));
+
+  yield addNewTabPageTab();
+  ({type, enhanced, title, suggested} = getData(0));
+  Cu.reportError("SUGGEST " + suggested);
+  ok(suggested.indexOf("Suggested for <strong> Technology </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'Technology' enthusiasts");
+
+
+    // Test server provided explanation string without category override.
+  delete suggestedLink.adgroup_name;
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
+    "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]})));
+
+  yield addNewTabPageTab();
+  ({type, enhanced, title, suggested} = getData(0));
+  Cu.reportError("SUGGEST " + suggested);
+  ok(suggested.indexOf("Suggested for <strong> Web Education </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'Web Education' enthusiasts");
 }
--- a/browser/docs/DirectoryLinksProvider.rst
+++ b/browser/docs/DirectoryLinksProvider.rst
@@ -125,18 +125,20 @@ Below is an example directory source fil
               "imageURI": "https://tiles.cdn.mozilla.net/images/20e24aa2219ec7542cc8cf0fd79f0c81e16ebeac.11859.png",
               "title": "TurboTax",
               "type": "sponsored",
               "url": "https://turbotax.intuit.com/"
           }
       ],
       "suggested": [
           {
+              "adgroup_name": "open-source browser",
               "bgColor": "#cae1f4",
               "directoryId": 702,
+              "explanation": "Suggested for %1$S enthusiasts who visit sites like %2$S",
               "frecent_sites": [
                   "addons.mozilla.org",
                   "air.mozilla.org",
                   "blog.mozilla.org",
                   "bugzilla.mozilla.org",
                   "developer.mozilla.org",
                   "etherpad.mozilla.org",
                   "hacks.mozilla.org",
@@ -175,16 +177,21 @@ Each link object has various values that
 - ``bgColor`` - string css color for additional fill background color.
 - ``directoryId`` - id of the tile to be used during ping reporting
 
 Suggested Link Object Extras
 ----------------------------
 
 A suggested link has additional values:
 
+- ``adgroup_name`` - string to override the hardcoded display name of the
+  triggering set of sites in Firefox.
+- ``explanation`` - string to override the default explanation that appears
+  below a Suggested Tile. %1$S is replaced by the triggering adgroup name and
+  %2$S is replaced by the triggering site.
 - ``frecent_sites`` - array of strings of the sites that can trigger showing a
   Suggested Tile if the user has the site in one of the top 100 most-frecent
   pages. Only preapproved array of strings that are hardcoded into the
   DirectoryLinksProvider module are allowed.
 - ``frequency_caps`` - an object consisting of daily and total frequency caps
   that limit the number of times a Suggested Tile can be shown in the new tab
   per day and overall.
 - ``time_limits`` - an object consisting of start and end timestamps specifying
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -4,16 +4,17 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["DirectoryLinksProvider"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
+const ParserUtils =  Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
 
 Cu.importGlobalProperties(["XMLHttpRequest"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
@@ -612,17 +613,22 @@ let DirectoryLinksProvider = {
 
       rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
         // Only allow suggested links with approved frecent sites
         let name = this.getFrecentSitesName(link.frecent_sites);
         if (name == undefined) {
           return;
         }
 
-        link.targetedName = name;
+        let sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly |
+          ParserUtils.SanitizerDropForms |
+          ParserUtils.SanitizerDropNonCSSPresentation;
+
+        link.explanation = link.explanation ? ParserUtils.convertToPlainText(link.explanation, sanitizeFlags, 0) : "";
+        link.targetedName = ParserUtils.convertToPlainText(link.adgroup_name, sanitizeFlags, 0) || name;
         link.lastVisitDate = rawLinks.suggested.length - position;
 
         // We cache suggested tiles here but do not push any of them in the links list yet.
         // The decision for which suggested tile to include will be made separately.
         this._cacheSuggestedLinks(link);
         this._updateFrequencyCapSettings(link);
       });
 
--- a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
+++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
@@ -99,16 +99,26 @@ let suggestedTile3 = {
 let suggestedTile4 = {
   url: "http://sponsoredtile.com",
   type: "sponsored",
   lastVisitDate: 1,
   frecent_sites: [
     "sponsoredtarget.com"
   ]
 }
+let suggestedTile5 = {
+  url: "http://eviltile.com",
+  type: "affiliate",
+  lastVisitDate: 5,
+  explanation: "This is an evil tile <form><button formaction='javascript:alert(1)''>X</button></form> muhahaha",
+  adgroup_name: "WE ARE EVIL <link rel='import' href='test.svg'/>",
+  frecent_sites: [
+    "eviltarget.com"
+  ]
+}
 let someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"};
 
 function getHttpHandler(path) {
   let code = 200;
   let body = JSON.stringify(kHttpHandlerData[path]);
   if (path == kFailPath) {
     code = 204;
   }
@@ -437,16 +447,17 @@ add_task(function test_suggestedLinksMap
   do_check_eq(suggestedSites.indexOf("sponsoredtarget.com"), 5);
   do_check_eq(suggestedSites.length, Object.keys(expected_data).length);
 
   DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => {
     let suggestedLinksItr = suggestedLinks.values();
     for (let link of expected_data[site]) {
       let linkCopy = JSON.parse(JSON.stringify(link));
       linkCopy.targetedName = "testing map";
+      linkCopy.explanation = "";
       isIdentical(suggestedLinksItr.next().value, linkCopy);
     }
   })
 
   yield promiseCleanDirectoryLinksProvider();
   DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
 });
 
@@ -1683,8 +1694,30 @@ add_task(function test_DirectoryLinksPro
   do_check_false(data["http://bar.com"].hasOwnProperty("clicked"));
 
   yield promiseCleanDirectoryLinksProvider();
 });
 
 add_task(function test_DirectoryLinksProvider_anonymous() {
   do_check_true(DirectoryLinksProvider._newXHR().mozAnon);
 });
+
+add_task(function test_sanitizeExplanation() {
+  // Note: this is a basic test to ensure we applied sanitization to the link explanation.
+  // Full testing for appropriate sanitization is done in parser/xml/test/unit/test_sanitizer.js.
+
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "testing map";
+
+  let data = {"suggested": [suggestedTile5]};
+  let dataURI = 'data:application/json,' + encodeURIComponent(JSON.stringify(data));
+
+  yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
+  let links = yield fetchData();
+
+  let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()];
+  do_check_eq(suggestedSites.indexOf("eviltarget.com"), 0);
+  do_check_eq(suggestedSites.length, 1);
+
+  let suggestedLink = [...DirectoryLinksProvider._suggestedLinks.get(suggestedSites[0]).values()][0];
+  do_check_eq(suggestedLink.explanation, "This is an evil tile X muhahaha");
+  do_check_eq(suggestedLink.targetedName, "WE ARE EVIL ");
+});