We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Location of replies unclear for sources
As a source, it it unclear where I should go to see messages from the journalist.

When I choose the “Check for response” and choose “continue” it should clearly show me a “Messages” area of the screen, and say “No replies from the journalist”, or list the messages (if there are any).

We currently flash a message that says "you received a new reply", but this is confusing. It only appears once, so if the user refreshes the page or navigates away and back again, they will not see that message (they will still see the reply, until it is deleted).

Another option would be to make “submit new documents/messages” a separate page from “check for replies from the journalist” page.


--- 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 from sqlalchemy.exc import IntegrityError
21 
22 import config
23 import version
24 import crypto_util
25 import store
26 import background
27 import template_filters
28 from db import db_session, Source, Submission
29 from jinja2 import evalcontextfilter
30 
31 app = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)
32 app.config.from_object(config.SourceInterfaceFlaskConfig)
33 CsrfProtect(app)
34 
35 SUBMIT_DOC_NOTIFY_STR = "Thanks! We received your document"
36 SUBMIT_MSG_NOTIFY_STR = "Thanks! We received your message"
37 SUBMIT_CODENAME_NOTIFY_STR = "Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages."
38 
39 app.jinja_env.globals['version'] = version.__version__
40 if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
41     app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
42     app.jinja_env.globals['use_custom_header_image'] = True
43 else:
44     app.jinja_env.globals['header_image'] = 'logo.png'
45     app.jinja_env.globals['use_custom_header_image'] = False
46 
47 app.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat
48 app.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)
49 
50 @app.teardown_appcontext
51 def shutdown_session(exception=None):
52     """Automatically remove database sessions at the end of the request, or
53     when the application shuts down"""
54     db_session.remove()
55 
56 
57 def logged_in():
58     return 'logged_in' in session
59 
60 
61 def login_required(f):
62     @wraps(f)
63     def decorated_function(*args, **kwargs):
64         if not logged_in():
65             return redirect(url_for('login'))
66         return f(*args, **kwargs)
67     return decorated_function
68 
69 
70 def ignore_static(f):
71     """Only executes the wrapped function if we're not loading a static resource."""
72     @wraps(f)
73     def decorated_function(*args, **kwargs):
74         if request.path.startswith('/static'):
75             return  # don't execute the decorated function
76         return f(*args, **kwargs)
77     return decorated_function
78 
79 
80 @app.before_request
81 @ignore_static
82 def setup_g():
83     """Store commonly used values in Flask's special g object"""
84     # ignore_static here because `crypto_util.hash_codename` is scrypt (very
85     # time consuming), and we don't need to waste time running if we're just
86     # serving a static resource that won't need to access these common values.
87     if logged_in():
88         g.codename = session['codename']
89         g.sid = crypto_util.hash_codename(g.codename)
90         try:
91             g.source = Source.query.filter(Source.filesystem_id == g.sid).one()
92         except MultipleResultsFound as e:
93             app.logger.error("Found multiple Sources when one was expected: %s" % (e,))
94             abort(500)
95         except NoResultFound as e:
96             app.logger.error("Found no Sources when one was expected: %s" % (e,))
97             del session['logged_in']
98             del session['codename']
99             return redirect(url_for('index'))
100         g.loc = store.path(g.sid)
101 
102 
103 @app.before_request
104 @ignore_static
105 def check_tor2web():
106         # ignore_static here so we only flash a single message warning about Tor2Web,
107         # corresponding to the intial page load.
108     if 'X-tor2web' in request.headers:
109         flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '
110               'This <strong>does not</strong> provide anonymity. '
111               '<a href="/tor2web-warning">Why is this dangerous?</a>',
112               "banner-warning")
113 
114 
115 @app.route('/')
116 def index():
117     return render_template('index.html')
118 
119 
120 def generate_unique_codename(num_words):
121     """Generate random codenames until we get an unused one"""
122     while True:
123         codename = crypto_util.genrandomid(num_words)
124         sid = crypto_util.hash_codename(codename) # scrypt (slow)
125         matching_sources = Source.query.filter(Source.filesystem_id == sid).all()
126         if len(matching_sources) == 0:
127             return codename
128 
129 
130 @app.route('/generate', methods=('GET', 'POST'))
131 def generate():
132     # Popping this key prevents errors when a logged in user returns to /generate.
133     # TODO: is this the best experience? A logged in user will be automatically
134     # logged out if they navigate to /generate by accident, which could be
135     # confusing. It might be better to instead redirect them to the lookup
136     # page, or inform them that they're logged in.
137     session.pop('logged_in', None)
138 
139     number_words = 8
140     if request.method == 'POST':
141         number_words = int(request.form['number-words'])
142         if number_words not in range(7, 11):
143             abort(403)
144 
145     codename = generate_unique_codename(number_words)
146     session['codename'] = codename
147     return render_template('generate.html', codename=codename)
148 
149 
150 @app.route('/create', methods=['POST'])
151 def create():
152     sid = crypto_util.hash_codename(session['codename'])
153 
154     source = Source(sid, crypto_util.display_id())
155     db_session.add(source)
156     try:
157         db_session.commit()
158     except IntegrityError as e: 
159         app.logger.error("Attempt to create a source with duplicate codename: %s" % (e,))
160     else:
161         os.mkdir(store.path(sid))
162 
163     session['logged_in'] = True
164     return redirect(url_for('lookup'))
165 
166 
167 @app.route('/lookup', methods=('GET',))
168 @login_required
169 def lookup():
170     replies = []
171     for fn in os.listdir(g.loc):
172         if fn.endswith('-reply.gpg'):
173             try:
174                 msg = crypto_util.decrypt(g.codename,
175                         file(store.path(g.sid, fn)).read()).decode("utf-8")
176             except UnicodeDecodeError:
177                 app.logger.error("Could not decode reply %s" % fn)
178             else:
179                 date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)
180                 replies.append(dict(id=fn, date=date, msg=msg))
181 
182     def async_genkey(sid, codename):
183         with app.app_context():
184             background.execute(lambda: crypto_util.genkeypair(sid, codename))
185 
186     # Generate a keypair to encrypt replies from the journalist
187     # Only do this if the journalist has flagged the source as one
188     # that they would like to reply to. (Issue #140.)
189     if not crypto_util.getkey(g.sid) and g.source.flagged:
190         async_genkey(g.sid, g.codename)
191 
192     # if this was a redirect from the login page, flash a message if there are
193     # no replies to clarify "check for replies" flow (#393)
194     if request.args.get('from_login') == '1' and len(replies) == 0:
195         flash("There are no replies at this time. You can submit more documents from this codename below.", "notification")
196 
197     return render_template('lookup.html', codename=g.codename, replies=replies,
198             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
199 
200 
201 def normalize_timestamps(sid):
202     """
203     Update the timestamps on all of the source's submissions to match that of
204     the latest submission. This minimizes metadata that could be useful to
205     investigators. See #301.
206     """
207     sub_paths = [ store.path(sid, submission.filename)
208                   for submission in g.source.submissions ]
209     if len(sub_paths) > 1:
210         args = ["touch"]
211         args.extend(sub_paths[:-1])
212         rc = subprocess.call(args)
213         if rc != 0:
214             app.logger.warning("Couldn't normalize submission timestamps (touch exited with %d)" % rc)
215 
216 
217 @app.route('/submit', methods=('POST',))
218 @login_required
219 def submit():
220     msg = request.form['msg']
221     fh = request.files['fh']
222 
223     fnames = []
224     journalist_filename = g.source.journalist_filename()
225 
226     if msg:
227         g.source.interaction_count += 1
228         fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,
229             journalist_filename, msg))
230         flash("{}. {}".format(SUBMIT_MSG_NOTIFY_STR,
231                               SUBMIT_CODENAME_NOTIFY_STR), "notification")
232     if fh:
233         g.source.interaction_count += 1
234         fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,
235             journalist_filename, fh.filename, fh.stream))
236         flash("{} '{}'. {}".format(SUBMIT_DOC_NOTIFY_STR,
237                                    fh.filename or '[unnamed]',
238                                    SUBMIT_CODENAME_NOTIFY_STR), "notification")
239     for fname in fnames:
240         submission = Submission(g.source, fname)
241         db_session.add(submission)
242 
243     if g.source.pending:
244         g.source.pending = False
245 
246         # Generate a keypair now, if there's enough entropy (issue #303)
247         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
248         if entropy_avail >= 2400:
249             crypto_util.genkeypair(g.sid, g.codename)
250 
251     g.source.last_updated = datetime.utcnow()
252     db_session.commit()
253     normalize_timestamps(g.sid)
254 
255     return redirect(url_for('lookup'))
256 
257 
258 @app.route('/delete', methods=('POST',))
259 @login_required
260 def delete():
261     msgid = request.form['msgid']
262     assert '/' not in msgid
263     potential_files = os.listdir(g.loc)
264     if msgid not in potential_files:
265         abort(404)  # TODO are the checks necessary?
266     store.secure_unlink(store.path(g.sid, msgid))
267     flash("Reply deleted.", "notification")
268 
269     return redirect(url_for('lookup'))
270 
271 
272 def valid_codename(codename):
273     return os.path.exists(store.path(crypto_util.hash_codename(codename)))
274 
275 @app.route('/login', methods=('GET', 'POST'))
276 def login():
277     if request.method == 'POST':
278         codename = request.form['codename']
279         try:
280             valid = valid_codename(codename)
281         except crypto_util.CryptoException:
282             pass
283         else:
284             if valid:
285                 session.update(codename=codename, logged_in=True)
286                 return redirect(url_for('lookup', from_login='1'))
287         flash("Sorry, that is not a recognized codename.", "error")
288     return render_template('login.html')
289 
290 
291 @app.route('/howto-disable-js')
292 def howto_disable_js():
293     return render_template("howto-disable-js.html")
294 
295 
296 @app.route('/tor2web-warning')
297 def tor2web_warning():
298     return render_template("tor2web-warning.html")
299 
300 
301 @app.route('/journalist-key')
302 def download_journalist_pubkey():
303     journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
304     return send_file(StringIO(journalist_pubkey),
305                      mimetype="application/pgp-keys",
306                      attachment_filename=config.JOURNALIST_KEY + ".asc",
307                      as_attachment=True)
308 
309 
310 @app.route('/why-journalist-key')
311 def why_download_journalist_pubkey():
312     return render_template("why-journalist-key.html")
313 
314 
315 @app.errorhandler(404)
316 def page_not_found(error):
317     return render_template('notfound.html'), 404
318 
319 @app.errorhandler(500)
320 def internal_error(error):
321     return render_template('error.html'), 500
322 
323 def write_pidfile():
324     pid = str(os.getpid())
325     with open(config.SOURCE_PIDFILE, 'w') as fp:
326         fp.write(pid)
327 
328 if __name__ == "__main__":
329     write_pidfile()
330     # TODO make sure debug is not on in production
331     app.run(debug=True, host='0.0.0.0', port=8080)
332 
333
```
--- 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
@@ -189,11 +189,6 @@
     if not crypto_util.getkey(g.sid) and g.source.flagged:
         async_genkey(g.sid, g.codename)
 
-    # 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 codename below.", "notification")
-
     return render_template('lookup.html', codename=g.codename, replies=replies,
             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
 
@@ -264,7 +259,7 @@
     if msgid not in potential_files:
         abort(404)  # TODO are the checks necessary?
     store.secure_unlink(store.path(g.sid, msgid))
-    flash("Reply deleted.", "notification")
+    flash("Reply deleted", "notification")
 
     return redirect(url_for('lookup'))

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -189,11 +189,6 @@\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 codename 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@@ -264,7 +259,7 @@\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+    flash(\"Reply deleted\", \"notification\")\n \n     return redirect(url_for('lookup'))\n", "issue": "Location of replies unclear for sources\nAs a source, it it unclear where I should go to see messages from the journalist.\n\nWhen I choose the \u201cCheck for response\u201d and choose \u201ccontinue\u201d it should clearly show me a \u201cMessages\u201d area of the screen, and say \u201cNo replies from the journalist\u201d, or list the messages (if there are any).\n\nWe currently flash a message that says \"you received a new reply\", but this is confusing. It only appears once, so if the user refreshes the page or navigates away and back again, they will not see that message (they will still see the reply, until it is deleted).\n\nAnother option would be to make \u201csubmit new documents/messages\u201d a separate page from \u201ccheck for replies from the journalist\u201d page.\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\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\nCsrfProtect(app)\n\nSUBMIT_DOC_NOTIFY_STR = \"Thanks! We received your document\"\nSUBMIT_MSG_NOTIFY_STR = \"Thanks! We received your message\"\nSUBMIT_CODENAME_NOTIFY_STR = \"Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages.\"\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'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\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    return 'logged_in' in session\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\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\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\n    codename = generate_unique_codename(number_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=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    try:\n        db_session.commit()\n    except IntegrityError as e: \n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\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                date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\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 codename 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\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(\"{}. {}\".format(SUBMIT_MSG_NOTIFY_STR,\n                              SUBMIT_CODENAME_NOTIFY_STR), \"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))\n        flash(\"{} '{}'. {}\".format(SUBMIT_DOC_NOTIFY_STR,\n                                   fh.filename or '[unnamed]',\n                                   SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\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.utcnow()\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        try:\n            valid = valid_codename(codename)\n        except crypto_util.CryptoException:\n            pass\n        else:\n            if valid:\n                session.update(codename=codename, logged_in=True)\n                return redirect(url_for('lookup', from_login='1'))\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@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\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\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\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\nCsrfProtect(app)\n\nSUBMIT_DOC_NOTIFY_STR = \"Thanks! We received your document\"\nSUBMIT_MSG_NOTIFY_STR = \"Thanks! We received your message\"\nSUBMIT_CODENAME_NOTIFY_STR = \"Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages.\"\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'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\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    return 'logged_in' in session\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\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\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\n    codename = generate_unique_codename(number_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=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    try:\n        db_session.commit()\n    except IntegrityError as e: \n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\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                date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\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, 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\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(\"{}. {}\".format(SUBMIT_MSG_NOTIFY_STR,\n                              SUBMIT_CODENAME_NOTIFY_STR), \"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))\n        flash(\"{} '{}'. {}\".format(SUBMIT_DOC_NOTIFY_STR,\n                                   fh.filename or '[unnamed]',\n                                   SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\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.utcnow()\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        try:\n            valid = valid_codename(codename)\n        except crypto_util.CryptoException:\n            pass\n        else:\n            if valid:\n                session.update(codename=codename, logged_in=True)\n                return redirect(url_for('lookup', from_login='1'))\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@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\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\n", "path": "securedrop/source.py"}]}