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