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