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