We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Selected codename length resets to default when a new codename is requested
If a source wants to request a 10 word codename on /generate, she can do so by selecting "10 words" in the dropdown and clicking on the refresh-button on the right. If she is not happy with the new codename, she will have to select "10 words" in the dropdown again before clicking the refresh-button. This happens because the selected codename length resets to default when a new codename is requested. Seeing how we do not store any state, I am not sure if there is a good way to solve this.


--- 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     return render_template('lookup.html', codename=g.codename, replies=replies,
193             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
194 
195 
196 def normalize_timestamps(sid):
197     """
198     Update the timestamps on all of the source's submissions to match that of
199     the latest submission. This minimizes metadata that could be useful to
200     investigators. See #301.
201     """
202     sub_paths = [ store.path(sid, submission.filename)
203                   for submission in g.source.submissions ]
204     if len(sub_paths) > 1:
205         args = ["touch"]
206         args.extend(sub_paths[:-1])
207         rc = subprocess.call(args)
208         if rc != 0:
209             app.logger.warning("Couldn't normalize submission timestamps (touch exited with %d)" % rc)
210 
211 
212 @app.route('/submit', methods=('POST',))
213 @login_required
214 def submit():
215     msg = request.form['msg']
216     fh = request.files['fh']
217 
218     fnames = []
219     journalist_filename = g.source.journalist_filename()
220 
221     if msg:
222         g.source.interaction_count += 1
223         fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,
224             journalist_filename, msg))
225         flash("{}. {}".format(SUBMIT_MSG_NOTIFY_STR,
226                               SUBMIT_CODENAME_NOTIFY_STR), "notification")
227     if fh:
228         g.source.interaction_count += 1
229         fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,
230             journalist_filename, fh.filename, fh.stream))
231         flash("{} '{}'. {}".format(SUBMIT_DOC_NOTIFY_STR,
232                                    fh.filename or '[unnamed]',
233                                    SUBMIT_CODENAME_NOTIFY_STR), "notification")
234     for fname in fnames:
235         submission = Submission(g.source, fname)
236         db_session.add(submission)
237 
238     if g.source.pending:
239         g.source.pending = False
240 
241         # Generate a keypair now, if there's enough entropy (issue #303)
242         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
243         if entropy_avail >= 2400:
244             crypto_util.genkeypair(g.sid, g.codename)
245 
246     g.source.last_updated = datetime.utcnow()
247     db_session.commit()
248     normalize_timestamps(g.sid)
249 
250     return redirect(url_for('lookup'))
251 
252 
253 @app.route('/delete', methods=('POST',))
254 @login_required
255 def delete():
256     msgid = request.form['msgid']
257     assert '/' not in msgid
258     potential_files = os.listdir(g.loc)
259     if msgid not in potential_files:
260         abort(404)  # TODO are the checks necessary?
261     store.secure_unlink(store.path(g.sid, msgid))
262     flash("Reply deleted", "notification")
263 
264     return redirect(url_for('lookup'))
265 
266 
267 def valid_codename(codename):
268     return os.path.exists(store.path(crypto_util.hash_codename(codename)))
269 
270 @app.route('/login', methods=('GET', 'POST'))
271 def login():
272     if request.method == 'POST':
273         codename = request.form['codename']
274         try:
275             valid = valid_codename(codename)
276         except crypto_util.CryptoException:
277             pass
278         else:
279             if valid:
280                 session.update(codename=codename, logged_in=True)
281                 return redirect(url_for('lookup', from_login='1'))
282         flash("Sorry, that is not a recognized codename.", "error")
283     return render_template('login.html')
284 
285 
286 @app.route('/howto-disable-js')
287 def howto_disable_js():
288     return render_template("howto-disable-js.html")
289 
290 
291 @app.route('/tor2web-warning')
292 def tor2web_warning():
293     return render_template("tor2web-warning.html")
294 
295 
296 @app.route('/journalist-key')
297 def download_journalist_pubkey():
298     journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
299     return send_file(StringIO(journalist_pubkey),
300                      mimetype="application/pgp-keys",
301                      attachment_filename=config.JOURNALIST_KEY + ".asc",
302                      as_attachment=True)
303 
304 
305 @app.route('/why-journalist-key')
306 def why_download_journalist_pubkey():
307     return render_template("why-journalist-key.html")
308 
309 
310 @app.errorhandler(404)
311 def page_not_found(error):
312     return render_template('notfound.html'), 404
313 
314 @app.errorhandler(500)
315 def internal_error(error):
316     return render_template('error.html'), 500
317 
318 def write_pidfile():
319     pid = str(os.getpid())
320     with open(config.SOURCE_PIDFILE, 'w') as fp:
321         fp.write(pid)
322 
323 if __name__ == "__main__":
324     write_pidfile()
325     # TODO make sure debug is not on in production
326     app.run(debug=True, host='0.0.0.0', port=8080)
327 
328
```
--- 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
@@ -136,15 +136,15 @@
     # page, or inform them that they're logged in.
     session.pop('logged_in', None)
 
-    number_words = 8
+    num_words = 8
     if request.method == 'POST':
-        number_words = int(request.form['number-words'])
-        if number_words not in range(7, 11):
+        num_words = int(request.form['number-words'])
+        if num_words not in range(7, 11):
             abort(403)
 
-    codename = generate_unique_codename(number_words)
+    codename = generate_unique_codename(num_words)
     session['codename'] = codename
-    return render_template('generate.html', codename=codename)
+    return render_template('generate.html', codename=codename, num_words=num_words)
 
 
 @app.route('/create', methods=['POST'])

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -136,15 +136,15 @@\n     # page, or inform them that they're logged in.\n     session.pop('logged_in', None)\n \n-    number_words = 8\n+    num_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+        num_words = int(request.form['number-words'])\n+        if num_words not in range(7, 11):\n             abort(403)\n \n-    codename = generate_unique_codename(number_words)\n+    codename = generate_unique_codename(num_words)\n     session['codename'] = codename\n-    return render_template('generate.html', codename=codename)\n+    return render_template('generate.html', codename=codename, num_words=num_words)\n \n \n @app.route('/create', methods=['POST'])\n", "issue": "Selected codename length resets to default when a new codename is requested\nIf a source wants to request a 10 word codename on /generate, she can do so by selecting \"10 words\" in the dropdown and clicking on the refresh-button on the right. If she is not happy with the new codename, she will have to select \"10 words\" in the dropdown again before clicking the refresh-button. This happens because the selected codename length resets to default when a new codename is requested. Seeing how we do not store any state, I am not sure if there is a good way to solve this.\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    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    num_words = 8\n    if request.method == 'POST':\n        num_words = int(request.form['number-words'])\n        if num_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(num_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename, num_words=num_words)\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"}]}