Repository: freedomofpress/securedrop

Issue:
Make Tor Browser logout instructions more user friendly
This is a UX suggestion by @ninavizz to improve the notification displayed to sources upon logout (click on image below to see her suggestion for this issue in greater detail (and a nice icon we can use)):

![screen shot 2017-01-17 at 5 08 18 pm](https://cloud.githubusercontent.com/assets/7832803/22050226/d2d2f5f2-dced-11e6-9192-d4689c5fd995.png)



Discussion:
Hey folks! I'm not a developer, otherwise I'd do this myself... but to those who are and would like to code this, I offer the below (with a hearty "thank you!"). Thx, @redshiftzero 

Note: It's important that the hue of the dialog bubble *not* appear red, as that's been established to cause anxiety and punitive feelings, with many users. So—just FYI. :)

`#673466` Title: Important!
`#55555` Text: "Thank you for exiting your session! Please select “New Identity” from the Onion button in the Tor browser, to clear all history of your SecureDrop usage from this device."

Icon: in the code, please include the CC credit:
“Fingerprint” icon is by Travis Avery from the Noun Project.

Icon's SVG code: 
`<svg xmlns="http://www.w3.org/2000/svg" width="135" height="99" viewBox="0 0 135.3 98.8"><style>.a{fill:#673466;}</style><path d="M9.4 57.4c1.2-2.1 3.1-3.2 6.1-3.2 1.8 0 3.6 0.4 5.5 1.2l17.7 7.4c0.4 3.3 3.3 6 6.7 6h23.1c0.7 0 1.2-0.6 1.2-1.2 0-0.7-0.6-1.2-1.2-1.2H45.5c-2.4 0-4.4-1.9-4.4-4.3l0 0c0.1-2.3 2-4.3 4.4-4.3l0 0h11.9c3.5 0 12.3-2.8 17.1-4.4 4.1-1.4 8.1-2 11.9-1.8 2 0.1 3.8 0.4 5.2 0.9 3.1 1 9 4.4 12.7 6.6 1 0.6 1.9 1 2.5 1.4v8.3 17.6l-3.8-2.9c-4.1-3.1-9.2-4.5-14.4-4.2 -5.2 0.3-9 0.8-11.1 1.2l-11.8 2.5c-5.1 1-10.3 1-14.9-0.3 -1.5-0.4-2.9-0.9-3.9-1.4L10.5 60.9c-0.7-0.3-1-0.8-1.2-1.4C9.1 58.9 9.2 58.2 9.4 57.4z" class="a"/><path d="M111.8 56.3h3.4c1.3 0 2.4 1.1 2.4 2.4v30.8c0 1.3-1.1 2.4-2.4 2.4h-3.4c-1.3 0-2.4-1.1-2.4-2.4V58.8C109.4 57.4 110.4 56.4 111.8 56.3z" class="a"/><path d="M32.4 43.5c-0.8-4.5-1.2-9.2-1.2-13.9 0-0.4 0-0.7 0-1.1 0 0 0 0 0 0 0-4.1 1.3-7.8 3.4-10.8 0.4-0.6 1.1-0.9 1.8-0.9 0.5 0 0.9 0.1 1.2 0.4 0.6 0.4 0.9 1 0.9 1.8 0 0.5-0.1 0.9-0.4 1.2 -1.7 2.4-2.7 5.3-2.7 8.5 0 0 0 0 0-0.1 0 0.4 0 0.7 0 1.1 0 4.5 0.4 8.9 1.1 12.7 0 0.1 0 0.3 0 0.4 0 1.1-0.8 1.9-1.8 2.1 -0.1 0-0.3 0-0.4 0 0 0 0 0 0 0 -1.1 0-1.9-0.8-2.1-1.8L32.4 43.5zM41 15.6c0 0 0 0 0 0 -0.7 0-1.4-0.4-1.8-0.9 -0.2-0.3-0.4-0.8-0.4-1.2 0-0.7 0.4-1.4 0.9-1.7 3.2-2.1 7.1-3.3 11.2-3.3 4.1 0 7.8 1.2 11 3.2 0.6 0.4 1 1 1 1.8 0 0.4-0.1 0.8-0.4 1.2 -0.4 0.6-1.1 1-1.8 1 -0.4 0-0.9-0.1-1.1-0.3 -2.5-1.6-5.4-2.5-8.6-2.5 -3.2 0-6.2 1-8.7 2.6C41.9 15.4 41.5 15.6 41 15.6L41 15.6 41 15.6zM60.6 39.4c0.2-0.1 0.5-0.1 0.7-0.1 0.9 0 1.7 0.6 2.1 1.5 0.5 1.4 1.2 2.6 2.1 3.7 0.3 0.4 0.5 0.8 0.5 1.3 0 0.7-0.3 1.3-0.8 1.7 -0.4 0.3-0.8 0.5-1.3 0.5 -0.7 0-1.3-0.3-1.7-0.8 -1.2-1.5-2.1-3.1-2.8-5 -0.1-0.2-0.1-0.5-0.1-0.7 0-0.9 0.6-1.7 1.5-2L60.6 39.4zM66.4 28.6c0-3.2-1-6.2-2.8-8.5 -0.3-0.3-0.4-0.8-0.4-1.2 0-0.7 0.4-1.4 0.9-1.7 0.4-0.2 0.8-0.4 1.3-0.4 0.7 0 1.4 0.4 1.7 0.8 2.3 3.1 3.6 6.9 3.6 11 0 0.7-0.1 1.3-0.1 2 0 2.8 0.4 5.4 1.1 7.8 0.1 0.3 0.2 0.5 0.2 0.8 0 0.9-0.5 1.7-1.3 2 -0.3 0.1-0.5 0.2-0.8 0.2 -0.9 0-1.7-0.5-2-1.1 -1-3-1.5-6.1-1.5-9.4 0-0.7 0-1.5 0.1-2.1L66.4 28.6zM60.2 37.1c-0.1 0-0.1 0-0.2 0 0 0 0 0 0 0 -1.1 0-2-0.8-2.2-1.9 -0.2-1.9-0.3-4.1-0.3-6.6 -0.1-3.6-3.1-6.4-6.6-6.4 -3.6 0-6.5 2.8-6.6 6.4 0 0.7 0 1.3 0 2.1 0 0 0 0 0 0 0 1.2-0.9 2.1-2.1 2.1 0 0 0 0 0 0 -1.2 0-2.2-1-2.2-2.2 0-0.7 0-1.3 0-2.1 0.2-5.9 5-10.7 11-10.7 6 0 10.8 4.7 11 10.7 0 2.4 0.1 4.4 0.2 6.2 0 0.1 0 0.1 0 0.2 0 1.1-0.9 2.1-2 2.1V37.1zM53 28.6c0 9.6 1.2 15.1 4.3 19.7 0.2 0.3 0.4 0.7 0.4 1.2 0 0.8-0.4 1.4-1 1.8 -0.3 0.2-0.8 0.4-1.2 0.4 -0.8 0-1.4-0.4-1.8-1 -3.6-5.4-5-11.5-5-22.1 0-1.2 1-2.2 2.2-2.2s2.2 1 2.2 2.2V28.6zM44.7 37.1c0.3 4.1 1.4 8 2.9 11.4 0.1 0.3 0.2 0.6 0.2 0.9 0 0.9-0.5 1.6-1.3 2 -0.3 0.1-0.6 0.2-0.9 0.2 -0.9 0-1.6-0.5-1.9-1 -1.8-4-2.9-8.4-3.3-13.1 0 0 0-0.1 0-0.1 0-1.2 1-2.2 2.2-2.2 1.1 0 2 0.8 2.2 1.9L44.7 37.1z" class="a"/></svg>`
Hi! I'm interested in helping out with this project and it looks like these UI changes might be a good place to get started.

I've noticed there are a few Issues raised for the flash messages, so I'd be happy to do all these together since they rely on underlying styles for the flash messages being altered. Has anyone started work on these yet, or are you happy for me to have a look at this?


Hi @jcmcreate! Afaik nobody has started on this issue yet. If you would like to, by all means jump in!

There is also a big list of UI changes suggested [here](https://github.com/freedomofpress/securedrop/issues/1536) that I'm thinking I may turn into a checklist w/ associated "sub"-issues next week (a bit disorganized as is, but great stuff nonetheless). If you do start working on something, please do make a comment on the relevant issue, so others don't also start working on it.
@fowlslegs Thanks! I'm more than happy to work on this issue, and I'll comment in any others that I could work alongside on too. 

Are there any branches where the UI changes are being worked on at present?


@jcmcreate We tend to be pretty granular with PRs when possible because it makes review easier, and we end up getting more new code in faster when we do it one little piece at a time. So in order to make your PR granular, you'll want to branch off develop and generally implement just a single feature/fix. Though, if a number of features are related feel free to implement them in the same branch/PR (e.g., you could change all the flashed messages in a single PR, but don't also change some other UI element as well).
@fowlslegs Makes sense :) I'll do as you suggest and add all the flashes in a single PR. Some of the underlying styles for all the flashes have been altered slightly, so it would make sense to do them all in one bulk.

Resolving patch:
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -20,6 +20,7 @@
 import crypto_util
 import store
 import template_filters
+import util
 from db import db_session, Source, Submission, Reply, get_one_or_else
 from request_that_secures_file_uploads import RequestThatSecuresFileUploads
 from jinja2 import evalcontextfilter
@@ -292,18 +293,24 @@ def submit():
                 fh.stream))
 
     if first_submission:
-        flash(
-            "Thanks for submitting something to SecureDrop! Please check back later for replies.",
-            "notification")
+        flash(Markup("""{svg}<div class="message"><strong>Success!</strong>
+                     <p>Thank you for sending this information to us.
+                     Please check back later for replies. <a href="#codename-hint">
+                     Forgot your codename?</a></p></div>
+                     """.format(svg=util.svg('success_checkmark.svg'))),
+              "success")
     else:
-        if msg:
-            flash("Thanks! We received your message.", "notification")
-        if fh:
-            flash(
-                '{} "{}".'.format(
-                    "Thanks! We received your document",
-                    fh.filename or '[unnamed]'),
-                "notification")
+        if msg and not fh:
+            things = 'message'
+        elif not msg and fh:
+            things = 'document'
+        else:
+            things = 'message and document'
+
+        flash(Markup("""{svg}<div class="message"><p>Thanks! We received your
+                     {things}.</p></div>
+                     """.format(svg=util.svg('success_checkmark.svg'), things=things)),
+              "success")
 
     for fname in fnames:
         submission = Submission(g.source, fname)
@@ -392,17 +399,14 @@ def login():
 def logout():
     if logged_in():
         session.clear()
-        tor_msg = Markup("""<strong>Important:</strong><br>
-                         Thank you for logging out!<br>
-                         Please fully end your session by restarting
-                         Tor Browser:<br>
-                         1. Click the
-                           <img src='static/i/toronion.png' alt='Tor icon' />
-                           Tor onion icon in the toolbar above.<br>
-                         2. Click <strong>  New Identity</strong>.<br>
-                         3. Click <strong>Yes</strong> in the dialog box
-                           that appears.""")
-        flash(tor_msg, "error")
+        msg = Markup("""<div class="icon">{svg}</div>
+                     <div class="message"><strong>Important!</strong><br>
+                     <p>Thank you for exiting your session! Please select "New
+                     Identity" from the green Onion button in the Tor browser
+                     to clear all history of your SecureDrop usage from this
+                     device.</p></div>
+                     """.format(svg=util.svg('hand_with_fingerprint.svg')))
+        flash(msg, "important")
 
     return redirect(url_for('index'))
 
diff --git a/securedrop/store.py b/securedrop/store.py
--- a/securedrop/store.py
+++ b/securedrop/store.py
@@ -12,6 +12,7 @@
 from werkzeug import secure_filename
 
 from secure_tempfile import SecureTemporaryFile
+from util import PathException
 
 import logging
 log = logging.getLogger(__name__)
@@ -20,15 +21,6 @@
     "^(?P<index>\d+)\-[a-z0-9-_]*(?P<file_type>msg|doc\.(gz|zip)|reply)\.gpg$").match
 
 
-class PathException(Exception):
-
-    """An exception raised by `store.verify` when it encounters a bad path. A path
-    can be bad when it is not absolute, not normalized, not within
-    `config.STORE_DIR`, or doesn't match the filename format.
-    """
-    pass
-
-
 def verify(p):
     """Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and
     matches the filename format.
diff --git a/securedrop/util.py b/securedrop/util.py
new file mode 100644
--- /dev/null
+++ b/securedrop/util.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+from os import path
+
+from flask import current_app
+
+
+class PathException(Exception):
+
+    """An exception raised by `util.verify` when it encounters a bad path. A path
+    can be bad when it is not absolute or not normalized.
+    """
+    pass
+
+
+def svg(filename):
+    """Safely takes a filename and returns the contents of the file in the
+    static directory as a string.
+    """
+
+    if not filename.endswith('.svg'):
+        raise PathException('File must have .svg extension, but got {}'
+                            .format(filename))
+
+    target_path = path.join(path.abspath(current_app.static_folder), 'i', 'svg',
+                            filename)
+    if not path.isabs(target_path):
+        raise PathException('Expected path to SVG to the absolute and '
+                            'normalized, but found {}'.format(target_path))
+
+    with open(target_path) as f:
+        return f.read()


Test patch:
diff --git a/securedrop/tests/functional/journalist_navigation_steps.py b/securedrop/tests/functional/journalist_navigation_steps.py
--- a/securedrop/tests/functional/journalist_navigation_steps.py
+++ b/securedrop/tests/functional/journalist_navigation_steps.py
@@ -156,7 +156,7 @@ def _admin_adds_a_user(self):
 
         # Successfully verifying the code should redirect to the admin
         # interface, and flash a message indicating success
-        flashed_msgs = self.driver.find_elements_by_css_selector('p.flash')
+        flashed_msgs = self.driver.find_elements_by_css_selector('.flash')
         self.assertIn(("Two factor token successfully verified for user"
                        " {}!").format(self.new_user['username']),
                       [el.text for el in flashed_msgs])
diff --git a/securedrop/tests/functional/source_navigation_steps.py b/securedrop/tests/functional/source_navigation_steps.py
--- a/securedrop/tests/functional/source_navigation_steps.py
+++ b/securedrop/tests/functional/source_navigation_steps.py
@@ -61,10 +61,8 @@ def _source_submits_a_file(self):
             submit_button.click()
 
             notification = self.driver.find_element_by_css_selector(
-                'p.notification')
-            expected_notification = ('Thanks for submitting something '
-                                     'to SecureDrop! Please check back '
-                                     'later for replies.')
+                '.success')
+            expected_notification = 'Thank you for sending this information to us'
             self.assertIn(expected_notification, notification.text)
 
     def _source_submits_a_message(self):
@@ -76,12 +74,11 @@ def _source_submits_a_message(self):
         submit_button.click()
 
         notification = self.driver.find_element_by_css_selector(
-            'p.notification')
-        self.assertIn('Thanks for submitting something to SecureDrop!'
-                      ' Please check back later for replies.',
+            '.success')
+        self.assertIn('Thank you for sending this information to us',
                       notification.text)
 
     def _source_logs_out(self):
         logout_button = self.driver.find_element_by_id('logout').click()
-        notification = self.driver.find_element_by_css_selector('p.error')
-        self.assertIn('Thank you for logging out!', notification.text)
+        notification = self.driver.find_element_by_css_selector('.important')
+        self.assertIn('Thank you for exiting your session!', notification.text)
diff --git a/securedrop/tests/test_unit_source.py b/securedrop/tests/test_unit_source.py
--- a/securedrop/tests/test_unit_source.py
+++ b/securedrop/tests/test_unit_source.py
@@ -129,7 +129,7 @@ def test_login_and_logout(self):
             self.assertTrue(session['logged_in'])
             resp = c.get('/logout', follow_redirects=True)
             self.assertTrue(not session)
-            self.assertIn('Thank you for logging out!', resp.data)
+            self.assertIn('Thank you for exiting your session!', resp.data)
 
     def test_login_with_whitespace(self):
         """Test that codenames with leading or trailing whitespace still work"""
@@ -172,7 +172,7 @@ def test_initial_submission_notification(self):
         resp = self._dummy_submission()
         self.assertEqual(resp.status_code, 200)
         self.assertIn(
-            "Thanks for submitting something to SecureDrop! Please check back later for replies.",
+            "Thank you for sending this information to us",
             resp.data)
 
     def test_submit_message(self):
@@ -183,7 +183,7 @@ def test_submit_message(self):
             fh=(StringIO(''), ''),
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
-        self.assertIn("Thanks! We received your message.", resp.data)
+        self.assertIn("Thanks! We received your", resp.data)
 
     def test_submit_empty_message(self):
         self._new_codename()
@@ -206,7 +206,7 @@ def test_submit_big_message(self):
             fh=(StringIO(''), ''),
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
-        self.assertIn("Thanks! We received your message.", resp.data)
+        self.assertIn("Thanks! We received your", resp.data)
 
     def test_submit_file(self):
         self._new_codename()
@@ -216,12 +216,7 @@ def test_submit_file(self):
             fh=(StringIO('This is a test'), 'test.txt'),
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
-        self.assertIn(
-            escape(
-                '{} "{}"'.format(
-                    "Thanks! We received your document",
-                    "test.txt")),
-            resp.data)
+        self.assertIn('Thanks! We received you', resp.data)
 
     def test_submit_both(self):
         self._new_codename()
@@ -231,13 +226,7 @@ def test_submit_both(self):
             fh=(StringIO('This is a test'), 'test.txt'),
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
-        self.assertIn("Thanks! We received your message.", resp.data)
-        self.assertIn(
-            escape(
-                '{} "{}"'.format(
-                    "Thanks! We received your document",
-                    'test.txt')),
-            resp.data)
+        self.assertIn("Thanks! We received your", resp.data)
 
     @patch('gzip.GzipFile')
     def test_submit_sanitizes_filename(self, gzipfile):
diff --git a/securedrop/tests/test_unit_util.py b/securedrop/tests/test_unit_util.py
new file mode 100644
--- /dev/null
+++ b/securedrop/tests/test_unit_util.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+import source
+import unittest
+import util
+from util import PathException
+
+
+class TestUtil(unittest.TestCase):
+
+    def test_svg_valid(self):
+        with source.app.app_context():
+            res = util.svg('success_checkmark.svg')
+            self.assertIn('<svg', res)
+
+    def test_svg_bad_extension(self):
+        with source.app.app_context():
+            with self.assertRaises(PathException):
+                util.svg('config.py')
+
+    def test_svg_bad_path(self):
+        with source.app.app_context():
+            with self.assertRaises(PathException):
+                util.svg('../../../../etc/hosts')