We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add flash message when there are no replies on a sumission
The current flow if there are no replies on a submission make it look like I am just back on the submit flow.

It would be great to work this flow out a bit better in the future.

For now, if we could add a flash message to give an indication of the flow that would be great.

Suggested flash message: "There are no replies as yet on this submission. You can submit more documents under this code name below."


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/source.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 from datetime import datetime
4 import uuid
5 from functools import wraps
6 import zipfile
7 from cStringIO import StringIO
8 import subprocess
9 
10 import logging
11 # This module's logger is explicitly labeled so the correct logger is used,
12 # even when this is run from the command line (e.g. during development)
13 log = logging.getLogger('source')
14 
15 from flask import (Flask, request, render_template, session, redirect, url_for,
16                    flash, abort, g, send_file)
17 from flask_wtf.csrf import CsrfProtect
18 
19 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
20 
21 import config
22 import version
23 import crypto_util
24 import store
25 import background
26 import util
27 from db import db_session, Source, Submission
28 
29 app = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)
30 app.config.from_object(config.FlaskConfig)
31 CsrfProtect(app)
32 
33 app.jinja_env.globals['version'] = version.__version__
34 if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
35     app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
36     app.jinja_env.globals['use_custom_header_image'] = True
37 else:
38     app.jinja_env.globals['header_image'] = 'securedrop.png'
39     app.jinja_env.globals['use_custom_header_image'] = False
40 
41 
42 @app.teardown_appcontext
43 def shutdown_session(exception=None):
44     """Automatically remove database sessions at the end of the request, or
45     when the application shuts down"""
46     db_session.remove()
47 
48 
49 def logged_in():
50     if 'logged_in' in session:
51         return True
52 
53 
54 def login_required(f):
55     @wraps(f)
56     def decorated_function(*args, **kwargs):
57         if not logged_in():
58             return redirect(url_for('login'))
59         return f(*args, **kwargs)
60     return decorated_function
61 
62 
63 def ignore_static(f):
64     """Only executes the wrapped function if we're not loading a static resource."""
65     @wraps(f)
66     def decorated_function(*args, **kwargs):
67         if request.path.startswith('/static'):
68             return  # don't execute the decorated function
69         return f(*args, **kwargs)
70     return decorated_function
71 
72 
73 @app.before_request
74 @ignore_static
75 def setup_g():
76     """Store commonly used values in Flask's special g object"""
77     # ignore_static here because `crypto_util.hash_codename` is scrypt (very
78     # time consuming), and we don't need to waste time running if we're just
79     # serving a static resource that won't need to access these common values.
80     if logged_in():
81         g.codename = session['codename']
82         g.sid = crypto_util.hash_codename(g.codename)
83         try:
84             g.source = Source.query.filter(Source.filesystem_id == g.sid).one()
85         except MultipleResultsFound as e:
86             app.logger.error("Found multiple Sources when one was expected: %s" % (e,))
87             abort(500)
88         except NoResultFound as e:
89             app.logger.error("Found no Sources when one was expected: %s" % (e,))
90             del session['logged_in']
91             del session['codename']
92             return redirect(url_for('index'))
93         g.loc = store.path(g.sid)
94 
95 
96 @app.before_request
97 @ignore_static
98 def check_tor2web():
99         # ignore_static here so we only flash a single message warning about Tor2Web,
100         # corresponding to the intial page load.
101     if 'X-tor2web' in request.headers:
102         flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '
103               'This <strong>does not</strong> provide anonymity. '
104               '<a href="/tor2web-warning">Why is this dangerous?</a>',
105               "banner-warning")
106 
107 
108 @app.route('/')
109 def index():
110     return render_template('index.html')
111 
112 
113 @app.route('/generate', methods=('GET', 'POST'))
114 def generate():
115     number_words = 8
116     if request.method == 'POST':
117         number_words = int(request.form['number-words'])
118         if number_words not in range(7, 11):
119             abort(403)
120     session['codename'] = crypto_util.genrandomid(number_words)
121     session.pop('logged_in', None)
122     # TODO: make sure this codename isn't a repeat
123     return render_template('generate.html', codename=session['codename'])
124 
125 
126 @app.route('/create', methods=['POST'])
127 def create():
128     sid = crypto_util.hash_codename(session['codename'])
129 
130     source = Source(sid, crypto_util.display_id())
131     db_session.add(source)
132     db_session.commit()
133 
134     if os.path.exists(store.path(sid)):
135         # if this happens, we're not using very secure crypto
136         log.warning("Got a duplicate ID '%s'" % sid)
137     else:
138         os.mkdir(store.path(sid))
139 
140     session['logged_in'] = True
141     return redirect(url_for('lookup'))
142 
143 
144 @app.route('/lookup', methods=('GET',))
145 @login_required
146 def lookup():
147     replies = []
148     for fn in os.listdir(g.loc):
149         if fn.endswith('-reply.gpg'):
150             try:
151                 msg = crypto_util.decrypt(g.codename,
152                         file(store.path(g.sid, fn)).read()).decode("utf-8")
153             except UnicodeDecodeError:
154                 app.logger.error("Could not decode reply %s" % fn)
155             else:
156                 d = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)
157                 date = util.format_time(d)
158                 replies.append(dict(id=fn, date=date, msg=msg))
159 
160     def async_genkey(sid, codename):
161         with app.app_context():
162             background.execute(lambda: crypto_util.genkeypair(sid, codename))
163 
164     # Generate a keypair to encrypt replies from the journalist
165     # Only do this if the journalist has flagged the source as one
166     # that they would like to reply to. (Issue #140.)
167     if not crypto_util.getkey(g.sid) and g.source.flagged:
168         async_genkey(g.sid, g.codename)
169 
170     return render_template('lookup.html', codename=g.codename, msgs=replies,
171             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
172 
173 
174 def normalize_timestamps(sid):
175     """
176     Update the timestamps on all of the source's submissions to match that of
177     the latest submission. This minimizes metadata that could be useful to
178     investigators. See #301.
179     """
180     sub_paths = [ store.path(sid, submission.filename)
181                   for submission in g.source.submissions ]
182     if len(sub_paths) > 1:
183         args = ["touch"]
184         args.extend(sub_paths[:-1])
185         rc = subprocess.call(args)
186         if rc != 0:
187             app.logger.warning("Couldn't normalize submission timestamps (touch exited with %d)" % rc)
188 
189 
190 @app.route('/submit', methods=('POST',))
191 @login_required
192 def submit():
193     msg = request.form['msg']
194     fh = request.files['fh']
195     strip_metadata = True if 'notclean' in request.form else False
196 
197     fnames = []
198     journalist_filename = g.source.journalist_filename()
199 
200     if msg:
201         g.source.interaction_count += 1
202         fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,
203             journalist_filename, msg))
204         flash("Thanks! We received your message.", "notification")
205     if fh:
206         g.source.interaction_count += 1
207         fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,
208             journalist_filename, fh.filename, fh.stream, fh.content_type, strip_metadata))
209         flash("Thanks! We received your document '%s'."
210               % fh.filename or '[unnamed]', "notification")
211 
212     for fname in fnames:
213         submission = Submission(g.source, fname)
214         db_session.add(submission)
215 
216     if g.source.pending:
217         g.source.pending = False
218 
219         # Generate a keypair now, if there's enough entropy (issue #303)
220         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
221         if entropy_avail >= 2400:
222             crypto_util.genkeypair(g.sid, g.codename)
223 
224     g.source.last_updated = datetime.now()
225     db_session.commit()
226     normalize_timestamps(g.sid)
227 
228     return redirect(url_for('lookup'))
229 
230 
231 @app.route('/delete', methods=('POST',))
232 @login_required
233 def delete():
234     msgid = request.form['msgid']
235     assert '/' not in msgid
236     potential_files = os.listdir(g.loc)
237     if msgid not in potential_files:
238         abort(404)  # TODO are the checks necessary?
239     store.secure_unlink(store.path(g.sid, msgid))
240     flash("Reply deleted.", "notification")
241 
242     return redirect(url_for('lookup'))
243 
244 
245 def valid_codename(codename):
246     return os.path.exists(store.path(crypto_util.hash_codename(codename)))
247 
248 @app.route('/login', methods=('GET', 'POST'))
249 def login():
250     if request.method == 'POST':
251         codename = request.form['codename']
252         if valid_codename(codename):
253             session.update(codename=codename, logged_in=True)
254             return redirect(url_for('lookup'))
255         else:
256             flash("Sorry, that is not a recognized codename.", "error")
257     return render_template('login.html')
258 
259 
260 @app.route('/howto-disable-js')
261 def howto_disable_js():
262     return render_template("howto-disable-js.html")
263 
264 
265 @app.route('/tor2web-warning')
266 def tor2web_warning():
267     return render_template("tor2web-warning.html")
268 
269 
270 @app.route('/journalist-key')
271 def download_journalist_pubkey():
272     journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
273     return send_file(StringIO(journalist_pubkey),
274                      mimetype="application/pgp-keys",
275                      attachment_filename=config.JOURNALIST_KEY + ".asc",
276                      as_attachment=True)
277 
278 
279 @app.route('/why-journalist-key')
280 def why_download_journalist_pubkey():
281     return render_template("why-journalist-key.html")
282 
283 
284 _REDIRECT_URL_WHITELIST = ["http://tor2web.org/",
285         "https://www.torproject.org/download.html.en",
286         "https://tails.boum.org/",
287         "http://www.wired.com/threatlevel/2013/09/freedom-hosting-fbi/",
288         "http://www.theguardian.com/world/interactive/2013/oct/04/egotistical-giraffe-nsa-tor-document",
289         "https://addons.mozilla.org/en-US/firefox/addon/noscript/",
290         "http://noscript.net"]
291 
292 
293 @app.route('/redirect/<path:redirect_url>')
294 def redirect_hack(redirect_url):
295     # A hack to avoid referer leakage when a user clicks on an external link.
296     # TODO: Most likely will want to share this between source.py and
297     # journalist.py in the future.
298     if redirect_url not in _REDIRECT_URL_WHITELIST:
299         return 'Redirect not allowed'
300     else:
301         return render_template("redirect.html", redirect_url=redirect_url)
302 
303 
304 @app.errorhandler(404)
305 def page_not_found(error):
306     return render_template('notfound.html'), 404
307 
308 @app.errorhandler(500)
309 def internal_error(error):
310     return render_template('error.html'), 500
311 
312 if __name__ == "__main__":
313     # TODO make sure debug is not on in production
314     app.run(debug=True, host='0.0.0.0', port=8080)
315
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -167,7 +167,12 @@
     if not crypto_util.getkey(g.sid) and g.source.flagged:
         async_genkey(g.sid, g.codename)
 
-    return render_template('lookup.html', codename=g.codename, msgs=replies,
+    # if this was a redirect from the login page, flash a message if there are
+    # no replies to clarify "check for replies" flow (#393)
+    if request.args.get('from_login') == '1' and len(replies) == 0:
+        flash("There are no replies at this time. You can submit more documents from this code name below.", "notification")
+
+    return render_template('lookup.html', codename=g.codename, replies=replies,
             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
 
 
@@ -251,7 +256,7 @@
         codename = request.form['codename']
         if valid_codename(codename):
             session.update(codename=codename, logged_in=True)
-            return redirect(url_for('lookup'))
+            return redirect(url_for('lookup', from_login='1'))
         else:
             flash("Sorry, that is not a recognized codename.", "error")
     return render_template('login.html')

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -167,7 +167,12 @@\n     if not crypto_util.getkey(g.sid) and g.source.flagged:\n         async_genkey(g.sid, g.codename)\n \n-    return render_template('lookup.html', codename=g.codename, msgs=replies,\n+    # if this was a redirect from the login page, flash a message if there are\n+    # no replies to clarify \"check for replies\" flow (#393)\n+    if request.args.get('from_login') == '1' and len(replies) == 0:\n+        flash(\"There are no replies at this time. You can submit more documents from this code name below.\", \"notification\")\n+\n+    return render_template('lookup.html', codename=g.codename, replies=replies,\n             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n \n \n@@ -251,7 +256,7 @@\n         codename = request.form['codename']\n         if valid_codename(codename):\n             session.update(codename=codename, logged_in=True)\n-            return redirect(url_for('lookup'))\n+            return redirect(url_for('lookup', from_login='1'))\n         else:\n             flash(\"Sorry, that is not a recognized codename.\", \"error\")\n     return render_template('login.html')\n", "issue": "Add flash message when there are no replies on a sumission\nThe current flow if there are no replies on a submission make it look like I am just back on the submit flow.\n\nIt would be great to work this flow out a bit better in the future.\n\nFor now, if we could add a flash message to give an indication of the flow that would be great.\n\nSuggested flash message: \"There are no replies as yet on this submission. You can submit more documents under this code name below.\"\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport util\nfrom db import db_session, Source, Submission\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'securedrop.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    if 'logged_in' in session:\n        return True\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    number_words = 8\n    if request.method == 'POST':\n        number_words = int(request.form['number-words'])\n        if number_words not in range(7, 11):\n            abort(403)\n    session['codename'] = crypto_util.genrandomid(number_words)\n    session.pop('logged_in', None)\n    # TODO: make sure this codename isn't a repeat\n    return render_template('generate.html', codename=session['codename'])\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    db_session.commit()\n\n    if os.path.exists(store.path(sid)):\n        # if this happens, we're not using very secure crypto\n        log.warning(\"Got a duplicate ID '%s'\" % sid)\n    else:\n        os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                d = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                date = util.format_time(d)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    def async_genkey(sid, codename):\n        with app.app_context():\n            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    return render_template('lookup.html', codename=g.codename, msgs=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n    strip_metadata = True if 'notclean' in request.form else False\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"Thanks! We received your message.\", \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream, fh.content_type, strip_metadata))\n        flash(\"Thanks! We received your document '%s'.\"\n              % fh.filename or '[unnamed]', \"notification\")\n\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            crypto_util.genkeypair(g.sid, g.codename)\n\n    g.source.last_updated = datetime.now()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted.\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        if valid_codename(codename):\n            session.update(codename=codename, logged_in=True)\n            return redirect(url_for('lookup'))\n        else:\n            flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n_REDIRECT_URL_WHITELIST = [\"http://tor2web.org/\",\n        \"https://www.torproject.org/download.html.en\",\n        \"https://tails.boum.org/\",\n        \"http://www.wired.com/threatlevel/2013/09/freedom-hosting-fbi/\",\n        \"http://www.theguardian.com/world/interactive/2013/oct/04/egotistical-giraffe-nsa-tor-document\",\n        \"https://addons.mozilla.org/en-US/firefox/addon/noscript/\",\n        \"http://noscript.net\"]\n\n\n@app.route('/redirect/<path:redirect_url>')\ndef redirect_hack(redirect_url):\n    # A hack to avoid referer leakage when a user clicks on an external link.\n    # TODO: Most likely will want to share this between source.py and\n    # journalist.py in the future.\n    if redirect_url not in _REDIRECT_URL_WHITELIST:\n        return 'Redirect not allowed'\n    else:\n        return render_template(\"redirect.html\", redirect_url=redirect_url)\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\nif __name__ == \"__main__\":\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n", "path": "securedrop/source.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport util\nfrom db import db_session, Source, Submission\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'securedrop.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    if 'logged_in' in session:\n        return True\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    number_words = 8\n    if request.method == 'POST':\n        number_words = int(request.form['number-words'])\n        if number_words not in range(7, 11):\n            abort(403)\n    session['codename'] = crypto_util.genrandomid(number_words)\n    session.pop('logged_in', None)\n    # TODO: make sure this codename isn't a repeat\n    return render_template('generate.html', codename=session['codename'])\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    db_session.commit()\n\n    if os.path.exists(store.path(sid)):\n        # if this happens, we're not using very secure crypto\n        log.warning(\"Got a duplicate ID '%s'\" % sid)\n    else:\n        os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                d = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                date = util.format_time(d)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    def async_genkey(sid, codename):\n        with app.app_context():\n            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    # if this was a redirect from the login page, flash a message if there are\n    # no replies to clarify \"check for replies\" flow (#393)\n    if request.args.get('from_login') == '1' and len(replies) == 0:\n        flash(\"There are no replies at this time. You can submit more documents from this code name below.\", \"notification\")\n\n    return render_template('lookup.html', codename=g.codename, replies=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n    strip_metadata = True if 'notclean' in request.form else False\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"Thanks! We received your message.\", \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream, fh.content_type, strip_metadata))\n        flash(\"Thanks! We received your document '%s'.\"\n              % fh.filename or '[unnamed]', \"notification\")\n\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            crypto_util.genkeypair(g.sid, g.codename)\n\n    g.source.last_updated = datetime.now()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted.\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        if valid_codename(codename):\n            session.update(codename=codename, logged_in=True)\n            return redirect(url_for('lookup', from_login='1'))\n        else:\n            flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n_REDIRECT_URL_WHITELIST = [\"http://tor2web.org/\",\n        \"https://www.torproject.org/download.html.en\",\n        \"https://tails.boum.org/\",\n        \"http://www.wired.com/threatlevel/2013/09/freedom-hosting-fbi/\",\n        \"http://www.theguardian.com/world/interactive/2013/oct/04/egotistical-giraffe-nsa-tor-document\",\n        \"https://addons.mozilla.org/en-US/firefox/addon/noscript/\",\n        \"http://noscript.net\"]\n\n\n@app.route('/redirect/<path:redirect_url>')\ndef redirect_hack(redirect_url):\n    # A hack to avoid referer leakage when a user clicks on an external link.\n    # TODO: Most likely will want to share this between source.py and\n    # journalist.py in the future.\n    if redirect_url not in _REDIRECT_URL_WHITELIST:\n        return 'Redirect not allowed'\n    else:\n        return render_template(\"redirect.html\", redirect_url=redirect_url)\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\nif __name__ == \"__main__\":\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n", "path": "securedrop/source.py"}]}