We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
0.9.0-rc2 does not allow text only submissions
## Description

I believe I found a bug in upgrading from `0.8` to `0.9rc2` in that sources can only submit documents or documents and messages. If I try to send _only_ text or a blank form field I get a "Bad Request, the browser or proxy sent a request that this server could not understand" error.


## Steps to Reproduce

First I installed 0.8.0 on hardware and create a journalist. I then ran ./qa-loader.py -m 25. I logged in and and see submissions. I then added the apt-test key and updated sources.list to `apt.freedom.press` to `apt-test.freedom.press`. Finally I ran `sudo cron-apt -i -s` to update to `0.9rc2`

## Expected Behavior

A source can send text to journalists.

## Actual Behavior

Error in the webapp.

## Comments

I also enabled apache debug logging and attempted to patch the source_app/ code to log anything related to CSRF violations and I was was not able to trigger a debug log. 


--- 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_app/main.py`
Content:
```
1 import operator
2 import os
3 import io
4 
5 from datetime import datetime
6 from flask import (Blueprint, render_template, flash, redirect, url_for, g,
7                    session, current_app, request, Markup, abort)
8 from flask_babel import gettext
9 from sqlalchemy.exc import IntegrityError
10 
11 from db import db
12 from models import Source, Submission, Reply, get_one_or_else
13 from source_app.decorators import login_required
14 from source_app.utils import (logged_in, generate_unique_codename,
15                               async_genkey, normalize_timestamps,
16                               valid_codename, get_entropy_estimate)
17 from source_app.forms import LoginForm
18 
19 
20 def make_blueprint(config):
21     view = Blueprint('main', __name__)
22 
23     @view.route('/')
24     def index():
25         return render_template('index.html')
26 
27     @view.route('/generate', methods=('GET', 'POST'))
28     def generate():
29         if logged_in():
30             flash(gettext(
31                 "You were redirected because you are already logged in. "
32                 "If you want to create a new account, you should log out "
33                 "first."),
34                   "notification")
35             return redirect(url_for('.lookup'))
36 
37         codename = generate_unique_codename(config)
38         session['codename'] = codename
39         session['new_user'] = True
40         return render_template('generate.html', codename=codename)
41 
42     @view.route('/org-logo')
43     def select_logo():
44         if os.path.exists(os.path.join(current_app.static_folder, 'i',
45                           'custom_logo.png')):
46             return redirect(url_for('static', filename='i/custom_logo.png'))
47         else:
48             return redirect(url_for('static', filename='i/logo.png'))
49 
50     @view.route('/create', methods=['POST'])
51     def create():
52         filesystem_id = current_app.crypto_util.hash_codename(
53             session['codename'])
54 
55         source = Source(filesystem_id, current_app.crypto_util.display_id())
56         db.session.add(source)
57         try:
58             db.session.commit()
59         except IntegrityError as e:
60             db.session.rollback()
61             current_app.logger.error(
62                 "Attempt to create a source with duplicate codename: %s" %
63                 (e,))
64 
65             # Issue 2386: don't log in on duplicates
66             del session['codename']
67             abort(500)
68         else:
69             os.mkdir(current_app.storage.path(filesystem_id))
70 
71         session['logged_in'] = True
72         return redirect(url_for('.lookup'))
73 
74     @view.route('/lookup', methods=('GET',))
75     @login_required
76     def lookup():
77         replies = []
78         source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \
79                                   .filter(Reply.deleted_by_source == False).all()  # noqa
80 
81         for reply in source_inbox:
82             reply_path = current_app.storage.path(
83                 g.filesystem_id,
84                 reply.filename,
85             )
86             try:
87                 with io.open(reply_path, "rb") as f:
88                     contents = f.read()
89                 reply.decrypted = current_app.crypto_util.decrypt(
90                     g.codename,
91                     contents).decode('utf-8')
92             except UnicodeDecodeError:
93                 current_app.logger.error("Could not decode reply %s" %
94                                          reply.filename)
95             else:
96                 reply.date = datetime.utcfromtimestamp(
97                     os.stat(reply_path).st_mtime)
98                 replies.append(reply)
99 
100         # Sort the replies by date
101         replies.sort(key=operator.attrgetter('date'), reverse=True)
102 
103         # Generate a keypair to encrypt replies from the journalist
104         # Only do this if the journalist has flagged the source as one
105         # that they would like to reply to. (Issue #140.)
106         if not current_app.crypto_util.getkey(g.filesystem_id) and \
107                 g.source.flagged:
108             db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']
109             async_genkey(current_app.crypto_util,
110                          db_uri,
111                          g.filesystem_id,
112                          g.codename)
113 
114         return render_template(
115             'lookup.html',
116             codename=g.codename,
117             replies=replies,
118             flagged=g.source.flagged,
119             new_user=session.get('new_user', None),
120             haskey=current_app.crypto_util.getkey(
121                 g.filesystem_id))
122 
123     @view.route('/submit', methods=('POST',))
124     @login_required
125     def submit():
126         msg = request.form['msg']
127         fh = request.files['fh']
128 
129         # Don't submit anything if it was an "empty" submission. #878
130         if not (msg or fh):
131             flash(gettext(
132                 "You must enter a message or choose a file to submit."),
133                   "error")
134             return redirect(url_for('main.lookup'))
135 
136         fnames = []
137         journalist_filename = g.source.journalist_filename
138         first_submission = g.source.interaction_count == 0
139 
140         if msg:
141             g.source.interaction_count += 1
142             fnames.append(
143                 current_app.storage.save_message_submission(
144                     g.filesystem_id,
145                     g.source.interaction_count,
146                     journalist_filename,
147                     msg))
148         if fh:
149             g.source.interaction_count += 1
150             fnames.append(
151                 current_app.storage.save_file_submission(
152                     g.filesystem_id,
153                     g.source.interaction_count,
154                     journalist_filename,
155                     fh.filename,
156                     fh.stream))
157 
158         if first_submission:
159             msg = render_template('first_submission_flashed_message.html')
160             flash(Markup(msg), "success")
161 
162         else:
163             if msg and not fh:
164                 html_contents = gettext('Thanks! We received your message.')
165             elif not msg and fh:
166                 html_contents = gettext('Thanks! We received your document.')
167             else:
168                 html_contents = gettext('Thanks! We received your message and '
169                                         'document.')
170 
171             msg = render_template('next_submission_flashed_message.html',
172                                   html_contents=html_contents)
173             flash(Markup(msg), "success")
174 
175         for fname in fnames:
176             submission = Submission(g.source, fname)
177             db.session.add(submission)
178 
179         if g.source.pending:
180             g.source.pending = False
181 
182             # Generate a keypair now, if there's enough entropy (issue #303)
183             # (gpg reads 300 bytes from /dev/random)
184             entropy_avail = get_entropy_estimate()
185             if entropy_avail >= 2400:
186                 db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']
187 
188                 async_genkey(current_app.crypto_util,
189                              db_uri,
190                              g.filesystem_id,
191                              g.codename)
192                 current_app.logger.info("generating key, entropy: {}".format(
193                     entropy_avail))
194             else:
195                 current_app.logger.warn(
196                         "skipping key generation. entropy: {}".format(
197                                 entropy_avail))
198 
199         g.source.last_updated = datetime.utcnow()
200         db.session.commit()
201         normalize_timestamps(g.filesystem_id)
202 
203         return redirect(url_for('main.lookup'))
204 
205     @view.route('/delete', methods=('POST',))
206     @login_required
207     def delete():
208         """This deletes the reply from the source's inbox, but preserves
209         the history for journalists such that they can view conversation
210         history.
211         """
212 
213         query = Reply.query.filter(
214             Reply.filename == request.form['reply_filename'])
215         reply = get_one_or_else(query, current_app.logger, abort)
216         reply.deleted_by_source = True
217         db.session.add(reply)
218         db.session.commit()
219 
220         flash(gettext("Reply deleted"), "notification")
221         return redirect(url_for('.lookup'))
222 
223     @view.route('/delete-all', methods=('POST',))
224     @login_required
225     def batch_delete():
226         replies = Reply.query.filter(Reply.source_id == g.source.id) \
227                              .filter(Reply.deleted_by_source == False).all()  # noqa
228         if len(replies) == 0:
229             current_app.logger.error("Found no replies when at least one was "
230                                      "expected")
231             return redirect(url_for('.lookup'))
232 
233         for reply in replies:
234             reply.deleted_by_source = True
235             db.session.add(reply)
236         db.session.commit()
237 
238         flash(gettext("All replies have been deleted"), "notification")
239         return redirect(url_for('.lookup'))
240 
241     @view.route('/login', methods=('GET', 'POST'))
242     def login():
243         form = LoginForm()
244         if form.validate_on_submit():
245             codename = request.form['codename'].strip()
246             if valid_codename(codename):
247                 session.update(codename=codename, logged_in=True)
248                 return redirect(url_for('.lookup', from_login='1'))
249             else:
250                 current_app.logger.info(
251                         "Login failed for invalid codename")
252                 flash(gettext("Sorry, that is not a recognized codename."),
253                       "error")
254         return render_template('login.html', form=form)
255 
256     @view.route('/logout')
257     def logout():
258         if logged_in():
259             msg = render_template('logout_flashed_message.html')
260 
261             # Clear the session after we render the message so it's localized
262             # If a user specified a locale, save it and restore it
263             user_locale = g.locale
264             session.clear()
265             session['locale'] = user_locale
266 
267             flash(Markup(msg), "important hide-if-not-tor-browser")
268         return redirect(url_for('.index'))
269 
270     return view
271
```
--- 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_app/main.py b/securedrop/source_app/main.py
--- a/securedrop/source_app/main.py
+++ b/securedrop/source_app/main.py
@@ -124,7 +124,9 @@
     @login_required
     def submit():
         msg = request.form['msg']
-        fh = request.files['fh']
+        fh = None
+        if 'fh' in request.files:
+            fh = request.files['fh']
 
         # Don't submit anything if it was an "empty" submission. #878
         if not (msg or fh):

{"golden_diff": "diff --git a/securedrop/source_app/main.py b/securedrop/source_app/main.py\n--- a/securedrop/source_app/main.py\n+++ b/securedrop/source_app/main.py\n@@ -124,7 +124,9 @@\n     @login_required\n     def submit():\n         msg = request.form['msg']\n-        fh = request.files['fh']\n+        fh = None\n+        if 'fh' in request.files:\n+            fh = request.files['fh']\n \n         # Don't submit anything if it was an \"empty\" submission. #878\n         if not (msg or fh):\n", "issue": "0.9.0-rc2 does not allow text only submissions\n## Description\r\n\r\nI believe I found a bug in upgrading from `0.8` to `0.9rc2` in that sources can only submit documents or documents and messages. If I try to send _only_ text or a blank form field I get a \"Bad Request, the browser or proxy sent a request that this server could not understand\" error.\r\n\r\n\r\n## Steps to Reproduce\r\n\r\nFirst I installed 0.8.0 on hardware and create a journalist. I then ran ./qa-loader.py -m 25. I logged in and and see submissions. I then added the apt-test key and updated sources.list to `apt.freedom.press` to `apt-test.freedom.press`. Finally I ran `sudo cron-apt -i -s` to update to `0.9rc2`\r\n\r\n## Expected Behavior\r\n\r\nA source can send text to journalists.\r\n\r\n## Actual Behavior\r\n\r\nError in the webapp.\r\n\r\n## Comments\r\n\r\nI also enabled apache debug logging and attempted to patch the source_app/ code to log anything related to CSRF violations and I was was not able to trigger a debug log. \r\n\n", "before_files": [{"content": "import operator\nimport os\nimport io\n\nfrom datetime import datetime\nfrom flask import (Blueprint, render_template, flash, redirect, url_for, g,\n                   session, current_app, request, Markup, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\n\nfrom db import db\nfrom models import Source, Submission, Reply, get_one_or_else\nfrom source_app.decorators import login_required\nfrom source_app.utils import (logged_in, generate_unique_codename,\n                              async_genkey, normalize_timestamps,\n                              valid_codename, get_entropy_estimate)\nfrom source_app.forms import LoginForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/')\n    def index():\n        return render_template('index.html')\n\n    @view.route('/generate', methods=('GET', 'POST'))\n    def generate():\n        if logged_in():\n            flash(gettext(\n                \"You were redirected because you are already logged in. \"\n                \"If you want to create a new account, you should log out \"\n                \"first.\"),\n                  \"notification\")\n            return redirect(url_for('.lookup'))\n\n        codename = generate_unique_codename(config)\n        session['codename'] = codename\n        session['new_user'] = True\n        return render_template('generate.html', codename=codename)\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/create', methods=['POST'])\n    def create():\n        filesystem_id = current_app.crypto_util.hash_codename(\n            session['codename'])\n\n        source = Source(filesystem_id, current_app.crypto_util.display_id())\n        db.session.add(source)\n        try:\n            db.session.commit()\n        except IntegrityError as e:\n            db.session.rollback()\n            current_app.logger.error(\n                \"Attempt to create a source with duplicate codename: %s\" %\n                (e,))\n\n            # Issue 2386: don't log in on duplicates\n            del session['codename']\n            abort(500)\n        else:\n            os.mkdir(current_app.storage.path(filesystem_id))\n\n        session['logged_in'] = True\n        return redirect(url_for('.lookup'))\n\n    @view.route('/lookup', methods=('GET',))\n    @login_required\n    def lookup():\n        replies = []\n        source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \\\n                                  .filter(Reply.deleted_by_source == False).all()  # noqa\n\n        for reply in source_inbox:\n            reply_path = current_app.storage.path(\n                g.filesystem_id,\n                reply.filename,\n            )\n            try:\n                with io.open(reply_path, \"rb\") as f:\n                    contents = f.read()\n                reply.decrypted = current_app.crypto_util.decrypt(\n                    g.codename,\n                    contents).decode('utf-8')\n            except UnicodeDecodeError:\n                current_app.logger.error(\"Could not decode reply %s\" %\n                                         reply.filename)\n            else:\n                reply.date = datetime.utcfromtimestamp(\n                    os.stat(reply_path).st_mtime)\n                replies.append(reply)\n\n        # Sort the replies by date\n        replies.sort(key=operator.attrgetter('date'), reverse=True)\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 current_app.crypto_util.getkey(g.filesystem_id) and \\\n                g.source.flagged:\n            db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n            async_genkey(current_app.crypto_util,\n                         db_uri,\n                         g.filesystem_id,\n                         g.codename)\n\n        return render_template(\n            'lookup.html',\n            codename=g.codename,\n            replies=replies,\n            flagged=g.source.flagged,\n            new_user=session.get('new_user', None),\n            haskey=current_app.crypto_util.getkey(\n                g.filesystem_id))\n\n    @view.route('/submit', methods=('POST',))\n    @login_required\n    def submit():\n        msg = request.form['msg']\n        fh = request.files['fh']\n\n        # Don't submit anything if it was an \"empty\" submission. #878\n        if not (msg or fh):\n            flash(gettext(\n                \"You must enter a message or choose a file to submit.\"),\n                  \"error\")\n            return redirect(url_for('main.lookup'))\n\n        fnames = []\n        journalist_filename = g.source.journalist_filename\n        first_submission = g.source.interaction_count == 0\n\n        if msg:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_message_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    msg))\n        if fh:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_file_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    fh.filename,\n                    fh.stream))\n\n        if first_submission:\n            msg = render_template('first_submission_flashed_message.html')\n            flash(Markup(msg), \"success\")\n\n        else:\n            if msg and not fh:\n                html_contents = gettext('Thanks! We received your message.')\n            elif not msg and fh:\n                html_contents = gettext('Thanks! We received your document.')\n            else:\n                html_contents = gettext('Thanks! We received your message and '\n                                        'document.')\n\n            msg = render_template('next_submission_flashed_message.html',\n                                  html_contents=html_contents)\n            flash(Markup(msg), \"success\")\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            # (gpg reads 300 bytes from /dev/random)\n            entropy_avail = get_entropy_estimate()\n            if entropy_avail >= 2400:\n                db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n\n                async_genkey(current_app.crypto_util,\n                             db_uri,\n                             g.filesystem_id,\n                             g.codename)\n                current_app.logger.info(\"generating key, entropy: {}\".format(\n                    entropy_avail))\n            else:\n                current_app.logger.warn(\n                        \"skipping key generation. entropy: {}\".format(\n                                entropy_avail))\n\n        g.source.last_updated = datetime.utcnow()\n        db.session.commit()\n        normalize_timestamps(g.filesystem_id)\n\n        return redirect(url_for('main.lookup'))\n\n    @view.route('/delete', methods=('POST',))\n    @login_required\n    def delete():\n        \"\"\"This deletes the reply from the source's inbox, but preserves\n        the history for journalists such that they can view conversation\n        history.\n        \"\"\"\n\n        query = Reply.query.filter(\n            Reply.filename == request.form['reply_filename'])\n        reply = get_one_or_else(query, current_app.logger, abort)\n        reply.deleted_by_source = True\n        db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"Reply deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/delete-all', methods=('POST',))\n    @login_required\n    def batch_delete():\n        replies = Reply.query.filter(Reply.source_id == g.source.id) \\\n                             .filter(Reply.deleted_by_source == False).all()  # noqa\n        if len(replies) == 0:\n            current_app.logger.error(\"Found no replies when at least one was \"\n                                     \"expected\")\n            return redirect(url_for('.lookup'))\n\n        for reply in replies:\n            reply.deleted_by_source = True\n            db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"All replies have been deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\n        form = LoginForm()\n        if form.validate_on_submit():\n            codename = request.form['codename'].strip()\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                current_app.logger.info(\n                        \"Login failed for invalid codename\")\n                flash(gettext(\"Sorry, that is not a recognized codename.\"),\n                      \"error\")\n        return render_template('login.html', form=form)\n\n    @view.route('/logout')\n    def logout():\n        if logged_in():\n            msg = render_template('logout_flashed_message.html')\n\n            # Clear the session after we render the message so it's localized\n            # If a user specified a locale, save it and restore it\n            user_locale = g.locale\n            session.clear()\n            session['locale'] = user_locale\n\n            flash(Markup(msg), \"important hide-if-not-tor-browser\")\n        return redirect(url_for('.index'))\n\n    return view\n", "path": "securedrop/source_app/main.py"}], "after_files": [{"content": "import operator\nimport os\nimport io\n\nfrom datetime import datetime\nfrom flask import (Blueprint, render_template, flash, redirect, url_for, g,\n                   session, current_app, request, Markup, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\n\nfrom db import db\nfrom models import Source, Submission, Reply, get_one_or_else\nfrom source_app.decorators import login_required\nfrom source_app.utils import (logged_in, generate_unique_codename,\n                              async_genkey, normalize_timestamps,\n                              valid_codename, get_entropy_estimate)\nfrom source_app.forms import LoginForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/')\n    def index():\n        return render_template('index.html')\n\n    @view.route('/generate', methods=('GET', 'POST'))\n    def generate():\n        if logged_in():\n            flash(gettext(\n                \"You were redirected because you are already logged in. \"\n                \"If you want to create a new account, you should log out \"\n                \"first.\"),\n                  \"notification\")\n            return redirect(url_for('.lookup'))\n\n        codename = generate_unique_codename(config)\n        session['codename'] = codename\n        session['new_user'] = True\n        return render_template('generate.html', codename=codename)\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/create', methods=['POST'])\n    def create():\n        filesystem_id = current_app.crypto_util.hash_codename(\n            session['codename'])\n\n        source = Source(filesystem_id, current_app.crypto_util.display_id())\n        db.session.add(source)\n        try:\n            db.session.commit()\n        except IntegrityError as e:\n            db.session.rollback()\n            current_app.logger.error(\n                \"Attempt to create a source with duplicate codename: %s\" %\n                (e,))\n\n            # Issue 2386: don't log in on duplicates\n            del session['codename']\n            abort(500)\n        else:\n            os.mkdir(current_app.storage.path(filesystem_id))\n\n        session['logged_in'] = True\n        return redirect(url_for('.lookup'))\n\n    @view.route('/lookup', methods=('GET',))\n    @login_required\n    def lookup():\n        replies = []\n        source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \\\n                                  .filter(Reply.deleted_by_source == False).all()  # noqa\n\n        for reply in source_inbox:\n            reply_path = current_app.storage.path(\n                g.filesystem_id,\n                reply.filename,\n            )\n            try:\n                with io.open(reply_path, \"rb\") as f:\n                    contents = f.read()\n                reply.decrypted = current_app.crypto_util.decrypt(\n                    g.codename,\n                    contents).decode('utf-8')\n            except UnicodeDecodeError:\n                current_app.logger.error(\"Could not decode reply %s\" %\n                                         reply.filename)\n            else:\n                reply.date = datetime.utcfromtimestamp(\n                    os.stat(reply_path).st_mtime)\n                replies.append(reply)\n\n        # Sort the replies by date\n        replies.sort(key=operator.attrgetter('date'), reverse=True)\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 current_app.crypto_util.getkey(g.filesystem_id) and \\\n                g.source.flagged:\n            db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n            async_genkey(current_app.crypto_util,\n                         db_uri,\n                         g.filesystem_id,\n                         g.codename)\n\n        return render_template(\n            'lookup.html',\n            codename=g.codename,\n            replies=replies,\n            flagged=g.source.flagged,\n            new_user=session.get('new_user', None),\n            haskey=current_app.crypto_util.getkey(\n                g.filesystem_id))\n\n    @view.route('/submit', methods=('POST',))\n    @login_required\n    def submit():\n        msg = request.form['msg']\n        fh = None\n        if 'fh' in request.files:\n            fh = request.files['fh']\n\n        # Don't submit anything if it was an \"empty\" submission. #878\n        if not (msg or fh):\n            flash(gettext(\n                \"You must enter a message or choose a file to submit.\"),\n                  \"error\")\n            return redirect(url_for('main.lookup'))\n\n        fnames = []\n        journalist_filename = g.source.journalist_filename\n        first_submission = g.source.interaction_count == 0\n\n        if msg:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_message_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    msg))\n        if fh:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_file_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    fh.filename,\n                    fh.stream))\n\n        if first_submission:\n            msg = render_template('first_submission_flashed_message.html')\n            flash(Markup(msg), \"success\")\n\n        else:\n            if msg and not fh:\n                html_contents = gettext('Thanks! We received your message.')\n            elif not msg and fh:\n                html_contents = gettext('Thanks! We received your document.')\n            else:\n                html_contents = gettext('Thanks! We received your message and '\n                                        'document.')\n\n            msg = render_template('next_submission_flashed_message.html',\n                                  html_contents=html_contents)\n            flash(Markup(msg), \"success\")\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            # (gpg reads 300 bytes from /dev/random)\n            entropy_avail = get_entropy_estimate()\n            if entropy_avail >= 2400:\n                db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n\n                async_genkey(current_app.crypto_util,\n                             db_uri,\n                             g.filesystem_id,\n                             g.codename)\n                current_app.logger.info(\"generating key, entropy: {}\".format(\n                    entropy_avail))\n            else:\n                current_app.logger.warn(\n                        \"skipping key generation. entropy: {}\".format(\n                                entropy_avail))\n\n        g.source.last_updated = datetime.utcnow()\n        db.session.commit()\n        normalize_timestamps(g.filesystem_id)\n\n        return redirect(url_for('main.lookup'))\n\n    @view.route('/delete', methods=('POST',))\n    @login_required\n    def delete():\n        \"\"\"This deletes the reply from the source's inbox, but preserves\n        the history for journalists such that they can view conversation\n        history.\n        \"\"\"\n\n        query = Reply.query.filter(\n            Reply.filename == request.form['reply_filename'])\n        reply = get_one_or_else(query, current_app.logger, abort)\n        reply.deleted_by_source = True\n        db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"Reply deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/delete-all', methods=('POST',))\n    @login_required\n    def batch_delete():\n        replies = Reply.query.filter(Reply.source_id == g.source.id) \\\n                             .filter(Reply.deleted_by_source == False).all()  # noqa\n        if len(replies) == 0:\n            current_app.logger.error(\"Found no replies when at least one was \"\n                                     \"expected\")\n            return redirect(url_for('.lookup'))\n\n        for reply in replies:\n            reply.deleted_by_source = True\n            db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"All replies have been deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\n        form = LoginForm()\n        if form.validate_on_submit():\n            codename = request.form['codename'].strip()\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                current_app.logger.info(\n                        \"Login failed for invalid codename\")\n                flash(gettext(\"Sorry, that is not a recognized codename.\"),\n                      \"error\")\n        return render_template('login.html', form=form)\n\n    @view.route('/logout')\n    def logout():\n        if logged_in():\n            msg = render_template('logout_flashed_message.html')\n\n            # Clear the session after we render the message so it's localized\n            # If a user specified a locale, save it and restore it\n            user_locale = g.locale\n            session.clear()\n            session['locale'] = user_locale\n\n            flash(Markup(msg), \"important hide-if-not-tor-browser\")\n        return redirect(url_for('.index'))\n\n    return view\n", "path": "securedrop/source_app/main.py"}]}