.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(),
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 # set result['open_group'] = True when the template changes from the previous result
698 # set result['close_group'] = True when the template changes on the next result
699 if current_template != result.template:
700 result.open_group = True
701 if previous_result:
702 previous_result.close_group = True # pylint: disable=unsupported-assignment-operation
703 current_template = result.template
704 previous_result = result
705
706 if previous_result:
707 previous_result.close_group = True
708
709 # 4.a RSS
710
711 if output_format == 'rss':
712 response_rss = render(
713 'opensearch_response_rss.xml',
714 results=results,
715 q=sxng_request.form['q'],
716 number_of_results=result_container.number_of_results,
717 )
718 return Response(response_rss, mimetype='text/xml')
719
720 # 4.b HTML
721
722 # suggestions: use RawTextQuery to get the suggestion URLs with the same bang
723 suggestion_urls = list(
724 map(
725 lambda suggestion: {'url': raw_text_query.changeQuery(suggestion).getFullQuery(), 'title': suggestion},
726 result_container.suggestions,
727 )
728 )
729
730 correction_urls = list(
731 map(
732 lambda correction: {'url': raw_text_query.changeQuery(correction).getFullQuery(), 'title': correction},
733 result_container.corrections,
734 )
735 )
736
737 # engine_timings: get engine response times sorted from slowest to fastest
738 engine_timings = sorted(result_container.get_timings(), reverse=True, key=lambda e: e.total)
739 max_response_time = engine_timings[0].total if engine_timings else None
740 engine_timings_pairs = [(timing.engine, timing.total) for timing in engine_timings]
741
742 # search_query.lang contains the user choice (all, auto, en, ...)
743 # when the user choice is "auto", search.search_query.lang contains the detected language
744 # otherwise it is equals to search_query.lang
745 return render(
746 # fmt: off
747 'results.html',
748 results = results,
749 q=sxng_request.form['q'],
750 selected_categories = search_query.categories,
751 pageno = search_query.pageno,
752 time_range = search_query.time_range or '',
753 number_of_results = format_decimal(result_container.number_of_results),
754 suggestions = suggestion_urls,
755 answers = result_container.answers,
756 corrections = correction_urls,
757 infoboxes = result_container.infoboxes,
758 engine_data = result_container.engine_data,
759 paging = result_container.paging,
760 unresponsive_engines = webutils.get_translated_errors(
761 result_container.unresponsive_engines
762 ),
763 current_locale = sxng_request.preferences.get_value("locale"),
764 current_language = selected_locale,
765 search_language = match_locale(
766 search_obj.search_query.lang,
767 settings['search']['languages'],
768 fallback=sxng_request.preferences.get_value("language")
769 ),
770 timeout_limit = sxng_request.form.get('timeout_limit', None),
771 timings = engine_timings_pairs,
772 max_response_time = max_response_time
773 # fmt: on
774 )
775
776
777@app.route('/about', methods=['GET'])
778def about():
779 """Redirect to about page"""
780 # custom_url_for is going to add the locale
781 return redirect(custom_url_for('info', pagename='about'))
782
783
784@app.route('/info/<locale>/<pagename>', methods=['GET'])
785def info(pagename, locale):
786 """Render page of online user documentation"""
787 page = infopage.INFO_PAGES.get_page(pagename, locale)
788 if page is None:
789 flask.abort(404)
790
791 user_locale = sxng_request.preferences.get_value('locale')
792 return render(
793 'info.html',
794 all_pages=infopage.INFO_PAGES.iter_pages(user_locale, fallback_to_default=True),
795 active_page=page,
796 active_pagename=pagename,
797 )
798
799
800@app.route('/autocompleter', methods=['GET', 'POST'])
802 """Return autocompleter results"""
803
804 # run autocompleter
805 results = []
806
807 # set blocked engines
808 disabled_engines = sxng_request.preferences.engines.get_disabled()
809
810 # parse query
811 raw_text_query = RawTextQuery(sxng_request.form.get('q', ''), disabled_engines)
812 sug_prefix = raw_text_query.getQuery()
813
814 for obj in searx.answerers.STORAGE.ask(sug_prefix):
815 if isinstance(obj, Answer):
816 results.append(obj.answer)
817
818 # normal autocompletion results only appear if no inner results returned
819 # and there is a query part
820 if len(raw_text_query.autocomplete_list) == 0 and len(sug_prefix) > 0:
821
822 # get SearXNG's locale and autocomplete backend from cookie
823 sxng_locale = sxng_request.preferences.get_value('language')
824 backend_name = sxng_request.preferences.get_value('autocomplete')
825
826 for result in search_autocomplete(backend_name, sug_prefix, sxng_locale):
827 # attention: this loop will change raw_text_query object and this is
828 # the reason why the sug_prefix was stored before (see above)
829 if result != sug_prefix:
830 results.append(raw_text_query.changeQuery(result).getFullQuery())
831
832 if len(raw_text_query.autocomplete_list) > 0:
833 for autocomplete_text in raw_text_query.autocomplete_list:
834 results.append(raw_text_query.get_autocomplete_full_query(autocomplete_text))
835
836 if sxng_request.headers.get('X-Requested-With') == 'XMLHttpRequest':
837 # the suggestion request comes from the searx search form
838 suggestions = json.dumps(results)
839 mimetype = 'application/json'
840 else:
841 # the suggestion request comes from browser's URL bar
842 suggestions = json.dumps([sug_prefix, results])
843 mimetype = 'application/x-suggestions+json'
844
845 suggestions = escape(suggestions, False)
846 return Response(suggestions, mimetype=mimetype)
847
848
849@app.route('/preferences', methods=['GET', 'POST'])
851 """Render preferences page && save user preferences"""
852
853 # pylint: disable=too-many-locals, too-many-return-statements, too-many-branches
854 # pylint: disable=too-many-statements
855
856 # save preferences using the link the /preferences?preferences=...
857 if sxng_request.args.get('preferences') or sxng_request.form.get('preferences'):
858 resp = make_response(redirect(url_for('index', _external=True)))
859 return sxng_request.preferences.save(resp)
860
861 # save preferences
862 if sxng_request.method == 'POST':
863 resp = make_response(redirect(url_for('index', _external=True)))
864 try:
865 sxng_request.preferences.parse_form(sxng_request.form)
866 except ValidationException:
867 sxng_request.errors.append(gettext('Invalid settings, please edit your preferences'))
868 return resp
869 return sxng_request.preferences.save(resp)
870
871 # render preferences
872 image_proxy = sxng_request.preferences.get_value('image_proxy') # pylint: disable=redefined-outer-name
873 disabled_engines = sxng_request.preferences.engines.get_disabled()
874 allowed_plugins = sxng_request.preferences.plugins.get_enabled()
875
876 # stats for preferences page
877 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
878
879 engines_by_category = {}
880
881 for c in categories: # pylint: disable=consider-using-dict-items
882 engines_by_category[c] = [e for e in categories[c] if e.name in filtered_engines]
883 # sort the engines alphabetically since the order in settings.yml is meaningless.
884 list.sort(engines_by_category[c], key=lambda e: e.name)
885
886 # get first element [0], the engine time,
887 # and then the second element [1] : the time (the first one is the label)
888 stats = {} # pylint: disable=redefined-outer-name
889 max_rate95 = 0
890 for _, e in filtered_engines.items():
891 h = histogram('engine', e.name, 'time', 'total')
892 median = round(h.percentage(50), 1) if h.count > 0 else None
893 rate80 = round(h.percentage(80), 1) if h.count > 0 else None
894 rate95 = round(h.percentage(95), 1) if h.count > 0 else None
895
896 max_rate95 = max(max_rate95, rate95 or 0)
897
898 result_count_sum = histogram('engine', e.name, 'result', 'count').sum
899 successful_count = counter('engine', e.name, 'search', 'count', 'successful')
900 result_count = int(result_count_sum / float(successful_count)) if successful_count else 0
901
902 stats[e.name] = {
903 'time': median,
904 'rate80': rate80,
905 'rate95': rate95,
906 'warn_timeout': e.timeout > settings['outgoing']['request_timeout'],
907 'supports_selected_language': e.traits.is_locale_supported(
908 str(sxng_request.preferences.get_value('language') or 'all')
909 ),
910 'result_count': result_count,
911 }
912 # end of stats
913
914 # reliabilities
915 reliabilities = {}
916 engine_errors = get_engine_errors(filtered_engines)
917 checker_results = checker_get_result()
918 checker_results = (
919 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
920 )
921 for _, e in filtered_engines.items():
922 checker_result = checker_results.get(e.name, {})
923 checker_success = checker_result.get('success', True)
924 errors = engine_errors.get(e.name) or []
925 if counter('engine', e.name, 'search', 'count', 'sent') == 0:
926 # no request
927 reliability = None
928 elif checker_success and not errors:
929 reliability = 100
930 elif 'simple' in checker_result.get('errors', {}):
931 # the basic (simple) test doesn't work: the engine is broken according to the checker
932 # even if there is no exception
933 reliability = 0
934 else:
935 # pylint: disable=consider-using-generator
936 reliability = 100 - sum([error['percentage'] for error in errors if not error.get('secondary')])
937
938 reliabilities[e.name] = {
939 'reliability': reliability,
940 'errors': [],
941 'checker': checker_results.get(e.name, {}).get('errors', {}).keys(),
942 }
943 # keep the order of the list checker_results[e.name]['errors'] and deduplicate.
944 # the first element has the highest percentage rate.
945 reliabilities_errors = []
946 for error in errors:
947 error_user_text = None
948 if error.get('secondary') or 'exception_classname' not in error:
949 continue
950 error_user_text = exception_classname_to_text.get(error.get('exception_classname'))
951 if not error:
952 error_user_text = exception_classname_to_text[None]
953 if error_user_text not in reliabilities_errors:
954 reliabilities_errors.append(error_user_text)
955 reliabilities[e.name]['errors'] = reliabilities_errors
956
957 # supports
958 supports = {}
959 for _, e in filtered_engines.items():
960 supports_selected_language = e.traits.is_locale_supported(
961 str(sxng_request.preferences.get_value('language') or 'all')
962 )
963 safesearch = e.safesearch
964 time_range_support = e.time_range_support
965 for checker_test_name in checker_results.get(e.name, {}).get('errors', {}):
966 if supports_selected_language and checker_test_name.startswith('lang_'):
967 supports_selected_language = '?'
968 elif safesearch and checker_test_name == 'safesearch':
969 safesearch = '?'
970 elif time_range_support and checker_test_name == 'time_range':
971 time_range_support = '?'
972 supports[e.name] = {
973 'supports_selected_language': supports_selected_language,
974 'safesearch': safesearch,
975 'time_range_support': time_range_support,
976 }
977
978 return render(
979 # fmt: off
980 'preferences.html',
981 preferences = True,
982 selected_categories = get_selected_categories(sxng_request.preferences, sxng_request.form),
983 locales = LOCALE_NAMES,
984 current_locale = sxng_request.preferences.get_value("locale"),
985 image_proxy = image_proxy,
986 engines_by_category = engines_by_category,
987 stats = stats,
988 max_rate95 = max_rate95,
989 reliabilities = reliabilities,
990 supports = supports,
991 answer_storage = searx.answerers.STORAGE.info,
992 disabled_engines = disabled_engines,
993 autocomplete_backends = autocomplete_backends,
994 favicon_resolver_names = favicons.proxy.CFG.resolver_map.keys(),
995 shortcuts = {y: x for x, y in engine_shortcuts.items()},
996 themes = themes,
997 plugins_storage = searx.plugins.STORAGE.info,
998 current_doi_resolver = get_doi_resolver(),
999 allowed_plugins = allowed_plugins,
1000 preferences_url_params = sxng_request.preferences.get_as_url_params(),
1001 locked_preferences = get_setting("preferences.lock", []),
1002 doi_resolvers = get_setting("doi_resolvers", {}),
1003 # fmt: on
1004 )
1005
1006
1007app.add_url_rule('/favicon_proxy', methods=['GET'], endpoint="favicon_proxy", view_func=favicons.favicon_proxy)
1008
1009
1010@app.route('/image_proxy', methods=['GET'])
1012 # pylint: disable=too-many-return-statements, too-many-branches
1013
1014 url = sxng_request.args.get('url')
1015 if not url:
1016 return '', 400
1017
1018 if not is_hmac_of(settings['server']['secret_key'], url.encode(), sxng_request.args.get('h', '')):
1019 return '', 400
1020
1021 maximum_size = 5 * 1024 * 1024
1022 forward_resp = False
1023 resp = None
1024 try:
1025 request_headers = {
1026 'User-Agent': gen_useragent(),
1027 'Accept': 'image/webp,*/*',
1028 'Accept-Encoding': 'gzip, deflate',
1029 'Sec-GPC': '1',
1030 'DNT': '1',
1031 }
1032 set_context_network_name('image_proxy')
1033 resp, stream = http_stream(method='GET', url=url, headers=request_headers, allow_redirects=True)
1034 content_length = resp.headers.get('Content-Length')
1035 if content_length and content_length.isdigit() and int(content_length) > maximum_size:
1036 return 'Max size', 400
1037
1038 if resp.status_code != 200:
1039 logger.debug('image-proxy: wrong response code: %i', resp.status_code)
1040 if resp.status_code >= 400:
1041 return '', resp.status_code
1042 return '', 400
1043
1044 if not resp.headers.get('Content-Type', '').startswith('image/') and not resp.headers.get(
1045 'Content-Type', ''
1046 ).startswith('binary/octet-stream'):
1047 logger.debug('image-proxy: wrong content-type: %s', resp.headers.get('Content-Type', ''))
1048 return '', 400
1049
1050 forward_resp = True
1051 except httpx.HTTPError:
1052 logger.exception('HTTP error')
1053 return '', 400
1054 finally:
1055 if resp and not forward_resp:
1056 # the code is about to return an HTTP 400 error to the browser
1057 # we make sure to close the response between searxng and the HTTP server
1058 try:
1059 resp.close()
1060 except httpx.HTTPError:
1061 logger.exception('HTTP error on closing')
1062
1063 def close_stream():
1064 nonlocal resp, stream
1065 try:
1066 if resp:
1067 resp.close()
1068 del resp
1069 del stream
1070 except httpx.HTTPError as e:
1071 logger.debug('Exception while closing response', e)
1072
1073 try:
1074 headers = dict_subset(resp.headers, {'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'})
1075 response = Response(stream, mimetype=resp.headers['Content-Type'], headers=headers, direct_passthrough=True)
1076 response.call_on_close(close_stream)
1077 return response
1078 except httpx.HTTPError:
1079 close_stream()
1080 return '', 400
1081
1082
1083@app.route('/engine_descriptions.json', methods=['GET'])
1085 locale = get_locale().split('_')[0]
1086 result = ENGINE_DESCRIPTIONS['en'].copy()
1087 if locale != 'en':
1088 for engine, description in ENGINE_DESCRIPTIONS.get(locale, {}).items():
1089 result[engine] = description
1090 for engine, description in result.items():
1091 if len(description) == 2 and description[1] == 'ref':
1092 ref_engine, ref_lang = description[0].split(':')
1093 description = ENGINE_DESCRIPTIONS[ref_lang][ref_engine]
1094 if isinstance(description, str):
1095 description = [description, 'wikipedia']
1096 result[engine] = description
1097
1098 # overwrite by about:description (from settings)
1099 for engine_name, engine_mod in engines.items():
1100 descr = getattr(engine_mod, 'about', {}).get('description', None)
1101 if descr is not None:
1102 result[engine_name] = [descr, "SearXNG config"]
1103
1104 return jsonify(result)
1105
1106
1107@app.route('/stats', methods=['GET'])
1108def stats():
1109 """Render engine statistics page."""
1110 sort_order = sxng_request.args.get('sort', default='name', type=str)
1111 selected_engine_name = sxng_request.args.get('engine', default=None, type=str)
1112
1113 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1114 if selected_engine_name:
1115 if selected_engine_name not in filtered_engines:
1116 selected_engine_name = None
1117 else:
1118 filtered_engines = [selected_engine_name]
1119
1120 checker_results = checker_get_result()
1121 checker_results = (
1122 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
1123 )
1124
1125 engine_stats = get_engines_stats(filtered_engines)
1126 engine_reliabilities = get_reliabilities(filtered_engines, checker_results)
1127
1128 if sort_order not in STATS_SORT_PARAMETERS:
1129 sort_order = 'name'
1130
1131 reverse, key_name, default_value = STATS_SORT_PARAMETERS[sort_order]
1132
1133 def get_key(engine_stat):
1134 reliability = engine_reliabilities.get(engine_stat['name'], {}).get('reliability', 0)
1135 reliability_order = 0 if reliability else 1
1136 if key_name == 'reliability':
1137 key = reliability
1138 reliability_order = 0
1139 else:
1140 key = engine_stat.get(key_name) or default_value
1141 if reverse:
1142 reliability_order = 1 - reliability_order
1143 return (reliability_order, key, engine_stat['name'])
1144
1145 technical_report = []
1146 for error in engine_reliabilities.get(selected_engine_name, {}).get('errors', []):
1147 technical_report.append(
1148 f"\
1149 Error: {error['exception_classname'] or error['log_message']} \
1150 Parameters: {error['log_parameters']} \
1151 File name: {error['filename'] }:{ error['line_no'] } \
1152 Error Function: {error['function']} \
1153 Code: {error['code']} \
1154 ".replace(
1155 ' ' * 12, ''
1156 ).strip()
1157 )
1158 technical_report = ' '.join(technical_report)
1159
1160 engine_stats['time'] = sorted(engine_stats['time'], reverse=reverse, key=get_key)
1161 return render(
1162 # fmt: off
1163 'stats.html',
1164 sort_order = sort_order,
1165 engine_stats = engine_stats,
1166 engine_reliabilities = engine_reliabilities,
1167 selected_engine_name = selected_engine_name,
1168 searx_git_branch = GIT_BRANCH,
1169 technical_report = technical_report,
1170 # fmt: on
1171 )
1172
1173
1174@app.route('/stats/errors', methods=['GET'])
1176 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1177 result = get_engine_errors(filtered_engines)
1178 return jsonify(result)
1179
1180
1181@app.route('/stats/checker', methods=['GET'])
1183 result = checker_get_result()
1184 return jsonify(result)
1185
1186
1187@app.route('/metrics')
1189 password = settings['general'].get("open_metrics")
1190
1191 if not (settings['general'].get("enable_metrics") and password):
1192 return Response('open metrics is disabled', status=404, mimetype='text/plain')
1193
1194 if not sxng_request.authorization or sxng_request.authorization.password != password:
1195 return Response('access forbidden', status=401, mimetype='text/plain')
1196
1197 filtered_engines = dict(filter(lambda kv: sxng_request.preferences.validate_token(kv[1]), engines.items()))
1198
1199 checker_results = checker_get_result()
1200 checker_results = (
1201 checker_results['engines'] if checker_results['status'] == 'ok' and 'engines' in checker_results else {}
1202 )
1203
1204 engine_stats = get_engines_stats(filtered_engines)
1205 engine_reliabilities = get_reliabilities(filtered_engines, checker_results)
1206 metrics_text = openmetrics(engine_stats, engine_reliabilities)
1207
1208 return Response(metrics_text, mimetype='text/plain')
1209
1210
1211@app.route('/robots.txt', methods=['GET'])
1213 return Response(
1214 """User-agent: *
1215Allow: /info/en/about
1216Disallow: /stats
1217Disallow: /image_proxy
1218Disallow: /preferences
1219Disallow: /*?*q=*
1220""",
1221 mimetype='text/plain',
1222 )
1223
1224
1225@app.route('/opensearch.xml', methods=['GET'])
1227 method = sxng_request.preferences.get_value('method')
1228 autocomplete = sxng_request.preferences.get_value('autocomplete')
1229
1230 # chrome/chromium only supports HTTP GET....
1231 if sxng_request.headers.get('User-Agent', '').lower().find('webkit') >= 0:
1232 method = 'GET'
1233
1234 if method not in ('POST', 'GET'):
1235 method = 'POST'
1236
1237 ret = render('opensearch.xml', opensearch_method=method, autocomplete=autocomplete)
1238 resp = Response(response=ret, status=200, mimetype="application/opensearchdescription+xml")
1239 return resp
1240
1241
1242@app.route('/favicon.ico')
1244 theme = sxng_request.preferences.get_value("theme")
1245 return send_from_directory(
1246 os.path.join(app.root_path, settings['ui']['static_path'], 'themes', theme, 'img'), # type: ignore
1247 'favicon.png',
1248 mimetype='image/vnd.microsoft.icon',
1249 )
1250
1251
1252@app.route('/clear_cookies')
1254 resp = make_response(redirect(url_for('index', _external=True)))
1255 for cookie_name in sxng_request.cookies:
1256 resp.delete_cookie(cookie_name)
1257 return resp
1258
1259
1260@app.route('/config')
1262 """Return configuration in JSON format."""
1263 _engines = []
1264 for name, engine in engines.items():
1265 if not sxng_request.preferences.validate_token(engine):
1266 continue
1267
1268 _languages = engine.traits.languages.keys()
1269 _engines.append(
1270 {
1271 'name': name,
1272 'categories': engine.categories,
1273 'shortcut': engine.shortcut,
1274 'enabled': not engine.disabled,
1275 'paging': engine.paging,
1276 'language_support': engine.language_support,
1277 'languages': list(_languages),
1278 'regions': list(engine.traits.regions.keys()),
1279 'safesearch': engine.safesearch,
1280 'time_range_support': engine.time_range_support,
1281 'timeout': engine.timeout,
1282 }
1283 )
1284
1285 _plugins = []
1286 for _ in searx.plugins.STORAGE:
1287 _plugins.append({'name': _.id, 'enabled': _.active})
1288
1289 _limiter_cfg = limiter.get_cfg()
1290
1291 return jsonify(
1292 {
1293 'categories': list(categories.keys()),
1294 'engines': _engines,
1295 'plugins': _plugins,
1296 'instance_name': settings['general']['instance_name'],
1297 'locales': LOCALE_NAMES,
1298 'default_locale': settings['ui']['default_locale'],
1299 'autocomplete': settings['search']['autocomplete'],
1300 'safe_search': settings['search']['safe_search'],
1301 'default_theme': settings['ui']['default_theme'],
1302 'version': VERSION_STRING,
1303 'brand': {
1304 'PRIVACYPOLICY_URL': get_setting('general.privacypolicy_url'),
1305 'CONTACT_URL': get_setting('general.contact_url'),
1306 'GIT_URL': GIT_URL,
1307 'GIT_BRANCH': GIT_BRANCH,
1308 'DOCS_URL': get_setting('brand.docs_url'),
1309 },
1310 'limiter': {
1311 'enabled': limiter.is_installed(),
1312 'botdetection.ip_limit.link_token': _limiter_cfg.get('botdetection.ip_limit.link_token'),
1313 'botdetection.ip_lists.pass_searxng_org': _limiter_cfg.get('botdetection.ip_lists.pass_searxng_org'),
1314 },
1315 'doi_resolvers': list(settings['doi_resolvers'].keys()),
1316 'default_doi_resolver': settings['default_doi_resolver'],
1317 'public_instance': settings['server']['public_instance'],
1318 }
1319 )
1320
1321
1322@app.errorhandler(404)
1324 return render('404.html'), 404
1325
1326
1327# see https://flask.palletsprojects.com/en/1.1.x/cli/
1328# True if "FLASK_APP=searx/webapp.py FLASK_ENV=development flask run"
1329flask_run_development = (
1330 os.environ.get("FLASK_APP") is not None and os.environ.get("FLASK_ENV") == 'development' and is_flask_run_cmdline()
1331)
1332
1333# True if reload feature is activated of werkzeug, False otherwise (including uwsgi, etc..)
1334# __name__ != "__main__" if searx.webapp is imported (make test, make docs, uwsgi...)
1335# see run() at the end of this file : searx_debug activates the reload feature.
1336werkzeug_reloader = flask_run_development or (searx_debug and __name__ == "__main__")
1337
1338# initialize the engines except on the first run of the werkzeug server.
1339if not werkzeug_reloader or (werkzeug_reloader and os.environ.get("WERKZEUG_RUN_MAIN") == "true"):
1340 locales_initialize()
1341 redis_initialize()
1344 enable_checker=True,
1345 check_network=True,
1346 enable_metrics=get_setting("general.enable_metrics"),
1347 )
1348 limiter.initialize(app, settings)
1349 favicons.init()
1350
1351
1352def run():
1353 logger.debug('starting webserver on %s:%s', settings['server']['bind_address'], settings['server']['port'])
1354 app.run(
1355 debug=searx_debug,
1356 use_debugger=searx_debug,
1357 port=settings['server']['port'],
1358 host=settings['server']['bind_address'],
1359 threaded=True,
1360 extra_files=[DEFAULT_SETTINGS_FILE],
1361 )
1362
1363
1364application = app
1365patch_application(app)
1366
1367if __name__ == "__main__":
1368 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:174
engine_descriptions()
Definition webapp.py:1084
morty_proxify(str url)
Definition webapp.py:267
info(pagename, locale)
Definition webapp.py:785
autocompleter()
Definition webapp.py:801
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:1323
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:1188
get_setting(name, default=_unset)
Definition __init__.py:69