.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
webapp.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# SPDX-License-Identifier: AGPL-3.0-or-later
3"""WebApp"""
4# pylint: disable=use-dict-literal
5from __future__ import annotations
6
7import json
8import os
9import sys
10import base64
11
12from timeit import default_timer
13from html import escape
14from io import StringIO
15import typing
16
17import urllib
18import urllib.parse
19from urllib.parse import urlencode, urlparse, unquote
20
21import warnings
22import httpx
23
24from pygments import highlight
25from pygments.lexers import get_lexer_by_name
26from pygments.formatters import HtmlFormatter # pylint: disable=no-name-in-module
27
28from whitenoise import WhiteNoise
29from whitenoise.base import Headers
30
31import flask
32
33from flask import (
34 Flask,
35 render_template,
36 url_for,
37 make_response,
38 redirect,
39 send_from_directory,
40)
41from flask.wrappers import Response
42from flask.json import jsonify
43
44from flask_babel import (
45 Babel,
46 gettext,
47 format_decimal,
48)
49
50import searx
51from searx.extended_types import sxng_request
52from searx import (
53 logger,
54 get_setting,
55 settings,
56)
57
58from searx import infopage
59from searx import limiter
60from searx.botdetection import link_token, ProxyFix
61
62from searx.data import ENGINE_DESCRIPTIONS
63from searx.result_types import Answer
64from searx.settings_defaults import OUTPUT_FORMATS
65from searx.settings_loader import DEFAULT_SETTINGS_FILE
66from searx.exceptions import SearxParameterException
67from searx.engines import (
68 DEFAULT_CATEGORY,
69 categories,
70 engines,
71 engine_shortcuts,
72)
73
74from searx import webutils
75from searx.webutils import (
76 highlight_content,
77 get_result_templates,
78 get_themes,
79 exception_classname_to_text,
80 new_hmac,
81 is_hmac_of,
82 group_engines_in_tab,
83)
84from searx.webadapter import (
85 get_search_query_from_webapp,
86 get_selected_categories,
87 parse_lang,
88)
89from searx.utils import gen_useragent, dict_subset
90from searx.version import VERSION_STRING, GIT_URL, GIT_BRANCH
91from searx.query import RawTextQuery
92from searx.plugins.oa_doi_rewrite import get_doi_resolver
93from searx.preferences import (
94 Preferences,
95 ClientPref,
96 ValidationException,
97)
98import searx.answerers
99import searx.plugins
100
101
102from searx.metrics import get_engines_stats, get_engine_errors, get_reliabilities, histogram, counter, openmetrics
103from searx.flaskfix import patch_application
104
105from searx.locales import (
106 LOCALE_BEST_MATCH,
107 LOCALE_NAMES,
108 RTL_LOCALES,
109 localeselector,
110 locales_initialize,
111 match_locale,
112)
113
114# renaming names from searx imports ...
115from searx.autocomplete import search_autocomplete, backends as autocomplete_backends
116from searx import favicons
117
118from searx.valkeydb import initialize as valkey_initialize
119from searx.sxng_locales import sxng_locales
120import searx.search
121from searx.network import stream as http_stream, set_context_network_name
122from searx.search.checker import get_result as checker_get_result
123
124
125logger = logger.getChild('webapp')
126
127warnings.simplefilter("always")
128
129# about static
130logger.debug('static directory is %s', settings['ui']['static_path'])
131
132# about templates
133logger.debug('templates directory is %s', settings['ui']['templates_path'])
134default_theme = settings['ui']['default_theme']
135templates_path = settings['ui']['templates_path']
136themes = get_themes(templates_path)
137result_templates = get_result_templates(templates_path)
138
139STATS_SORT_PARAMETERS = {
140 'name': (False, 'name', ''),
141 'score': (True, 'score_per_result', 0),
142 'result_count': (True, 'result_count', 0),
143 'time': (False, 'total', 0),
144 'reliability': (False, 'reliability', 100),
145}
146
147# Flask app
148app = Flask(__name__, static_folder=None, template_folder=templates_path)
149
150app.jinja_env.trim_blocks = True
151app.jinja_env.lstrip_blocks = True
152app.jinja_env.add_extension('jinja2.ext.loopcontrols') # pylint: disable=no-member
153app.jinja_env.filters['group_engines_in_tab'] = group_engines_in_tab # pylint: disable=no-member
154app.secret_key = settings['server']['secret_key']
155
156
158 locale = localeselector()
159 logger.debug("%s uses locale `%s`", urllib.parse.quote(sxng_request.url), locale)
160 return locale
161
162
163babel = Babel(app, locale_selector=get_locale)
164
165
166def _get_browser_language(req, lang_list):
167 client = ClientPref.from_http_request(req)
168 locale = match_locale(client.locale_tag, lang_list, fallback='en')
169 return locale
170
171
173 """Get locale name for <html lang="...">
174 Chrom* browsers don't detect the language when there is a subtag (ie a territory).
175 For example "zh-TW" is detected but not "zh-Hant-TW".
176 This function returns a locale without the subtag.
177 """
178 parts = locale.split('-')
179 return parts[0].lower() + '-' + parts[-1].upper()
180
181
182# code-highlighter
183@app.template_filter('code_highlighter')
184def code_highlighter(codelines, language=None):
185 if not language:
186 language = 'text'
187
188 try:
189 # find lexer by programming language
190 lexer = get_lexer_by_name(language, stripall=True)
191
192 except Exception as e: # pylint: disable=broad-except
193 logger.warning("pygments lexer: %s " % e)
194 # if lexer is not found, using default one
195 lexer = get_lexer_by_name('text', stripall=True)
196
197 html_code = ''
198 tmp_code = ''
199 last_line = None
200 line_code_start = None
201
202 # parse lines
203 for line, code in codelines:
204 if not last_line:
205 line_code_start = line
206
207 # new codeblock is detected
208 if last_line is not None and last_line + 1 != line:
209
210 # highlight last codepart
211 formatter = HtmlFormatter(linenos='inline', linenostart=line_code_start, cssclass="code-highlight")
212 html_code = html_code + highlight(tmp_code, lexer, formatter)
213
214 # reset conditions for next codepart
215 tmp_code = ''
216 line_code_start = line
217
218 # add codepart
219 tmp_code += code + '\n'
220
221 # update line
222 last_line = line
223
224 # highlight last codepart
225 formatter = HtmlFormatter(linenos='inline', linenostart=line_code_start, cssclass="code-highlight")
226 html_code = html_code + highlight(tmp_code, lexer, formatter)
227
228 return html_code
229
230
231def get_result_template(theme_name: str, template_name: str):
232 themed_path = theme_name + '/result_templates/' + template_name
233 if themed_path in result_templates:
234 return themed_path
235 return 'result_templates/' + template_name
236
237
238_STATIC_FILES: list[str] = []
239
240
241def custom_url_for(endpoint: str, **values):
242 global _STATIC_FILES # pylint: disable=global-statement
243 if not _STATIC_FILES:
244 _STATIC_FILES = webutils.get_static_file_list()
245
246 # handled by WhiteNoise
247 if endpoint == "static" and values.get("filename"):
248
249 # We need to verify the "filename" argument: in the jinja templates
250 # there could be call like:
251 # url_for('static', filename='img/favicon.png')
252 # which should map to:
253 # static/themes/<theme_name>/img/favicon.png
254
255 arg_filename = values["filename"]
256 if arg_filename not in _STATIC_FILES:
257 # try file in the current theme
258 theme_name = sxng_request.preferences.get_value("theme")
259 theme_filename = f"themes/{theme_name}/{arg_filename}"
260 if theme_filename in _STATIC_FILES:
261 values["filename"] = theme_filename
262
263 app_prefix = url_for("index")
264 return f"{app_prefix}static/{values['filename']}"
265
266 if endpoint == "info" and "locale" not in values:
267
268 # We need to verify the "locale" argument: in the jinja templates there
269 # could be call like:
270 # url_for('info', pagename='about')
271 # which should map to:
272 # info/<locale>/about
273
274 locale = sxng_request.preferences.get_value("locale")
275 if infopage.INFO_PAGES.get_page(values["pagename"], locale) is None:
276 locale = infopage.INFO_PAGES.locale_default
277 values["locale"] = locale
278
279 return url_for(endpoint, **values)
280
281
282def image_proxify(url: str):
283 if not url:
284 return url
285
286 if url.startswith('//'):
287 url = 'https:' + url
288
289 if not sxng_request.preferences.get_value('image_proxy'):
290 return url
291
292 if url.startswith('data:image/'):
293 # 50 is an arbitrary number to get only the beginning of the image.
294 partial_base64 = url[len('data:image/') : 50].split(';')
295 if (
296 len(partial_base64) == 2
297 and partial_base64[0] in ['gif', 'png', 'jpeg', 'pjpeg', 'webp', 'tiff', 'bmp']
298 and partial_base64[1].startswith('base64,')
299 ):
300 return url
301 return None
302
303 h = new_hmac(settings['server']['secret_key'], url.encode())
304
305 return '{0}?{1}'.format(url_for('image_proxy'), urlencode(dict(url=url.encode(), h=h)))
306
307
309 return {
310 # when there is autocompletion
311 'no_item_found': gettext('No item found'),
312 # /preferences: the source of the engine description (wikipedata, wikidata, website)
313 'Source': gettext('Source'),
314 # infinite scroll
315 'error_loading_next_page': gettext('Error loading the next page'),
316 }
317
318
319def get_enabled_categories(category_names: typing.Iterable[str]):
320 """The categories in ``category_names```for which there is no active engine
321 are filtered out and a reduced list is returned."""
322
323 enabled_engines = [item[0] for item in sxng_request.preferences.engines.get_enabled()]
324 enabled_categories = set()
325 for engine_name in enabled_engines:
326 enabled_categories.update(engines[engine_name].categories)
327 return [x for x in category_names if x in enabled_categories]
328
329
330def get_pretty_url(parsed_url: urllib.parse.ParseResult):
331 url_formatting_pref = sxng_request.preferences.get_value('url_formatting')
332
333 if url_formatting_pref == 'full':
334 return [parsed_url.geturl()]
335
336 if url_formatting_pref == 'host':
337 return [parsed_url.netloc]
338
339 path = parsed_url.path
340 path = path[:-1] if len(path) > 0 and path[-1] == '/' else path
341 path = unquote(path.replace("/", " › "))
342 return [parsed_url.scheme + "://" + parsed_url.netloc, path]
343
344
346 req_pref = sxng_request.preferences
347 return {
348 'autocomplete': req_pref.get_value('autocomplete'),
349 'autocomplete_min': get_setting('search.autocomplete_min'),
350 'method': req_pref.get_value('method'),
351 'infinite_scroll': req_pref.get_value('infinite_scroll'),
352 'translations': get_translations(),
353 'search_on_category_select': req_pref.get_value('search_on_category_select'),
354 'hotkeys': req_pref.get_value('hotkeys'),
355 'url_formatting': req_pref.get_value('url_formatting'),
356 'theme_static_path': custom_url_for('static', filename='themes/simple'),
357 'results_on_new_tab': req_pref.get_value('results_on_new_tab'),
358 'favicon_resolver': req_pref.get_value('favicon_resolver'),
359 'advanced_search': req_pref.get_value('advanced_search'),
360 'query_in_title': req_pref.get_value('query_in_title'),
361 'safesearch': str(req_pref.get_value('safesearch')),
362 'theme': req_pref.get_value('theme'),
363 'doi_resolver': get_doi_resolver(),
364 }
365
366
367def render(template_name: str, **kwargs):
368 # values from the preferences
369 # pylint: disable=too-many-statements
370 client_settings = get_client_settings()
371 kwargs['client_settings'] = str(
372 base64.b64encode(
373 bytes(
374 json.dumps(client_settings),
375 encoding='utf-8',
376 )
377 ),
378 encoding='utf-8',
379 )
380 kwargs['preferences'] = sxng_request.preferences
381 kwargs.update(client_settings)
382
383 # values from the HTTP requests
384 kwargs['endpoint'] = 'results' if 'q' in kwargs else sxng_request.endpoint
385 kwargs['cookies'] = sxng_request.cookies
386 kwargs['errors'] = sxng_request.errors
387 kwargs['link_token'] = link_token.get_token()
388
389 kwargs['categories_as_tabs'] = list(settings['categories_as_tabs'].keys())
390 kwargs['categories'] = get_enabled_categories(settings['categories_as_tabs'].keys())
391 kwargs['DEFAULT_CATEGORY'] = DEFAULT_CATEGORY
392
393 # i18n
394 kwargs['sxng_locales'] = [l for l in sxng_locales if l[0] in settings['search']['languages']]
395
396 locale = sxng_request.preferences.get_value('locale')
397 kwargs['locale_rfc5646'] = _get_locale_rfc5646(locale)
398
399 if locale in RTL_LOCALES and 'rtl' not in kwargs:
400 kwargs['rtl'] = True
401
402 if 'current_language' not in kwargs:
403 kwargs['current_language'] = parse_lang(sxng_request.preferences, {}, RawTextQuery('', []))
404
405 # values from settings
406 kwargs['search_formats'] = [x for x in settings['search']['formats'] if x != 'html']
407 kwargs['instance_name'] = get_setting('general.instance_name')
408 kwargs['searx_version'] = VERSION_STRING
409 kwargs['searx_git_url'] = GIT_URL
410 kwargs['enable_metrics'] = get_setting('general.enable_metrics')
411 kwargs['get_setting'] = get_setting
412 kwargs['get_pretty_url'] = get_pretty_url
413
414 # values from settings: donation_url
415 donation_url = get_setting('general.donation_url')
416 if donation_url is True:
417 donation_url = custom_url_for('info', pagename='donate')
418 kwargs['donation_url'] = donation_url
419
420 # helpers to create links to other pages
421 kwargs['url_for'] = custom_url_for # override url_for function in templates
422 kwargs['image_proxify'] = image_proxify
423 kwargs['favicon_url'] = favicons.favicon_url
424 kwargs['cache_url'] = settings['ui']['cache_url']
425 kwargs['get_result_template'] = get_result_template
426 kwargs['opensearch_url'] = (
427 url_for('opensearch')
428 + '?'
429 + urlencode(
430 {
431 'method': sxng_request.preferences.get_value('method'),
432 'autocomplete': sxng_request.preferences.get_value('autocomplete'),
433 }
434 )
435 )
436 kwargs['urlparse'] = urlparse
437
438 start_time = default_timer()
439 result = render_template('{}/{}'.format(kwargs['theme'], template_name), **kwargs)
440 sxng_request.render_time += default_timer() - start_time # pylint: disable=assigning-non-slot
441
442 return result
443
444
445@app.before_request
447 sxng_request.start_time = default_timer() # pylint: disable=assigning-non-slot
448 sxng_request.render_time = 0 # pylint: disable=assigning-non-slot
449 sxng_request.timings = [] # pylint: disable=assigning-non-slot
450 sxng_request.errors = [] # pylint: disable=assigning-non-slot
451
452 client_pref = ClientPref.from_http_request(sxng_request)
453 # pylint: disable=redefined-outer-name
454 preferences = Preferences(themes, list(categories.keys()), engines, searx.plugins.STORAGE, client_pref)
455
456 user_agent = sxng_request.headers.get('User-Agent', '').lower()
457 if 'webkit' in user_agent and 'android' in user_agent:
458 preferences.key_value_settings['method'].value = 'GET'
459 sxng_request.preferences = preferences # pylint: disable=assigning-non-slot
460
461 try:
462 preferences.parse_dict(sxng_request.cookies)
463
464 except Exception as e: # pylint: disable=broad-except
465 logger.exception(e, exc_info=True)
466 sxng_request.errors.append(gettext('Invalid settings, please edit your preferences'))
467
468 # merge GET, POST vars
469 # HINT request.form is of type werkzeug.datastructures.ImmutableMultiDict
470 sxng_request.form = dict(sxng_request.form.items()) # type: ignore
471 for k, v in sxng_request.args.items():
472 if k not in sxng_request.form:
473 sxng_request.form[k] = v
474
475 if sxng_request.form.get('preferences'):
476 preferences.parse_encoded_data(sxng_request.form['preferences'])
477 else:
478 try:
479 preferences.parse_dict(sxng_request.form)
480 except Exception as e: # pylint: disable=broad-except
481 logger.exception(e, exc_info=True)
482 sxng_request.errors.append(gettext('Invalid settings'))
483
484 # language is defined neither in settings nor in preferences
485 # use browser headers
486 if not preferences.get_value("language"):
487 language = _get_browser_language(sxng_request, settings['search']['languages'])
488 preferences.parse_dict({"language": language})
489 logger.debug('set language %s (from browser)', preferences.get_value("language"))
490
491 # UI locale is defined neither in settings nor in preferences
492 # use browser headers
493 if not preferences.get_value("locale"):
494 locale = _get_browser_language(sxng_request, LOCALE_NAMES.keys())
495 preferences.parse_dict({"locale": locale})
496 logger.debug('set locale %s (from browser)', preferences.get_value("locale"))
497
498 # request.user_plugins
499 sxng_request.user_plugins = [] # pylint: disable=assigning-non-slot
500 allowed_plugins = preferences.plugins.get_enabled()
501 disabled_plugins = preferences.plugins.get_disabled()
502 for plugin in searx.plugins.STORAGE:
503 if (plugin.id not in disabled_plugins) or plugin.id in allowed_plugins:
504 sxng_request.user_plugins.append(plugin.id)
505
506
507@app.after_request
508def add_default_headers(response: flask.Response):
509 # set default http headers
510 for header, value in settings['server']['default_http_headers'].items():
511 if header in response.headers:
512 continue
513 response.headers[header] = value
514 return response
515
516
517@app.after_request
518def post_request(response: flask.Response):
519 total_time = default_timer() - sxng_request.start_time
520 timings_all = [
521 'total;dur=' + str(round(total_time * 1000, 3)),
522 'render;dur=' + str(round(sxng_request.render_time * 1000, 3)),
523 ]
524 if len(sxng_request.timings) > 0:
525 timings = sorted(sxng_request.timings, key=lambda t: t.total)
526 timings_total = [
527 'total_' + str(i) + '_' + t.engine + ';dur=' + str(round(t.total * 1000, 3)) for i, t in enumerate(timings)
528 ]
529 timings_load = [
530 'load_' + str(i) + '_' + t.engine + ';dur=' + str(round(t.load * 1000, 3))
531 for i, t in enumerate(timings)
532 if t.load
533 ]
534 timings_all = timings_all + timings_total + timings_load
535 response.headers.add('Server-Timing', ', '.join(timings_all))
536 return response
537
538
539def index_error(output_format: str, error_message: str):
540 if output_format == 'json':
541 return Response(json.dumps({'error': error_message}), mimetype='application/json')
542 if output_format == 'csv':
543 response = Response('', mimetype='application/csv')
544 cont_disp = 'attachment;Filename=searx.csv'
545 response.headers.add('Content-Disposition', cont_disp)
546 return response
547
548 if output_format == 'rss':
549 response_rss = render(
550 'opensearch_response_rss.xml',
551 results=[],
552 q=sxng_request.form['q'] if 'q' in sxng_request.form else '',
553 number_of_results=0,
554 error_message=error_message,
555 )
556 return Response(response_rss, mimetype='text/xml')
557
558 # html
559 sxng_request.errors.append(gettext('search error'))
560 return render(
561 # fmt: off
562 'index.html',
563 selected_categories=get_selected_categories(sxng_request.preferences, sxng_request.form),
564 # fmt: on
565 )
566
567
568@app.route('/', methods=['GET', 'POST'])
569def index():
570 """Render index page."""
571
572 # redirect to search if there's a query in the request
573 if sxng_request.form.get('q'):
574 query = ('?' + sxng_request.query_string.decode()) if sxng_request.query_string else ''
575 return redirect(url_for('search') + query, 308)
576
577 return render(
578 # fmt: off
579 'index.html',
580 selected_categories=get_selected_categories(sxng_request.preferences, sxng_request.form),
581 current_locale = sxng_request.preferences.get_value("locale"),
582 # fmt: on
583 )
584
585
586@app.route('/healthz', methods=['GET'])
587def health():
588 return Response('OK', mimetype='text/plain')
589
590
591@app.route('/client<token>.css', methods=['GET', 'POST'])
592def client_token(token=None):
593 link_token.ping(sxng_request, token)
594 return Response('', mimetype='text/css', headers={"Cache-Control": "no-store, max-age=0"})
595
596
597@app.route('/rss.xsl', methods=['GET', 'POST'])
599 return render_template(
600 f"{sxng_request.preferences.get_value('theme')}/rss.xsl",
601 url_for=custom_url_for,
602 )
603
604
605@app.route('/search', methods=['GET', 'POST'])
606def search():
607 """Search query in q and return results.
608
609 Supported outputs: html, json, csv, rss.
610 """
611 # pylint: disable=too-many-locals, too-many-return-statements, too-many-branches
612 # pylint: disable=too-many-statements
613
614 # output_format
615 output_format = sxng_request.form.get('format', 'html')
616 if output_format not in OUTPUT_FORMATS:
617 output_format = 'html'
618
619 if output_format not in settings['search']['formats']:
620 flask.abort(403)
621
622 # check if there is query (not None and not an empty string)
623 if not sxng_request.form.get('q'):
624 if output_format == 'html':
625 return render(
626 # fmt: off
627 'index.html',
628 selected_categories=get_selected_categories(sxng_request.preferences, sxng_request.form),
629 # fmt: on
630 )
631 return index_error(output_format, 'No query'), 400
632
633 # search
634 search_query = None
635 raw_text_query = None
636 result_container = None
637 try:
638 search_query, raw_text_query, _, _, selected_locale = get_search_query_from_webapp(
639 sxng_request.preferences, sxng_request.form
640 )
641 search_obj = searx.search.SearchWithPlugins(search_query, sxng_request, sxng_request.user_plugins)
642 result_container = search_obj.search()
643
644 except SearxParameterException as e:
645 logger.exception('search error: SearxParameterException')
646 return index_error(output_format, e.message), 400
647 except Exception as e: # pylint: disable=broad-except
648 logger.exception(e, exc_info=True)
649 return index_error(output_format, gettext('search error')), 500
650
651 # 1. check if the result is a redirect for an external bang
652 if result_container.redirect_url:
653 return redirect(result_container.redirect_url)
654
655 # 2. add Server-Timing header for measuring performance characteristics of
656 # web applications
657 sxng_request.timings = result_container.get_timings() # pylint: disable=assigning-non-slot
658
659 # 3. formats without a template
660
661 if output_format == 'json':
662
663 response = webutils.get_json_response(search_query, result_container)
664 return Response(response, mimetype='application/json')
665
666 if output_format == 'csv':
667
668 csv = webutils.CSVWriter(StringIO())
669 webutils.write_csv_response(csv, result_container)
670 csv.stream.seek(0)
671
672 response = Response(csv.stream.read(), mimetype='application/csv')
673 cont_disp = 'attachment;Filename=searx_-_{0}.csv'.format(search_query.query)
674 response.headers.add('Content-Disposition', cont_disp)
675 return response
676
677 # 4. formats rendered by a template / RSS & HTML
678
679 current_template = None
680 previous_result = None
681
682 results = result_container.get_ordered_results()
683
684 if search_query.redirect_to_first_result and results:
685 return redirect(results[0]['url'], 302)
686
687 for result in results:
688 if output_format == 'html':
689 if 'content' in result and result['content']:
690 result['content'] = highlight_content(escape(result['content'][:1024]), search_query.query)
691 if 'title' in result and result['title']:
692 result['title'] = highlight_content(escape(result['title'] or ''), search_query.query)
693
694 # set result['open_group'] = True when the template changes from the previous result
695 # set result['close_group'] = True when the template changes on the next result
696 if current_template != result.template:
697 result.open_group = True
698 if previous_result:
699 previous_result.close_group = True # pylint: disable=unsupported-assignment-operation
700 current_template = result.template
701 previous_result = result
702
703 if previous_result:
704 previous_result.close_group = True
705
706 # 4.a RSS
707
708 if output_format == 'rss':
709 response_rss = render(
710 'opensearch_response_rss.xml',
711 results=results,
712 q=sxng_request.form['q'],
713 number_of_results=result_container.number_of_results,
714 )
715 return Response(response_rss, mimetype='text/xml')
716
717 # 4.b HTML
718
719 # suggestions: use RawTextQuery to get the suggestion URLs with the same bang
720 suggestion_urls = list(
721 map(
722 lambda suggestion: {'url': raw_text_query.changeQuery(suggestion).getFullQuery(), 'title': suggestion},
723 result_container.suggestions,
724 )
725 )
726
727 correction_urls = list(
728 map(
729 lambda correction: {'url': raw_text_query.changeQuery(correction).getFullQuery(), 'title': correction},
730 result_container.corrections,
731 )
732 )
733
734 # engine_timings: get engine response times sorted from slowest to fastest
735 engine_timings = sorted(result_container.get_timings(), reverse=True, key=lambda e: e.total)
736 max_response_time = engine_timings[0].total if engine_timings else None
737 engine_timings_pairs = [(timing.engine, timing.total) for timing in engine_timings]
738
739 # search_query.lang contains the user choice (all, auto, en, ...)
740 # when the user choice is "auto", search.search_query.lang contains the detected language
741 # otherwise it is equals to search_query.lang
742 return render(
743 # fmt: off
744 'results.html',
745 results = results,
746 q=sxng_request.form['q'],
747 selected_categories = search_query.categories,
748 pageno = search_query.pageno,
749 time_range = search_query.time_range or '',
750 number_of_results = format_decimal(result_container.number_of_results),
751 suggestions = suggestion_urls,
752 answers = result_container.answers,
753 corrections = correction_urls,
754 infoboxes = result_container.infoboxes,
755 engine_data = result_container.engine_data,
756 paging = result_container.paging,
757 unresponsive_engines = webutils.get_translated_errors(
758 result_container.unresponsive_engines
759 ),
760 current_locale = sxng_request.preferences.get_value("locale"),
761 current_language = selected_locale,
762 search_language = match_locale(
763 search_obj.search_query.lang,
764 settings['search']['languages'],
765 fallback=sxng_request.preferences.get_value("language")
766 ),
767 timeout_limit = sxng_request.form.get('timeout_limit', None),
768 timings = engine_timings_pairs,
769 max_response_time = max_response_time
770 # fmt: on
771 )
772
773
774@app.route('/about', methods=['GET'])
775def about():
776 """Redirect to about page"""
777 # custom_url_for is going to add the locale
778 return redirect(custom_url_for('info', pagename='about'))
779
780
781@app.route('/info/<locale>/<pagename>', methods=['GET'])
782def info(pagename, locale):
783 """Render page of online user documentation"""
784 page = infopage.INFO_PAGES.get_page(pagename, locale)
785 if page is None:
786 flask.abort(404)
787
788 user_locale = sxng_request.preferences.get_value('locale')
789 return render(
790 'info.html',
791 all_pages=infopage.INFO_PAGES.iter_pages(user_locale, fallback_to_default=True),
792 active_page=page,
793 active_pagename=pagename,
794 )
795
796
797@app.route('/autocompleter', methods=['GET', 'POST'])
799 """Return autocompleter results"""
800
801 # run autocompleter
802 results = []
803
804 # set blocked engines
805 disabled_engines = sxng_request.preferences.engines.get_disabled()
806
807 # parse query
808 raw_text_query = RawTextQuery(sxng_request.form.get('q', ''), disabled_engines)
809 sug_prefix = raw_text_query.getQuery()
810
811 for obj in searx.answerers.STORAGE.ask(sug_prefix):
812 if isinstance(obj, Answer):
813 results.append(obj.answer)
814
815 # normal autocompletion results only appear if no inner results returned
816 # and there is a query part
817 if len(raw_text_query.autocomplete_list) == 0 and len(sug_prefix) > 0:
818
819 # get SearXNG's locale and autocomplete backend from cookie
820 sxng_locale = sxng_request.preferences.get_value('language')
821 backend_name = sxng_request.preferences.get_value('autocomplete')
822
823 for result in search_autocomplete(backend_name, sug_prefix, sxng_locale):
824 # attention: this loop will change raw_text_query object and this is
825 # the reason why the sug_prefix was stored before (see above)
826 if result != sug_prefix:
827 results.append(raw_text_query.changeQuery(result).getFullQuery())
828
829 if len(raw_text_query.autocomplete_list) > 0:
830 for autocomplete_text in raw_text_query.autocomplete_list:
831 results.append(raw_text_query.get_autocomplete_full_query(autocomplete_text))
832
833 if sxng_request.headers.get('X-Requested-With') == 'XMLHttpRequest':
834 # the suggestion request comes from the searx search form
835 suggestions = json.dumps(results)
836 mimetype = 'application/json'
837 else:
838 # the suggestion request comes from browser's URL bar
839 suggestions = json.dumps([sug_prefix, results])
840 mimetype = 'application/x-suggestions+json'
841
842 suggestions = escape(suggestions, False)
843 return Response(suggestions, mimetype=mimetype)
844
845
846@app.route('/preferences', methods=['GET', 'POST'])
848 """Render preferences page && save user preferences"""
849
850 # pylint: disable=too-many-locals, too-many-return-statements, too-many-branches
851 # pylint: disable=too-many-statements
852
853 # save preferences using the link the /preferences?preferences=...
854 if sxng_request.args.get('preferences') or sxng_request.form.get('preferences'):
855 resp = make_response(redirect(url_for('index', _external=True)))
856 return sxng_request.preferences.save(resp)
857
858 # save preferences
859 if sxng_request.method == 'POST':
860 resp = make_response(redirect(url_for('index', _external=True)))
861 try:
862 sxng_request.preferences.parse_form(sxng_request.form)
863 except ValidationException:
864 sxng_request.errors.append(gettext('Invalid settings, please edit your preferences'))
865 return resp
866 return sxng_request.preferences.save(resp)
867
868 # render preferences
869 image_proxy = sxng_request.preferences.get_value('image_proxy') # pylint: disable=redefined-outer-name
870 disabled_engines = sxng_request.preferences.engines.get_disabled()
871 allowed_plugins = sxng_request.preferences.plugins.get_enabled()
872
873 # stats for preferences page
874 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
875
876 engines_by_category = {}
877
878 for c in categories: # pylint: disable=consider-using-dict-items
879 engines_by_category[c] = [e for e in categories[c] if e.name in filtered_engines]
880 # sort the engines alphabetically since the order in settings.yml is meaningless.
881 list.sort(engines_by_category[c], key=lambda e: e.name)
882
883 # get first element [0], the engine time,
884 # and then the second element [1] : the time (the first one is the label)
885 stats = {} # pylint: disable=redefined-outer-name
886 max_rate95 = 0
887 for _, e in filtered_engines.items():
888 h = histogram('engine', e.name, 'time', 'total')
889 median = round(h.percentage(50), 1) if h.count > 0 else None
890 rate80 = round(h.percentage(80), 1) if h.count > 0 else None
891 rate95 = round(h.percentage(95), 1) if h.count > 0 else None
892
893 max_rate95 = max(max_rate95, rate95 or 0)
894
895 result_count_sum = histogram('engine', e.name, 'result', 'count').sum
896 successful_count = counter('engine', e.name, 'search', 'count', 'successful')
897 result_count = int(result_count_sum / float(successful_count)) if successful_count else 0
898
899 stats[e.name] = {
900 'time': median,
901 'rate80': rate80,
902 'rate95': rate95,
903 'warn_timeout': e.timeout > settings['outgoing']['request_timeout'],
904 'supports_selected_language': e.traits.is_locale_supported(
905 str(sxng_request.preferences.get_value('language') or 'all')
906 ),
907 'result_count': result_count,
908 }
909 # end of stats
910
911 # reliabilities
912 reliabilities = {}
913 engine_errors = get_engine_errors(filtered_engines)
914 checker_results = checker_get_result()
915 checker_results = (
916 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
917 )
918 for _, e in filtered_engines.items():
919 checker_result = checker_results.get(e.name, {})
920 checker_success = checker_result.get('success', True)
921 errors = engine_errors.get(e.name) or []
922 if counter('engine', e.name, 'search', 'count', 'sent') == 0:
923 # no request
924 reliability = None
925 elif checker_success and not errors:
926 reliability = 100
927 elif 'simple' in checker_result.get('errors', {}):
928 # the basic (simple) test doesn't work: the engine is broken according to the checker
929 # even if there is no exception
930 reliability = 0
931 else:
932 # pylint: disable=consider-using-generator
933 reliability = 100 - sum([error['percentage'] for error in errors if not error.get('secondary')])
934
935 reliabilities[e.name] = {
936 'reliability': reliability,
937 'errors': [],
938 'checker': checker_results.get(e.name, {}).get('errors', {}).keys(),
939 }
940 # keep the order of the list checker_results[e.name]['errors'] and deduplicate.
941 # the first element has the highest percentage rate.
942 reliabilities_errors = []
943 for error in errors:
944 error_user_text = None
945 if error.get('secondary') or 'exception_classname' not in error:
946 continue
947 error_user_text = exception_classname_to_text.get(error.get('exception_classname'))
948 if not error:
949 error_user_text = exception_classname_to_text[None]
950 if error_user_text not in reliabilities_errors:
951 reliabilities_errors.append(error_user_text)
952 reliabilities[e.name]['errors'] = reliabilities_errors
953
954 # supports
955 supports = {}
956 for _, e in filtered_engines.items():
957 supports_selected_language = e.traits.is_locale_supported(
958 str(sxng_request.preferences.get_value('language') or 'all')
959 )
960 safesearch = e.safesearch
961 time_range_support = e.time_range_support
962 for checker_test_name in checker_results.get(e.name, {}).get('errors', {}):
963 if supports_selected_language and checker_test_name.startswith('lang_'):
964 supports_selected_language = '?'
965 elif safesearch and checker_test_name == 'safesearch':
966 safesearch = '?'
967 elif time_range_support and checker_test_name == 'time_range':
968 time_range_support = '?'
969 supports[e.name] = {
970 'supports_selected_language': supports_selected_language,
971 'safesearch': safesearch,
972 'time_range_support': time_range_support,
973 }
974
975 return render(
976 # fmt: off
977 'preferences.html',
978 preferences = True,
979 selected_categories = get_selected_categories(sxng_request.preferences, sxng_request.form),
980 locales = LOCALE_NAMES,
981 current_locale = sxng_request.preferences.get_value("locale"),
982 image_proxy = image_proxy,
983 engines_by_category = engines_by_category,
984 stats = stats,
985 max_rate95 = max_rate95,
986 reliabilities = reliabilities,
987 supports = supports,
988 answer_storage = searx.answerers.STORAGE.info,
989 disabled_engines = disabled_engines,
990 autocomplete_backends = autocomplete_backends,
991 favicon_resolver_names = favicons.proxy.CFG.resolver_map.keys(),
992 shortcuts = {y: x for x, y in engine_shortcuts.items()},
993 themes = themes,
994 plugins_storage = searx.plugins.STORAGE.info,
995 current_doi_resolver = get_doi_resolver(),
996 allowed_plugins = allowed_plugins,
997 preferences_url_params = sxng_request.preferences.get_as_url_params(),
998 locked_preferences = get_setting("preferences.lock", []),
999 doi_resolvers = get_setting("doi_resolvers", {}),
1000 # fmt: on
1001 )
1002
1003
1004app.add_url_rule('/favicon_proxy', methods=['GET'], endpoint="favicon_proxy", view_func=favicons.favicon_proxy)
1005
1006
1007@app.route('/image_proxy', methods=['GET'])
1009 # pylint: disable=too-many-return-statements, too-many-branches
1010
1011 url = sxng_request.args.get('url')
1012 if not url:
1013 return '', 400
1014
1015 if not is_hmac_of(settings['server']['secret_key'], url.encode(), sxng_request.args.get('h', '')):
1016 return '', 400
1017
1018 maximum_size = 5 * 1024 * 1024
1019 forward_resp = False
1020 resp = None
1021 try:
1022 request_headers = {
1023 'User-Agent': gen_useragent(),
1024 'Accept': 'image/webp,*/*',
1025 'Accept-Encoding': 'gzip, deflate',
1026 'Sec-GPC': '1',
1027 'DNT': '1',
1028 }
1029 set_context_network_name('image_proxy')
1030 resp, stream = http_stream(method='GET', url=url, headers=request_headers, allow_redirects=True)
1031 content_length = resp.headers.get('Content-Length')
1032 if content_length and content_length.isdigit() and int(content_length) > maximum_size:
1033 return 'Max size', 400
1034
1035 if resp.status_code != 200:
1036 logger.debug('image-proxy: wrong response code: %i', resp.status_code)
1037 if resp.status_code >= 400:
1038 return '', resp.status_code
1039 return '', 400
1040
1041 if not resp.headers.get('Content-Type', '').startswith('image/') and not resp.headers.get(
1042 'Content-Type', ''
1043 ).startswith('binary/octet-stream'):
1044 logger.debug('image-proxy: wrong content-type: %s', resp.headers.get('Content-Type', ''))
1045 return '', 400
1046
1047 forward_resp = True
1048 except httpx.HTTPError:
1049 logger.exception('HTTP error')
1050 return '', 400
1051 finally:
1052 if resp and not forward_resp:
1053 # the code is about to return an HTTP 400 error to the browser
1054 # we make sure to close the response between searxng and the HTTP server
1055 try:
1056 resp.close()
1057 except httpx.HTTPError:
1058 logger.exception('HTTP error on closing')
1059
1060 def close_stream():
1061 nonlocal resp, stream
1062 try:
1063 if resp:
1064 resp.close()
1065 del resp
1066 del stream
1067 except httpx.HTTPError as e:
1068 logger.debug('Exception while closing response', e)
1069
1070 try:
1071 headers = dict_subset(resp.headers, {'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'})
1072 response = Response(stream, mimetype=resp.headers['Content-Type'], headers=headers, direct_passthrough=True)
1073 response.call_on_close(close_stream)
1074 return response
1075 except httpx.HTTPError:
1076 close_stream()
1077 return '', 400
1078
1079
1080@app.route('/engine_descriptions.json', methods=['GET'])
1082 sxng_ui_lang_tag = get_locale().replace("_", "-")
1083 sxng_ui_lang_tag = LOCALE_BEST_MATCH.get(sxng_ui_lang_tag, sxng_ui_lang_tag)
1084
1085 result = ENGINE_DESCRIPTIONS['en'].copy()
1086 if sxng_ui_lang_tag != 'en':
1087 for engine, description in ENGINE_DESCRIPTIONS.get(sxng_ui_lang_tag, {}).items():
1088 result[engine] = description
1089 for engine, description in result.items():
1090 if len(description) == 2 and description[1] == 'ref':
1091 ref_engine, ref_lang = description[0].split(':')
1092 description = ENGINE_DESCRIPTIONS[ref_lang][ref_engine]
1093 if isinstance(description, str):
1094 description = [description, 'wikipedia']
1095 result[engine] = description
1096
1097 # overwrite by about:description (from settings)
1098 for engine_name, engine_mod in engines.items():
1099 descr = getattr(engine_mod, 'about', {}).get('description', None)
1100 if descr is not None:
1101 result[engine_name] = [descr, "SearXNG config"]
1102
1103 return jsonify(result)
1104
1105
1106@app.route('/stats', methods=['GET'])
1107def stats():
1108 """Render engine statistics page."""
1109 sort_order = sxng_request.args.get('sort', default='name', type=str)
1110 selected_engine_name = sxng_request.args.get('engine', default=None, type=str)
1111
1112 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1113 if selected_engine_name:
1114 if selected_engine_name not in filtered_engines:
1115 selected_engine_name = None
1116 else:
1117 filtered_engines = [selected_engine_name]
1118
1119 checker_results = checker_get_result()
1120 checker_results = (
1121 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
1122 )
1123
1124 engine_stats = get_engines_stats(filtered_engines)
1125 engine_reliabilities = get_reliabilities(filtered_engines, checker_results)
1126
1127 if sort_order not in STATS_SORT_PARAMETERS:
1128 sort_order = 'name'
1129
1130 reverse, key_name, default_value = STATS_SORT_PARAMETERS[sort_order]
1131
1132 def get_key(engine_stat):
1133 reliability = engine_reliabilities.get(engine_stat['name'], {}).get('reliability', 0)
1134 reliability_order = 0 if reliability else 1
1135 if key_name == 'reliability':
1136 key = reliability
1137 reliability_order = 0
1138 else:
1139 key = engine_stat.get(key_name) or default_value
1140 if reverse:
1141 reliability_order = 1 - reliability_order
1142 return (reliability_order, key, engine_stat['name'])
1143
1144 technical_report = []
1145 for error in engine_reliabilities.get(selected_engine_name, {}).get('errors', []):
1146 technical_report.append(
1147 f"\
1148 Error: {error['exception_classname'] or error['log_message']} \
1149 Parameters: {error['log_parameters']} \
1150 File name: {error['filename'] }:{ error['line_no'] } \
1151 Error Function: {error['function']} \
1152 Code: {error['code']} \
1153 ".replace(
1154 ' ' * 12, ''
1155 ).strip()
1156 )
1157 technical_report = ' '.join(technical_report)
1158
1159 engine_stats['time'] = sorted(engine_stats['time'], reverse=reverse, key=get_key)
1160 return render(
1161 # fmt: off
1162 'stats.html',
1163 sort_order = sort_order,
1164 engine_stats = engine_stats,
1165 engine_reliabilities = engine_reliabilities,
1166 selected_engine_name = selected_engine_name,
1167 searx_git_branch = GIT_BRANCH,
1168 technical_report = technical_report,
1169 # fmt: on
1170 )
1171
1172
1173@app.route('/stats/errors', methods=['GET'])
1175 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1176 result = get_engine_errors(filtered_engines)
1177 return jsonify(result)
1178
1179
1180@app.route('/stats/checker', methods=['GET'])
1182 result = checker_get_result()
1183 return jsonify(result)
1184
1185
1186@app.route('/metrics')
1188 password = settings['general'].get("open_metrics")
1189
1190 if not (settings['general'].get("enable_metrics") and password):
1191 return Response('open metrics is disabled', status=404, mimetype='text/plain')
1192
1193 if not sxng_request.authorization or sxng_request.authorization.password != password:
1194 return Response('access forbidden', status=401, mimetype='text/plain')
1195
1196 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1197
1198 checker_results = checker_get_result()
1199 checker_results = (
1200 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
1201 )
1202
1203 engine_stats = get_engines_stats(filtered_engines)
1204 engine_reliabilities = get_reliabilities(filtered_engines, checker_results)
1205 metrics_text = openmetrics(engine_stats, engine_reliabilities)
1206
1207 return Response(metrics_text, mimetype='text/plain')
1208
1209
1210@app.route('/robots.txt', methods=['GET'])
1212 return Response(
1213 """User-agent: *
1214Allow: /info/en/about
1215Disallow: /stats
1216Disallow: /image_proxy
1217Disallow: /preferences
1218Disallow: /*?*q=*
1219""",
1220 mimetype='text/plain',
1221 )
1222
1223
1224@app.route('/opensearch.xml', methods=['GET'])
1226 method = sxng_request.preferences.get_value('method')
1227 autocomplete = sxng_request.preferences.get_value('autocomplete')
1228
1229 # chrome/chromium only supports HTTP GET....
1230 if sxng_request.headers.get('User-Agent', '').lower().find('webkit') >= 0:
1231 method = 'GET'
1232
1233 if method not in ('POST', 'GET'):
1234 method = 'POST'
1235
1236 ret = render('opensearch.xml', opensearch_method=method, autocomplete=autocomplete)
1237 resp = Response(response=ret, status=200, mimetype="application/opensearchdescription+xml")
1238 return resp
1239
1240
1241@app.route('/favicon.ico')
1243 theme = sxng_request.preferences.get_value("theme")
1244 return send_from_directory(
1245 os.path.join(app.root_path, settings['ui']['static_path'], 'themes', theme, 'img'), # type: ignore
1246 'favicon.png',
1247 mimetype='image/vnd.microsoft.icon',
1248 )
1249
1250
1251@app.route('/clear_cookies')
1253 resp = make_response(redirect(url_for('index', _external=True)))
1254 for cookie_name in sxng_request.cookies:
1255 resp.delete_cookie(cookie_name)
1256 return resp
1257
1258
1259@app.route('/config')
1261 """Return configuration in JSON format."""
1262 _engines = []
1263 for name, engine in engines.items():
1264 if not sxng_request.preferences.validate_token(engine):
1265 continue
1266
1267 _languages = engine.traits.languages.keys()
1268 _engines.append(
1269 {
1270 'name': name,
1271 'categories': engine.categories,
1272 'shortcut': engine.shortcut,
1273 'enabled': not engine.disabled,
1274 'paging': engine.paging,
1275 'language_support': engine.language_support,
1276 'languages': list(_languages),
1277 'regions': list(engine.traits.regions.keys()),
1278 'safesearch': engine.safesearch,
1279 'time_range_support': engine.time_range_support,
1280 'timeout': engine.timeout,
1281 }
1282 )
1283
1284 _plugins = []
1285 for _ in searx.plugins.STORAGE:
1286 _plugins.append({'name': _.id, 'enabled': _.active})
1287
1288 _limiter_cfg = limiter.get_cfg()
1289
1290 return jsonify(
1291 {
1292 'categories': list(categories.keys()),
1293 'engines': _engines,
1294 'plugins': _plugins,
1295 'instance_name': settings['general']['instance_name'],
1296 'locales': LOCALE_NAMES,
1297 'default_locale': settings['ui']['default_locale'],
1298 'autocomplete': settings['search']['autocomplete'],
1299 'safe_search': settings['search']['safe_search'],
1300 'default_theme': settings['ui']['default_theme'],
1301 'version': VERSION_STRING,
1302 'brand': {
1303 'PRIVACYPOLICY_URL': get_setting('general.privacypolicy_url'),
1304 'CONTACT_URL': get_setting('general.contact_url'),
1305 'GIT_URL': GIT_URL,
1306 'GIT_BRANCH': GIT_BRANCH,
1307 'DOCS_URL': get_setting('brand.docs_url'),
1308 },
1309 'limiter': {
1310 'enabled': limiter.is_installed(),
1311 'botdetection.ip_limit.link_token': _limiter_cfg.get('botdetection.ip_limit.link_token'),
1312 'botdetection.ip_lists.pass_searxng_org': _limiter_cfg.get('botdetection.ip_lists.pass_searxng_org'),
1313 },
1314 'doi_resolvers': list(settings['doi_resolvers'].keys()),
1315 'default_doi_resolver': settings['default_doi_resolver'],
1316 'public_instance': settings['server']['public_instance'],
1317 }
1318 )
1319
1320
1321@app.errorhandler(404)
1323 return render('404.html'), 404
1324
1325
1326def run():
1327 """Runs the application on a local development server.
1328
1329 This run method is only called when SearXNG is started via ``__main__``::
1330
1331 python -m searx.webapp
1332
1333 Do not use :ref:`run() <flask.Flask.run>` in a production setting. It is
1334 not intended to meet security and performance requirements for a production
1335 server.
1336
1337 It is not recommended to use this function for development with automatic
1338 reloading as this is badly supported. Instead you should be using the flask
1339 command line script’s run support::
1340
1341 flask --app searx.webapp run --debug --reload --host 127.0.0.1 --port 8888
1342
1343 .. _Flask.run: https://flask.palletsprojects.com/en/stable/api/#flask.Flask.run
1344 """
1345
1346 host: str = get_setting("server.bind_address") # type: ignore
1347 port: int = get_setting("server.port") # type: ignore
1348
1349 if searx.sxng_debug:
1350 logger.debug("run local development server (DEBUG) on %s:%s", host, port)
1351 app.run(
1352 debug=True,
1353 port=port,
1354 host=host,
1355 threaded=True,
1356 extra_files=[DEFAULT_SETTINGS_FILE],
1357 )
1358 else:
1359 logger.debug("run local development server on %s:%s", host, port)
1360 app.run(port=port, host=host, threaded=True)
1361
1362
1363def init():
1364
1365 if searx.sxng_debug or app.debug:
1366 app.debug = True
1367 searx.sxng_debug = True
1368
1369 # check secret_key in production
1370
1371 if not app.debug and get_setting("server.secret_key") == 'ultrasecretkey':
1372 logger.error("server.secret_key is not changed. Please use something else instead of ultrasecretkey.")
1373 sys.exit(1)
1374
1375 locales_initialize()
1376 valkey_initialize()
1378
1379 metrics: bool = get_setting("general.enable_metrics") # type: ignore
1380 searx.search.initialize(enable_checker=True, check_network=True, enable_metrics=metrics)
1381
1382 limiter.initialize(app, settings)
1383 favicons.init()
1384
1385
1386def static_headers(headers: Headers, _path: str, _url: str) -> None:
1387 headers['Cache-Control'] = 'public, max-age=30, stale-while-revalidate=60'
1388
1389 for header, value in settings['server']['default_http_headers'].items():
1390 # cast value to string, as WhiteNoise requires header values to be strings
1391 headers[header] = str(value)
1392
1393
1394app.wsgi_app = ProxyFix(app.wsgi_app)
1395app.wsgi_app = WhiteNoise(
1396 app.wsgi_app,
1397 root=settings['ui']['static_path'],
1398 prefix="static",
1399 max_age=None,
1400 allow_all_origins=False,
1401 add_headers_function=static_headers,
1402)
1403
1404patch_application(app)
1405
1406# remove when we drop support for uwsgi
1407application = app
1408
1409init()
1410
1411if __name__ == "__main__":
1412 run()
::1337x
Definition 1337x.py:1
initialize(app)
Definition __init__.py:108
initialize(settings_engines=None, enable_checker=False, check_network=False, enable_metrics=True)
Definition __init__.py:32
None static_headers(Headers headers, str _path, str _url)
Definition webapp.py:1386
_get_browser_language(req, lang_list)
Definition webapp.py:166
engine_descriptions()
Definition webapp.py:1081
info(pagename, locale)
Definition webapp.py:782
autocompleter()
Definition webapp.py:798
index_error(str output_format, str error_message)
Definition webapp.py:539
image_proxify(str url)
Definition webapp.py:282
get_client_settings()
Definition webapp.py:345
client_token(token=None)
Definition webapp.py:592
code_highlighter(codelines, language=None)
Definition webapp.py:184
get_translations()
Definition webapp.py:308
_get_locale_rfc5646(locale)
Definition webapp.py:172
render(str template_name, **kwargs)
Definition webapp.py:367
get_result_template(str theme_name, str template_name)
Definition webapp.py:231
add_default_headers(flask.Response response)
Definition webapp.py:508
post_request(flask.Response response)
Definition webapp.py:518
get_pretty_url(urllib.parse.ParseResult parsed_url)
Definition webapp.py:330
page_not_found(_e)
Definition webapp.py:1322
custom_url_for(str endpoint, **values)
Definition webapp.py:241
get_enabled_categories(typing.Iterable[str] category_names)
Definition webapp.py:319
stats_open_metrics()
Definition webapp.py:1187
get_setting(name, default=_unset)
Definition __init__.py:69