Browse Source

Merge branch 'master' into searchpy2

Alexandre Flament 8 years ago
parent
commit
e48f07a367
63 changed files with 392 additions and 201 deletions
  1. 46 0
      searx/answerers/__init__.py
  2. 50 0
      searx/answerers/random/answerer.py
  3. 51 0
      searx/answerers/statistics/answerer.py
  4. 4 13
      searx/engines/__init__.py
  5. 1 2
      searx/engines/archlinux.py
  6. 1 2
      searx/engines/base.py
  7. 6 11
      searx/engines/bing.py
  8. 2 3
      searx/engines/btdigg.py
  9. 1 2
      searx/engines/dailymotion.py
  10. 5 4
      searx/engines/deezer.py
  11. 2 3
      searx/engines/dictzone.py
  12. 1 2
      searx/engines/digg.py
  13. 1 2
      searx/engines/fdroid.py
  14. 3 11
      searx/engines/flickr.py
  15. 3 4
      searx/engines/flickr_noapi.py
  16. 2 3
      searx/engines/gigablast.py
  17. 1 2
      searx/engines/github.py
  18. 2 3
      searx/engines/google.py
  19. 1 2
      searx/engines/kickass.py
  20. 2 4
      searx/engines/nyaa.py
  21. 1 1
      searx/engines/openstreetmap.py
  22. 1 2
      searx/engines/piratebay.py
  23. 1 2
      searx/engines/reddit.py
  24. 2 10
      searx/engines/searchcode_doc.py
  25. 0 1
      searx/engines/seedpeer.py
  26. 5 4
      searx/engines/spotify.py
  27. 2 3
      searx/engines/stackoverflow.py
  28. 2 3
      searx/engines/startpage.py
  29. 2 3
      searx/engines/subtitleseeker.py
  30. 4 5
      searx/engines/swisscows.py
  31. 0 1
      searx/engines/tokyotoshokan.py
  32. 0 1
      searx/engines/torrentz.py
  33. 5 6
      searx/engines/translated.py
  34. 0 1
      searx/engines/wolframalpha_noapi.py
  35. 2 3
      searx/engines/yandex.py
  36. 1 1
      searx/plugins/doai_rewrite.py
  37. 17 14
      searx/preferences.py
  38. 5 4
      searx/results.py
  39. 9 0
      searx/search.py
  40. 1 0
      searx/settings.yml
  41. 1 0
      searx/settings_robot.yml
  42. 1 1
      searx/static/plugins/js/infinite_scroll.js
  43. 4 4
      searx/templates/courgette/opensearch_response_rss.xml
  44. 5 5
      searx/templates/courgette/results.html
  45. 4 4
      searx/templates/legacy/opensearch_response_rss.xml
  46. 5 5
      searx/templates/legacy/results.html
  47. 15 0
      searx/templates/oscar/base.html
  48. 4 4
      searx/templates/oscar/opensearch_response_rss.xml
  49. 29 0
      searx/templates/oscar/preferences.html
  50. 6 1
      searx/templates/oscar/result_templates/images.html
  51. 7 7
      searx/templates/oscar/results.html
  52. 3 3
      searx/templates/pix-art/results.html
  53. 13 0
      searx/utils.py
  54. 20 11
      searx/webapp.py
  55. 2 4
      tests/unit/engines/test_bing.py
  56. 1 1
      tests/unit/engines/test_deezer.py
  57. 3 3
      tests/unit/engines/test_flickr.py
  58. 3 3
      tests/unit/engines/test_flickr_noapi.py
  59. 2 2
      tests/unit/engines/test_kickass.py
  60. 0 3
      tests/unit/engines/test_searchcode_doc.py
  61. 1 1
      tests/unit/engines/test_spotify.py
  62. 16 0
      tests/unit/test_answerers.py
  63. 2 1
      tests/unit/test_webapp.py

+ 46 - 0
searx/answerers/__init__.py

@@ -0,0 +1,46 @@
+from os import listdir
+from os.path import realpath, dirname, join, isdir
+from searx.utils import load_module
+from collections import defaultdict
+
+
+answerers_dir = dirname(realpath(__file__))
+
+
+def load_answerers():
+    answerers = []
+    for filename in listdir(answerers_dir):
+        if not isdir(join(answerers_dir, filename)):
+            continue
+        module = load_module('answerer.py', join(answerers_dir, filename))
+        if not hasattr(module, 'keywords') or not isinstance(module.keywords, tuple) or not len(module.keywords):
+            exit(2)
+        answerers.append(module)
+    return answerers
+
+
+def get_answerers_by_keywords(answerers):
+    by_keyword = defaultdict(list)
+    for answerer in answerers:
+        for keyword in answerer.keywords:
+            for keyword in answerer.keywords:
+                by_keyword[keyword].append(answerer.answer)
+    return by_keyword
+
+
+def ask(query):
+    results = []
+    query_parts = filter(None, query.query.split())
+
+    if query_parts[0] not in answerers_by_keywords:
+        return results
+
+    for answerer in answerers_by_keywords[query_parts[0]]:
+        result = answerer(query)
+        if result:
+            results.append(result)
+    return results
+
+
+answerers = load_answerers()
+answerers_by_keywords = get_answerers_by_keywords(answerers)

+ 50 - 0
searx/answerers/random/answerer.py

@@ -0,0 +1,50 @@
+import random
+import string
+from flask_babel import gettext
+
+# required answerer attribute
+# specifies which search query keywords triggers this answerer
+keywords = ('random',)
+
+random_int_max = 2**31
+
+random_string_letters = string.lowercase + string.digits + string.uppercase
+
+
+def random_string():
+    return u''.join(random.choice(random_string_letters)
+                    for _ in range(random.randint(8, 32)))
+
+
+def random_float():
+    return unicode(random.random())
+
+
+def random_int():
+    return unicode(random.randint(-random_int_max, random_int_max))
+
+
+random_types = {u'string': random_string,
+                u'int': random_int,
+                u'float': random_float}
+
+
+# required answerer function
+# can return a list of results (any result type) for a given query
+def answer(query):
+    parts = query.query.split()
+    if len(parts) != 2:
+        return []
+
+    if parts[1] not in random_types:
+        return []
+
+    return [{'answer': random_types[parts[1]]()}]
+
+
+# required answerer function
+# returns information about the answerer
+def self_info():
+    return {'name': gettext('Random value generator'),
+            'description': gettext('Generate different random values'),
+            'examples': [u'random {}'.format(x) for x in random_types]}

+ 51 - 0
searx/answerers/statistics/answerer.py

@@ -0,0 +1,51 @@
+from functools import reduce
+from operator import mul
+
+from flask_babel import gettext
+
+keywords = ('min',
+            'max',
+            'avg',
+            'sum',
+            'prod')
+
+
+# required answerer function
+# can return a list of results (any result type) for a given query
+def answer(query):
+    parts = query.query.split()
+
+    if len(parts) < 2:
+        return []
+
+    try:
+        args = map(float, parts[1:])
+    except:
+        return []
+
+    func = parts[0]
+    answer = None
+
+    if func == 'min':
+        answer = min(args)
+    elif func == 'max':
+        answer = max(args)
+    elif func == 'avg':
+        answer = sum(args) / len(args)
+    elif func == 'sum':
+        answer = sum(args)
+    elif func == 'prod':
+        answer = reduce(mul, args, 1)
+
+    if answer is None:
+        return []
+
+    return [{'answer': unicode(answer)}]
+
+
+# required answerer function
+# returns information about the answerer
+def self_info():
+    return {'name': gettext('Statistics functions'),
+            'description': gettext('Compute {functions} of the arguments').format(functions='/'.join(keywords)),
+            'examples': ['avg 123 548 2.04 24.2']}

+ 4 - 13
searx/engines/__init__.py

@@ -16,13 +16,13 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
 (C) 2013- by Adam Tauber, <asciimoo@gmail.com>
 (C) 2013- by Adam Tauber, <asciimoo@gmail.com>
 '''
 '''
 
 
-from os.path import realpath, dirname, splitext, join
+from os.path import realpath, dirname
 import sys
 import sys
-from imp import load_source
 from flask_babel import gettext
 from flask_babel import gettext
 from operator import itemgetter
 from operator import itemgetter
 from searx import settings
 from searx import settings
 from searx import logger
 from searx import logger
+from searx.utils import load_module
 
 
 
 
 logger = logger.getChild('engines')
 logger = logger.getChild('engines')
@@ -32,6 +32,7 @@ engine_dir = dirname(realpath(__file__))
 engines = {}
 engines = {}
 
 
 categories = {'general': []}
 categories = {'general': []}
+_initialized = False
 
 
 engine_shortcuts = {}
 engine_shortcuts = {}
 engine_default_args = {'paging': False,
 engine_default_args = {'paging': False,
@@ -46,16 +47,6 @@ engine_default_args = {'paging': False,
                        'time_range_support': False}
                        'time_range_support': False}
 
 
 
 
-def load_module(filename):
-    modname = splitext(filename)[0]
-    if modname in sys.modules:
-        del sys.modules[modname]
-    filepath = join(engine_dir, filename)
-    module = load_source(modname, filepath)
-    module.name = modname
-    return module
-
-
 def load_engine(engine_data):
 def load_engine(engine_data):
 
 
     if '_' in engine_data['name']:
     if '_' in engine_data['name']:
@@ -65,7 +56,7 @@ def load_engine(engine_data):
     engine_module = engine_data['engine']
     engine_module = engine_data['engine']
 
 
     try:
     try:
-        engine = load_module(engine_module + '.py')
+        engine = load_module(engine_module + '.py', engine_dir)
     except:
     except:
         logger.exception('Cannot load engine "{}"'.format(engine_module))
         logger.exception('Cannot load engine "{}"'.format(engine_module))
         return None
         return None

+ 1 - 2
searx/engines/archlinux.py

@@ -12,7 +12,6 @@
 """
 """
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text
@@ -135,7 +134,7 @@ def response(resp):
     for result in dom.xpath(xpath_results):
     for result in dom.xpath(xpath_results):
         link = result.xpath(xpath_link)[0]
         link = result.xpath(xpath_link)[0]
         href = urljoin(base_url, link.attrib.get('href'))
         href = urljoin(base_url, link.attrib.get('href'))
-        title = escape(extract_text(link))
+        title = extract_text(link)
 
 
         results.append({'url': href,
         results.append({'url': href,
                         'title': title})
                         'title': title})

+ 1 - 2
searx/engines/base.py

@@ -16,7 +16,6 @@
 from lxml import etree
 from lxml import etree
 from urllib import urlencode
 from urllib import urlencode
 from searx.utils import searx_useragent
 from searx.utils import searx_useragent
-from cgi import escape
 from datetime import datetime
 from datetime import datetime
 import re
 import re
 
 
@@ -94,7 +93,7 @@ def response(resp):
                 url = item.text
                 url = item.text
 
 
             elif item.attrib["name"] == "dcdescription":
             elif item.attrib["name"] == "dcdescription":
-                content = escape(item.text[:300])
+                content = item.text[:300]
                 if len(item.text) > 300:
                 if len(item.text) > 300:
                     content += "..."
                     content += "..."
 
 

+ 6 - 11
searx/engines/bing.py

@@ -14,7 +14,6 @@
 """
 """
 
 
 from urllib import urlencode
 from urllib import urlencode
-from cgi import escape
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text
 
 
@@ -32,18 +31,14 @@ search_string = 'search?{query}&first={offset}'
 def request(query, params):
 def request(query, params):
     offset = (params['pageno'] - 1) * 10 + 1
     offset = (params['pageno'] - 1) * 10 + 1
 
 
-    if params['language'] == 'all':
-        language = 'en-US'
-    else:
-        language = params['language'].replace('_', '-')
+    if params['language'] != 'all':
+        query = u'language:{} {}'.format(params['language'].split('_')[0].upper(),
+                                         query.decode('utf-8')).encode('utf-8')
 
 
     search_path = search_string.format(
     search_path = search_string.format(
-        query=urlencode({'q': query, 'setmkt': language}),
+        query=urlencode({'q': query}),
         offset=offset)
         offset=offset)
 
 
-    params['cookies']['SRCHHPGUSR'] = \
-        'NEWWND=0&NRSLT=-1&SRCHLANG=' + language.split('-')[0]
-
     params['url'] = base_url + search_path
     params['url'] = base_url + search_path
     return params
     return params
 
 
@@ -65,7 +60,7 @@ def response(resp):
         link = result.xpath('.//h3/a')[0]
         link = result.xpath('.//h3/a')[0]
         url = link.attrib.get('href')
         url = link.attrib.get('href')
         title = extract_text(link)
         title = extract_text(link)
-        content = escape(extract_text(result.xpath('.//p')))
+        content = extract_text(result.xpath('.//p'))
 
 
         # append result
         # append result
         results.append({'url': url,
         results.append({'url': url,
@@ -77,7 +72,7 @@ def response(resp):
         link = result.xpath('.//h2/a')[0]
         link = result.xpath('.//h2/a')[0]
         url = link.attrib.get('href')
         url = link.attrib.get('href')
         title = extract_text(link)
         title = extract_text(link)
-        content = escape(extract_text(result.xpath('.//p')))
+        content = extract_text(result.xpath('.//p'))
 
 
         # append result
         # append result
         results.append({'url': url,
         results.append({'url': url,

+ 2 - 3
searx/engines/btdigg.py

@@ -11,7 +11,6 @@
 """
 """
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import quote
 from urllib import quote
 from lxml import html
 from lxml import html
 from operator import itemgetter
 from operator import itemgetter
@@ -51,8 +50,8 @@ def response(resp):
     for result in search_res:
     for result in search_res:
         link = result.xpath('.//td[@class="torrent_name"]//a')[0]
         link = result.xpath('.//td[@class="torrent_name"]//a')[0]
         href = urljoin(url, link.attrib.get('href'))
         href = urljoin(url, link.attrib.get('href'))
-        title = escape(extract_text(link))
-        content = escape(extract_text(result.xpath('.//pre[@class="snippet"]')[0]))
+        title = extract_text(link)
+        content = extract_text(result.xpath('.//pre[@class="snippet"]')[0])
         content = "<br />".join(content.split("\n"))
         content = "<br />".join(content.split("\n"))
 
 
         filesize = result.xpath('.//span[@class="attr_val"]/text()')[0].split()[0]
         filesize = result.xpath('.//span[@class="attr_val"]/text()')[0].split()[0]

+ 1 - 2
searx/engines/dailymotion.py

@@ -14,7 +14,6 @@
 
 
 from urllib import urlencode
 from urllib import urlencode
 from json import loads
 from json import loads
-from cgi import escape
 from datetime import datetime
 from datetime import datetime
 
 
 # engine dependent config
 # engine dependent config
@@ -57,7 +56,7 @@ def response(resp):
     for res in search_res['list']:
     for res in search_res['list']:
         title = res['title']
         title = res['title']
         url = res['url']
         url = res['url']
-        content = escape(res['description'])
+        content = res['description']
         thumbnail = res['thumbnail_360_url']
         thumbnail = res['thumbnail_360_url']
         publishedDate = datetime.fromtimestamp(res['created_time'], None)
         publishedDate = datetime.fromtimestamp(res['created_time'], None)
         embedded = embedded_url.format(videoid=res['id'])
         embedded = embedded_url.format(videoid=res['id'])

+ 5 - 4
searx/engines/deezer.py

@@ -51,10 +51,11 @@ def response(resp):
             if url.startswith('http://'):
             if url.startswith('http://'):
                 url = 'https' + url[4:]
                 url = 'https' + url[4:]
 
 
-            content = result['artist']['name'] +\
-                " &bull; " +\
-                result['album']['title'] +\
-                " &bull; " + result['title']
+            content = '{} - {} - {}'.format(
+                result['artist']['name'],
+                result['album']['title'],
+                result['title'])
+
             embedded = embedded_url.format(audioid=result['id'])
             embedded = embedded_url.format(audioid=result['id'])
 
 
             # append result
             # append result

+ 2 - 3
searx/engines/dictzone.py

@@ -12,7 +12,6 @@
 import re
 import re
 from urlparse import urljoin
 from urlparse import urljoin
 from lxml import html
 from lxml import html
-from cgi import escape
 from searx.utils import is_valid_lang
 from searx.utils import is_valid_lang
 
 
 categories = ['general']
 categories = ['general']
@@ -62,8 +61,8 @@ def response(resp):
 
 
         results.append({
         results.append({
             'url': urljoin(resp.url, '?%d' % k),
             'url': urljoin(resp.url, '?%d' % k),
-            'title': escape(from_result.text_content()),
-            'content': escape('; '.join(to_results))
+            'title': from_result.text_content(),
+            'content': '; '.join(to_results)
         })
         })
 
 
     return results
     return results

+ 1 - 2
searx/engines/digg.py

@@ -13,7 +13,6 @@
 from urllib import quote_plus
 from urllib import quote_plus
 from json import loads
 from json import loads
 from lxml import html
 from lxml import html
-from cgi import escape
 from dateutil import parser
 from dateutil import parser
 
 
 # engine dependent config
 # engine dependent config
@@ -56,7 +55,7 @@ def response(resp):
         url = result.attrib.get('data-contenturl')
         url = result.attrib.get('data-contenturl')
         thumbnail = result.xpath('.//img')[0].attrib.get('src')
         thumbnail = result.xpath('.//img')[0].attrib.get('src')
         title = ''.join(result.xpath(title_xpath))
         title = ''.join(result.xpath(title_xpath))
-        content = escape(''.join(result.xpath(content_xpath)))
+        content = ''.join(result.xpath(content_xpath))
         pubdate = result.xpath(pubdate_xpath)[0].attrib.get('datetime')
         pubdate = result.xpath(pubdate_xpath)[0].attrib.get('datetime')
         publishedDate = parser.parse(pubdate)
         publishedDate = parser.parse(pubdate)
 
 

+ 1 - 2
searx/engines/fdroid.py

@@ -9,7 +9,6 @@
  @parse        url, title, content
  @parse        url, title, content
 """
 """
 
 
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text
 from lxml import html
 from lxml import html
@@ -43,7 +42,7 @@ def response(resp):
         img_src = app.xpath('.//img/@src')[0]
         img_src = app.xpath('.//img/@src')[0]
 
 
         content = extract_text(app.xpath('./p')[0])
         content = extract_text(app.xpath('./p')[0])
-        content = escape(content.replace(title, '', 1).strip())
+        content = content.replace(title, '', 1).strip()
 
 
         results.append({'url': url,
         results.append({'url': url,
                         'title': title,
                         'title': title,

+ 3 - 11
searx/engines/flickr.py

@@ -77,21 +77,13 @@ def response(resp):
 
 
         url = build_flickr_url(photo['owner'], photo['id'])
         url = build_flickr_url(photo['owner'], photo['id'])
 
 
-        title = photo['title']
-
-        content = '<span class="photo-author">' +\
-                  photo['ownername'] +\
-                  '</span><br />' +\
-                  '<span class="description">' +\
-                  photo['description']['_content'] +\
-                  '</span>'
-
         # append result
         # append result
         results.append({'url': url,
         results.append({'url': url,
-                        'title': title,
+                        'title': photo['title'],
                         'img_src': img_src,
                         'img_src': img_src,
                         'thumbnail_src': thumbnail_src,
                         'thumbnail_src': thumbnail_src,
-                        'content': content,
+                        'content': photo['description']['_content'],
+                        'author': photo['ownername'],
                         'template': 'images.html'})
                         'template': 'images.html'})
 
 
     # return results
     # return results

+ 3 - 4
searx/engines/flickr_noapi.py

@@ -102,16 +102,15 @@ def response(resp):
 
 
         title = photo.get('title', '')
         title = photo.get('title', '')
 
 
-        content = '<span class="photo-author">' +\
-                  photo['username'] +\
-                  '</span><br />'
+        author = photo['username']
 
 
         # append result
         # append result
         results.append({'url': url,
         results.append({'url': url,
                         'title': title,
                         'title': title,
                         'img_src': img_src,
                         'img_src': img_src,
                         'thumbnail_src': thumbnail_src,
                         'thumbnail_src': thumbnail_src,
-                        'content': content,
+                        'content': '',
+                        'author': author,
                         'template': 'images.html'})
                         'template': 'images.html'})
 
 
     return results
     return results

+ 2 - 3
searx/engines/gigablast.py

@@ -10,7 +10,6 @@
  @parse       url, title, content
  @parse       url, title, content
 """
 """
 
 
-from cgi import escape
 from json import loads
 from json import loads
 from random import randint
 from random import randint
 from time import time
 from time import time
@@ -78,8 +77,8 @@ def response(resp):
     for result in response_json['results']:
     for result in response_json['results']:
         # append result
         # append result
         results.append({'url': result['url'],
         results.append({'url': result['url'],
-                        'title': escape(result['title']),
-                        'content': escape(result['sum'])})
+                        'title': result['title'],
+                        'content': result['sum']})
 
 
     # return results
     # return results
     return results
     return results

+ 1 - 2
searx/engines/github.py

@@ -12,7 +12,6 @@
 
 
 from urllib import urlencode
 from urllib import urlencode
 from json import loads
 from json import loads
-from cgi import escape
 
 
 # engine dependent config
 # engine dependent config
 categories = ['it']
 categories = ['it']
@@ -48,7 +47,7 @@ def response(resp):
         url = res['html_url']
         url = res['html_url']
 
 
         if res['description']:
         if res['description']:
-            content = escape(res['description'][:500])
+            content = res['description'][:500]
         else:
         else:
             content = ''
             content = ''
 
 

+ 2 - 3
searx/engines/google.py

@@ -9,7 +9,6 @@
 # @parse       url, title, content, suggestion
 # @parse       url, title, content, suggestion
 
 
 import re
 import re
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from urlparse import urlparse, parse_qsl
 from urlparse import urlparse, parse_qsl
 from lxml import html, etree
 from lxml import html, etree
@@ -155,7 +154,7 @@ def parse_url(url_string, google_hostname):
 def extract_text_from_dom(result, xpath):
 def extract_text_from_dom(result, xpath):
     r = result.xpath(xpath)
     r = result.xpath(xpath)
     if len(r) > 0:
     if len(r) > 0:
-        return escape(extract_text(r[0]))
+        return extract_text(r[0])
     return None
     return None
 
 
 
 
@@ -264,7 +263,7 @@ def response(resp):
     # parse suggestion
     # parse suggestion
     for suggestion in dom.xpath(suggestion_xpath):
     for suggestion in dom.xpath(suggestion_xpath):
         # append suggestion
         # append suggestion
-        results.append({'suggestion': escape(extract_text(suggestion))})
+        results.append({'suggestion': extract_text(suggestion)})
 
 
     # return results
     # return results
     return results
     return results

+ 1 - 2
searx/engines/kickass.py

@@ -11,7 +11,6 @@
 """
 """
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import quote
 from urllib import quote
 from lxml import html
 from lxml import html
 from operator import itemgetter
 from operator import itemgetter
@@ -57,7 +56,7 @@ def response(resp):
         link = result.xpath('.//a[@class="cellMainLink"]')[0]
         link = result.xpath('.//a[@class="cellMainLink"]')[0]
         href = urljoin(url, link.attrib['href'])
         href = urljoin(url, link.attrib['href'])
         title = extract_text(link)
         title = extract_text(link)
-        content = escape(extract_text(result.xpath(content_xpath)))
+        content = extract_text(result.xpath(content_xpath))
         seed = extract_text(result.xpath('.//td[contains(@class, "green")]'))
         seed = extract_text(result.xpath('.//td[contains(@class, "green")]'))
         leech = extract_text(result.xpath('.//td[contains(@class, "red")]'))
         leech = extract_text(result.xpath('.//td[contains(@class, "red")]'))
         filesize_info = extract_text(result.xpath('.//td[contains(@class, "nobr")]'))
         filesize_info = extract_text(result.xpath('.//td[contains(@class, "nobr")]'))

+ 2 - 4
searx/engines/nyaa.py

@@ -9,7 +9,6 @@
  @parse        url, title, content, seed, leech, torrentfile
  @parse        url, title, content, seed, leech, torrentfile
 """
 """
 
 
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text
@@ -78,7 +77,7 @@ def response(resp):
 
 
         # torrent title
         # torrent title
         page_a = result.xpath(xpath_title)[0]
         page_a = result.xpath(xpath_title)[0]
-        title = escape(extract_text(page_a))
+        title = extract_text(page_a)
 
 
         # link to the page
         # link to the page
         href = page_a.attrib.get('href')
         href = page_a.attrib.get('href')
@@ -90,7 +89,7 @@ def response(resp):
         try:
         try:
             file_size, suffix = result.xpath(xpath_filesize)[0].split(' ')
             file_size, suffix = result.xpath(xpath_filesize)[0].split(' ')
             file_size = int(float(file_size) * get_filesize_mul(suffix))
             file_size = int(float(file_size) * get_filesize_mul(suffix))
-        except Exception as e:
+        except:
             file_size = None
             file_size = None
 
 
         # seed count
         # seed count
@@ -105,7 +104,6 @@ def response(resp):
         # content string contains all information not included into template
         # content string contains all information not included into template
         content = 'Category: "{category}". Downloaded {downloads} times.'
         content = 'Category: "{category}". Downloaded {downloads} times.'
         content = content.format(category=category, downloads=downloads)
         content = content.format(category=category, downloads=downloads)
-        content = escape(content)
 
 
         results.append({'url': href,
         results.append({'url': href,
                         'title': title,
                         'title': title,

+ 1 - 1
searx/engines/openstreetmap.py

@@ -43,7 +43,7 @@ def response(resp):
         if 'display_name' not in r:
         if 'display_name' not in r:
             continue
             continue
 
 
-        title = r['display_name']
+        title = r['display_name'] or u''
         osm_type = r.get('osm_type', r.get('type'))
         osm_type = r.get('osm_type', r.get('type'))
         url = result_base_url.format(osm_type=osm_type,
         url = result_base_url.format(osm_type=osm_type,
                                      osm_id=r['osm_id'])
                                      osm_id=r['osm_id'])

+ 1 - 2
searx/engines/piratebay.py

@@ -9,7 +9,6 @@
 # @parse       url, title, content, seed, leech, magnetlink
 # @parse       url, title, content, seed, leech, magnetlink
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import quote
 from urllib import quote
 from lxml import html
 from lxml import html
 from operator import itemgetter
 from operator import itemgetter
@@ -62,7 +61,7 @@ def response(resp):
         link = result.xpath('.//div[@class="detName"]//a')[0]
         link = result.xpath('.//div[@class="detName"]//a')[0]
         href = urljoin(url, link.attrib.get('href'))
         href = urljoin(url, link.attrib.get('href'))
         title = extract_text(link)
         title = extract_text(link)
-        content = escape(extract_text(result.xpath(content_xpath)))
+        content = extract_text(result.xpath(content_xpath))
         seed, leech = result.xpath('.//td[@align="right"]/text()')[:2]
         seed, leech = result.xpath('.//td[@align="right"]/text()')[:2]
 
 
         # convert seed to int if possible
         # convert seed to int if possible

+ 1 - 2
searx/engines/reddit.py

@@ -11,7 +11,6 @@
 """
 """
 
 
 import json
 import json
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from urlparse import urlparse, urljoin
 from urlparse import urlparse, urljoin
 from datetime import datetime
 from datetime import datetime
@@ -68,7 +67,7 @@ def response(resp):
             img_results.append(params)
             img_results.append(params)
         else:
         else:
             created = datetime.fromtimestamp(data['created_utc'])
             created = datetime.fromtimestamp(data['created_utc'])
-            content = escape(data['selftext'])
+            content = data['selftext']
             if len(content) > 500:
             if len(content) > 500:
                 content = content[:500] + '...'
                 content = content[:500] + '...'
             params['content'] = content
             params['content'] = content

+ 2 - 10
searx/engines/searchcode_doc.py

@@ -44,20 +44,12 @@ def response(resp):
     # parse results
     # parse results
     for result in search_results.get('results', []):
     for result in search_results.get('results', []):
         href = result['url']
         href = result['url']
-        title = "[" + result['type'] + "] " +\
-                result['namespace'] +\
-                " " + result['name']
-        content = '<span class="highlight">[' +\
-                  result['type'] + "] " +\
-                  result['name'] + " " +\
-                  result['synopsis'] +\
-                  "</span><br />" +\
-                  result['description']
+        title = "[{}] {} {}".format(result['type'], result['namespace'], result['name'])
 
 
         # append result
         # append result
         results.append({'url': href,
         results.append({'url': href,
                         'title': title,
                         'title': title,
-                        'content': content})
+                        'content': result['description']})
 
 
     # return results
     # return results
     return results
     return results

+ 0 - 1
searx/engines/seedpeer.py

@@ -9,7 +9,6 @@
 # @parse       url, title, content, seed, leech, magnetlink
 # @parse       url, title, content, seed, leech, magnetlink
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import quote
 from urllib import quote
 from lxml import html
 from lxml import html
 from operator import itemgetter
 from operator import itemgetter

+ 5 - 4
searx/engines/spotify.py

@@ -46,10 +46,11 @@ def response(resp):
         if result['type'] == 'track':
         if result['type'] == 'track':
             title = result['name']
             title = result['name']
             url = result['external_urls']['spotify']
             url = result['external_urls']['spotify']
-            content = result['artists'][0]['name'] +\
-                " &bull; " +\
-                result['album']['name'] +\
-                " &bull; " + result['name']
+            content = '{} - {} - {}'.format(
+                result['artists'][0]['name'],
+                result['album']['name'],
+                result['name'])
+
             embedded = embedded_url.format(audioid=result['id'])
             embedded = embedded_url.format(audioid=result['id'])
 
 
             # append result
             # append result

+ 2 - 3
searx/engines/stackoverflow.py

@@ -11,7 +11,6 @@
 """
 """
 
 
 from urlparse import urljoin
 from urlparse import urljoin
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text
@@ -48,8 +47,8 @@ def response(resp):
     for result in dom.xpath(results_xpath):
     for result in dom.xpath(results_xpath):
         link = result.xpath(link_xpath)[0]
         link = result.xpath(link_xpath)[0]
         href = urljoin(url, link.attrib.get('href'))
         href = urljoin(url, link.attrib.get('href'))
-        title = escape(extract_text(link))
-        content = escape(extract_text(result.xpath(content_xpath)))
+        title = extract_text(link)
+        content = extract_text(result.xpath(content_xpath))
 
 
         # append result
         # append result
         results.append({'url': href,
         results.append({'url': href,

+ 2 - 3
searx/engines/startpage.py

@@ -11,7 +11,6 @@
 # @todo        paging
 # @todo        paging
 
 
 from lxml import html
 from lxml import html
-from cgi import escape
 from dateutil import parser
 from dateutil import parser
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 import re
 import re
@@ -79,10 +78,10 @@ def response(resp):
         if re.match(r"^http(s|)://(www\.)?ixquick\.com/do/search\?.*$", url):
         if re.match(r"^http(s|)://(www\.)?ixquick\.com/do/search\?.*$", url):
             continue
             continue
 
 
-        title = escape(extract_text(link))
+        title = extract_text(link)
 
 
         if result.xpath('./p[@class="desc clk"]'):
         if result.xpath('./p[@class="desc clk"]'):
-            content = escape(extract_text(result.xpath('./p[@class="desc clk"]')))
+            content = extract_text(result.xpath('./p[@class="desc clk"]'))
         else:
         else:
             content = ''
             content = ''
 
 

+ 2 - 3
searx/engines/subtitleseeker.py

@@ -10,7 +10,6 @@
  @parse       url, title, content
  @parse       url, title, content
 """
 """
 
 
-from cgi import escape
 from urllib import quote_plus
 from urllib import quote_plus
 from lxml import html
 from lxml import html
 from searx.languages import language_codes
 from searx.languages import language_codes
@@ -59,7 +58,7 @@ def response(resp):
         elif search_lang:
         elif search_lang:
             href = href + search_lang + '/'
             href = href + search_lang + '/'
 
 
-        title = escape(extract_text(link))
+        title = extract_text(link)
 
 
         content = extract_text(result.xpath('.//div[contains(@class,"red")]'))
         content = extract_text(result.xpath('.//div[contains(@class,"red")]'))
         content = content + " - "
         content = content + " - "
@@ -75,7 +74,7 @@ def response(resp):
         # append result
         # append result
         results.append({'url': href,
         results.append({'url': href,
                         'title': title,
                         'title': title,
-                        'content': escape(content)})
+                        'content': content})
 
 
     # return results
     # return results
     return results
     return results

+ 4 - 5
searx/engines/swisscows.py

@@ -10,7 +10,6 @@
  @parse       url, title, content
  @parse       url, title, content
 """
 """
 
 
-from cgi import escape
 from json import loads
 from json import loads
 from urllib import urlencode, unquote
 from urllib import urlencode, unquote
 import re
 import re
@@ -78,7 +77,7 @@ def response(resp):
 
 
             # append result
             # append result
             results.append({'url': result['SourceUrl'],
             results.append({'url': result['SourceUrl'],
-                            'title': escape(result['Title']),
+                            'title': result['Title'],
                             'content': '',
                             'content': '',
                             'img_src': img_url,
                             'img_src': img_url,
                             'template': 'images.html'})
                             'template': 'images.html'})
@@ -90,8 +89,8 @@ def response(resp):
 
 
             # append result
             # append result
             results.append({'url': result_url,
             results.append({'url': result_url,
-                            'title': escape(result_title),
-                            'content': escape(result_content)})
+                            'title': result_title,
+                            'content': result_content})
 
 
     # parse images
     # parse images
     for result in json.get('Images', []):
     for result in json.get('Images', []):
@@ -100,7 +99,7 @@ def response(resp):
 
 
         # append result
         # append result
         results.append({'url': result['SourceUrl'],
         results.append({'url': result['SourceUrl'],
-                        'title': escape(result['Title']),
+                        'title': result['Title'],
                         'content': '',
                         'content': '',
                         'img_src': img_url,
                         'img_src': img_url,
                         'template': 'images.html'})
                         'template': 'images.html'})

+ 0 - 1
searx/engines/tokyotoshokan.py

@@ -11,7 +11,6 @@
 """
 """
 
 
 import re
 import re
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text

+ 0 - 1
searx/engines/torrentz.py

@@ -12,7 +12,6 @@
 """
 """
 
 
 import re
 import re
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.engines.xpath import extract_text
 from searx.engines.xpath import extract_text

+ 5 - 6
searx/engines/translated.py

@@ -9,7 +9,6 @@
  @parse       url, title, content
  @parse       url, title, content
 """
 """
 import re
 import re
-from cgi import escape
 from searx.utils import is_valid_lang
 from searx.utils import is_valid_lang
 
 
 categories = ['general']
 categories = ['general']
@@ -52,14 +51,14 @@ def request(query, params):
 def response(resp):
 def response(resp):
     results = []
     results = []
     results.append({
     results.append({
-        'url': escape(web_url.format(
+        'url': web_url.format(
             from_lang=resp.search_params['from_lang'][2],
             from_lang=resp.search_params['from_lang'][2],
             to_lang=resp.search_params['to_lang'][2],
             to_lang=resp.search_params['to_lang'][2],
-            query=resp.search_params['query'])),
-        'title': escape('[{0}-{1}] {2}'.format(
+            query=resp.search_params['query']),
+        'title': '[{0}-{1}] {2}'.format(
             resp.search_params['from_lang'][1],
             resp.search_params['from_lang'][1],
             resp.search_params['to_lang'][1],
             resp.search_params['to_lang'][1],
-            resp.search_params['query'])),
-        'content': escape(resp.json()['responseData']['translatedText'])
+            resp.search_params['query']),
+        'content': resp.json()['responseData']['translatedText']
     })
     })
     return results
     return results

+ 0 - 1
searx/engines/wolframalpha_noapi.py

@@ -8,7 +8,6 @@
 # @stable      no
 # @stable      no
 # @parse       url, infobox
 # @parse       url, infobox
 
 
-from cgi import escape
 from json import loads
 from json import loads
 from time import time
 from time import time
 from urllib import urlencode
 from urllib import urlencode

+ 2 - 3
searx/engines/yandex.py

@@ -9,7 +9,6 @@
  @parse       url, title, content
  @parse       url, title, content
 """
 """
 
 
-from cgi import escape
 from urllib import urlencode
 from urllib import urlencode
 from lxml import html
 from lxml import html
 from searx.search import logger
 from searx.search import logger
@@ -52,8 +51,8 @@ def response(resp):
     for result in dom.xpath(results_xpath):
     for result in dom.xpath(results_xpath):
         try:
         try:
             res = {'url': result.xpath(url_xpath)[0],
             res = {'url': result.xpath(url_xpath)[0],
-                   'title': escape(''.join(result.xpath(title_xpath))),
-                   'content': escape(''.join(result.xpath(content_xpath)))}
+                   'title': ''.join(result.xpath(title_xpath)),
+                   'content': ''.join(result.xpath(content_xpath))}
         except:
         except:
             logger.exception('yandex parse crash')
             logger.exception('yandex parse crash')
             continue
             continue

+ 1 - 1
searx/plugins/doai_rewrite.py

@@ -27,5 +27,5 @@ def on_result(request, search, result):
             if doi.endswith(suffix):
             if doi.endswith(suffix):
                 doi = doi[:-len(suffix)]
                 doi = doi[:-len(suffix)]
         result['url'] = 'http://doai.io/' + doi
         result['url'] = 'http://doai.io/' + doi
-        result['parsed_url'] = urlparse(ctx['result']['url'])
+        result['parsed_url'] = urlparse(result['url'])
     return True
     return True

+ 17 - 14
searx/preferences.py

@@ -49,28 +49,32 @@ class StringSetting(Setting):
 class EnumStringSetting(Setting):
 class EnumStringSetting(Setting):
     """Setting of a value which can only come from the given choices"""
     """Setting of a value which can only come from the given choices"""
 
 
+    def _validate_selection(self, selection):
+        if selection not in self.choices:
+            raise ValidationException('Invalid value: "{0}"'.format(selection))
+
     def _post_init(self):
     def _post_init(self):
         if not hasattr(self, 'choices'):
         if not hasattr(self, 'choices'):
             raise MissingArgumentException('Missing argument: choices')
             raise MissingArgumentException('Missing argument: choices')
-
-        if self.value != '' and self.value not in self.choices:
-            raise ValidationException('Invalid default value: {0}'.format(self.value))
+        self._validate_selection(self.value)
 
 
     def parse(self, data):
     def parse(self, data):
-        if data not in self.choices and data != self.value:
-            raise ValidationException('Invalid choice: {0}'.format(data))
+        self._validate_selection(data)
         self.value = data
         self.value = data
 
 
 
 
 class MultipleChoiceSetting(EnumStringSetting):
 class MultipleChoiceSetting(EnumStringSetting):
     """Setting of values which can only come from the given choices"""
     """Setting of values which can only come from the given choices"""
 
 
+    def _validate_selections(self, selections):
+        for item in selections:
+            if item not in self.choices:
+                raise ValidationException('Invalid value: "{0}"'.format(selections))
+
     def _post_init(self):
     def _post_init(self):
         if not hasattr(self, 'choices'):
         if not hasattr(self, 'choices'):
             raise MissingArgumentException('Missing argument: choices')
             raise MissingArgumentException('Missing argument: choices')
-        for item in self.value:
-            if item not in self.choices:
-                raise ValidationException('Invalid default value: {0}'.format(self.value))
+        self._validate_selections(self.value)
 
 
     def parse(self, data):
     def parse(self, data):
         if data == '':
         if data == '':
@@ -78,9 +82,7 @@ class MultipleChoiceSetting(EnumStringSetting):
             return
             return
 
 
         elements = data.split(',')
         elements = data.split(',')
-        for item in elements:
-            if item not in self.choices:
-                raise ValidationException('Invalid choice: {0}'.format(item))
+        self._validate_selections(elements)
         self.value = elements
         self.value = elements
 
 
     def parse_form(self, data):
     def parse_form(self, data):
@@ -214,11 +216,12 @@ class Preferences(object):
         super(Preferences, self).__init__()
         super(Preferences, self).__init__()
 
 
         self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories),
         self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories),
-                                   'language': EnumStringSetting('all', choices=LANGUAGE_CODES),
+                                   'language': EnumStringSetting(settings['search']['language'],
+                                                                 choices=LANGUAGE_CODES),
                                    'locale': EnumStringSetting(settings['ui']['default_locale'],
                                    'locale': EnumStringSetting(settings['ui']['default_locale'],
-                                                               choices=settings['locales'].keys()),
+                                                               choices=settings['locales'].keys() + ['']),
                                    'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
                                    'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
-                                                                     choices=autocomplete.backends.keys()),
+                                                                     choices=autocomplete.backends.keys() + ['']),
                                    'image_proxy': MapSetting(settings['server']['image_proxy'],
                                    'image_proxy': MapSetting(settings['server']['image_proxy'],
                                                              map={'': settings['server']['image_proxy'],
                                                              map={'': settings['server']['image_proxy'],
                                                                   '0': False,
                                                                   '0': False,

+ 5 - 4
searx/results.py

@@ -146,16 +146,17 @@ class ResultContainer(object):
                 self._number_of_results.append(result['number_of_results'])
                 self._number_of_results.append(result['number_of_results'])
                 results.remove(result)
                 results.remove(result)
 
 
-        with RLock():
-            engines[engine_name].stats['search_count'] += 1
-            engines[engine_name].stats['result_count'] += len(results)
+        if engine_name in engines:
+            with RLock():
+                engines[engine_name].stats['search_count'] += 1
+                engines[engine_name].stats['result_count'] += len(results)
 
 
         if not results:
         if not results:
             return
             return
 
 
         self.results[engine_name].extend(results)
         self.results[engine_name].extend(results)
 
 
-        if not self.paging and engines[engine_name].paging:
+        if not self.paging and engine_name in engines and engines[engine_name].paging:
             self.paging = True
             self.paging = True
 
 
         for i, result in enumerate(results):
         for i, result in enumerate(results):

+ 9 - 0
searx/search.py

@@ -24,6 +24,7 @@ import searx.poolrequests as requests_lib
 from searx.engines import (
 from searx.engines import (
     categories, engines
     categories, engines
 )
 )
+from searx.answerers import ask
 from searx.utils import gen_useragent
 from searx.utils import gen_useragent
 from searx.query import RawTextQuery, SearchQuery
 from searx.query import RawTextQuery, SearchQuery
 from searx.results import ResultContainer
 from searx.results import ResultContainer
@@ -300,6 +301,14 @@ class Search(object):
         # start time
         # start time
         start_time = time()
         start_time = time()
 
 
+        # answeres ?
+        answerers_results = ask(self.search_query)
+
+        if answerers_results:
+            for results in answerers_results:
+                self.result_container.extend('answer', results)
+            return self.result_container
+
         # init vars
         # init vars
         requests = []
         requests = []
 
 

+ 1 - 0
searx/settings.yml

@@ -5,6 +5,7 @@ general:
 search:
 search:
     safe_search : 0 # Filter results. 0: None, 1: Moderate, 2: Strict
     safe_search : 0 # Filter results. 0: None, 1: Moderate, 2: Strict
     autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "wikipedia" - leave blank to turn it off by default
     autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "wikipedia" - leave blank to turn it off by default
+    language : "all"
 
 
 server:
 server:
     port : 8888
     port : 8888

+ 1 - 0
searx/settings_robot.yml

@@ -5,6 +5,7 @@ general:
 search:
 search:
     safe_search : 0
     safe_search : 0
     autocomplete : ""
     autocomplete : ""
+    language: "all"
 
 
 server:
 server:
     port : 11111
     port : 11111

+ 1 - 1
searx/static/plugins/js/infinite_scroll.js

@@ -5,7 +5,7 @@ $(document).ready(function() {
             var formData = $('#pagination form:last').serialize();
             var formData = $('#pagination form:last').serialize();
             if (formData) {
             if (formData) {
                 $('#pagination').html('<div class="loading-spinner"></div>');
                 $('#pagination').html('<div class="loading-spinner"></div>');
-                $.post('/', formData, function (data) {
+                $.post('./', formData, function (data) {
                     var body = $(data);
                     var body = $(data);
                     $('#pagination').remove();
                     $('#pagination').remove();
                     $('#main_results').append('<hr/>');
                     $('#main_results').append('<hr/>');

+ 4 - 4
searx/templates/courgette/opensearch_response_rss.xml

@@ -3,14 +3,14 @@
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:atom="http://www.w3.org/2005/Atom">
      xmlns:atom="http://www.w3.org/2005/Atom">
   <channel>
   <channel>
-    <title>Searx search: {{ q }}</title>
-    <link>{{ base_url }}?q={{ q }}</link>
-    <description>Search results for "{{ q }}" - searx</description>
+    <title>Searx search: {{ q|e }}</title>
+    <link>{{ base_url }}?q={{ q|e }}</link>
+    <description>Search results for "{{ q|e }}" - searx</description>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
-    <opensearch:Query role="request" searchTerms="{{ q }}" startPage="1" />
+    <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
     {% for r in results %}
     {% for r in results %}
     <item>
     <item>
       <title>{{ r.title }}</title>
       <title>{{ r.title }}</title>

+ 5 - 5
searx/templates/courgette/results.html

@@ -1,6 +1,6 @@
 {% extends "courgette/base.html" %}
 {% extends "courgette/base.html" %}
-{% block title %}{{ q }} - {% endblock %}
-{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}">{% endblock %}
+{% block title %}{{ q|e }} - {% endblock %}
+{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}">{% endblock %}
 {% block content %}
 {% block content %}
 <div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>{{ _('preferences') }}</span></a></div>
 <div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>{{ _('preferences') }}</span></a></div>
 <div class="small search center">
 <div class="small search center">
@@ -17,7 +17,7 @@
             {% for output_type in ('csv', 'json', 'rss') %}
             {% for output_type in ('csv', 'json', 'rss') %}
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
                 <div class="left">
                 <div class="left">
-                    <input type="hidden" name="q" value="{{ q }}" />
+                    <input type="hidden" name="q" value="{{ q|e }}" />
                     <input type="hidden" name="format" value="{{ output_type }}" />
                     <input type="hidden" name="format" value="{{ output_type }}" />
                     {% for category in selected_categories %}
                     {% for category in selected_categories %}
                     <input type="hidden" name="category_{{ category }}" value="1"/>
                     <input type="hidden" name="category_{{ category }}" value="1"/>
@@ -62,7 +62,7 @@
         {% if pageno > 1 %}
         {% if pageno > 1 %}
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
                 <div class="left">
                 <div class="left">
-                    <input type="hidden" name="q" value="{{ q }}" />
+                    <input type="hidden" name="q" value="{{ q|e }}" />
                     {% for category in selected_categories %}
                     {% for category in selected_categories %}
                     <input type="hidden" name="category_{{ category }}" value="1"/>
                     <input type="hidden" name="category_{{ category }}" value="1"/>
                     {% endfor %}
                     {% endfor %}
@@ -76,7 +76,7 @@
                 {% for category in selected_categories %}
                 {% for category in selected_categories %}
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 {% endfor %}
                 {% endfor %}
-                <input type="hidden" name="q" value="{{ q }}" />
+                <input type="hidden" name="q" value="{{ q|e }}" />
                 <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                 <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                 <input type="submit" value="{{ _('next page') }} >>" />
                 <input type="submit" value="{{ _('next page') }} >>" />
             </div>
             </div>

+ 4 - 4
searx/templates/legacy/opensearch_response_rss.xml

@@ -3,14 +3,14 @@
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:atom="http://www.w3.org/2005/Atom">
      xmlns:atom="http://www.w3.org/2005/Atom">
   <channel>
   <channel>
-    <title>Searx search: {{ q }}</title>
-    <link>{{ base_url }}?q={{ q }}</link>
-    <description>Search results for "{{ q }}" - searx</description>
+    <title>Searx search: {{ q|e }}</title>
+    <link>{{ base_url }}?q={{ q|e }}</link>
+    <description>Search results for "{{ q|e }}" - searx</description>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
-    <opensearch:Query role="request" searchTerms="{{ q }}" startPage="1" />
+    <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
     {% for r in results %}
     {% for r in results %}
     <item>
     <item>
       <title>{{ r.title }}</title>
       <title>{{ r.title }}</title>

+ 5 - 5
searx/templates/legacy/results.html

@@ -1,6 +1,6 @@
 {% extends "legacy/base.html" %}
 {% extends "legacy/base.html" %}
-{% block title %}{{ q }} - {% endblock %}
-{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}">{% endblock %}
+{% block title %}{{ q|e }} - {% endblock %}
+{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}">{% endblock %}
 {% block content %}
 {% block content %}
 <div class="preferences_container right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
 <div class="preferences_container right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
 <div class="small search center">
 <div class="small search center">
@@ -18,7 +18,7 @@
         {% for output_type in ('csv', 'json', 'rss') %}
         {% for output_type in ('csv', 'json', 'rss') %}
         <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
         <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
             <div class="left">
             <div class="left">
-            <input type="hidden" name="q" value="{{ q }}" />
+            <input type="hidden" name="q" value="{{ q|e }}" />
             <input type="hidden" name="format" value="{{ output_type }}" />
             <input type="hidden" name="format" value="{{ output_type }}" />
             {% for category in selected_categories %}
             {% for category in selected_categories %}
             <input type="hidden" name="category_{{ category }}" value="1"/>
             <input type="hidden" name="category_{{ category }}" value="1"/>
@@ -73,7 +73,7 @@
         {% if pageno > 1 %}
         {% if pageno > 1 %}
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
             <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
                 <div class="{% if rtl %}right{% else %}left{% endif %}">
                 <div class="{% if rtl %}right{% else %}left{% endif %}">
-                <input type="hidden" name="q" value="{{ q }}" />
+                <input type="hidden" name="q" value="{{ q|e }}" />
                 {% for category in selected_categories %}
                 {% for category in selected_categories %}
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 {% endfor %}
                 {% endfor %}
@@ -87,7 +87,7 @@
                 {% for category in selected_categories %}
                 {% for category in selected_categories %}
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 <input type="hidden" name="category_{{ category }}" value="1"/>
                 {% endfor %}
                 {% endfor %}
-                <input type="hidden" name="q" value="{{ q }}" />
+                <input type="hidden" name="q" value="{{ q|e }}" />
                 <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                 <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                 <input type="submit" value="{{ _('next page') }} >>" />
                 <input type="submit" value="{{ _('next page') }} >>" />
             </div>
             </div>

+ 15 - 0
searx/templates/oscar/base.html

@@ -1,3 +1,4 @@
+{% from 'oscar/macros.html' import icon %}
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"{% if rtl %} dir="rtl"{% endif %}>
 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"{% if rtl %} dir="rtl"{% endif %}>
 <head>
 <head>
@@ -54,6 +55,20 @@
 <body>
 <body>
     {% include 'oscar/navbar.html' %}
     {% include 'oscar/navbar.html' %}
     <div class="container">
     <div class="container">
+    {% if errors %}
+        <div class="alert alert-danger fade in" role="alert">
+            <button class="close" data-dismiss="alert" type="button">
+                <span aria-hidden="true">×</span>
+                <span class="sr-only">{{ _('Close') }}</span>
+            </button>
+            <strong class="lead">{{ icon('info-sign') }} {{ _('Error!') }}</strong>
+            <ul>
+            {% for message in errors %}
+                <li>{{ message }}</li>
+            {% endfor %}
+            </ul>
+        </div>
+    {% endif %}
 
 
     {% block site_alert_error %}
     {% block site_alert_error %}
     {% endblock %}
     {% endblock %}

+ 4 - 4
searx/templates/oscar/opensearch_response_rss.xml

@@ -3,14 +3,14 @@
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:atom="http://www.w3.org/2005/Atom">
      xmlns:atom="http://www.w3.org/2005/Atom">
   <channel>
   <channel>
-    <title>Searx search: {{ q }}</title>
-    <link>{{ base_url }}?q={{ q }}</link>
-    <description>Search results for "{{ q }}" - searx</description>
+    <title>Searx search: {{ q|e }}</title>
+    <link>{{ base_url }}?q={{ q|e }}</link>
+    <description>Search results for "{{ q|e }}" - searx</description>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:startIndex>1</opensearch:startIndex>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
     <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
-    <opensearch:Query role="request" searchTerms="{{ q }}" startPage="1" />
+    <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
     {% for r in results %}
     {% for r in results %}
     <item>
     <item>
       <title>{{ r.title }}</title>
       <title>{{ r.title }}</title>

+ 29 - 0
searx/templates/oscar/preferences.html

@@ -12,6 +12,7 @@
           <li class="active"><a href="#tab_general" role="tab" data-toggle="tab">{{ _('General') }}</a></li>
           <li class="active"><a href="#tab_general" role="tab" data-toggle="tab">{{ _('General') }}</a></li>
           <li><a href="#tab_engine" role="tab" data-toggle="tab">{{ _('Engines') }}</a></li>
           <li><a href="#tab_engine" role="tab" data-toggle="tab">{{ _('Engines') }}</a></li>
           <li><a href="#tab_plugins" role="tab" data-toggle="tab">{{ _('Plugins') }}</a></li>
           <li><a href="#tab_plugins" role="tab" data-toggle="tab">{{ _('Plugins') }}</a></li>
+          {% if answerers %}<li><a href="#tab_answerers" role="tab" data-toggle="tab">{{ _('Answerers') }}</a></li>{% endif %}
           <li><a href="#tab_cookies" role="tab" data-toggle="tab">{{ _('Cookies') }}</a></li>
           <li><a href="#tab_cookies" role="tab" data-toggle="tab">{{ _('Cookies') }}</a></li>
         </ul>
         </ul>
 
 
@@ -224,6 +225,34 @@
                 </fieldset>
                 </fieldset>
             </div>
             </div>
 
 
+            {% if answerers %}
+            <div class="tab-pane active_if_nojs" id="tab_answerers">
+                <noscript>
+                    <h3>{{ _('Answerers') }}</h3>
+                </noscript>
+                <p class="text-muted" style="margin:20px 0;">
+                    {{ _('This is the list of searx\'s instant answering modules.') }}
+                </p>
+                <table class="table table-striped">
+                    <tr>
+                        <th class="text-muted">{{ _('Name') }}</th>
+                        <th class="text-muted">{{ _('Keywords') }}</th>
+                        <th class="text-muted">{{ _('Description') }}</th>
+                        <th class="text-muted">{{ _('Examples') }}</th>
+                    </tr>
+
+                    {% for answerer in answerers %}
+                    <tr>
+                        <td class="text-muted">{{ answerer.info.name }}</td>
+                        <td class="text-muted">{{ answerer.keywords|join(', ') }}</td>
+                        <td class="text-muted">{{ answerer.info.description }}</td>
+                        <td class="text-muted">{{ answerer.info.examples|join(', ') }}</td>
+                    </tr>
+                    {% endfor %}
+                </table>
+            </div>
+            {% endif %}
+
             <div class="tab-pane active_if_nojs" id="tab_cookies">
             <div class="tab-pane active_if_nojs" id="tab_cookies">
                 <noscript>
                 <noscript>
                     <h3>{{ _('Cookies') }}</h3>
                     <h3>{{ _('Cookies') }}</h3>

+ 6 - 1
searx/templates/oscar/result_templates/images.html

@@ -13,7 +13,12 @@
             </div>
             </div>
             <div class="modal-body">
             <div class="modal-body">
                 <img class="img-responsive center-block" src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}">
                 <img class="img-responsive center-block" src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}">
-                {% if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif %}
+                {% if result.author %}<span class="photo-author">{{ result.author }}</span><br />{% endif %}
+                {% if result.content %}
+                    <p class="result-content">
+                        {{ result.content }}
+                    </p>
+                {% endif %}
             </div>
             </div>
             <div class="modal-footer">
             <div class="modal-footer">
                 <div class="clearfix"></div>
                 <div class="clearfix"></div>

+ 7 - 7
searx/templates/oscar/results.html

@@ -1,6 +1,6 @@
 {% extends "oscar/base.html" %}
 {% extends "oscar/base.html" %}
-{% block title %}{{ q }} - {% endblock %}
-{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}&amp;time_range={{ time_range }}">{% endblock %}
+{% block title %}{{ q|e }} - {% endblock %}
+{% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ url_for('index') }}?q={{ q|urlencode }}&amp;format=rss&amp;{% for category in selected_categories %}category_{{ category }}=1&amp;{% endfor %}pageno={{ pageno }}&amp;time_range={{ time_range }}">{% endblock %}
 {% block content %}
 {% block content %}
     <div class="row">
     <div class="row">
         <div class="col-sm-8" id="main_results">
         <div class="col-sm-8" id="main_results">
@@ -37,9 +37,9 @@
             <div id="pagination">
             <div id="pagination">
                 <div class="pull-left">
                 <div class="pull-left">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
-                        <input type="hidden" name="q" value="{{ q }}" />
+                        <input type="hidden" name="q" value="{{ q|e }}" />
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
-                        <input type="hidden" name="q" value="{{ q }}" />
+                        <input type="hidden" name="q" value="{{ q|e }}" />
                         <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                         <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
                         <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-backward"></span> {{ _('next page') }}</button>
                         <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-backward"></span> {{ _('next page') }}</button>
@@ -59,7 +59,7 @@
             <div id="pagination">
             <div id="pagination">
                 <div class="pull-left">
                 <div class="pull-left">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="pull-left">
-                        <input type="hidden" name="q" value="{{ q }}" />
+                        <input type="hidden" name="q" value="{{ q|e }}" />
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
                         <input type="hidden" name="pageno" value="{{ pageno-1 }}" />
                         <input type="hidden" name="pageno" value="{{ pageno-1 }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
@@ -69,7 +69,7 @@
                 <div class="pull-right">
                 <div class="pull-right">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}"  class="pull-left">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}"  class="pull-left">
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1"/>{% endfor %}
-                        <input type="hidden" name="q" value="{{ q }}" />
+                        <input type="hidden" name="q" value="{{ q|e }}" />
                         <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                         <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
                         <input type="hidden" name="time_range" value="{{ time_range }}" />
                         <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-forward"></span> {{ _('next page') }}</button>
                         <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-forward"></span> {{ _('next page') }}</button>
@@ -130,7 +130,7 @@
                     <div class="clearfix"></div>
                     <div class="clearfix"></div>
                     {% for output_type in ('csv', 'json', 'rss') %}
                     {% for output_type in ('csv', 'json', 'rss') %}
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download">
                     <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download">
-                        <input type="hidden" name="q" value="{{ q }}">
+                        <input type="hidden" name="q" value="{{ q|e }}">
                         <input type="hidden" name="format" value="{{ output_type }}">
                         <input type="hidden" name="format" value="{{ output_type }}">
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1">{% endfor %}
                         {% for category in selected_categories %}<input type="hidden" name="category_{{ category }}" value="1">{% endfor %}
                         <input type="hidden" name="pageno" value="{{ pageno }}">
                         <input type="hidden" name="pageno" value="{{ pageno }}">

+ 3 - 3
searx/templates/pix-art/results.html

@@ -5,7 +5,7 @@
     {% endfor %}
     {% endfor %}
 {% else %}
 {% else %}
 {% extends "pix-art/base.html" %}
 {% extends "pix-art/base.html" %}
-{% block title %}{{ q }} - {% endblock %}
+{% block title %}{{ q|e }} - {% endblock %}
 {% block meta %}{% endblock %}
 {% block meta %}{% endblock %}
 {% block content %}
 {% block content %}
 <div id="logo"><a href="./"><img src="{{ url_for('static', filename='img/searx-pixel-small.png') }}" alt="searx Logo"/></a></div>
 <div id="logo"><a href="./"><img src="{{ url_for('static', filename='img/searx-pixel-small.png') }}" alt="searx Logo"/></a></div>
@@ -25,8 +25,8 @@
     </span>
     </span>
     <div id="pagination">
     <div id="pagination">
         <br />
         <br />
-        <input type="button" onclick="load_more('{{ q }}', {{ pageno+1 }})" id="load_more" value="{{ _('Load more...') }}" />
+        <input type="button" onclick="load_more('{{ q|e }}', {{ pageno+1 }})" id="load_more" value="{{ _('Load more...') }}" />
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-{% endif %}
+{% endif %}

+ 13 - 0
searx/utils.py

@@ -6,7 +6,10 @@ import re
 from babel.dates import format_date
 from babel.dates import format_date
 from codecs import getincrementalencoder
 from codecs import getincrementalencoder
 from HTMLParser import HTMLParser
 from HTMLParser import HTMLParser
+from imp import load_source
+from os.path import splitext, join
 from random import choice
 from random import choice
+import sys
 
 
 from searx.version import VERSION_STRING
 from searx.version import VERSION_STRING
 from searx.languages import language_codes
 from searx.languages import language_codes
@@ -285,3 +288,13 @@ def is_valid_lang(lang):
             if l[1].lower() == lang.lower():
             if l[1].lower() == lang.lower():
                 return (True, l[0][:2], l[1].lower())
                 return (True, l[0][:2], l[1].lower())
         return False
         return False
+
+
+def load_module(filename, module_dir):
+    modname = splitext(filename)[0]
+    if modname in sys.modules:
+        del sys.modules[modname]
+    filepath = join(module_dir, filename)
+    module = load_source(modname, filepath)
+    module.name = modname
+    return module

+ 20 - 11
searx/webapp.py

@@ -40,7 +40,7 @@ except:
     logger.critical("cannot import dependency: pygments")
     logger.critical("cannot import dependency: pygments")
     from sys import exit
     from sys import exit
     exit(1)
     exit(1)
-
+from cgi import escape
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 from urllib import urlencode
 from urllib import urlencode
 from urlparse import urlparse, urljoin
 from urlparse import urlparse, urljoin
@@ -62,11 +62,12 @@ from searx.utils import (
 )
 )
 from searx.version import VERSION_STRING
 from searx.version import VERSION_STRING
 from searx.languages import language_codes
 from searx.languages import language_codes
-from searx.search import Search, SearchWithPlugins, get_search_query_from_webapp
-from searx.query import RawTextQuery, SearchQuery
+from searx.search import SearchWithPlugins, get_search_query_from_webapp
+from searx.query import RawTextQuery
 from searx.autocomplete import searx_bang, backends as autocomplete_backends
 from searx.autocomplete import searx_bang, backends as autocomplete_backends
 from searx.plugins import plugins
 from searx.plugins import plugins
 from searx.preferences import Preferences, ValidationException
 from searx.preferences import Preferences, ValidationException
+from searx.answerers import answerers
 
 
 # check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
 # check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
 # They are needed for SSL connection without trouble, see #298
 # They are needed for SSL connection without trouble, see #298
@@ -344,6 +345,8 @@ def render(template_name, override_theme=None, **kwargs):
 
 
     kwargs['cookies'] = request.cookies
     kwargs['cookies'] = request.cookies
 
 
+    kwargs['errors'] = request.errors
+
     kwargs['instance_name'] = settings['general']['instance_name']
     kwargs['instance_name'] = settings['general']['instance_name']
 
 
     kwargs['results_on_new_tab'] = request.preferences.get_value('results_on_new_tab')
     kwargs['results_on_new_tab'] = request.preferences.get_value('results_on_new_tab')
@@ -364,15 +367,16 @@ def render(template_name, override_theme=None, **kwargs):
 
 
 @app.before_request
 @app.before_request
 def pre_request():
 def pre_request():
-    # merge GET, POST vars
+    request.errors = []
+
     preferences = Preferences(themes, categories.keys(), engines, plugins)
     preferences = Preferences(themes, categories.keys(), engines, plugins)
+    request.preferences = preferences
     try:
     try:
         preferences.parse_cookies(request.cookies)
         preferences.parse_cookies(request.cookies)
     except:
     except:
-        # TODO throw error message to the user
-        logger.warning('Invalid config')
-    request.preferences = preferences
+        request.errors.append(gettext('Invalid settings, please edit your preferences'))
 
 
+    # merge GET, POST vars
     # request.form
     # request.form
     request.form = dict(request.form.items())
     request.form = dict(request.form.items())
     for k, v in request.args.items():
     for k, v in request.args.items():
@@ -397,7 +401,7 @@ def index():
     Supported outputs: html, json, csv, rss.
     Supported outputs: html, json, csv, rss.
     """
     """
 
 
-    if not request.args and not request.form:
+    if request.form.get('q') is None:
         return render(
         return render(
             'index.html',
             'index.html',
         )
         )
@@ -411,6 +415,8 @@ def index():
         search = SearchWithPlugins(search_query, request)
         search = SearchWithPlugins(search_query, request)
         result_container = search.search()
         result_container = search.search()
     except:
     except:
+        request.errors.append(gettext('search error'))
+        logger.exception('search error')
         return render(
         return render(
             'index.html',
             'index.html',
         )
         )
@@ -427,8 +433,10 @@ def index():
     for result in results:
     for result in results:
         if output_format == 'html':
         if output_format == 'html':
             if 'content' in result and result['content']:
             if 'content' in result and result['content']:
-                result['content'] = highlight_content(result['content'][:1024], search_query.query.encode('utf-8'))
-            result['title'] = highlight_content(result['title'], search_query.query.encode('utf-8'))
+                result['content'] = highlight_content(escape(result['content'][:1024]),
+                                                      search_query.query.encode('utf-8'))
+            result['title'] = highlight_content(escape(result['title'] or u''),
+                                                search_query.query.encode('utf-8'))
         else:
         else:
             if result.get('content'):
             if result.get('content'):
                 result['content'] = html_to_text(result['content']).strip()
                 result['content'] = html_to_text(result['content']).strip()
@@ -572,7 +580,7 @@ def preferences():
         try:
         try:
             request.preferences.parse_form(request.form)
             request.preferences.parse_form(request.form)
         except ValidationException:
         except ValidationException:
-            # TODO use flash feature of flask
+            request.errors.append(gettext('Invalid settings, please edit your preferences'))
             return resp
             return resp
         return request.preferences.save(resp)
         return request.preferences.save(resp)
 
 
@@ -609,6 +617,7 @@ def preferences():
                   language_codes=language_codes,
                   language_codes=language_codes,
                   engines_by_category=categories,
                   engines_by_category=categories,
                   stats=stats,
                   stats=stats,
+                  answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers],
                   disabled_engines=disabled_engines,
                   disabled_engines=disabled_engines,
                   autocomplete_backends=autocomplete_backends,
                   autocomplete_backends=autocomplete_backends,
                   shortcuts={y: x for x, y in engine_shortcuts.items()},
                   shortcuts={y: x for x, y in engine_shortcuts.items()},

+ 2 - 4
tests/unit/engines/test_bing.py

@@ -14,14 +14,12 @@ class TestBingEngine(SearxTestCase):
         params = bing.request(query, dicto)
         params = bing.request(query, dicto)
         self.assertTrue('url' in params)
         self.assertTrue('url' in params)
         self.assertTrue(query in params['url'])
         self.assertTrue(query in params['url'])
+        self.assertTrue('language%3AFR' in params['url'])
         self.assertTrue('bing.com' in params['url'])
         self.assertTrue('bing.com' in params['url'])
-        self.assertTrue('SRCHHPGUSR' in params['cookies'])
-        self.assertTrue('fr' in params['cookies']['SRCHHPGUSR'])
 
 
         dicto['language'] = 'all'
         dicto['language'] = 'all'
         params = bing.request(query, dicto)
         params = bing.request(query, dicto)
-        self.assertTrue('SRCHHPGUSR' in params['cookies'])
-        self.assertTrue('en' in params['cookies']['SRCHHPGUSR'])
+        self.assertTrue('language' not in params['url'])
 
 
     def test_response(self):
     def test_response(self):
         self.assertRaises(AttributeError, bing.response, None)
         self.assertRaises(AttributeError, bing.response, None)

+ 1 - 1
tests/unit/engines/test_deezer.py

@@ -42,7 +42,7 @@ class TestDeezerEngine(SearxTestCase):
         self.assertEqual(len(results), 1)
         self.assertEqual(len(results), 1)
         self.assertEqual(results[0]['title'], 'Title of track')
         self.assertEqual(results[0]['title'], 'Title of track')
         self.assertEqual(results[0]['url'], 'https://www.deezer.com/track/1094042')
         self.assertEqual(results[0]['url'], 'https://www.deezer.com/track/1094042')
-        self.assertEqual(results[0]['content'], 'Artist Name &bull; Album Title &bull; Title of track')
+        self.assertEqual(results[0]['content'], 'Artist Name - Album Title - Title of track')
         self.assertTrue('100' in results[0]['embedded'])
         self.assertTrue('100' in results[0]['embedded'])
 
 
         json = r"""
         json = r"""

+ 3 - 3
tests/unit/engines/test_flickr.py

@@ -52,7 +52,7 @@ class TestFlickrEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertTrue('o.jpg' in results[0]['img_src'])
         self.assertTrue('o.jpg' in results[0]['img_src'])
         self.assertTrue('n.jpg' in results[0]['thumbnail_src'])
         self.assertTrue('n.jpg' in results[0]['thumbnail_src'])
-        self.assertTrue('Owner' in results[0]['content'])
+        self.assertTrue('Owner' in results[0]['author'])
         self.assertTrue('Description' in results[0]['content'])
         self.assertTrue('Description' in results[0]['content'])
 
 
         json = r"""
         json = r"""
@@ -76,7 +76,7 @@ class TestFlickrEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertTrue('z.jpg' in results[0]['img_src'])
         self.assertTrue('z.jpg' in results[0]['img_src'])
         self.assertTrue('z.jpg' in results[0]['thumbnail_src'])
         self.assertTrue('z.jpg' in results[0]['thumbnail_src'])
-        self.assertTrue('Owner' in results[0]['content'])
+        self.assertTrue('Owner' in results[0]['author'])
         self.assertTrue('Description' in results[0]['content'])
         self.assertTrue('Description' in results[0]['content'])
 
 
         json = r"""
         json = r"""
@@ -100,7 +100,7 @@ class TestFlickrEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/66847915@N08/15751017054')
         self.assertTrue('o.jpg' in results[0]['img_src'])
         self.assertTrue('o.jpg' in results[0]['img_src'])
         self.assertTrue('o.jpg' in results[0]['thumbnail_src'])
         self.assertTrue('o.jpg' in results[0]['thumbnail_src'])
-        self.assertTrue('Owner' in results[0]['content'])
+        self.assertTrue('Owner' in results[0]['author'])
         self.assertTrue('Description' in results[0]['content'])
         self.assertTrue('Description' in results[0]['content'])
 
 
         json = r"""
         json = r"""

+ 3 - 3
tests/unit/engines/test_flickr_noapi.py

@@ -145,7 +145,7 @@ class TestFlickrNoapiEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertIn('k.jpg', results[0]['img_src'])
         self.assertIn('k.jpg', results[0]['img_src'])
         self.assertIn('n.jpg', results[0]['thumbnail_src'])
         self.assertIn('n.jpg', results[0]['thumbnail_src'])
-        self.assertIn('Owner', results[0]['content'])
+        self.assertIn('Owner', results[0]['author'])
 
 
         # no n size, only the z size
         # no n size, only the z size
         json = """
         json = """
@@ -188,7 +188,7 @@ class TestFlickrNoapiEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertIn('z.jpg', results[0]['img_src'])
         self.assertIn('z.jpg', results[0]['img_src'])
         self.assertIn('z.jpg', results[0]['thumbnail_src'])
         self.assertIn('z.jpg', results[0]['thumbnail_src'])
-        self.assertIn('Owner', results[0]['content'])
+        self.assertIn('Owner', results[0]['author'])
 
 
         # no z or n size
         # no z or n size
         json = """
         json = """
@@ -231,7 +231,7 @@ class TestFlickrNoapiEngine(SearxTestCase):
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertEqual(results[0]['url'], 'https://www.flickr.com/photos/59729010@N00/14001294434')
         self.assertIn('o.jpg', results[0]['img_src'])
         self.assertIn('o.jpg', results[0]['img_src'])
         self.assertIn('o.jpg', results[0]['thumbnail_src'])
         self.assertIn('o.jpg', results[0]['thumbnail_src'])
-        self.assertIn('Owner', results[0]['content'])
+        self.assertIn('Owner', results[0]['author'])
 
 
         # no image test
         # no image test
         json = """
         json = """

+ 2 - 2
tests/unit/engines/test_kickass.py

@@ -98,7 +98,7 @@ class TestKickassEngine(SearxTestCase):
         self.assertEqual(len(results), 1)
         self.assertEqual(len(results), 1)
         self.assertEqual(results[0]['title'], 'This should be the title')
         self.assertEqual(results[0]['title'], 'This should be the title')
         self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
         self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
-        self.assertEqual(results[0]['content'], 'Posted by riri in Other &gt; Unsorted')
+        self.assertEqual(results[0]['content'], 'Posted by riri in Other > Unsorted')
         self.assertEqual(results[0]['seed'], 10)
         self.assertEqual(results[0]['seed'], 10)
         self.assertEqual(results[0]['leech'], 1)
         self.assertEqual(results[0]['leech'], 1)
         self.assertEqual(results[0]['filesize'], 449)
         self.assertEqual(results[0]['filesize'], 449)
@@ -381,7 +381,7 @@ class TestKickassEngine(SearxTestCase):
         self.assertEqual(len(results), 5)
         self.assertEqual(len(results), 5)
         self.assertEqual(results[0]['title'], 'This should be the title')
         self.assertEqual(results[0]['title'], 'This should be the title')
         self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
         self.assertEqual(results[0]['url'], 'https://kickass.cd/url.html')
-        self.assertEqual(results[0]['content'], 'Posted by riri in Other &gt; Unsorted')
+        self.assertEqual(results[0]['content'], 'Posted by riri in Other > Unsorted')
         self.assertEqual(results[0]['seed'], 10)
         self.assertEqual(results[0]['seed'], 10)
         self.assertEqual(results[0]['leech'], 1)
         self.assertEqual(results[0]['leech'], 1)
         self.assertEqual(results[0]['files'], 4)
         self.assertEqual(results[0]['files'], 4)

+ 0 - 3
tests/unit/engines/test_searchcode_doc.py

@@ -56,9 +56,6 @@ class TestSearchcodeDocEngine(SearxTestCase):
         self.assertEqual(len(results), 1)
         self.assertEqual(len(results), 1)
         self.assertEqual(results[0]['title'], '[Type] Namespace test')
         self.assertEqual(results[0]['title'], '[Type] Namespace test')
         self.assertEqual(results[0]['url'], 'http://url')
         self.assertEqual(results[0]['url'], 'http://url')
-        self.assertIn('Synopsis', results[0]['content'])
-        self.assertIn('Type', results[0]['content'])
-        self.assertIn('test', results[0]['content'])
         self.assertIn('Description', results[0]['content'])
         self.assertIn('Description', results[0]['content'])
 
 
         json = r"""
         json = r"""

+ 1 - 1
tests/unit/engines/test_spotify.py

@@ -90,7 +90,7 @@ class TestSpotifyEngine(SearxTestCase):
         self.assertEqual(len(results), 1)
         self.assertEqual(len(results), 1)
         self.assertEqual(results[0]['title'], 'Title of track')
         self.assertEqual(results[0]['title'], 'Title of track')
         self.assertEqual(results[0]['url'], 'https://open.spotify.com/track/2GzvFiedqW8hgqUpWcASZa')
         self.assertEqual(results[0]['url'], 'https://open.spotify.com/track/2GzvFiedqW8hgqUpWcASZa')
-        self.assertEqual(results[0]['content'], 'Artist Name &bull; Album Title &bull; Title of track')
+        self.assertEqual(results[0]['content'], 'Artist Name - Album Title - Title of track')
         self.assertIn('1000', results[0]['embedded'])
         self.assertIn('1000', results[0]['embedded'])
 
 
         json = """
         json = """

+ 16 - 0
tests/unit/test_answerers.py

@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+from mock import Mock
+
+from searx.answerers import answerers
+from searx.testing import SearxTestCase
+
+
+class AnswererTest(SearxTestCase):
+
+    def test_unicode_input(self):
+        query = Mock()
+        unicode_payload = u'árvíztűrő tükörfúrógép'
+        for answerer in answerers:
+            query.query = u'{} {}'.format(answerer.keywords[0], unicode_payload)
+            self.assertTrue(isinstance(answerer.answer(query), list))

+ 2 - 1
tests/unit/test_webapp.py

@@ -5,6 +5,7 @@ from mock import Mock
 from urlparse import ParseResult
 from urlparse import ParseResult
 from searx import webapp
 from searx import webapp
 from searx.testing import SearxTestCase
 from searx.testing import SearxTestCase
+from searx.search import Search
 
 
 
 
 class ViewsTestCase(SearxTestCase):
 class ViewsTestCase(SearxTestCase):
@@ -41,7 +42,7 @@ class ViewsTestCase(SearxTestCase):
                                                 results_number=lambda: 3,
                                                 results_number=lambda: 3,
                                                 results_length=lambda: len(self.test_results))
                                                 results_length=lambda: len(self.test_results))
 
 
-        webapp.Search.search = search_mock
+        Search.search = search_mock
 
 
         def get_current_theme_name_mock(override=None):
         def get_current_theme_name_mock(override=None):
             return 'legacy'
             return 'legacy'