on master help sign in
Merge pull request #2189 from dalf/architecture-clean-up

Architecture clean up

commit8d47142f35199866b26ee4e9149bbb8b058a6873 authorAdam Tauber <asciimoo@gmail.com> dateMon 28 Sep 2020, 14:56 (6 years ago) parentc2a6f14f2f3300
7 files changed+384−300
searx/query.py+8−34 searx/search.py+58−179 searx/webadapter.py+235 searx/webapp.py+7−21 tests/unit/test_search.py+19−65 tests/unit/test_webadapter.py+54 utils/standalone_searx.py+3−1
searx/query.py+8−34
@@ -20,9 +20,8 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
2020import re
2121
2222from searx.languages import language_codes
23−from searx.engines import (
24− categories, engines, engine_shortcuts
25−)
23+from searx.engines import categories, engines, engine_shortcuts
24+from searx.search import EngineRef
2625
2726
2827VALID_LANGUAGE_CODE = re.compile(r'^[a-z]{2,3}(-[a-zA-Z]{2})?$')
@@ -40,7 +39,7 @@ class RawTextQuery:
4039 self.disabled_engines = disabled_engines
4140
4241 self.query_parts = []
43− self.engines = []
42+ self.enginerefs = []
4443 self.languages = []
4544 self.timeout_limit = None
4645 self.external_bang = None
@@ -135,26 +134,21 @@ class RawTextQuery:
135134 parse_next = True
136135 engine_name = engine_shortcuts[prefix]
137136 if engine_name in engines:
138− self.engines.append({'category': 'none',
139− 'name': engine_name,
140− 'from_bang': True})
137+ self.enginerefs.append(EngineRef(engine_name, 'none', True))
141138
142139 # check if prefix is equal with engine name
143140 elif prefix in engines:
144141 parse_next = True
145− self.engines.append({'category': 'none',
146− 'name': prefix,
147− 'from_bang': True})
142+ self.enginerefs.append(EngineRef(prefix, 'none', True))
148143
149144 # check if prefix is equal with categorie name
150145 elif prefix in categories:
151146 # using all engines for that search, which
152147 # are declared under that categorie name
153148 parse_next = True
154− self.engines.extend({'category': prefix,
155− 'name': engine.name}
156− for engine in categories[prefix]
157− if (engine.name, prefix) not in self.disabled_engines)
149+ self.enginerefs.extend(EngineRef(engine.name, prefix)
150+ for engine in categories[prefix]
151+ if (engine.name, prefix) not in self.disabled_engines)
158152
159153 if query_part[0] == '!':
160154 self.specific = True
@@ -178,23 +172,3 @@ class RawTextQuery:
178172 def getFullQuery(self):
179173 # get full querry including whitespaces
180174 return ''.join(self.query_parts)
181−
182−
183−class SearchQuery:
184− """container for all the search parameters (query, language, etc...)"""
185−
186− def __init__(self, query, engines, categories, lang, safesearch, pageno, time_range,
187− timeout_limit=None, preferences=None, external_bang=None):
188− self.query = query
189− self.engines = engines
190− self.categories = categories
191− self.lang = lang
192− self.safesearch = safesearch
193− self.pageno = pageno
194− self.time_range = None if time_range in ('', 'None', None) else time_range
195− self.timeout_limit = timeout_limit
196− self.preferences = preferences
197− self.external_bang = external_bang
198−
199− def __str__(self):
200− return self.query + ";" + str(self.engines)
searx/search.py+58−179
@@ -15,27 +15,22 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
1515(C) 2013- by Adam Tauber, <asciimoo@gmail.com>
1616'''
1717
18+import typing
1819import gc
19−import sys
2020import threading
2121from time import time
2222from uuid import uuid4
2323from _thread import start_new_thread
2424
25−from flask_babel import gettext
2625import requests.exceptions
2726import searx.poolrequests as requests_lib
28−from searx.engines import (
29− categories, engines, settings
30−)
27+from searx.engines import engines, settings
3128from searx.answerers import ask
3229from searx.external_bang import get_bang_url
3330from searx.utils import gen_useragent
34−from searx.query import RawTextQuery, SearchQuery, VALID_LANGUAGE_CODE
3531from searx.results import ResultContainer
3632from searx import logger
3733from searx.plugins import plugins
38−from searx.exceptions import SearxParameterException
3934
4035
4136logger = logger.getChild('search')
@@ -53,6 +48,49 @@ else:
5348 exit(1)
5449
5550
51+class EngineRef:
52+
53+ __slots__ = 'name', 'category', 'from_bang'
54+
55+ def __init__(self, name: str, category: str, from_bang: bool=False):
56+ self.name = name
57+ self.category = category
58+ self.from_bang = from_bang
59+
60+ def __str__(self):
61+ return "(" + self.name + "," + self.category + "," + str(self.from_bang) + ")"
62+
63+
64+class SearchQuery:
65+ """container for all the search parameters (query, language, etc...)"""
66+
67+ __slots__ = 'query', 'engineref_list', 'categories', 'lang', 'safesearch', 'pageno', 'time_range',\
68+ 'timeout_limit', 'external_bang'
69+
70+ def __init__(self,
71+ query: str,
72+ engineref_list: typing.List[EngineRef],
73+ categories: typing.List[str],
74+ lang: str,
75+ safesearch: bool,
76+ pageno: int,
77+ time_range: typing.Optional[str],
78+ timeout_limit: typing.Optional[float]=None,
79+ external_bang: typing.Optional[str]=False):
80+ self.query = query
81+ self.engineref_list = engineref_list
82+ self.categories = categories
83+ self.lang = lang
84+ self.safesearch = safesearch
85+ self.pageno = pageno
86+ self.time_range = time_range
87+ self.timeout_limit = timeout_limit
88+ self.external_bang = external_bang
89+
90+ def __str__(self):
91+ return self.query + ";" + str(self.engineref_list)
92+
93+
5694def send_http_request(engine, request_params):
5795 # create dictionary which contain all
5896 # informations about the request
@@ -247,167 +285,11 @@ def default_request_params():
247285 }
248286
249287
250−# remove duplicate queries.
251−# FIXME: does not fix "!music !soundcloud", because the categories are 'none' and 'music'
252−def deduplicate_query_engines(query_engines):
253− uniq_query_engines = {q["category"] + '|' + q["name"]: q for q in query_engines}
254− return uniq_query_engines.values()
255−
256−
257−def get_search_query_from_webapp(preferences, form):
258− # no text for the query ?
259− if not form.get('q'):
260− raise SearxParameterException('q', '')
261−
262− # set blocked engines
263− disabled_engines = preferences.engines.get_disabled()
264−
265− # parse query, if tags are set, which change
266− # the serch engine or search-language
267− raw_text_query = RawTextQuery(form['q'], disabled_engines)
268−
269− # set query
270− query = raw_text_query.getQuery()
271−
272− # get and check page number
273− pageno_param = form.get('pageno', '1')
274− if not pageno_param.isdigit() or int(pageno_param) < 1:
275− raise SearxParameterException('pageno', pageno_param)
276− query_pageno = int(pageno_param)
277−
278− # get language
279− # set specific language if set on request, query or preferences
280− # TODO support search with multible languages
281− if len(raw_text_query.languages):
282− query_lang = raw_text_query.languages[-1]
283− elif 'language' in form:
284− query_lang = form.get('language')
285− else:
286− query_lang = preferences.get_value('language')
287−
288− # check language
289− if not VALID_LANGUAGE_CODE.match(query_lang):
290− raise SearxParameterException('language', query_lang)
291−
292− # get safesearch
293− if 'safesearch' in form:
294− query_safesearch = form.get('safesearch')
295− # first check safesearch
296− if not query_safesearch.isdigit():
297− raise SearxParameterException('safesearch', query_safesearch)
298− query_safesearch = int(query_safesearch)
299− else:
300− query_safesearch = preferences.get_value('safesearch')
301−
302− # safesearch : second check
303− if query_safesearch < 0 or query_safesearch > 2:
304− raise SearxParameterException('safesearch', query_safesearch)
305−
306− # get time_range
307− query_time_range = form.get('time_range')
308−
309− # check time_range
310− if query_time_range not in ('None', None, '', 'day', 'week', 'month', 'year'):
311− raise SearxParameterException('time_range', query_time_range)
312−
313− # query_engines
314− query_engines = raw_text_query.engines
315−
316− # timeout_limit
317− query_timeout = raw_text_query.timeout_limit
318− if query_timeout is None and 'timeout_limit' in form:
319− raw_time_limit = form.get('timeout_limit')
320− if raw_time_limit in ['None', '']:
321− raw_time_limit = None
322− else:
323− try:
324− query_timeout = float(raw_time_limit)
325− except ValueError:
326− raise SearxParameterException('timeout_limit', raw_time_limit)
327−
328− # query_categories
329− query_categories = []
330−
331− # if engines are calculated from query,
332− # set categories by using that informations
333− if query_engines and raw_text_query.specific:
334− additional_categories = set()
335− for engine in query_engines:
336− if 'from_bang' in engine and engine['from_bang']:
337− additional_categories.add('none')
338− else:
339− additional_categories.add(engine['category'])
340− query_categories = list(additional_categories)
341−
342− # otherwise, using defined categories to
343− # calculate which engines should be used
344− else:
345− # set categories/engines
346− load_default_categories = True
347− for pd_name, pd in form.items():
348− if pd_name == 'categories':
349− query_categories.extend(categ for categ in map(str.strip, pd.split(',')) if categ in categories)
350− elif pd_name == 'engines':
351− pd_engines = [{'category': engines[engine].categories[0],
352− 'name': engine}
353− for engine in map(str.strip, pd.split(',')) if engine in engines]
354− if pd_engines:
355− query_engines.extend(pd_engines)
356− load_default_categories = False
357− elif pd_name.startswith('category_'):
358− category = pd_name[9:]
359−
360− # if category is not found in list, skip
361− if category not in categories:
362− continue
363−
364− if pd != 'off':
365− # add category to list
366− query_categories.append(category)
367− elif category in query_categories:
368− # remove category from list if property is set to 'off'
369− query_categories.remove(category)
370−
371− if not load_default_categories:
372− if not query_categories:
373− query_categories = list(set(engine['category']
374− for engine in query_engines))
375− else:
376− # if no category is specified for this search,
377− # using user-defined default-configuration which
378− # (is stored in cookie)
379− if not query_categories:
380− cookie_categories = preferences.get_value('categories')
381− for ccateg in cookie_categories:
382− if ccateg in categories:
383− query_categories.append(ccateg)
384−
385− # if still no category is specified, using general
386− # as default-category
387− if not query_categories:
388− query_categories = ['general']
389−
390− # using all engines for that search, which are
391− # declared under the specific categories
392− for categ in query_categories:
393− query_engines.extend({'category': categ,
394− 'name': engine.name}
395− for engine in categories[categ]
396− if (engine.name, categ) not in disabled_engines)
397−
398− query_engines = deduplicate_query_engines(query_engines)
399− external_bang = raw_text_query.external_bang
400−
401− return (SearchQuery(query, query_engines, query_categories,
402− query_lang, query_safesearch, query_pageno,
403− query_time_range, query_timeout, preferences,
404− external_bang=external_bang),
405− raw_text_query)
406−
407−
408288class Search:
409289 """Search information container"""
410290
291+ __slots__ = "search_query", "result_container", "start_time", "actual_timeout"
292+
411293 def __init__(self, search_query):
412294 # init vars
413295 super().__init__()
@@ -444,9 +326,6 @@ class Search:
444326 return False
445327
446328 def _is_accepted(self, engine_name, engine):
447− if not self.search_query.preferences.validate_token(engine):
448− return False
449−
450329 # skip suspended engines
451330 if engine.suspend_end_time >= time():
452331 logger.debug('Engine currently suspended: %s', engine_name)
@@ -462,13 +341,13 @@ class Search:
462341
463342 return True
464343
465− def _get_params(self, selected_engine, user_agent):
466− if selected_engine['name'] not in engines:
344+ def _get_params(self, engineref, user_agent):
345+ if engineref.name not in engines:
467346 return None, None
468347
469− engine = engines[selected_engine['name']]
348+ engine = engines[engineref.name]
470349
471− if not self._is_accepted(selected_engine['name'], engine):
350+ if not self._is_accepted(engineref.name, engine):
472351 return None, None
473352
474353 # set default request parameters
@@ -485,15 +364,13 @@ class Search:
485364 request_params['safesearch'] = self.search_query.safesearch
486365 request_params['time_range'] = self.search_query.time_range
487366
488− request_params['category'] = selected_engine['category']
367+ request_params['category'] = engineref.category
489368 request_params['pageno'] = self.search_query.pageno
490369
491370 return request_params, engine.timeout
492371
493372 # do search-request
494373 def _get_requests(self):
495− global number_of_searches
496−
497374 # init vars
498375 requests = []
499376
@@ -505,14 +382,14 @@ class Search:
505382 default_timeout = 0
506383
507384 # start search-reqest for all selected engines
508− for selected_engine in self.search_query.engines:
385+ for engineref in self.search_query.engineref_list:
509386 # set default request parameters
510− request_params, engine_timeout = self._get_params(selected_engine, user_agent)
387+ request_params, engine_timeout = self._get_params(engineref, user_agent)
511388 if request_params is None:
512389 continue
513390
514391 # append request to list
515− requests.append((selected_engine['name'], self.search_query.query, request_params))
392+ requests.append((engineref.name, self.search_query.query, request_params))
516393
517394 # update default_timeout
518395 default_timeout = max(default_timeout, engine_timeout)
@@ -535,7 +412,7 @@ class Search:
535412 actual_timeout = min(query_timeout, max_request_timeout)
536413
537414 logger.debug("actual_timeout={0} (default_timeout={1}, ?timeout_limit={2}, max_request_timeout={3})"
538− .format(self.actual_timeout, default_timeout, query_timeout, max_request_timeout))
415+ .format(actual_timeout, default_timeout, query_timeout, max_request_timeout))
539416
540417 return requests, actual_timeout
541418
@@ -567,6 +444,8 @@ class Search:
567444class SearchWithPlugins(Search):
568445 """Similar to the Search class but call the plugins."""
569446
447+ __slots__ = 'ordered_plugin_list', 'request'
448+
570449 def __init__(self, search_query, ordered_plugin_list, request):
571450 super().__init__(search_query)
572451 self.ordered_plugin_list = ordered_plugin_list
searx/webadapter.pynew file+235
@@ -0,0 +1,235 @@
1+from typing import Dict, List, Optional, Tuple
2+from searx.exceptions import SearxParameterException
3+from searx.query import RawTextQuery, VALID_LANGUAGE_CODE
4+from searx.engines import categories, engines
5+from searx.search import SearchQuery, EngineRef
6+from searx.preferences import Preferences
7+
8+
9+# remove duplicate queries.
10+# FIXME: does not fix "!music !soundcloud", because the categories are 'none' and 'music'
11+def deduplicate_engineref_list(engineref_list: List[EngineRef]) -> List[EngineRef]:
12+ engineref_dict = {q.category + '|' + q.name: q for q in engineref_list}
13+ return engineref_dict.values()
14+
15+
16+def validate_engineref_list(engineref_list: List[EngineRef], preferences: Preferences)\
17+ -> Tuple[List[EngineRef], List[EngineRef], List[EngineRef]]:
18+ """Validate query_engines according to the preferences
19+
20+ Returns:
21+ List[EngineRef]: list of existing engines with a validated token
22+ List[EngineRef]: list of unknown engine
23+ List[EngineRef]: list of engine with invalid token according to the preferences
24+ """
25+ valid = []
26+ unknown = []
27+ no_token = []
28+ for engineref in engineref_list:
29+ if engineref.name not in engines:
30+ unknown.append(engineref)
31+ continue
32+
33+ engine = engines[engineref.name]
34+ if not preferences.validate_token(engine):
35+ no_token.append(engineref)
36+ continue
37+
38+ valid.append(engineref)
39+ return valid, unknown, no_token
40+
41+
42+def parse_pageno(form: Dict[str, str]) -> int:
43+ pageno_param = form.get('pageno', '1')
44+ if not pageno_param.isdigit() or int(pageno_param) < 1:
45+ raise SearxParameterException('pageno', pageno_param)
46+ return int(pageno_param)
47+
48+
49+def parse_lang(preferences: Preferences, form: Dict[str, str], raw_text_query: RawTextQuery) -> str:
50+ # get language
51+ # set specific language if set on request, query or preferences
52+ # TODO support search with multible languages
53+ if len(raw_text_query.languages):
54+ query_lang = raw_text_query.languages[-1]
55+ elif 'language' in form:
56+ query_lang = form.get('language')
57+ else:
58+ query_lang = preferences.get_value('language')
59+
60+ # check language
61+ if not VALID_LANGUAGE_CODE.match(query_lang):
62+ raise SearxParameterException('language', query_lang)
63+
64+ return query_lang
65+
66+
67+def parse_safesearch(preferences: Preferences, form: Dict[str, str]) -> int:
68+ if 'safesearch' in form:
69+ query_safesearch = form.get('safesearch')
70+ # first check safesearch
71+ if not query_safesearch.isdigit():
72+ raise SearxParameterException('safesearch', query_safesearch)
73+ query_safesearch = int(query_safesearch)
74+ else:
75+ query_safesearch = preferences.get_value('safesearch')
76+
77+ # safesearch : second check
78+ if query_safesearch < 0 or query_safesearch > 2:
79+ raise SearxParameterException('safesearch', query_safesearch)
80+
81+ return query_safesearch
82+
83+
84+def parse_time_range(form: Dict[str, str]) -> str:
85+ query_time_range = form.get('time_range')
86+ # check time_range
87+ query_time_range = None if query_time_range in ('', 'None') else query_time_range
88+ if query_time_range not in (None, 'day', 'week', 'month', 'year'):
89+ raise SearxParameterException('time_range', query_time_range)
90+ return query_time_range
91+
92+
93+def parse_timeout(form: Dict[str, str], raw_text_query: RawTextQuery) -> Optional[float]:
94+ query_timeout = raw_text_query.timeout_limit
95+ if query_timeout is None and 'timeout_limit' in form:
96+ raw_time_limit = form.get('timeout_limit')
97+ if raw_time_limit in ['None', '']:
98+ return None
99+ else:
100+ try:
101+ return float(raw_time_limit)
102+ except ValueError:
103+ raise SearxParameterException('timeout_limit', raw_time_limit)
104+
105+
106+def parse_specific(raw_text_query: RawTextQuery) -> Tuple[List[EngineRef], List[str]]:
107+ query_engineref_list = raw_text_query.enginerefs
108+ additional_categories = set()
109+ for engineref in raw_text_query.enginerefs:
110+ if engineref.from_bang:
111+ additional_categories.add('none')
112+ else:
113+ additional_categories.add(engineref.category)
114+ query_categories = list(additional_categories)
115+ return query_engineref_list, query_categories
116+
117+
118+def parse_category_form(query_categories: List[str], name: str, value: str) -> None:
119+ if name == 'categories':
120+ query_categories.extend(categ for categ in map(str.strip, value.split(',')) if categ in categories)
121+ elif name.startswith('category_'):
122+ category = name[9:]
123+
124+ # if category is not found in list, skip
125+ if category not in categories:
126+ return
127+
128+ if value != 'off':
129+ # add category to list
130+ query_categories.append(category)
131+ elif category in query_categories:
132+ # remove category from list if property is set to 'off'
133+ query_categories.remove(category)
134+
135+
136+def get_selected_categories(preferences: Preferences, form: Dict[str, str]) -> List[str]:
137+ selected_categories = []
138+
139+ if form is not None:
140+ for name, value in form.items():
141+ parse_category_form(selected_categories, name, value)
142+
143+ # if no category is specified for this search,
144+ # using user-defined default-configuration which
145+ # (is stored in cookie)
146+ if not selected_categories:
147+ cookie_categories = preferences.get_value('categories')
148+ for ccateg in cookie_categories:
149+ selected_categories.append(ccateg)
150+
151+ # if still no category is specified, using general
152+ # as default-category
153+ if not selected_categories:
154+ selected_categories = ['general']
155+
156+ return selected_categories
157+
158+
159+def parse_generic(preferences: Preferences, form: Dict[str, str], disabled_engines: List[str])\
160+ -> Tuple[List[EngineRef], List[str]]:
161+ query_engineref_list = []
162+ query_categories = []
163+
164+ # set categories/engines
165+ load_default_categories = True
166+ for pd_name, pd in form.items():
167+ if pd_name == 'engines':
168+ pd_engines = [EngineRef(engine_name, engines[engine_name].categories[0])
169+ for engine_name in map(str.strip, pd.split(',')) if engine_name in engines]
170+ if pd_engines:
171+ query_engineref_list.extend(pd_engines)
172+ load_default_categories = False
173+ else:
174+ parse_category_form(query_categories, pd_name, pd)
175+
176+ if not load_default_categories:
177+ if not query_categories:
178+ query_categories = list(set(engine['category']
179+ for engine in query_engineref_list))
180+ else:
181+ if not query_categories:
182+ query_categories = get_selected_categories(preferences, None)
183+
184+ # using all engines for that search, which are
185+ # declared under the specific categories
186+ for categ in query_categories:
187+ query_engineref_list.extend(EngineRef(engine.name, categ)
188+ for engine in categories[categ]
189+ if (engine.name, categ) not in disabled_engines)
190+
191+ return query_engineref_list, query_categories
192+
193+
194+def get_search_query_from_webapp(preferences: Preferences, form: Dict[str, str])\
195+ -> Tuple[SearchQuery, RawTextQuery, List[EngineRef], List[EngineRef]]:
196+ # no text for the query ?
197+ if not form.get('q'):
198+ raise SearxParameterException('q', '')
199+
200+ # set blocked engines
201+ disabled_engines = preferences.engines.get_disabled()
202+
203+ # parse query, if tags are set, which change
204+ # the serch engine or search-language
205+ raw_text_query = RawTextQuery(form['q'], disabled_engines)
206+
207+ # set query
208+ query = raw_text_query.getQuery()
209+ query_pageno = parse_pageno(form)
210+ query_lang = parse_lang(preferences, form, raw_text_query)
211+ query_safesearch = parse_safesearch(preferences, form)
212+ query_time_range = parse_time_range(form)
213+ query_timeout = parse_timeout(form, raw_text_query)
214+ external_bang = raw_text_query.external_bang
215+
216+ if raw_text_query.enginerefs and raw_text_query.specific:
217+ # if engines are calculated from query,
218+ # set categories by using that informations
219+ query_engineref_list, query_categories = parse_specific(raw_text_query)
220+ else:
221+ # otherwise, using defined categories to
222+ # calculate which engines should be used
223+ query_engineref_list, query_categories = parse_generic(preferences, form, disabled_engines)
224+
225+ query_engineref_list = deduplicate_engineref_list(query_engineref_list)
226+ query_engineref_list, query_engineref_list_unknown, query_engineref_list_notoken =\
227+ validate_engineref_list(query_engineref_list, preferences)
228+
229+ return (SearchQuery(query, query_engineref_list, query_categories,
230+ query_lang, query_safesearch, query_pageno,
231+ query_time_range, query_timeout,
232+ external_bang=external_bang),
233+ raw_text_query,
234+ query_engineref_list_unknown,
235+ query_engineref_list_notoken)
searx/webapp.py+7−21
@@ -67,10 +67,11 @@ from searx.webutils import (
6767 get_static_files, get_result_templates, get_themes,
6868 prettify_url, new_hmac
6969)
70+from searx.webadapter import get_search_query_from_webapp, get_selected_categories
7071from searx.utils import html_to_text, gen_useragent, dict_subset, match_language
7172from searx.version import VERSION_STRING
7273from searx.languages import language_codes as languages
73−from searx.search import SearchWithPlugins, get_search_query_from_webapp
74+from searx.search import SearchWithPlugins
7475from searx.query import RawTextQuery
7576from searx.autocomplete import searx_bang, backends as autocomplete_backends
7677from searx.plugins import plugins
@@ -347,25 +348,6 @@ def render(template_name, override_theme=None, **kwargs):
347348 _get_ordered_categories()
348349 if x in enabled_categories]
349350
350− if 'all_categories' not in kwargs:
351− kwargs['all_categories'] = _get_ordered_categories()
352−
353− if 'selected_categories' not in kwargs:
354− kwargs['selected_categories'] = []
355− for arg in request.args:
356− if arg.startswith('category_'):
357− c = arg.split('_', 1)[1]
358− if c in categories:
359− kwargs['selected_categories'].append(c)
360−
361− if not kwargs['selected_categories']:
362− cookie_categories = request.preferences.get_value('categories')
363− for ccateg in cookie_categories:
364− kwargs['selected_categories'].append(ccateg)
365−
366− if not kwargs['selected_categories']:
367− kwargs['selected_categories'] = ['general']
368−
369351 if 'autocomplete' not in kwargs:
370352 kwargs['autocomplete'] = request.preferences.get_value('autocomplete')
371353
@@ -524,6 +506,7 @@ def index_error(output_format, error_message):
524506 request.errors.append(gettext('search error'))
525507 return render(
526508 'index.html',
509+ selected_categories=get_selected_categories(request.preferences, request.form),
527510 )
528511
529512
@@ -545,6 +528,7 @@ def index():
545528 if output_format == 'html':
546529 return render(
547530 'index.html',
531+ selected_categories=get_selected_categories(request.preferences, request.form),
548532 )
549533 else:
550534 return index_error(output_format, 'No query'), 400
@@ -554,7 +538,7 @@ def index():
554538 raw_text_query = None
555539 result_container = None
556540 try:
557− search_query, raw_text_query = get_search_query_from_webapp(request.preferences, request.form)
541+ search_query, raw_text_query, _, _ = get_search_query_from_webapp(request.preferences, request.form)
558542 # search = Search(search_query) # without plugins
559543 search = SearchWithPlugins(search_query, request.user_plugins, request)
560544
@@ -825,6 +809,8 @@ def preferences():
825809 # end of stats
826810
827811 return render('preferences.html',
812+ selected_categories=get_selected_categories(request.preferences, request.form),
813+ all_categories=_get_ordered_categories(),
828814 locales=settings['locales'],
829815 current_locale=request.preferences.get_value("locale"),
830816 image_proxy=image_proxy,
tests/unit/test_search.py+19−65
@@ -1,16 +1,14 @@
11# -*- coding: utf-8 -*-
22
33from searx.testing import SearxTestCase
4−from searx.preferences import Preferences
5−from searx.engines import engines
6−
4+from searx.search import SearchQuery, EngineRef
75import searx.search
6+import searx.engines
87
98
109SAFESEARCH = 0
1110PAGENO = 1
1211PUBLIC_ENGINE_NAME = 'general dummy'
13−PRIVATE_ENGINE_NAME = 'general private offline'
1412TEST_ENGINES = [
1513 {
1614 'name': PUBLIC_ENGINE_NAME,
@@ -20,15 +18,6 @@ TEST_ENGINES = [
2018 'timeout': 3.0,
2119 'tokens': [],
2220 },
23− {
24− 'name': PRIVATE_ENGINE_NAME,
25− 'engine': 'dummy-offline',
26− 'categories': 'general',
27− 'shortcut': 'do',
28− 'timeout': 3.0,
29− 'offline': True,
30− 'tokens': ['my-token'],
31− },
3221]
3322
3423
@@ -40,92 +29,57 @@ class SearchTestCase(SearxTestCase):
4029
4130 def test_timeout_simple(self):
4231 searx.search.max_request_timeout = None
43− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
44− ['general'], 'en-US', SAFESEARCH, PAGENO, None, None,
45− preferences=Preferences(['oscar'], ['general'], engines, []))
32+ search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
33+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, None)
4634 search = searx.search.Search(search_query)
4735 search.search()
4836 self.assertEqual(search.actual_timeout, 3.0)
4937
5038 def test_timeout_query_above_default_nomax(self):
5139 searx.search.max_request_timeout = None
52− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
53− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 5.0,
54− preferences=Preferences(['oscar'], ['general'], engines, []))
40+ search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
41+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, 5.0)
5542 search = searx.search.Search(search_query)
5643 search.search()
5744 self.assertEqual(search.actual_timeout, 3.0)
5845
5946 def test_timeout_query_below_default_nomax(self):
6047 searx.search.max_request_timeout = None
61− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
62− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 1.0,
63− preferences=Preferences(['oscar'], ['general'], engines, []))
48+ search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
49+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, 1.0)
6450 search = searx.search.Search(search_query)
6551 search.search()
6652 self.assertEqual(search.actual_timeout, 1.0)
6753
6854 def test_timeout_query_below_max(self):
6955 searx.search.max_request_timeout = 10.0
70− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
71− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 5.0,
72− preferences=Preferences(['oscar'], ['general'], engines, []))
56+ search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
57+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, 5.0)
7358 search = searx.search.Search(search_query)
7459 search.search()
7560 self.assertEqual(search.actual_timeout, 5.0)
7661
7762 def test_timeout_query_above_max(self):
7863 searx.search.max_request_timeout = 10.0
79− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
80− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 15.0,
81− preferences=Preferences(['oscar'], ['general'], engines, []))
64+ search_query = SearchQuery('test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
65+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, 15.0)
8266 search = searx.search.Search(search_query)
8367 search.search()
8468 self.assertEqual(search.actual_timeout, 10.0)
8569
86− def test_query_private_engine_without_token(self):
87− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PRIVATE_ENGINE_NAME}],
88− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 2.0,
89− preferences=Preferences(['oscar'], ['general'], engines, []))
90− search = searx.search.Search(search_query)
91− results = search.search()
92− self.assertEqual(results.results_length(), 0)
93−
94− def test_query_private_engine_with_incorrect_token(self):
95− preferences_with_tokens = Preferences(['oscar'], ['general'], engines, [])
96− preferences_with_tokens.parse_dict({'tokens': 'bad-token'})
97− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PRIVATE_ENGINE_NAME}],
98− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 2.0,
99− preferences=preferences_with_tokens)
100− search = searx.search.Search(search_query)
101− results = search.search()
102− self.assertEqual(results.results_length(), 0)
103−
104− def test_query_private_engine_with_correct_token(self):
105− preferences_with_tokens = Preferences(['oscar'], ['general'], engines, [])
106− preferences_with_tokens.parse_dict({'tokens': 'my-token'})
107− search_query = searx.query.SearchQuery('test', [{'category': 'general', 'name': PRIVATE_ENGINE_NAME}],
108− ['general'], 'en-US', SAFESEARCH, PAGENO, None, 2.0,
109− preferences=preferences_with_tokens)
110− search = searx.search.Search(search_query)
111− results = search.search()
112− self.assertEqual(results.results_length(), 1)
113−
11470 def test_external_bang(self):
115− search_query = searx.query.SearchQuery('yes yes',
116− [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
117− ['general'], 'en-US', SAFESEARCH, PAGENO, None, None,
118− preferences=Preferences(['oscar'], ['general'], engines, [],),
119− external_bang="yt")
71+ search_query = SearchQuery('yes yes',
72+ [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
73+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, None,
74+ external_bang="yt")
12075 search = searx.search.Search(search_query)
12176 results = search.search()
12277 # For checking if the user redirected with the youtube external bang
12378 self.assertTrue(results.redirect_url is not None)
12479
125− search_query = searx.query.SearchQuery('youtube never gonna give you up',
126− [{'category': 'general', 'name': PUBLIC_ENGINE_NAME}],
127− ['general'], 'en-US', SAFESEARCH, PAGENO, None, None,
128− preferences=Preferences(['oscar'], ['general'], engines, []),)
80+ search_query = SearchQuery('youtube never gonna give you up',
81+ [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
82+ ['general'], 'en-US', SAFESEARCH, PAGENO, None, None)
12983
13084 search = searx.search.Search(search_query)
13185 results = search.search()
tests/unit/test_webadapter.pynew file+54
@@ -0,0 +1,54 @@
1+# -*- coding: utf-8 -*-
2+
3+from searx.testing import SearxTestCase
4+from searx.preferences import Preferences
5+from searx.engines import engines
6+
7+import searx.search
8+from searx.search import EngineRef, SearchQuery
9+from searx.webadapter import validate_engineref_list
10+
11+
12+PRIVATE_ENGINE_NAME = 'general private offline'
13+TEST_ENGINES = [
14+ {
15+ 'name': PRIVATE_ENGINE_NAME,
16+ 'engine': 'dummy-offline',
17+ 'categories': 'general',
18+ 'shortcut': 'do',
19+ 'timeout': 3.0,
20+ 'offline': True,
21+ 'tokens': ['my-token'],
22+ },
23+]
24+SEARCHQUERY = [EngineRef(PRIVATE_ENGINE_NAME, 'general')]
25+
26+
27+class ValidateQueryCase(SearxTestCase):
28+
29+ @classmethod
30+ def setUpClass(cls):
31+ searx.engines.initialize_engines(TEST_ENGINES)
32+
33+ def test_query_private_engine_without_token(self):
34+ preferences = Preferences(['oscar'], ['general'], engines, [])
35+ valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences)
36+ self.assertEqual(len(valid), 0)
37+ self.assertEqual(len(unknown), 0)
38+ self.assertEqual(len(invalid_token), 1)
39+
40+ def test_query_private_engine_with_incorrect_token(self):
41+ preferences_with_tokens = Preferences(['oscar'], ['general'], engines, [])
42+ preferences_with_tokens.parse_dict({'tokens': 'bad-token'})
43+ valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences_with_tokens)
44+ self.assertEqual(len(valid), 0)
45+ self.assertEqual(len(unknown), 0)
46+ self.assertEqual(len(invalid_token), 1)
47+
48+ def test_query_private_engine_with_correct_token(self):
49+ preferences_with_tokens = Preferences(['oscar'], ['general'], engines, [])
50+ preferences_with_tokens.parse_dict({'tokens': 'my-token'})
51+ valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences_with_tokens)
52+ self.assertEqual(len(valid), 1)
53+ self.assertEqual(len(unknown), 0)
54+ self.assertEqual(len(invalid_token), 0)
utils/standalone_searx.py+3−1
@@ -30,7 +30,9 @@ import codecs
3030import searx.query
3131import searx.search
3232import searx.engines
33+import searx.webapdater
3334import searx.preferences
35+import searx.webadapter
3436import argparse
3537
3638searx.engines.initialize_engines(settings['engines'])
@@ -64,7 +66,7 @@ form = {
6466preferences = searx.preferences.Preferences(['oscar'], searx.engines.categories.keys(), searx.engines.engines, [])
6567preferences.key_value_settings['safesearch'].parse(args.safesearch)
6668
67−search_query, raw_text_query = searx.search.get_search_query_from_webapp(preferences, form)
69+search_query, raw_text_query, _, _ = searx.webadapter.get_search_query_from_webapp(preferences, form)
6870search = searx.search.Search(search_query)
6971result_container = search.search()
7072