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