We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add clearer action reminder in Journalist Interface to enable v3
As part of the 1.7.0 release, we've agreed that we want to improve our reminder in the Journalist Interface to enable v3 onion services. The current banner looks as follows:

![Screen Shot 2020-12-15 at 15 18 06-fullpage](https://user-images.githubusercontent.com/213636/102284729-913e1880-3ee9-11eb-9518-8b685d2b0e5a.png)

You can also see it on https://demo-journalist.securedrop.org/ after logging in using the dev credentials.

The plan of record is as follows:
- SecureDrop 1.7.0 (released in January) will still fully support v2 onion services
- SecureDrop 1.8.0 (released in February) will support Ubuntu 20.04 (#4768), and _exclusively_ support v3 onion services for Focal installs, without changing the behavior on Xenial.
- After April 30 (Ubuntu 16.04 EOL), instances not upgraded to Ubuntu 20.04 will self-disable.

In other words, the v3 switch is unavoidable due to the Xenial end-of-life. Our goal with the 1.7.0 release is to get more admins to make the switch prior to a reinstall on Focal, to make the process a bit easier for them. The specific action we need to motivate:

1) *If v3 is already enabled, but v2 services are still available:* Disable v2 services, ensure that all journalists/admins have v3 creds, and ensure that landing page points to v3 onion.

2) *If v3 is not enabled yet:* Enable v3 services (can still run them alongside v2 for a bit, per docs, then go v3-only).

We've discussed that this could potentially be done via different banners for 1) and 2) in the Journalist Interface. Let's kick around language/UX a bit in the comments, and I'll add the final agreed upon spec to the top-level issue.

## User Story

As an administrator, I want to be reminded of critical actions I must take to keep my instance running, so that I'm not caught by surprise.

--- 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/journalist_app/__init__.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 from datetime import datetime, timedelta
4 from flask import (Flask, session, redirect, url_for, flash, g, request,
5                    render_template)
6 from flask_assets import Environment
7 from flask_babel import gettext
8 from flask_wtf.csrf import CSRFProtect, CSRFError
9 from os import path
10 import sys
11 from werkzeug.exceptions import default_exceptions
12 
13 import i18n
14 import template_filters
15 import version
16 
17 from crypto_util import CryptoUtil
18 from db import db
19 from journalist_app import account, admin, api, main, col
20 from journalist_app.utils import (get_source, logged_in,
21                                   JournalistInterfaceSessionInterface,
22                                   cleanup_expired_revoked_tokens)
23 from models import InstanceConfig, Journalist
24 from store import Storage
25 
26 import typing
27 # https://www.python.org/dev/peps/pep-0484/#runtime-or-type-checking
28 if typing.TYPE_CHECKING:
29     # flake8 can not understand type annotation yet.
30     # That is why all type annotation relative import
31     # statements has to be marked as noqa.
32     # http://flake8.pycqa.org/en/latest/user/error-codes.html?highlight=f401
33     from sdconfig import SDConfig  # noqa: F401
34     from typing import Optional, Union, Tuple, Any  # noqa: F401
35     from werkzeug import Response  # noqa: F401
36     from werkzeug.exceptions import HTTPException  # noqa: F401
37 
38 _insecure_views = ['main.login', 'main.select_logo', 'static']
39 
40 
41 def create_app(config: 'SDConfig') -> Flask:
42     app = Flask(__name__,
43                 template_folder=config.JOURNALIST_TEMPLATES_DIR,
44                 static_folder=path.join(config.SECUREDROP_ROOT, 'static'))
45 
46     app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)
47     app.session_interface = JournalistInterfaceSessionInterface()
48 
49     csrf = CSRFProtect(app)
50     Environment(app)
51 
52     app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
53     app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
54     db.init_app(app)
55 
56     v2_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))
57     v3_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))
58     app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)
59 
60     # TODO: Attaching a Storage dynamically like this disables all type checking (and
61     # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
62     app.storage = Storage(config.STORE_DIR,
63                           config.TEMP_DIR,
64                           config.JOURNALIST_KEY)
65 
66     # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and
67     # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
68     app.crypto_util = CryptoUtil(
69         scrypt_params=config.SCRYPT_PARAMS,
70         scrypt_id_pepper=config.SCRYPT_ID_PEPPER,
71         scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,
72         securedrop_root=config.SECUREDROP_ROOT,
73         nouns_file=config.NOUNS,
74         adjectives_file=config.ADJECTIVES,
75         gpg_key_dir=config.GPG_KEY_DIR,
76     )
77 
78     @app.errorhandler(CSRFError)
79     def handle_csrf_error(e: CSRFError) -> 'Response':
80         # render the message first to ensure it's localized.
81         msg = gettext('You have been logged out due to inactivity.')
82         session.clear()
83         flash(msg, 'error')
84         return redirect(url_for('main.login'))
85 
86     def _handle_http_exception(
87         error: 'HTTPException'
88     ) -> 'Tuple[Union[Response, str], Optional[int]]':
89         # Workaround for no blueprint-level 404/5 error handlers, see:
90         # https://github.com/pallets/flask/issues/503#issuecomment-71383286
91         handler = list(app.error_handler_spec['api'][error.code].values())[0]
92         if request.path.startswith('/api/') and handler:
93             return handler(error)
94 
95         return render_template('error.html', error=error), error.code
96 
97     for code in default_exceptions:
98         app.errorhandler(code)(_handle_http_exception)
99 
100     i18n.setup_app(config, app)
101 
102     app.jinja_env.trim_blocks = True
103     app.jinja_env.lstrip_blocks = True
104     app.jinja_env.globals['version'] = version.__version__
105     app.jinja_env.filters['rel_datetime_format'] = \
106         template_filters.rel_datetime_format
107     app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat
108 
109     @app.before_first_request
110     def expire_blacklisted_tokens() -> None:
111         cleanup_expired_revoked_tokens()
112 
113     @app.before_request
114     def load_instance_config() -> None:
115         app.instance_config = InstanceConfig.get_current()
116 
117     @app.before_request
118     def setup_g() -> 'Optional[Response]':
119         """Store commonly used values in Flask's special g object"""
120         if 'expires' in session and datetime.utcnow() >= session['expires']:
121             session.clear()
122             flash(gettext('You have been logged out due to inactivity.'),
123                   'error')
124 
125         uid = session.get('uid', None)
126         if uid:
127             user = Journalist.query.get(uid)
128             if user and 'nonce' in session and \
129                session['nonce'] != user.session_nonce:
130                 session.clear()
131                 flash(gettext('You have been logged out due to password change'),
132                       'error')
133 
134         session['expires'] = datetime.utcnow() + \
135             timedelta(minutes=getattr(config,
136                                       'SESSION_EXPIRATION_MINUTES',
137                                       120))
138 
139         # Work around https://github.com/lepture/flask-wtf/issues/275
140         # -- after upgrading from Python 2 to Python 3, any existing
141         # session's csrf_token value will be retrieved as bytes,
142         # causing a TypeError. This simple fix, deleting the existing
143         # token, was suggested in the issue comments. This code will
144         # be safe to remove after Python 2 reaches EOL in 2020, and no
145         # supported SecureDrop installations can still have this
146         # problem.
147         if sys.version_info.major > 2 and type(session.get('csrf_token')) is bytes:
148             del session['csrf_token']
149 
150         uid = session.get('uid', None)
151         if uid:
152             g.user = Journalist.query.get(uid)
153 
154         g.locale = i18n.get_locale(config)
155         g.text_direction = i18n.get_text_direction(g.locale)
156         g.html_lang = i18n.locale_to_rfc_5646(g.locale)
157         g.locales = i18n.get_locale2name()
158 
159         if app.instance_config.organization_name:
160             g.organization_name = app.instance_config.organization_name
161         else:
162             g.organization_name = gettext('SecureDrop')
163 
164         if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
165             g.show_v2_onion_eol_warning = True
166 
167         if request.path.split('/')[1] == 'api':
168             pass  # We use the @token_required decorator for the API endpoints
169         else:  # We are not using the API
170             if request.endpoint not in _insecure_views and not logged_in():
171                 return redirect(url_for('main.login'))
172 
173         if request.method == 'POST':
174             filesystem_id = request.form.get('filesystem_id')
175             if filesystem_id:
176                 g.filesystem_id = filesystem_id
177                 g.source = get_source(filesystem_id)
178 
179         return None
180 
181     app.register_blueprint(main.make_blueprint(config))
182     app.register_blueprint(account.make_blueprint(config),
183                            url_prefix='/account')
184     app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')
185     app.register_blueprint(col.make_blueprint(config), url_prefix='/col')
186     api_blueprint = api.make_blueprint(config)
187     app.register_blueprint(api_blueprint, url_prefix='/api/v1')
188     csrf.exempt(api_blueprint)
189 
190     return app
191
```
--- 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/journalist_app/__init__.py b/securedrop/journalist_app/__init__.py
--- a/securedrop/journalist_app/__init__.py
+++ b/securedrop/journalist_app/__init__.py
@@ -53,8 +53,13 @@
     app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
     db.init_app(app)
 
-    v2_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))
-    v3_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))
+    def _url_exists(u: str) -> bool:
+        return path.exists(path.join(config.SECUREDROP_DATA_ROOT, u))
+
+    v2_enabled = _url_exists('source_v2_url') or ((not _url_exists('source_v2_url'))
+                                                  and (not _url_exists('source_v3_url')))
+    v3_enabled = _url_exists('source_v3_url')
+
     app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)
 
     # TODO: Attaching a Storage dynamically like this disables all type checking (and
@@ -161,9 +166,12 @@
         else:
             g.organization_name = gettext('SecureDrop')
 
-        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
+        if app.config['V2_ONION_ENABLED'] and not app.config['V3_ONION_ENABLED']:
             g.show_v2_onion_eol_warning = True
 
+        if app.config['V2_ONION_ENABLED'] and app.config['V3_ONION_ENABLED']:
+            g.show_v2_onion_migration_warning = True
+
         if request.path.split('/')[1] == 'api':
             pass  # We use the @token_required decorator for the API endpoints
         else:  # We are not using the API

{"golden_diff": "diff --git a/securedrop/journalist_app/__init__.py b/securedrop/journalist_app/__init__.py\n--- a/securedrop/journalist_app/__init__.py\n+++ b/securedrop/journalist_app/__init__.py\n@@ -53,8 +53,13 @@\n     app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI\n     db.init_app(app)\n \n-    v2_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))\n-    v3_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))\n+    def _url_exists(u: str) -> bool:\n+        return path.exists(path.join(config.SECUREDROP_DATA_ROOT, u))\n+\n+    v2_enabled = _url_exists('source_v2_url') or ((not _url_exists('source_v2_url'))\n+                                                  and (not _url_exists('source_v3_url')))\n+    v3_enabled = _url_exists('source_v3_url')\n+\n     app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)\n \n     # TODO: Attaching a Storage dynamically like this disables all type checking (and\n@@ -161,9 +166,12 @@\n         else:\n             g.organization_name = gettext('SecureDrop')\n \n-        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:\n+        if app.config['V2_ONION_ENABLED'] and not app.config['V3_ONION_ENABLED']:\n             g.show_v2_onion_eol_warning = True\n \n+        if app.config['V2_ONION_ENABLED'] and app.config['V3_ONION_ENABLED']:\n+            g.show_v2_onion_migration_warning = True\n+\n         if request.path.split('/')[1] == 'api':\n             pass  # We use the @token_required decorator for the API endpoints\n         else:  # We are not using the API\n", "issue": "Add clearer action reminder in Journalist Interface to enable v3\nAs part of the 1.7.0 release, we've agreed that we want to improve our reminder in the Journalist Interface to enable v3 onion services. The current banner looks as follows:\r\n\r\n![Screen Shot 2020-12-15 at 15 18 06-fullpage](https://user-images.githubusercontent.com/213636/102284729-913e1880-3ee9-11eb-9518-8b685d2b0e5a.png)\r\n\r\nYou can also see it on https://demo-journalist.securedrop.org/ after logging in using the dev credentials.\r\n\r\nThe plan of record is as follows:\r\n- SecureDrop 1.7.0 (released in January) will still fully support v2 onion services\r\n- SecureDrop 1.8.0 (released in February) will support Ubuntu 20.04 (#4768), and _exclusively_ support v3 onion services for Focal installs, without changing the behavior on Xenial.\r\n- After April 30 (Ubuntu 16.04 EOL), instances not upgraded to Ubuntu 20.04 will self-disable.\r\n\r\nIn other words, the v3 switch is unavoidable due to the Xenial end-of-life. Our goal with the 1.7.0 release is to get more admins to make the switch prior to a reinstall on Focal, to make the process a bit easier for them. The specific action we need to motivate:\r\n\r\n1) *If v3 is already enabled, but v2 services are still available:* Disable v2 services, ensure that all journalists/admins have v3 creds, and ensure that landing page points to v3 onion.\r\n\r\n2) *If v3 is not enabled yet:* Enable v3 services (can still run them alongside v2 for a bit, per docs, then go v3-only).\r\n\r\nWe've discussed that this could potentially be done via different banners for 1) and 2) in the Journalist Interface. Let's kick around language/UX a bit in the comments, and I'll add the final agreed upon spec to the top-level issue.\r\n\r\n## User Story\r\n\r\nAs an administrator, I want to be reminded of critical actions I must take to keep my instance running, so that I'm not caught by surprise.\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\nfrom datetime import datetime, timedelta\nfrom flask import (Flask, session, redirect, url_for, flash, g, request,\n                   render_template)\nfrom flask_assets import Environment\nfrom flask_babel import gettext\nfrom flask_wtf.csrf import CSRFProtect, CSRFError\nfrom os import path\nimport sys\nfrom werkzeug.exceptions import default_exceptions\n\nimport i18n\nimport template_filters\nimport version\n\nfrom crypto_util import CryptoUtil\nfrom db import db\nfrom journalist_app import account, admin, api, main, col\nfrom journalist_app.utils import (get_source, logged_in,\n                                  JournalistInterfaceSessionInterface,\n                                  cleanup_expired_revoked_tokens)\nfrom models import InstanceConfig, Journalist\nfrom store import Storage\n\nimport typing\n# https://www.python.org/dev/peps/pep-0484/#runtime-or-type-checking\nif typing.TYPE_CHECKING:\n    # flake8 can not understand type annotation yet.\n    # That is why all type annotation relative import\n    # statements has to be marked as noqa.\n    # http://flake8.pycqa.org/en/latest/user/error-codes.html?highlight=f401\n    from sdconfig import SDConfig  # noqa: F401\n    from typing import Optional, Union, Tuple, Any  # noqa: F401\n    from werkzeug import Response  # noqa: F401\n    from werkzeug.exceptions import HTTPException  # noqa: F401\n\n_insecure_views = ['main.login', 'main.select_logo', 'static']\n\n\ndef create_app(config: 'SDConfig') -> Flask:\n    app = Flask(__name__,\n                template_folder=config.JOURNALIST_TEMPLATES_DIR,\n                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))\n\n    app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)\n    app.session_interface = JournalistInterfaceSessionInterface()\n\n    csrf = CSRFProtect(app)\n    Environment(app)\n\n    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False\n    app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI\n    db.init_app(app)\n\n    v2_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))\n    v3_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))\n    app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)\n\n    # TODO: Attaching a Storage dynamically like this disables all type checking (and\n    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored\n    app.storage = Storage(config.STORE_DIR,\n                          config.TEMP_DIR,\n                          config.JOURNALIST_KEY)\n\n    # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and\n    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored\n    app.crypto_util = CryptoUtil(\n        scrypt_params=config.SCRYPT_PARAMS,\n        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,\n        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,\n        securedrop_root=config.SECUREDROP_ROOT,\n        nouns_file=config.NOUNS,\n        adjectives_file=config.ADJECTIVES,\n        gpg_key_dir=config.GPG_KEY_DIR,\n    )\n\n    @app.errorhandler(CSRFError)\n    def handle_csrf_error(e: CSRFError) -> 'Response':\n        # render the message first to ensure it's localized.\n        msg = gettext('You have been logged out due to inactivity.')\n        session.clear()\n        flash(msg, 'error')\n        return redirect(url_for('main.login'))\n\n    def _handle_http_exception(\n        error: 'HTTPException'\n    ) -> 'Tuple[Union[Response, str], Optional[int]]':\n        # Workaround for no blueprint-level 404/5 error handlers, see:\n        # https://github.com/pallets/flask/issues/503#issuecomment-71383286\n        handler = list(app.error_handler_spec['api'][error.code].values())[0]\n        if request.path.startswith('/api/') and handler:\n            return handler(error)\n\n        return render_template('error.html', error=error), error.code\n\n    for code in default_exceptions:\n        app.errorhandler(code)(_handle_http_exception)\n\n    i18n.setup_app(config, app)\n\n    app.jinja_env.trim_blocks = True\n    app.jinja_env.lstrip_blocks = True\n    app.jinja_env.globals['version'] = version.__version__\n    app.jinja_env.filters['rel_datetime_format'] = \\\n        template_filters.rel_datetime_format\n    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat\n\n    @app.before_first_request\n    def expire_blacklisted_tokens() -> None:\n        cleanup_expired_revoked_tokens()\n\n    @app.before_request\n    def load_instance_config() -> None:\n        app.instance_config = InstanceConfig.get_current()\n\n    @app.before_request\n    def setup_g() -> 'Optional[Response]':\n        \"\"\"Store commonly used values in Flask's special g object\"\"\"\n        if 'expires' in session and datetime.utcnow() >= session['expires']:\n            session.clear()\n            flash(gettext('You have been logged out due to inactivity.'),\n                  'error')\n\n        uid = session.get('uid', None)\n        if uid:\n            user = Journalist.query.get(uid)\n            if user and 'nonce' in session and \\\n               session['nonce'] != user.session_nonce:\n                session.clear()\n                flash(gettext('You have been logged out due to password change'),\n                      'error')\n\n        session['expires'] = datetime.utcnow() + \\\n            timedelta(minutes=getattr(config,\n                                      'SESSION_EXPIRATION_MINUTES',\n                                      120))\n\n        # Work around https://github.com/lepture/flask-wtf/issues/275\n        # -- after upgrading from Python 2 to Python 3, any existing\n        # session's csrf_token value will be retrieved as bytes,\n        # causing a TypeError. This simple fix, deleting the existing\n        # token, was suggested in the issue comments. This code will\n        # be safe to remove after Python 2 reaches EOL in 2020, and no\n        # supported SecureDrop installations can still have this\n        # problem.\n        if sys.version_info.major > 2 and type(session.get('csrf_token')) is bytes:\n            del session['csrf_token']\n\n        uid = session.get('uid', None)\n        if uid:\n            g.user = Journalist.query.get(uid)\n\n        g.locale = i18n.get_locale(config)\n        g.text_direction = i18n.get_text_direction(g.locale)\n        g.html_lang = i18n.locale_to_rfc_5646(g.locale)\n        g.locales = i18n.get_locale2name()\n\n        if app.instance_config.organization_name:\n            g.organization_name = app.instance_config.organization_name\n        else:\n            g.organization_name = gettext('SecureDrop')\n\n        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:\n            g.show_v2_onion_eol_warning = True\n\n        if request.path.split('/')[1] == 'api':\n            pass  # We use the @token_required decorator for the API endpoints\n        else:  # We are not using the API\n            if request.endpoint not in _insecure_views and not logged_in():\n                return redirect(url_for('main.login'))\n\n        if request.method == 'POST':\n            filesystem_id = request.form.get('filesystem_id')\n            if filesystem_id:\n                g.filesystem_id = filesystem_id\n                g.source = get_source(filesystem_id)\n\n        return None\n\n    app.register_blueprint(main.make_blueprint(config))\n    app.register_blueprint(account.make_blueprint(config),\n                           url_prefix='/account')\n    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')\n    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')\n    api_blueprint = api.make_blueprint(config)\n    app.register_blueprint(api_blueprint, url_prefix='/api/v1')\n    csrf.exempt(api_blueprint)\n\n    return app\n", "path": "securedrop/journalist_app/__init__.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\nfrom datetime import datetime, timedelta\nfrom flask import (Flask, session, redirect, url_for, flash, g, request,\n                   render_template)\nfrom flask_assets import Environment\nfrom flask_babel import gettext\nfrom flask_wtf.csrf import CSRFProtect, CSRFError\nfrom os import path\nimport sys\nfrom werkzeug.exceptions import default_exceptions\n\nimport i18n\nimport template_filters\nimport version\n\nfrom crypto_util import CryptoUtil\nfrom db import db\nfrom journalist_app import account, admin, api, main, col\nfrom journalist_app.utils import (get_source, logged_in,\n                                  JournalistInterfaceSessionInterface,\n                                  cleanup_expired_revoked_tokens)\nfrom models import InstanceConfig, Journalist\nfrom store import Storage\n\nimport typing\n# https://www.python.org/dev/peps/pep-0484/#runtime-or-type-checking\nif typing.TYPE_CHECKING:\n    # flake8 can not understand type annotation yet.\n    # That is why all type annotation relative import\n    # statements has to be marked as noqa.\n    # http://flake8.pycqa.org/en/latest/user/error-codes.html?highlight=f401\n    from sdconfig import SDConfig  # noqa: F401\n    from typing import Optional, Union, Tuple, Any  # noqa: F401\n    from werkzeug import Response  # noqa: F401\n    from werkzeug.exceptions import HTTPException  # noqa: F401\n\n_insecure_views = ['main.login', 'main.select_logo', 'static']\n\n\ndef create_app(config: 'SDConfig') -> Flask:\n    app = Flask(__name__,\n                template_folder=config.JOURNALIST_TEMPLATES_DIR,\n                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))\n\n    app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)\n    app.session_interface = JournalistInterfaceSessionInterface()\n\n    csrf = CSRFProtect(app)\n    Environment(app)\n\n    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False\n    app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI\n    db.init_app(app)\n\n    def _url_exists(u: str) -> bool:\n        return path.exists(path.join(config.SECUREDROP_DATA_ROOT, u))\n\n    v2_enabled = _url_exists('source_v2_url') or ((not _url_exists('source_v2_url'))\n                                                  and (not _url_exists('source_v3_url')))\n    v3_enabled = _url_exists('source_v3_url')\n\n    app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)\n\n    # TODO: Attaching a Storage dynamically like this disables all type checking (and\n    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored\n    app.storage = Storage(config.STORE_DIR,\n                          config.TEMP_DIR,\n                          config.JOURNALIST_KEY)\n\n    # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and\n    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored\n    app.crypto_util = CryptoUtil(\n        scrypt_params=config.SCRYPT_PARAMS,\n        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,\n        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,\n        securedrop_root=config.SECUREDROP_ROOT,\n        nouns_file=config.NOUNS,\n        adjectives_file=config.ADJECTIVES,\n        gpg_key_dir=config.GPG_KEY_DIR,\n    )\n\n    @app.errorhandler(CSRFError)\n    def handle_csrf_error(e: CSRFError) -> 'Response':\n        # render the message first to ensure it's localized.\n        msg = gettext('You have been logged out due to inactivity.')\n        session.clear()\n        flash(msg, 'error')\n        return redirect(url_for('main.login'))\n\n    def _handle_http_exception(\n        error: 'HTTPException'\n    ) -> 'Tuple[Union[Response, str], Optional[int]]':\n        # Workaround for no blueprint-level 404/5 error handlers, see:\n        # https://github.com/pallets/flask/issues/503#issuecomment-71383286\n        handler = list(app.error_handler_spec['api'][error.code].values())[0]\n        if request.path.startswith('/api/') and handler:\n            return handler(error)\n\n        return render_template('error.html', error=error), error.code\n\n    for code in default_exceptions:\n        app.errorhandler(code)(_handle_http_exception)\n\n    i18n.setup_app(config, app)\n\n    app.jinja_env.trim_blocks = True\n    app.jinja_env.lstrip_blocks = True\n    app.jinja_env.globals['version'] = version.__version__\n    app.jinja_env.filters['rel_datetime_format'] = \\\n        template_filters.rel_datetime_format\n    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat\n\n    @app.before_first_request\n    def expire_blacklisted_tokens() -> None:\n        cleanup_expired_revoked_tokens()\n\n    @app.before_request\n    def load_instance_config() -> None:\n        app.instance_config = InstanceConfig.get_current()\n\n    @app.before_request\n    def setup_g() -> 'Optional[Response]':\n        \"\"\"Store commonly used values in Flask's special g object\"\"\"\n        if 'expires' in session and datetime.utcnow() >= session['expires']:\n            session.clear()\n            flash(gettext('You have been logged out due to inactivity.'),\n                  'error')\n\n        uid = session.get('uid', None)\n        if uid:\n            user = Journalist.query.get(uid)\n            if user and 'nonce' in session and \\\n               session['nonce'] != user.session_nonce:\n                session.clear()\n                flash(gettext('You have been logged out due to password change'),\n                      'error')\n\n        session['expires'] = datetime.utcnow() + \\\n            timedelta(minutes=getattr(config,\n                                      'SESSION_EXPIRATION_MINUTES',\n                                      120))\n\n        # Work around https://github.com/lepture/flask-wtf/issues/275\n        # -- after upgrading from Python 2 to Python 3, any existing\n        # session's csrf_token value will be retrieved as bytes,\n        # causing a TypeError. This simple fix, deleting the existing\n        # token, was suggested in the issue comments. This code will\n        # be safe to remove after Python 2 reaches EOL in 2020, and no\n        # supported SecureDrop installations can still have this\n        # problem.\n        if sys.version_info.major > 2 and type(session.get('csrf_token')) is bytes:\n            del session['csrf_token']\n\n        uid = session.get('uid', None)\n        if uid:\n            g.user = Journalist.query.get(uid)\n\n        g.locale = i18n.get_locale(config)\n        g.text_direction = i18n.get_text_direction(g.locale)\n        g.html_lang = i18n.locale_to_rfc_5646(g.locale)\n        g.locales = i18n.get_locale2name()\n\n        if app.instance_config.organization_name:\n            g.organization_name = app.instance_config.organization_name\n        else:\n            g.organization_name = gettext('SecureDrop')\n\n        if app.config['V2_ONION_ENABLED'] and not app.config['V3_ONION_ENABLED']:\n            g.show_v2_onion_eol_warning = True\n\n        if app.config['V2_ONION_ENABLED'] and app.config['V3_ONION_ENABLED']:\n            g.show_v2_onion_migration_warning = True\n\n        if request.path.split('/')[1] == 'api':\n            pass  # We use the @token_required decorator for the API endpoints\n        else:  # We are not using the API\n            if request.endpoint not in _insecure_views and not logged_in():\n                return redirect(url_for('main.login'))\n\n        if request.method == 'POST':\n            filesystem_id = request.form.get('filesystem_id')\n            if filesystem_id:\n                g.filesystem_id = filesystem_id\n                g.source = get_source(filesystem_id)\n\n        return None\n\n    app.register_blueprint(main.make_blueprint(config))\n    app.register_blueprint(account.make_blueprint(config),\n                           url_prefix='/account')\n    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')\n    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')\n    api_blueprint = api.make_blueprint(config)\n    app.register_blueprint(api_blueprint, url_prefix='/api/v1')\n    csrf.exempt(api_blueprint)\n\n    return app\n", "path": "securedrop/journalist_app/__init__.py"}]}