Bug 1533074 - Implement Fingerprinting and Cryptomining annotation features - Part 4 - tests, r=dimi
☠☠ backed out by 4d0c32fbf17c ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 14 Mar 2019 06:32:40 +0000
changeset 521848 7e6a8fadff5b
parent 521847 2a0494fed543
child 521849 36c6a7178a5c
push id10870
push usernbeleuzu@mozilla.com
push dateFri, 15 Mar 2019 20:00:07 +0000
treeherdermozilla-beta@c594aee5b7a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdimi
bugs1533074
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1533074 - Implement Fingerprinting and Cryptomining annotation features - Part 4 - tests, r=dimi Differential Revision: https://phabricator.services.mozilla.com/D22471
browser/base/content/test/trackingUI/browser_trackingUI_cryptominers.js
browser/base/content/test/trackingUI/browser_trackingUI_fingerprinters.js
dom/script/ScriptLoader.cpp
toolkit/components/url-classifier/tests/mochitest/features.js
toolkit/components/url-classifier/tests/mochitest/mochitest.ini
toolkit/components/url-classifier/tests/mochitest/test_annotation_vs_TP.html
toolkit/components/url-classifier/tests/mochitest/test_cryptomining_annotate.html
toolkit/components/url-classifier/tests/mochitest/test_fingerprinting_annotate.html
--- a/browser/base/content/test/trackingUI/browser_trackingUI_cryptominers.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_cryptominers.js
@@ -1,24 +1,27 @@
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TRACKING_PAGE = "http://example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
-const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const CM_PROTECTION_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const CM_ANNOTATION_PREF = "privacy.trackingprotection.cryptomining.annotate.enabled";
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({set: [
     [ ContentBlocking.prefIntroCount, ContentBlocking.MAX_INTROS ],
     [ "urlclassifier.features.cryptomining.blacklistHosts", "cryptomining.example.com" ],
+    [ "urlclassifier.features.cryptomining.annotate.blacklistHosts", "cryptomining.example.com" ],
     [ "privacy.trackingprotection.enabled", false ],
     [ "privacy.trackingprotection.annotate_channels", false ],
     [ "privacy.trackingprotection.fingerprinting.enabled", false ],
+    [ "privacy.trackingprotection.fingerprinting.annotate.enabled", false ],
   ]});
 });
 
 async function testIdentityState(hasException) {
   let promise = BrowserTestUtils.openNewForegroundTab({url: TRACKING_PAGE, gBrowser});
   let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
 
   if (hasException) {
@@ -99,19 +102,21 @@ async function testSubview(hasException)
     ContentBlocking.enableForCurrentPage();
     await loaded;
   }
 
   BrowserTestUtils.removeTab(tab);
 }
 
 add_task(async function test() {
-  Services.prefs.setBoolPref(CM_PREF, true);
+  Services.prefs.setBoolPref(CM_PROTECTION_PREF, true);
+  Services.prefs.setBoolPref(CM_ANNOTATION_PREF, true);
 
   await testIdentityState(false);
   await testIdentityState(true);
 
   await testSubview(false);
   await testSubview(true);
 
-  Services.prefs.clearUserPref(CM_PREF);
+  Services.prefs.clearUserPref(CM_PROTECTION_PREF);
+  Services.prefs.clearUserPref(CM_ANNOTATION_PREF);
 });
 
--- a/browser/base/content/test/trackingUI/browser_trackingUI_fingerprinters.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_fingerprinters.js
@@ -1,24 +1,27 @@
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TRACKING_PAGE = "http://example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
-const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const FP_PROTECTION_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const FP_ANNOTATION_PREF = "privacy.trackingprotection.fingerprinting.annotate.enabled";
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({set: [
     [ ContentBlocking.prefIntroCount, ContentBlocking.MAX_INTROS ],
     [ "urlclassifier.features.fingerprinting.blacklistHosts", "fingerprinting.example.com" ],
+    [ "urlclassifier.features.fingerprinting.annotate.blacklistHosts", "fingerprinting.example.com" ],
     [ "privacy.trackingprotection.enabled", false ],
     [ "privacy.trackingprotection.annotate_channels", false ],
     [ "privacy.trackingprotection.cryptomining.enabled", false ],
+    [ "privacy.trackingprotection.cryptomining.annotate.enabled", false ],
   ]});
 });
 
 async function testIdentityState(hasException) {
   let promise = BrowserTestUtils.openNewForegroundTab({url: TRACKING_PAGE, gBrowser});
   let [tab] = await Promise.all([promise, waitForContentBlockingEvent()]);
 
   if (hasException) {
@@ -99,19 +102,20 @@ async function testSubview(hasException)
     ContentBlocking.enableForCurrentPage();
     await loaded;
   }
 
   BrowserTestUtils.removeTab(tab);
 }
 
 add_task(async function test() {
-  Services.prefs.setBoolPref(FP_PREF, true);
+  Services.prefs.setBoolPref(FP_PROTECTION_PREF, true);
+  Services.prefs.setBoolPref(FP_ANNOTATION_PREF, true);
 
   await testIdentityState(false);
   await testIdentityState(true);
 
   await testSubview(false);
   await testSubview(true);
 
-  Services.prefs.clearUserPref(FP_PREF);
+  Services.prefs.clearUserPref(FP_PROTECTION_PREF);
+  Services.prefs.clearUserPref(FP_ANNOTATION_PREF);
 });
-
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -3335,17 +3335,17 @@ void ScriptLoader::ReportPreloadErrorsTo
     }
   }
 }
 
 void ScriptLoader::HandleLoadError(ScriptLoadRequest* aRequest,
                                    nsresult aResult) {
   /*
    * Handle script not loading error because source was an tracking URL (or
-   * fingerprinting, cryptoming, etc).
+   * fingerprinting, cryptomining, etc).
    * We make a note of this script node by including it in a dedicated
    * array of blocked tracking nodes under its parent document.
    */
   if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(
           aResult)) {
     nsCOMPtr<nsIContent> cont = do_QueryInterface(aRequest->Element());
     mDocument->AddBlockedNodeByClassifier(cont);
   }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/features.js
@@ -0,0 +1,156 @@
+var tests = [
+  // Config is an array with 4 elements:
+  // - annotation blacklist
+  // - annotation whitelist
+  // - tracking blacklist
+  // - tracking whitelist
+
+  // All disabled.
+  { config: [ false, false, false, false ], loadExpected: true,  annotationExpected: false },
+
+  // Just whitelisted.
+  { config: [ false, false, false, true  ], loadExpected: true,  annotationExpected: false },
+
+  // Just blacklisted.
+  { config: [ false, false, true,  false ], loadExpected: false, annotationExpected: false },
+
+  // whitelist + blacklist => whitelist wins
+  { config: [ false, false, true,  true  ], loadExpected: true,  annotationExpected: false },
+
+  // just annotated in whitelist.
+  { config: [ false, true,  false, false ], loadExpected: true,  annotationExpected: false },
+
+  // TP and annotation whitelisted.
+  { config: [ false, true,  false, true  ], loadExpected: true,  annotationExpected: false },
+
+  // Annotation whitelisted, but TP blacklisted.
+  { config: [ false, true,  true,  false ], loadExpected: false, annotationExpected: false },
+
+  // Annotation whitelisted. TP blacklisted and whitelisted: whitelist wins.
+  { config: [ false, true,  true,  true  ], loadExpected: true,  annotationExpected: false },
+
+  // Just blacklist annotated.
+  { config: [ true,  false, false, false ], loadExpected: true,  annotationExpected: true },
+
+  // annotated but TP whitelisted.
+  { config: [ true,  false, false, true  ], loadExpected: true,  annotationExpected: true },
+
+  // annotated and blacklisted.
+  { config: [ true,  false, true,  false ], loadExpected: false, annotationExpected: false },
+
+  // annotated, TP blacklisted and whitelisted: whitelist wins.
+  { config: [ true,  false, true,  true  ], loadExpected: true,  annotationExpected: true },
+
+  // annotated in white and blacklist.
+  { config: [ true,  true,  false, false ], loadExpected: true,  annotationExpected: false },
+
+  // annotated in white and blacklist. TP Whiteslited
+  { config: [ true,  true,  false, true  ], loadExpected: true,  annotationExpected: false },
+
+  // everywhere. TP whitelist wins.
+  { config: [ true,  true,  true,  true  ], loadExpected: true,  annotationExpected: false },
+];
+
+function prefBlacklistValue(value) {
+  return value ? "example.com" : "";
+}
+
+function prefWhitelistValue(value) {
+  return value ? "mochi.test" : "";
+}
+
+async function runTest(test, expectedFlag, expectedTrackingResource, prefs) {
+  let config = [
+    [ "urlclassifier.trackingAnnotationTable.testEntries", prefBlacklistValue(test.config[0]) ],
+    [ "urlclassifier.features.fingerprinting.annotate.blacklistHosts", prefBlacklistValue(test.config[0]) ],
+    [ "urlclassifier.features.cryptomining.annotate.blacklistHosts", prefBlacklistValue(test.config[0]) ],
+
+    [ "urlclassifier.trackingAnnotationWhitelistTable.testEntries", prefWhitelistValue(test.config[1]) ],
+    [ "urlclassifier.features.fingerprinting.annotate.whitelistHosts", prefWhitelistValue(test.config[1]) ],
+    [ "urlclassifier.features.cryptomining.annotate.whitelistHosts", prefWhitelistValue(test.config[1]) ],
+
+    [ "urlclassifier.trackingTable.testEntries", prefBlacklistValue(test.config[2]) ],
+    [ "urlclassifier.features.fingerprinting.blacklistHosts", prefBlacklistValue(test.config[2]) ],
+    [ "urlclassifier.features.cryptomining.blacklistHosts", prefBlacklistValue(test.config[2]) ],
+
+    [ "urlclassifier.trackingWhitelistTable.testEntries", prefWhitelistValue(test.config[3]) ],
+    [ "urlclassifier.features.fingerprinting.whitelistHosts", prefWhitelistValue(test.config[3]) ],
+    [ "urlclassifier.features.cryptomining.whitelistHosts", prefWhitelistValue(test.config[3]) ],
+  ];
+
+  info("Testing: " + config.toSource() + "\n");
+
+  await SpecialPowers.pushPrefEnv({set: config.concat(prefs) });
+
+  // This promise will be resolved when the chromeScript knows if the channel
+  // is annotated or not.
+  let annotationPromise;
+  if (test.loadExpected) {
+    info("We want to have annotation information");
+    annotationPromise = new Promise(resolve => {
+      chromeScript.addMessageListener("last-channel-flags",
+                                      data => resolve(data),
+                                      {once: true});
+    });
+  }
+
+  // Let's load an image with a random query string, just to avoid network cache.
+  let result = await new Promise(resolve => {
+    let image = new Image();
+    image.src = "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?" + Math.random();
+    image.onload = _ => resolve(true);
+    image.onerror = _ => resolve(false);
+  });
+
+  is(result, test.loadExpected, "The loading happened correctly");
+
+  if (annotationPromise) {
+    let data = await annotationPromise;
+    is(!!data.classificationFlags, test.annotationExpected, "The annotation happened correctly");
+    if (test.annotationExpected) {
+      is(data.classificationFlags, expectedFlag, "Correct flag");
+      is(data.isTrackingResource, expectedTrackingResource, "Tracking resource flag matches");
+    }
+  }
+}
+
+var chromeScript;
+
+function runTests(flag, prefs, trackingResource) {
+  chromeScript = SpecialPowers.loadChromeScript(_ => {
+    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+    function onExamResp(subject, topic, data) {
+      let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+      if (!channel ||
+          !channel.URI.spec.startsWith("http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg")) {
+        return;
+      }
+
+      // eslint-disable-next-line no-undef
+      sendAsyncMessage("last-channel-flags", {
+        classificationFlags: channel.classificationFlags,
+        isTrackingResource: channel.isTrackingResource,
+      });
+    }
+
+    // eslint-disable-next-line no-undef
+    addMessageListener("done", __ => {
+      Services.obs.removeObserver(onExamResp, "http-on-examine-response");
+    });
+
+    Services.obs.addObserver(onExamResp, "http-on-examine-response");
+
+    // eslint-disable-next-line no-undef
+    sendAsyncMessage("start-test");
+  });
+
+  chromeScript.addMessageListener("start-test", async _ => {
+    for (let test in tests) {
+      await runTest(tests[test], flag, trackingResource, prefs);
+    }
+
+    chromeScript.sendAsyncMessage("done");
+    SimpleTest.finish();
+  }, {once: true});
+}
--- a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -27,23 +27,26 @@ support-files =
   dnt.sjs
   update.sjs
   bad.css
   bad.css^headers^
   gethash.sjs
   gethashFrame.html
   seek.webm
   cache.sjs
+  features.js
 
 [test_classifier.html]
 skip-if = (os == 'linux' && debug) #Bug 1199778
 [test_classifier_match.html]
 [test_classifier_worker.html]
 [test_classify_ping.html]
 skip-if = (verify && debug && (os == 'win' || os == 'mac'))
 [test_classify_track.html]
 [test_gethash.html]
 [test_bug1254766.html]
 [test_cachemiss.html]
 skip-if = verify
 [test_annotation_vs_TP.html]
 [test_fingerprinting.html]
+[test_fingerprinting_annotate.html]
 [test_cryptomining.html]
+[test_cryptomining_annotate.html]
--- a/toolkit/components/url-classifier/tests/mochitest/test_annotation_vs_TP.html
+++ b/toolkit/components/url-classifier/tests/mochitest/test_annotation_vs_TP.html
@@ -1,154 +1,27 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test the relationship between annotation vs TP</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="features.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 
 <body>
 <script class="testbody" type="text/javascript">
 
-var tests = [
-  // Config is an array with 4 elements:
-  // - annotation blacklist
-  // - annotation whitelist
-  // - tracking blacklist
-  // - tracking whitelist
-
-  // All disabled.
-  { config: [ false, false, false, false ], loadExpected: true,  annotationExpected: false },
-
-  // Just whitelisted.
-  { config: [ false, false, false, true  ], loadExpected: true,  annotationExpected: false },
-
-  // Just blacklisted.
-  { config: [ false, false, true,  false ], loadExpected: false, annotationExpected: false },
-
-  // whitelist + blacklist => whitelist wins
-  { config: [ false, false, true,  true  ], loadExpected: true,  annotationExpected: false },
-
-  // just annotated in whitelist.
-  { config: [ false, true,  false, false ], loadExpected: true,  annotationExpected: false },
-
-  // TP and annotation whitelisted.
-  { config: [ false, true,  false, true  ], loadExpected: true,  annotationExpected: false },
-
-  // Annotation whitelisted, but TP blacklisted.
-  { config: [ false, true,  true,  false ], loadExpected: false, annotationExpected: false },
-
-  // Annotation whitelisted. TP blacklisted and whitelisted: whitelist wins.
-  { config: [ false, true,  true,  true  ], loadExpected: true,  annotationExpected: false },
-
-  // Just blacklist annotated.
-  { config: [ true,  false, false, false ], loadExpected: true,  annotationExpected: true },
-
-  // annotated but TP whitelisted.
-  { config: [ true,  false, false, true  ], loadExpected: true,  annotationExpected: true },
-
-  // annotated and blacklisted.
-  { config: [ true,  false, true,  false ], loadExpected: false, annotationExpected: false },
-
-  // annotated, TP blacklisted and whitelisted: whitelist wins.
-  { config: [ true,  false, true,  true  ], loadExpected: true,  annotationExpected: true },
-
-  // annotated in white and blacklist.
-  { config: [ true,  true,  false, false ], loadExpected: true,  annotationExpected: false },
-
-  // annotated in white and blacklist. TP Whiteslited
-  { config: [ true,  true,  false, true  ], loadExpected: true,  annotationExpected: false },
-
-  // everywhere. TP whitelist wins.
-  { config: [ true,  true,  true,  true  ], loadExpected: true,  annotationExpected: false },
-];
-
-function prefBlacklistValue(value) {
-  return value ? "example.com" : "";
-}
-
-function prefWhitelistValue(value) {
-  return value ? "mochi.test" : "";
-}
-
-async function runTest(test) {
-  let config = [
-    [ "urlclassifier.trackingAnnotationTable.testEntries", prefBlacklistValue(test.config[0]) ],
-    [ "urlclassifier.trackingAnnotationWhitelistTable.testEntries", prefWhitelistValue(test.config[1]) ],
-    [ "urlclassifier.trackingTable.testEntries", prefBlacklistValue(test.config[2]) ],
-    [ "urlclassifier.trackingWhitelistTable.testEntries", prefWhitelistValue(test.config[3]) ],
-  ];
-
-  info("Testing: " + config.toSource() + "\n");
-
-  // Let's enable TP and annotation.
-  config.push(["privacy.trackingprotection.enabled", true]);
-  config.push(["privacy.trackingprotection.annotate_channels", true]);
-
-  await SpecialPowers.pushPrefEnv({set: config });
-
-  // This promise will be resolved when the chromeScript knows if the channel
-  // is annotated or not.
-  let annotationPromise;
-  if (test.loadExpected) {
-    info("We want to have annotation information");
-    annotationPromise = new Promise(resolve => {
-      chromeScript.addMessageListener("last-channel-status",
-                                      annotated => resolve(annotated),
-                                      {once: true});
-    });
-  }
-
-  // Let's load an image with a random query string, just to avoid network cache.
-  let result = await new Promise(resolve => {
-    let image = new Image();
-    image.src = "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?" + Math.random();
-    image.onload = _ => resolve(true);
-    image.onerror = _ => resolve(false);
-  });
-
-  is(result, test.loadExpected, "The loading happened correctly");
-
-  if (annotationPromise) {
-    is(await annotationPromise, test.annotationExpected, "The annotation happened correctly");
-  }
-}
-
-var chromeScript = SpecialPowers.loadChromeScript(_ => {
-  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-  function onExamResp(subject, topic, data) {
-    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
-    if (!channel ||
-        !channel.URI.spec.startsWith("http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg")) {
-      return;
-    }
-
-    // eslint-disable-next-line no-undef
-    sendAsyncMessage("last-channel-status", channel.isTrackingResource);
-  }
-
-  // eslint-disable-next-line no-undef
-  addMessageListener("done", __ => {
-    Services.obs.removeObserver(onExamResp, "http-on-examine-response");
-  });
-
-  Services.obs.addObserver(onExamResp, "http-on-examine-response");
-
-  // eslint-disable-next-line no-undef
-  sendAsyncMessage("start-test");
-});
-
-chromeScript.addMessageListener("start-test", async _ => {
-  for (let test in tests) {
-    await runTest(tests[test]);
-  }
-
-  chromeScript.sendAsyncMessage("done");
-  SimpleTest.finish();
-}, {once: true});
-
+runTests(SpecialPowers.Ci.nsIHttpChannel.CLASSIFIED_TRACKING,
+         [
+           ["privacy.trackingprotection.enabled", true],
+           ["privacy.trackingprotection.annotate_channels", true],
+           ["privacy.trackingprotection.fingerprinting.annotate.enabled", false],
+           ["privacy.trackingprotection.fingerprinting.enabled", false],
+           ["privacy.trackingprotection.cryptomining.annotate.enabled", false],
+           ["privacy.trackingprotection.cryptomining.enabled", false],
+         ],
+         true /* a tracking resource */);
 SimpleTest.waitForExplicitFinish();
 
 </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_cryptomining_annotate.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test the relationship between annotation vs blocking - cryptomining</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="features.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+runTests(SpecialPowers.Ci.nsIHttpChannel.CLASSIFIED_CRYPTOMINING,
+         [
+           ["privacy.trackingprotection.enabled", false],
+           ["privacy.trackingprotection.annotate_channels", false],
+           ["privacy.trackingprotection.fingerprinting.annotate.enabled", false],
+           ["privacy.trackingprotection.fingerprinting.enabled", false],
+           ["privacy.trackingprotection.cryptomining.annotate.enabled", true],
+           ["privacy.trackingprotection.cryptomining.enabled", true],
+         ],
+         false /* a tracking resource */);
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_fingerprinting_annotate.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test the relationship between annotation vs blocking - fingerprinting</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="features.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+runTests(SpecialPowers.Ci.nsIHttpChannel.CLASSIFIED_FINGERPRINTING,
+         [
+           ["privacy.trackingprotection.enabled", false],
+           ["privacy.trackingprotection.annotate_channels", false],
+           ["privacy.trackingprotection.fingerprinting.annotate.enabled", true],
+           ["privacy.trackingprotection.fingerprinting.enabled", true],
+           ["privacy.trackingprotection.cryptomining.annotate.enabled", false],
+           ["privacy.trackingprotection.cryptomining.enabled", false],
+         ],
+         true /* a tracking resource */);
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>