def make_flask_stack(conf): """ This has to pass the flask app through all the same middleware that Pylons used """ root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) debug = asbool(conf.get('debug', conf.get('DEBUG', False))) testing = asbool(conf.get('testing', conf.get('TESTING', False))) app = flask_app = CKANFlask(__name__, static_url_path='') # Register storage for accessing group images, site logo, etc. storage_folder = [] storage = uploader.get_storage_path() if storage: storage_folder = [os.path.join(storage, 'storage')] # Static files folders (core and extensions) public_folder = config.get(u'ckan.base_public_folder') app.static_folder = config.get( 'extra_public_paths', '' ).split(',') + [os.path.join(root, public_folder)] + storage_folder app.jinja_options = jinja_extensions.get_jinja_env_options() app.jinja_env.policies['ext.i18n.trimmed'] = True app.debug = debug app.testing = testing app.template_folder = os.path.join(root, 'templates') app.app_ctx_globals_class = CKAN_AppCtxGlobals app.url_rule_class = CKAN_Rule # Update Flask config with the CKAN values. We use the common config # object as values might have been modified on `load_environment` if config: app.config.update(config) else: app.config.update(conf) # Do all the Flask-specific stuff before adding other middlewares # Secret key needed for flask-debug-toolbar and sessions if not app.config.get('SECRET_KEY'): app.config['SECRET_KEY'] = config.get('beaker.session.secret') if not app.config.get('SECRET_KEY'): raise RuntimeError(u'You must provide a value for the secret key' ' with the SECRET_KEY config option') root_path = config.get('ckan.root_path', None) if debug: from flask_debugtoolbar import DebugToolbarExtension app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False debug_ext = DebugToolbarExtension() # register path that includes `ckan.site_root` before # initializing debug app. In such a way, our route receives # higher precedence. # TODO: After removal of Pylons code, switch to # `APPLICATION_ROOT` config value for flask application. Right # now it's a bad option because we are handling both pylons # and flask urls inside helpers and splitting this logic will # bring us tons of headache. if root_path: app.add_url_rule( root_path.replace('{{LANG}}', '').rstrip('/') + '/_debug_toolbar/static/<path:filename>', '_debug_toolbar.static', debug_ext.send_static_file ) debug_ext.init_app(app) from werkzeug.debug import DebuggedApplication app.wsgi_app = DebuggedApplication(app.wsgi_app, True) # Use Beaker as the Flask session interface class BeakerSessionInterface(SessionInterface): def open_session(self, app, request): if 'beaker.session' in request.environ: return request.environ['beaker.session'] def save_session(self, app, session, response): session.save() namespace = 'beaker.session.' session_opts = {k.replace('beaker.', ''): v for k, v in six.iteritems(config) if k.startswith(namespace)} if (not session_opts.get('session.data_dir') and session_opts.get('session.type', 'file') == 'file'): cache_dir = conf.get('cache_dir') or conf.get('cache.dir') session_opts['session.data_dir'] = '{data_dir}/sessions'.format( data_dir=cache_dir) app.wsgi_app = SessionMiddleware(app.wsgi_app, session_opts) app.session_interface = BeakerSessionInterface() # Add Jinja2 extensions and filters app.jinja_env.filters['empty_and_escape'] = \ jinja_extensions.empty_and_escape # Common handlers for all requests app.before_request(ckan_before_request) app.after_request(ckan_after_request) # Template context processors app.context_processor(helper_functions) app.context_processor(c_object) @app.context_processor def ungettext_alias(): u''' Provide `ungettext` as an alias of `ngettext` for backwards compatibility ''' return dict(ungettext=ungettext) # Babel _ckan_i18n_dir = i18n.get_ckan_i18n_dir() pairs = [ (_ckan_i18n_dir, u'ckan') ] + [ (p.i18n_directory(), p.i18n_domain()) for p in PluginImplementations(ITranslation) ] i18n_dirs, i18n_domains = zip(*pairs) app.config[u'BABEL_TRANSLATION_DIRECTORIES'] = ';'.join(i18n_dirs) app.config[u'BABEL_DOMAIN'] = 'ckan' app.config[u'BABEL_MULTIPLE_DOMAINS'] = ';'.join(i18n_domains) babel = CKANBabel(app) babel.localeselector(get_locale) # WebAssets _setup_webassets(app) # Auto-register all blueprints defined in the `views` folder _register_core_blueprints(app) _register_error_handler(app) # Set up each IBlueprint extension as a Flask Blueprint for plugin in PluginImplementations(IBlueprint): if hasattr(plugin, 'get_blueprint'): plugin_blueprints = plugin.get_blueprint() if not isinstance(plugin_blueprints, list): plugin_blueprints = [plugin_blueprints] for blueprint in plugin_blueprints: app.register_extension_blueprint(blueprint) lib_plugins.register_package_blueprints(app) lib_plugins.register_group_blueprints(app) # Set flask routes in named_routes # TODO: refactor whatever helper is using this to not do it if 'routes.named_routes' not in config: config['routes.named_routes'] = {} for rule in app.url_map.iter_rules(): if '.' not in rule.endpoint: continue controller, action = rule.endpoint.split('.') needed = list(rule.arguments - set(rule.defaults or {})) route = { rule.endpoint: { 'action': action, 'controller': controller, 'highlight_actions': action, 'needed': needed } } config['routes.named_routes'].update(route) # Start other middleware for plugin in PluginImplementations(IMiddleware): app = plugin.make_middleware(app, config) for plugin in PluginImplementations(IMiddleware): try: app = plugin.make_error_log_middleware(app, config) except AttributeError: log.critical('Middleware class {0} is missing the method' 'make_error_log_middleware.' .format(plugin.__class__.__name__)) # Initialize repoze.who who_parser = WhoConfig(conf['here']) who_parser.parse(open(conf['who.config_file'])) app = PluggableAuthenticationMiddleware( RepozeAdapterMiddleware(app), who_parser.identifiers, who_parser.authenticators, who_parser.challengers, who_parser.mdproviders, who_parser.request_classifier, who_parser.challenge_decider, logging.getLogger('repoze.who'), logging.WARN, # ignored who_parser.remote_user_key ) # Update the main CKAN config object with the Flask specific keys # that were set here or autogenerated flask_config_keys = set(flask_app.config.keys()) - set(config.keys()) for key in flask_config_keys: config[key] = flask_app.config[key] # Prevent the host from request to be added to the new header location. app = HostHeaderMiddleware(app) if six.PY3: app = I18nMiddleware(app) if asbool(config.get('ckan.tracking_enabled', 'false')): app = TrackingMiddleware(app, config) # Add a reference to the actual Flask app so it's easier to access app._wsgi_app = flask_app return app
def make_flask_stack(conf: Union[Config, CKANConfig]) -> CKANApp: """ This has to pass the flask app through all the same middleware that Pylons used """ root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) debug = asbool(conf.get('debug', conf.get('DEBUG', False))) testing = asbool(conf.get('testing', conf.get('TESTING', False))) app = flask_app = CKANFlask(__name__, static_url_path='') # Register storage for accessing group images, site logo, etc. storage_folder = [] storage = uploader.get_storage_path() if storage: storage_folder = [os.path.join(storage, 'storage')] # Static files folders (core and extensions) public_folder = config.get_value(u'ckan.base_public_folder') app.static_folder = config.get_value('extra_public_paths').split(',') + [ os.path.join(root, public_folder) ] + storage_folder app.jinja_options = jinja_extensions.get_jinja_env_options() app.jinja_env.policies['ext.i18n.trimmed'] = True app.debug = debug app.testing = testing app.template_folder = os.path.join(root, 'templates') app.app_ctx_globals_class = CKAN_AppCtxGlobals app.url_rule_class = CKAN_Rule # Update Flask config with the CKAN values. We use the common config # object as values might have been modified on `load_environment` if config: app.config.update(config) else: app.config.update(conf) # Do all the Flask-specific stuff before adding other middlewares # Secret key needed for flask-debug-toolbar and sessions if not app.config.get('SECRET_KEY'): app.config['SECRET_KEY'] = config.get_value('beaker.session.secret') if not app.config.get('SECRET_KEY'): raise RuntimeError(u'You must provide a value for the secret key' ' with the SECRET_KEY config option') root_path = config.get_value('ckan.root_path') if debug: from flask_debugtoolbar import DebugToolbarExtension app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False debug_ext = DebugToolbarExtension() # register path that includes `ckan.site_root` before # initializing debug app. In such a way, our route receives # higher precedence. # TODO: After removal of Pylons code, switch to # `APPLICATION_ROOT` config value for flask application. Right # now it's a bad option because we are handling both pylons # and flask urls inside helpers and splitting this logic will # bring us tons of headache. if root_path: app.add_url_rule( root_path.replace('{{LANG}}', '').rstrip('/') + '/_debug_toolbar/static/<path:filename>', '_debug_toolbar.static', debug_ext.send_static_file) debug_ext.init_app(app) from werkzeug.debug import DebuggedApplication app.wsgi_app = DebuggedApplication(app.wsgi_app, True) # Use Beaker as the Flask session interface class BeakerSessionInterface(SessionInterface): def open_session(self, app: Any, request: Any): if 'beaker.session' in request.environ: return request.environ['beaker.session'] def save_session(self, app: Any, session: Any, response: Any): session.save() namespace = 'beaker.session.' session_opts = { k.replace('beaker.', ''): v for k, v in config.items() if k.startswith(namespace) } if (not session_opts.get('session.data_dir') and session_opts.get('session.type', 'file') == 'file'): cache_dir = conf.get('cache_dir') or conf.get('cache.dir') session_opts['session.data_dir'] = '{data_dir}/sessions'.format( data_dir=cache_dir) app.wsgi_app = RootPathMiddleware(app.wsgi_app) app.wsgi_app = SessionMiddleware(app.wsgi_app, session_opts) app.session_interface = BeakerSessionInterface() # Add Jinja2 extensions and filters app.jinja_env.filters['empty_and_escape'] = \ jinja_extensions.empty_and_escape # Common handlers for all requests # # flask types do not mention that it's possible to return a response from # the `before_request` callback app.before_request(ckan_before_request) app.after_request(ckan_after_request) # Template context processors app.context_processor(helper_functions) app.context_processor(c_object) app.context_processor(_ungettext_alias) # Babel _ckan_i18n_dir = i18n.get_ckan_i18n_dir() pairs = [cast("tuple[str, str]", (_ckan_i18n_dir, u'ckan')) ] + [(p.i18n_directory(), p.i18n_domain()) for p in reversed(list(PluginImplementations(ITranslation)))] i18n_dirs, i18n_domains = zip(*pairs) app.config[u'BABEL_TRANSLATION_DIRECTORIES'] = ';'.join(i18n_dirs) app.config[u'BABEL_DOMAIN'] = 'ckan' app.config[u'BABEL_MULTIPLE_DOMAINS'] = ';'.join(i18n_domains) app.config[u'BABEL_DEFAULT_TIMEZONE'] = str(h.get_display_timezone()) babel = CKANBabel(app) babel.localeselector(get_locale) # WebAssets _setup_webassets(app) # Auto-register all blueprints defined in the `views` folder _register_core_blueprints(app) _register_error_handler(app) # Set up each IBlueprint extension as a Flask Blueprint for plugin in PluginImplementations(IBlueprint): plugin_blueprints = plugin.get_blueprint() if not isinstance(plugin_blueprints, list): plugin_blueprints = [plugin_blueprints] for blueprint in plugin_blueprints: app.register_extension_blueprint(blueprint) lib_plugins.register_package_blueprints(app) lib_plugins.register_group_blueprints(app) # Start other middleware for plugin in PluginImplementations(IMiddleware): app = plugin.make_middleware(app, config) for plugin in PluginImplementations(IMiddleware): try: app = plugin.make_error_log_middleware(app, config) except AttributeError: log.critical('Middleware class {0} is missing the method' 'make_error_log_middleware.'.format( plugin.__class__.__name__)) # Initialize flask-login login_manager = LoginManager() login_manager.init_app(app) # make anonymous_user an instance of CKAN custom class login_manager.anonymous_user = model.AnonymousUser # The name of the view to redirect to when the user needs to log in. login_manager.login_view = config.get_value("ckan.auth.login_view") @login_manager.user_loader def load_user(user_id: str) -> Optional["model.User"]: # type: ignore return model.User.get(user_id) @login_manager.request_loader def load_user_from_request(request): # type: ignore user = _get_user_for_apitoken() return user # Update the main CKAN config object with the Flask specific keys # that were set here or autogenerated flask_config_keys = set(flask_app.config.keys()) - set(config.keys()) for key in flask_config_keys: config[key] = flask_app.config[key] # Prevent the host from request to be added to the new header location. app = HostHeaderMiddleware(app) app = I18nMiddleware(app) if config.get_value('ckan.tracking_enabled'): app = TrackingMiddleware(app, config) # Add a reference to the actual Flask app so it's easier to access # type_ignore_reason: custom attribute app._wsgi_app = flask_app # type: ignore return app