From 7cc3bfc451532bb0fd074a7e95887e9f23e1fcb1 Mon Sep 17 00:00:00 2001
From: diosmosis <benaka@piwik.pro>
Date: Sun, 26 Apr 2015 20:05:43 -0700
Subject: [PATCH] Fixes #3135, fix opt-out form on Safari browsers by opening
 new window that sets the cookie.

This PR allows us to set 3rd party cookies in the opt-out form for Piwik. It works by opening a new window on form submission, reloading the new window, and setting the cookie on this reload. It is necessary to reload the window, because the session cookie isn't set, so the nonce won't be detected & so, the ignore cookie won't be set.

It works whether JavaScript is enabled or not, and other browsers still get the better UX.

The new window is closed immediately after opening, if JS is enabled.

There is also a new UI test for the opt out form, plus a small change to the UI testing framework to allow switching the user agent during tests.
---
 plugins/CoreAdminHome/Controller.php          | 35 ++++++++---
 plugins/CoreAdminHome/lang/en.json            |  3 +-
 plugins/CoreAdminHome/templates/optOut.twig   | 42 ++++++++++++-
 tests/UI/specs/OptOutForm_spec.js             | 59 +++++++++++++++++++
 .../support/page-renderer.js                  |  5 ++
 tests/resources/overlay-test-site/index.html  |  3 +
 6 files changed, 134 insertions(+), 13 deletions(-)
 create mode 100644 tests/UI/specs/OptOutForm_spec.js

diff --git a/plugins/CoreAdminHome/Controller.php b/plugins/CoreAdminHome/Controller.php
index eacbb60406..3102d401d2 100644
--- a/plugins/CoreAdminHome/Controller.php
+++ b/plugins/CoreAdminHome/Controller.php
@@ -321,20 +321,28 @@ class Controller extends ControllerAdmin
     public function optOut()
     {
         $trackVisits = !IgnoreCookie::isIgnoreCookieFound();
-        
+
         $dntChecker = new DoNotTrackHeaderChecker();
         $dntFound = $dntChecker->isDoNotTrackFound();
-        
-        $nonce    = Common::getRequestVar('nonce', false);
-        $language = Common::getRequestVar('language', '');
-        if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) {
-            Nonce::discardNonce('Piwik_OptOut');
-            IgnoreCookie::setIgnoreCookie();
-            $trackVisits = !$trackVisits;
-        }
 
+        $setCookieInNewWindow = Common::getRequestVar('setCookieInNewWindow', false, 'int');
+        if ($setCookieInNewWindow) {
+            $reloadUrl = Url::getCurrentQueryStringWithParametersModified(array(
+                'showConfirmOnly' => 1,
+                'setCookieInNewWindow' => 0,
+            ));
+        } else {
+            $reloadUrl = false;
 
+            $nonce = Common::getRequestVar('nonce', false);
+            if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) {
+                Nonce::discardNonce('Piwik_OptOut');
+                IgnoreCookie::setIgnoreCookie();
+                $trackVisits = !$trackVisits;
+            }
+        }
 
+        $language = Common::getRequestVar('language', '');
         $lang = APILanguagesManager::getInstance()->isLanguageAvailable($language)
             ? $language
             : LanguagesManager::getLanguageCodeForCurrentUser();
@@ -348,9 +356,18 @@ class Controller extends ControllerAdmin
         $view->trackVisits = $trackVisits;
         $view->nonce = Nonce::getNonce('Piwik_OptOut', 3600);
         $view->language = $lang;
+        $view->isSafari = $this->isUserAgentSafari();
+        $view->showConfirmOnly = Common::getRequestVar('showConfirmOnly', false, 'int');
+        $view->reloadUrl = $reloadUrl;
         return $view->render();
     }
 
+    private function isUserAgentSafari()
+    {
+        $userAgent = @$_SERVER['HTTP_USER_AGENT'] ?: '';
+        return strpos($userAgent, 'Safari') !== false && strpos($userAgent, 'Chrome') === false;
+    }
+
     public function uploadCustomLogo()
     {
         Piwik::checkUserHasSuperUserAccess();
diff --git a/plugins/CoreAdminHome/lang/en.json b/plugins/CoreAdminHome/lang/en.json
index c1f4d1db8e..df9df65574 100644
--- a/plugins/CoreAdminHome/lang/en.json
+++ b/plugins/CoreAdminHome/lang/en.json
@@ -86,6 +86,7 @@
         "YouAreOptedIn": "You are currently opted in.",
         "YouAreOptedOut": "You are currently opted out.",
         "YouMayOptOut": "You may choose not to have a unique web analytics cookie identification number assigned to your computer to avoid the aggregation and analysis of data collected on this website.",
-        "YouMayOptOutBis": "To make that choice, please click below to receive an opt-out cookie."
+        "YouMayOptOutBis": "To make that choice, please click below to receive an opt-out cookie.",
+        "OptingYouOut": "Opting you out, please wait..."
     }
 }
diff --git a/plugins/CoreAdminHome/templates/optOut.twig b/plugins/CoreAdminHome/templates/optOut.twig
index 5315747a48..55cd9da01d 100644
--- a/plugins/CoreAdminHome/templates/optOut.twig
+++ b/plugins/CoreAdminHome/templates/optOut.twig
@@ -2,25 +2,60 @@
 <html>
 <head>
     <meta charset="utf-8">
+    {% if reloadUrl %}
+        <meta http-equiv="refresh" content="0; url={{ reloadUrl }}&amp;nonce={{ nonce }}" />
+    {% endif %}
+    <script>
+        function submitForm(event, form, loadInNewWindow) {
+            event.preventDefault();
+
+            if (loadInNewWindow) {
+                var newWindow = window.open(form.action + '&time=' + Date.now());
+
+                // when the new window loads, reload this page
+                newWindow.addEventListener('unload', function () {
+                    window.location.reload();
+                }, false);
+            } else {
+                form.submit();
+            }
+        }
+    </script>
 </head>
 <body>
 {% if dntFound %}
     {{ 'CoreAdminHome_OptOutDntFound'|translate }}
+{% elseif reloadUrl %}
+    {# empty #}
 {% else %}
+    {# if only showing confirmation (because we're in a new window), we only display the success message if JS is disabled.
+     # otherwise we try to close the window immediately.
+     #}
+    {% if showConfirmOnly %}
+    <p>{{ 'CoreAdminHome_OptingYouOut'|translate }}</p><script>window.close();</script>
+    <noscript>
+    {% endif %}
+
     {% if not trackVisits %}
         {{ 'CoreAdminHome_OptOutComplete'|translate }}
-	<br/>
+	    <br/>
         {{ 'CoreAdminHome_OptOutCompleteBis'|translate }}
     {% else %}
         {{ 'CoreAdminHome_YouMayOptOut'|translate }}
         <br/>
         {{ 'CoreAdminHome_YouMayOptOutBis'|translate }}
     {% endif %}
+
+    {% if showConfirmOnly %}</noscript>{% endif %}
+
     <br/><br/>
-    <form method="post" action="?module=CoreAdminHome&amp;action=optOut{% if language %}&amp;language={{ language }}{% endif %}">
+
+    {% if not showConfirmOnly %}
+    {% set loadInNewWindow = isSafari and trackVisits %}
+    <form method="post" action="?module=CoreAdminHome&amp;action=optOut{% if language %}&amp;language={{ language }}{% endif %}{% if loadInNewWindow %}&amp;setCookieInNewWindow=1{% endif %}" {% if loadInNewWindow %}target="_blank"{% endif %}>
         <input type="hidden" name="nonce" value="{{ nonce }}" />
         <input type="hidden" name="fuzz" value="{{ "now"|date }}" />
-        <input onclick="this.form.submit()" type="checkbox" id="trackVisits" name="trackVisits" {% if trackVisits %}checked="checked"{% endif %} />
+        <input onclick="submitForm(event, this.form, {{ loadInNewWindow|default(0) }});" type="checkbox" id="trackVisits" name="trackVisits" {% if trackVisits %}checked="checked"{% endif %} />
         <label for="trackVisits"><strong>
         {% if trackVisits %}
             {{ 'CoreAdminHome_YouAreOptedIn'|translate }} {{ 'CoreAdminHome_ClickHereToOptOut'|translate }}
@@ -32,6 +67,7 @@
             <button type="submit">{{ 'General_Save'|translate }}</button>
         </noscript>
     </form>
+    {% endif %}
 {% endif %}
 </body>
 </html>
diff --git a/tests/UI/specs/OptOutForm_spec.js b/tests/UI/specs/OptOutForm_spec.js
new file mode 100644
index 0000000000..f703eb4139
--- /dev/null
+++ b/tests/UI/specs/OptOutForm_spec.js
@@ -0,0 +1,59 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * Opt-out form tests
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+// NOTE: this test actually tests safari-specific opt out form behavior, since phantomjs' user-agent string
+//       is similar to Safari's
+describe("OptOutForm", function () {
+    this.timeout(0);
+
+    var siteUrl = "/tests/resources/overlay-test-site-real/index.html",
+        safariUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
+        chromeUserAgent = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36";
+
+    it("should display correctly when embedded in another site", function (done) {
+        expect.screenshot('loaded').to.be.captureSelector('iframe#optOutIframe', function (page) {
+            page.userAgent = chromeUserAgent;
+            page.load(siteUrl);
+        }, done);
+    });
+
+    it("should reload the iframe when clicking the opt out checkbox and display an empty checkbox", function (done) {
+        expect.screenshot('opted-out').to.be.captureSelector('iframe#optOutIframe', function (page) {
+            page.evaluate(function () {
+                $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+            });
+            page.wait(1000); // wait for iframe to reload
+        }, done);
+    });
+
+    it("should correctly show the checkbox unchecked after reloading after opting-out", function (done) {
+        expect.screenshot('opted-out').to.be.captureSelector('opted-out-reload', 'iframe#optOutIframe', function (page) {
+            page.userAgent = chromeUserAgent;
+            page.load(siteUrl);
+        }, done);
+    });
+
+    it("should correctly show display opted-in form when cookies are cleared", function (done) {
+        expect.screenshot('loaded').to.be.captureSelector('safari-loaded', 'iframe#optOutIframe', function (page) {
+            page.webpage.clearCookies();
+
+            page.userAgent = safariUserAgent;
+            page.load(siteUrl);
+        }, done);
+    });
+
+    it("should correclty set opt-out cookie on safari", function (done) {
+        expect.screenshot('opted-out').to.be.captureSelector('safari-opted-out', 'iframe#optOutIframe', function (page) {
+            page.evaluate(function () {
+                $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+            });
+            page.load(siteUrl); // reload to check that cookie was set
+        }, done);
+    });
+});
\ No newline at end of file
diff --git a/tests/lib/screenshot-testing/support/page-renderer.js b/tests/lib/screenshot-testing/support/page-renderer.js
index 4f66a728f2..9f9d1a3995 100644
--- a/tests/lib/screenshot-testing/support/page-renderer.js
+++ b/tests/lib/screenshot-testing/support/page-renderer.js
@@ -12,6 +12,7 @@ var VERBOSE = false;
 // TODO: should refactor, move all event queueing logic to PageAutomation class and add .frame method to change context
 var PageRenderer = function (baseUrl) {
     this.webpage = null;
+    this.userAgent = null;
 
     this.queuedEvents = [];
     this.pageLogs = [];
@@ -34,6 +35,10 @@ PageRenderer.prototype._recreateWebPage = function () {
 
     this.webpage = require('webpage').create();
     this.webpage.viewportSize = {width:1350, height:768};
+    if (this.userAgent) {
+        this.webpage.settings.userAgent = this.userAgent;
+    }
+
     this._setupWebpageEvents();
 };
 
diff --git a/tests/resources/overlay-test-site/index.html b/tests/resources/overlay-test-site/index.html
index c6911af4db..27517450b0 100644
--- a/tests/resources/overlay-test-site/index.html
+++ b/tests/resources/overlay-test-site/index.html
@@ -69,6 +69,9 @@
         </p>
       </div>
 
+      <!-- opt out frame -->
+      <iframe id="optOutIframe" src="../../../index.php?module=CoreAdminHome&action=optOut&language=en"></iframe>
+
     </div> <!-- /container -->
 
     <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
-- 
GitLab