Browse Source

Merge branch 'master' into flask_perimeter

Alexandre Flament 8 years ago
parent
commit
e67dfaaac7
38 changed files with 366 additions and 562 deletions
  1. 2 2
      manage.sh
  2. 10 0
      searx/engines/google.py
  3. 4 4
      searx/engines/xpath.py
  4. 32 0
      searx/exceptions.py
  5. 2 1
      searx/preferences.py
  6. 4 0
      searx/results.py
  7. 42 23
      searx/search.py
  8. 11 0
      searx/settings.yml
  9. 0 0
      searx/static/themes/oscar/css/logicodev.min.css
  10. 0 0
      searx/static/themes/oscar/css/pointhi.min.css
  11. 0 23
      searx/static/themes/oscar/less/logicodev/advanced.less
  12. 20 78
      searx/static/themes/oscar/less/logicodev/navbar.less
  13. 6 5
      searx/static/themes/oscar/less/logicodev/results.less
  14. 23 0
      searx/static/themes/oscar/less/logicodev/search.less
  15. 1 0
      searx/static/themes/oscar/less/logicodev/variables.less
  16. 17 25
      searx/static/themes/oscar/less/pointhi/navbar.less
  17. 62 0
      searx/templates/__common__/about.html
  18. 0 0
      searx/templates/__common__/opensearch.xml
  19. 6 0
      searx/templates/__common__/opensearch_response_rss.xml
  20. 1 62
      searx/templates/courgette/about.html
  21. 1 62
      searx/templates/legacy/about.html
  22. 0 28
      searx/templates/legacy/opensearch.xml
  23. 0 23
      searx/templates/legacy/opensearch_response_rss.xml
  24. 1 62
      searx/templates/oscar/about.html
  25. 9 3
      searx/templates/oscar/advanced.html
  26. 5 0
      searx/templates/oscar/base.html
  27. 1 1
      searx/templates/oscar/languages.html
  28. 3 3
      searx/templates/oscar/macros.html
  29. 8 13
      searx/templates/oscar/navbar.html
  30. 0 28
      searx/templates/oscar/opensearch.xml
  31. 0 23
      searx/templates/oscar/opensearch_response_rss.xml
  32. 14 9
      searx/templates/oscar/results.html
  33. 15 3
      searx/templates/oscar/search.html
  34. 1 1
      searx/templates/oscar/time-range.html
  35. 1 62
      searx/templates/pix-art/about.html
  36. 2 0
      searx/utils.py
  37. 59 18
      searx/webapp.py
  38. 3 0
      tests/unit/test_webapp.py

+ 2 - 2
manage.sh

@@ -6,12 +6,12 @@ SEARX_DIR="$BASE_DIR/searx"
 ACTION=$1
 ACTION=$1
 
 
 update_packages() {
 update_packages() {
-    pip install --upgrade -r "$BASE_DIR/requirements.txt"
+    pip install -r "$BASE_DIR/requirements.txt"
 }
 }
 
 
 update_dev_packages() {
 update_dev_packages() {
     update_packages
     update_packages
-    pip install --upgrade -r "$BASE_DIR/requirements-dev.txt"
+    pip install -r "$BASE_DIR/requirements-dev.txt"
 }
 }
 
 
 check_geckodriver() {
 check_geckodriver() {

+ 10 - 0
searx/engines/google.py

@@ -112,6 +112,7 @@ title_xpath = './/h3'
 content_xpath = './/span[@class="st"]'
 content_xpath = './/span[@class="st"]'
 content_misc_xpath = './/div[@class="f slp"]'
 content_misc_xpath = './/div[@class="f slp"]'
 suggestion_xpath = '//p[@class="_Bmc"]'
 suggestion_xpath = '//p[@class="_Bmc"]'
+spelling_suggestion_xpath = '//a[@class="spell"]'
 
 
 # map : detail location
 # map : detail location
 map_address_xpath = './/div[@class="s"]//table//td[2]/span/text()'
 map_address_xpath = './/div[@class="s"]//table//td[2]/span/text()'
@@ -220,6 +221,12 @@ def response(resp):
     instant_answer = dom.xpath('//div[@id="_vBb"]//text()')
     instant_answer = dom.xpath('//div[@id="_vBb"]//text()')
     if instant_answer:
     if instant_answer:
         results.append({'answer': u' '.join(instant_answer)})
         results.append({'answer': u' '.join(instant_answer)})
+    try:
+        results_num = int(dom.xpath('//div[@id="resultStats"]//text()')[0]
+                          .split()[1].replace(',', ''))
+        results.append({'number_of_results': results_num})
+    except:
+        pass
 
 
     # parse results
     # parse results
     for result in dom.xpath(results_xpath):
     for result in dom.xpath(results_xpath):
@@ -275,6 +282,9 @@ def response(resp):
         # append suggestion
         # append suggestion
         results.append({'suggestion': extract_text(suggestion)})
         results.append({'suggestion': extract_text(suggestion)})
 
 
+    for correction in dom.xpath(spelling_suggestion_xpath):
+        results.append({'correction': extract_text(correction)})
+
     # return results
     # return results
     return results
     return results
 
 

+ 4 - 4
searx/engines/xpath.py

@@ -31,8 +31,6 @@ if xpath_results is a string element, then it's already done
 def extract_text(xpath_results):
 def extract_text(xpath_results):
     if type(xpath_results) == list:
     if type(xpath_results) == list:
         # it's list of result : concat everything using recursive call
         # it's list of result : concat everything using recursive call
-        if not xpath_results:
-            raise Exception('Empty url resultset')
         result = ''
         result = ''
         for e in xpath_results:
         for e in xpath_results:
             result = result + extract_text(e)
             result = result + extract_text(e)
@@ -48,6 +46,8 @@ def extract_text(xpath_results):
 
 
 
 
 def extract_url(xpath_results, search_url):
 def extract_url(xpath_results, search_url):
+    if xpath_results == []:
+        raise Exception('Empty url resultset')
     url = extract_text(xpath_results)
     url = extract_text(xpath_results)
 
 
     if url.startswith('//'):
     if url.startswith('//'):
@@ -103,8 +103,8 @@ def response(resp):
     if results_xpath:
     if results_xpath:
         for result in dom.xpath(results_xpath):
         for result in dom.xpath(results_xpath):
             url = extract_url(result.xpath(url_xpath), search_url)
             url = extract_url(result.xpath(url_xpath), search_url)
-            title = extract_text(result.xpath(title_xpath)[0])
-            content = extract_text(result.xpath(content_xpath)[0])
+            title = extract_text(result.xpath(title_xpath))
+            content = extract_text(result.xpath(content_xpath))
             results.append({'url': url, 'title': title, 'content': content})
             results.append({'url': url, 'title': title, 'content': content})
     else:
     else:
         for url, title, content in zip(
         for url, title, content in zip(

+ 32 - 0
searx/exceptions.py

@@ -0,0 +1,32 @@
+'''
+searx is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+searx is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with searx. If not, see < http://www.gnu.org/licenses/ >.
+
+(C) 2017- by Alexandre Flament, <alex@al-f.net>
+'''
+
+
+class SearxException(Exception):
+    pass
+
+
+class SearxParameterException(SearxException):
+
+    def __init__(self, name, value):
+        if value == '' or value is None:
+            message = 'Empty ' + name + ' parameter'
+        else:
+            message = 'Invalid value "' + value + '" for parameter ' + name
+        super(SearxParameterException, self).__init__(message)
+        self.parameter_name = name
+        self.parameter_value = value

+ 2 - 1
searx/preferences.py

@@ -130,7 +130,8 @@ class MapSetting(Setting):
         self.key = data
         self.key = data
 
 
     def save(self, name, resp):
     def save(self, name, resp):
-        resp.set_cookie(name, bytes(self.key), max_age=COOKIE_MAX_AGE)
+        if hasattr(self, 'key'):
+            resp.set_cookie(name, bytes(self.key), max_age=COOKIE_MAX_AGE)
 
 
 
 
 class SwitchableSetting(Setting):
 class SwitchableSetting(Setting):

+ 4 - 0
searx/results.py

@@ -127,6 +127,7 @@ class ResultContainer(object):
         self.infoboxes = []
         self.infoboxes = []
         self.suggestions = set()
         self.suggestions = set()
         self.answers = set()
         self.answers = set()
+        self.corrections = set()
         self._number_of_results = []
         self._number_of_results = []
         self._ordered = False
         self._ordered = False
         self.paging = False
         self.paging = False
@@ -140,6 +141,9 @@ class ResultContainer(object):
             elif 'answer' in result:
             elif 'answer' in result:
                 self.answers.add(result['answer'])
                 self.answers.add(result['answer'])
                 results.remove(result)
                 results.remove(result)
+            elif 'correction' in result:
+                self.corrections.add(result['correction'])
+                results.remove(result)
             elif 'infobox' in result:
             elif 'infobox' in result:
                 self._merge_infobox(result)
                 self._merge_infobox(result)
                 results.remove(result)
                 results.remove(result)

+ 42 - 23
searx/search.py

@@ -31,11 +31,16 @@ from searx.query import RawTextQuery, SearchQuery
 from searx.results import ResultContainer
 from searx.results import ResultContainer
 from searx import logger
 from searx import logger
 from searx.plugins import plugins
 from searx.plugins import plugins
+from searx.languages import language_codes
+from searx.exceptions import SearxParameterException
 
 
 logger = logger.getChild('search')
 logger = logger.getChild('search')
 
 
 number_of_searches = 0
 number_of_searches = 0
 
 
+language_code_set = set(l[0].lower() for l in language_codes)
+language_code_set.add('all')
+
 
 
 def send_http_request(engine, request_params, start_time, timeout_limit):
 def send_http_request(engine, request_params, start_time, timeout_limit):
     # for page_load_time stats
     # for page_load_time stats
@@ -182,33 +187,13 @@ def default_request_params():
 
 
 
 
 def get_search_query_from_webapp(preferences, form):
 def get_search_query_from_webapp(preferences, form):
-    query = None
-    query_engines = []
-    query_categories = []
-    query_pageno = 1
-    query_lang = 'all'
-    query_time_range = None
+    # no text for the query ?
+    if not form.get('q'):
+        raise SearxParameterException('q', '')
 
 
     # set blocked engines
     # set blocked engines
     disabled_engines = preferences.engines.get_disabled()
     disabled_engines = preferences.engines.get_disabled()
 
 
-    # set specific language if set
-    query_lang = preferences.get_value('language')
-
-    # safesearch
-    query_safesearch = preferences.get_value('safesearch')
-
-    # TODO better exceptions
-    if not form.get('q'):
-        raise Exception('noquery')
-
-    # set pagenumber
-    pageno_param = form.get('pageno', '1')
-    if not pageno_param.isdigit() or int(pageno_param) < 1:
-        pageno_param = 1
-
-    query_pageno = int(pageno_param)
-
     # parse query, if tags are set, which change
     # parse query, if tags are set, which change
     # the serch engine or search-language
     # the serch engine or search-language
     raw_text_query = RawTextQuery(form['q'], disabled_engines)
     raw_text_query = RawTextQuery(form['q'], disabled_engines)
@@ -217,6 +202,13 @@ def get_search_query_from_webapp(preferences, form):
     # set query
     # set query
     query = raw_text_query.getSearchQuery()
     query = raw_text_query.getSearchQuery()
 
 
+    # get and check page number
+    pageno_param = form.get('pageno', '1')
+    if not pageno_param.isdigit() or int(pageno_param) < 1:
+        raise SearxParameterException('pageno', pageno_param)
+    query_pageno = int(pageno_param)
+
+    # get language
     # set specific language if set on request, query or preferences
     # set specific language if set on request, query or preferences
     # TODO support search with multible languages
     # TODO support search with multible languages
     if len(raw_text_query.languages):
     if len(raw_text_query.languages):
@@ -226,10 +218,37 @@ def get_search_query_from_webapp(preferences, form):
     else:
     else:
         query_lang = preferences.get_value('language')
         query_lang = preferences.get_value('language')
 
 
+    # check language
+    if query_lang.lower() not in language_code_set:
+        raise SearxParameterException('language', query_lang)
+
+    # get safesearch
+    if 'safesearch' in form:
+        query_safesearch = form.get('safesearch')
+        # first check safesearch
+        if not query_safesearch.isdigit():
+            raise SearxParameterException('safesearch', query_safesearch)
+        query_safesearch = int(query_safesearch)
+    else:
+        query_safesearch = preferences.get_value('safesearch')
+
+    # safesearch : second check
+    if query_safesearch < 0 or query_safesearch > 2:
+        raise SearxParameterException('safesearch', query_safesearch)
+
+    # get time_range
     query_time_range = form.get('time_range')
     query_time_range = form.get('time_range')
 
 
+    # check time_range
+    if query_time_range not in ('None', None, '', 'day', 'week', 'month', 'year'):
+        raise SearxParameterException('time_range', query_time_range)
+
+    # query_engines
     query_engines = raw_text_query.engines
     query_engines = raw_text_query.engines
 
 
+    # query_categories
+    query_categories = []
+
     # if engines are calculated from query,
     # if engines are calculated from query,
     # set categories by using that informations
     # set categories by using that informations
     if query_engines and raw_text_query.specific:
     if query_engines and raw_text_query.specific:

+ 11 - 0
searx/settings.yml

@@ -462,6 +462,17 @@ engines:
 #        - ...
 #        - ...
 #    disabled : True
 #    disabled : True
 
 
+  - name : semantic scholar
+    engine : xpath
+    paging : True
+    search_url : https://www.semanticscholar.org/search?q={query}&sort=relevance&page={pageno}&ae=false
+    results_xpath : //article
+    url_xpath : .//div[@class="search-result-title"]/a/@href
+    title_xpath : .//div[@class="search-result-title"]/a
+    content_xpath : .//div[@class="search-result-abstract"]
+    shortcut : se
+    categories : science
+
   - name : spotify
   - name : spotify
     engine : spotify
     engine : spotify
     shortcut : stf
     shortcut : stf

File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/logicodev.min.css


File diff suppressed because it is too large
+ 0 - 0
searx/static/themes/oscar/css/pointhi.min.css


+ 0 - 23
searx/static/themes/oscar/less/logicodev/advanced.less

@@ -29,29 +29,6 @@
         font-weight: bold;
         font-weight: bold;
         border-bottom: @light-green 5px solid;
         border-bottom: @light-green 5px solid;
     }
     }
-    select {
-        appearance: none;
-        -webkit-appearance: none;
-        -moz-appearance: none;
-        font-size: 1.2rem;
-        font-weight:normal;
-        background-color: white;
-        border: @mild-gray 1px solid;
-        color: @dark-gray;
-        padding-bottom: 0.4rem;
-        padding-top: 0.4rem;
-        padding-left: 1rem;
-        padding-right: 5rem;
-        margin-right: 0.5rem;
-        background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAQAAACR313BAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
-AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZ
-cwAABFkAAARZAVnbJUkAAAAHdElNRQfgBxgLDwB20OFsAAAAbElEQVQY073OsQ3CMAAEwJMYwJGn
-sAehpoXJItltBkmcdZBYgIIiQoLglnz3ui+eP+bk5uneteTMZJa6OJuIqvYzSJoqwqBq8gdmTTW8
-6/dghxAUq4xsVYT9laBYXCw93Aajh7GPEF23t4fkBYevGFTANkPRAAAAJXRFWHRkYXRlOmNyZWF0
-ZQAyMDE2LTA3LTI0VDExOjU1OjU4KzAyOjAwRFqFOQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0w
-Ny0yNFQxMToxNTowMCswMjowMP7RDgQAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb
-7jwaAAAAAElFTkSuQmCC) 96% no-repeat;
-    }
 }
 }
 
 
 #check-advanced {
 #check-advanced {

+ 20 - 78
searx/static/themes/oscar/less/logicodev/navbar.less

@@ -1,89 +1,31 @@
-.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus{
+.searx-navbar {
     background: @black;
     background: @black;
-    color: @light-green;
-}
-
-.navbar > li > a {
-    padding: 0;
-    margin: 0;
-}
-
-.navbar-nav > li > a {
-    background: @black;
-    padding: 0 8px;
-    margin: 0;
-    line-height: 30px;
-}
-
-.navbar, .navbar-default, .menu {
-    background-color: @black;
-    border: none;
-    border-top: 4px solid @light-green;
-    padding-top: 5px;
-    color: @dim-gray !important;
-    font-weight: 700;
-    font-size: 1.1em;
-    text-transform: lowercase;
-    margin-bottom: 24px;
-    height: 30px;
-    line-height: 30px;
-
-    .navbar-nav > li > a{
-        color: @dim-gray;
+    height: 2.3rem;
+    font-size: 1.3rem;
+    line-height: 1.3rem;
+    padding: 0.5rem;
+    font-weight: bold;
+    margin-bottom: 0.8rem;
+
+    a, a:hover {
+        margin-right: 2.0rem;
+        color: white;
+        text-decoration: none;
     }
     }
 
 
-    .navbar-brand{
-        font-weight: 700;
+    .instance a {
         color: @light-green;
         color: @light-green;
-        line-height: 30px;
-        padding: 0 30px;
-        margin: 0;
+        margin-left: 2.0rem;
     }
     }
-    z-index: 10;
 }
 }
 
 
-// Hover color
-// http://stackoverflow.com/users/114029/leniel-macaferi
-.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {
-    color: @light-green;
-    background: @black;
-}
-
-.navbar-toggle {
-    margin-top: 0;
-}
+#main-logo {
+    margin-top: 20vh;
+    margin-bottom: 25px;
 
 
-.menu {
-    margin: 0;
-    padding: 0;
-    position: absolute;
-    top: 4px;
-    border: 0;
-    z-index: 1000000000;
-    height: 40px;
-    line-height: 40px;
-    ul {
-        padding: 0;
-        margin: 0;
-        li {
-            padding: 0 0.6em;
-            margin: 0;
-            float: left;
-            list-style: none;
-            a {
-                color: @dim-gray;
-            }
-        }
-        li.active a {
-            color: @light-green;
-        }
+    & > img {
+        max-width: 350px;
+        width: 80%;
     }
     }
 }
 }
 
 
-.menu-right {
-    right: 2em;
-}
-
-.menu-left {
-    left: 2em;
-}

+ 6 - 5
searx/static/themes/oscar/less/logicodev/results.less

@@ -1,6 +1,6 @@
 .result_header {
 .result_header {
-    margin-top: 6px;
-    margin-bottom: 4px;
+    margin-top: 0px;
+    margin-bottom: 2px;
     font-size: 16px;
     font-size: 16px;
 
 
     .favicon {
     .favicon {
@@ -41,10 +41,11 @@
 
 
 }
 }
 
 
-.external-link, .external-link a{
-    color: @green;
+.external-link {
+    color: @dark-green;
+    font-size: 12px;
 
 
-    a{
+    a {
         margin-right: 3px;
         margin-right: 3px;
     }
     }
 }
 }

+ 23 - 0
searx/static/themes/oscar/less/logicodev/search.less

@@ -54,3 +54,26 @@
      background-color: @green;
      background-color: @green;
      color: white;
      color: white;
  }
  }
+
+.custom-select {
+    appearance: none;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    font-size: 1.2rem;
+    font-weight:normal;
+    background-color: white;
+    border: @mild-gray 1px solid;
+    color: @dark-gray;
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAQAAACR313BAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
+AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZ
+cwAABFkAAARZAVnbJUkAAAAHdElNRQfgBxgLDwB20OFsAAAAbElEQVQY073OsQ3CMAAEwJMYwJGn
+sAehpoXJItltBkmcdZBYgIIiQoLglnz3ui+eP+bk5uneteTMZJa6OJuIqvYzSJoqwqBq8gdmTTW8
+6/dghxAUq4xsVYT9laBYXCw93Aajh7GPEF23t4fkBYevGFTANkPRAAAAJXRFWHRkYXRlOmNyZWF0
+ZQAyMDE2LTA3LTI0VDExOjU1OjU4KzAyOjAwRFqFOQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0w
+Ny0yNFQxMToxNTowMCswMjowMP7RDgQAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb
+7jwaAAAAAElFTkSuQmCC) 96% no-repeat;
+}
+
+.search-margin {
+    margin-bottom: 0.6em;
+}

+ 1 - 0
searx/static/themes/oscar/less/logicodev/variables.less

@@ -7,6 +7,7 @@
 @blue: #0088CC; 
 @blue: #0088CC; 
 @red: #F35E77;
 @red: #F35E77;
 @violet: #684898;
 @violet: #684898;
+@dark-green: #069025;
 @green: #2ecc71;
 @green: #2ecc71;
 @light-green: #01D7D4; 
 @light-green: #01D7D4; 
 @orange: #FFA92F;
 @orange: #FFA92F;

+ 17 - 25
searx/static/themes/oscar/less/pointhi/navbar.less

@@ -1,28 +1,20 @@
-.menu {
-    margin: 0;
-    padding: 0;
-    position: absolute;
-    top: 4px;
-    border: 0;
-    z-index: 1000000000;
-    height: 40px;
-    line-height: 40px;
-    ul {
-        padding: 0;
-        margin: 0;
-        li {
-            padding: 0 0.6em;
-            margin: 0;
-            float: left;
-            list-style: none;
-        }
-    }
-}
+.searx-navbar {
+    background: #eee;
+    color: #aaa;
+    height: 2.3rem;
+    font-size: 1.3rem;
+    line-height: 1.3rem;
+    padding: 0.5rem;
+    font-weight: bold;
+    margin-bottom: 1.3rem;
 
 
-.menu-right {
-    right: 2em;
-}
+    a, a:hover {
+        margin-right: 2.0rem;
+        text-decoration: none;
+    }
 
 
-.menu-left {
-    left: 2em;
+    .instance a {
+        color: #444;
+        margin-left: 2.0rem;
+    }
 }
 }

+ 62 - 0
searx/templates/__common__/about.html

@@ -0,0 +1,62 @@
+<div{% if rtl %} dir="ltr"{% endif %}>
+    <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
+
+    <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
+    </p>
+    <h2>Why use searx?</h2>
+    <ul>
+        <li>searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
+        <li>searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
+        <li>searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
+    </ul>
+    <p>If you do care about privacy, want to be a conscious user, or otherwise believe
+    in digital freedom, make searx your default search engine or run it on your own server</p>
+
+<h2>Technical details - How does it work?</h2>
+
+<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
+inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>.<br />
+It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, searx uses the search bar to perform GET requests.<br />
+Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
+</p>
+
+<h2>How can I make it my own?</h2>
+
+<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
+<br />The more decentralized the Internet is, the more freedom we have!</p>
+
+
+<h2>More about searx</h2>
+
+<ul>
+    <li><a href="https://github.com/asciimoo/searx">github</a></li>
+    <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
+    <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
+    <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
+    <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
+</ul>
+
+
+<hr />
+
+<h2 id="faq">FAQ</h2>
+
+<h3>How to add to firefox?</h3>
+<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
+
+<h2 id="dev_faq">Developer FAQ</h2>
+
+<h3>New engines?</h3>
+<ul>
+    <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
+    <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
+</ul>
+<p>Don't forget to restart searx after config edit!</p>
+
+<h3>Installation/WSGI support?</h3>
+<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
+
+<h3>How to debug engines?</h3>
+<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
+
+</div>

+ 0 - 0
searx/templates/courgette/opensearch.xml → searx/templates/__common__/opensearch.xml


+ 6 - 0
searx/templates/courgette/opensearch_response_rss.xml → searx/templates/__common__/opensearch_response_rss.xml

@@ -11,6 +11,12 @@
     <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|e }}" startPage="1" />
     <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
+    {% if error_message %}
+    <item>
+      <title>Error</title>
+      <description>{{ error_message|e }}</description>
+    </item>
+    {% endif %}
     {% for r in results %}
     {% for r in results %}
     <item>
     <item>
       <title>{{ r.title }}</title>
       <title>{{ r.title }}</title>

+ 1 - 62
searx/templates/courgette/about.html

@@ -1,66 +1,5 @@
 {% extends 'courgette/base.html' %}
 {% extends 'courgette/base.html' %}
 {% block content %}
 {% block content %}
 {% include 'courgette/github_ribbon.html' %}
 {% include 'courgette/github_ribbon.html' %}
-<div class="row"{% if rtl %} dir="ltr"{% endif %}>
-    <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
-
-    <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
-    </p>
-    <h2>Why use searx?</h2>
-    <ul>
-        <li>searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
-        <li>searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
-        <li>searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
-    </ul>
-    <p>If you do care about privacy, want to be a conscious user, or otherwise believe
-    in digital freedom, make searx your default search engine or run it on your own server</p>
-
-<h2>Technical details - How does it work?</h2>
-
-<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
-inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>.<br />
-It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, searx uses the search bar to perform GET requests.<br />
-Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
-</p>
-
-<h2>How can I make it my own?</h2>
-
-<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
-<br />The more decentralized the Internet, is the more freedom we have!</p>
-
-
-<h2>More about searx</h2>
-
-<ul>
-    <li><a href="https://github.com/asciimoo/searx">github</a></li>
-    <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
-    <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
-    <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
-    <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
-</ul>
-
-
-<hr />
-
-<h2 id="faq">FAQ</h2>
-
-<h3>How to add to firefox?</h3>
-<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
-
-<h2 id="dev_faq">Developer FAQ</h2>
-
-<h3>New engines?</h3>
-<ul>
-    <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
-    <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
-</ul>
-<p>Don't forget to restart searx after config edit!</p>
-
-<h3>Installation/WSGI support?</h3>
-<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
-
-<h3>How to debug engines?</h3>
-<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
-
-</div>
+{% include '__common__/about.html' %}
 {% endblock %}
 {% endblock %}

+ 1 - 62
searx/templates/legacy/about.html

@@ -1,66 +1,5 @@
 {% extends 'legacy/base.html' %}
 {% extends 'legacy/base.html' %}
 {% block content %}
 {% block content %}
 {% include 'legacy/github_ribbon.html' %}
 {% include 'legacy/github_ribbon.html' %}
-<div class="row"{% if rtl %} dir="ltr"{% endif %}>
-    <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
-
-    <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
-    </p>
-    <h2>Why use searx?</h2>
-    <ul>
-        <li>searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
-        <li>searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
-        <li>searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
-    </ul>
-    <p>If you do care about privacy, want to be a conscious user, or otherwise believe
-    in digital freedom, make searx your default search engine or run it on your own server</p>
-
-<h2>Technical details - How does it work?</h2>
-
-<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
-inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>.<br />
-It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, if searx used from the search bar it performs GET requests.<br />
-Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
-</p>
-
-<h2>How can I make it my own?</h2>
-
-<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
-<br />The more decentralized Internet is the more freedom we have!</p>
-
-
-<h2>More about searx</h2>
-
-<ul>
-    <li><a href="https://github.com/asciimoo/searx">github</a></li>
-    <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
-    <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
-    <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
-    <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
-</ul>
-
-
-<hr />
-
-<h2 id="faq">FAQ</h2>
-
-<h3>How to add to firefox?</h3>
-<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
-
-<h2 id="dev_faq">Developer FAQ</h2>
-
-<h3>New engines?</h3>
-<ul>
-    <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
-    <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
-</ul>
-<p>Don't forget to restart searx after config edit!</p>
-
-<h3>Installation/WSGI support?</h3>
-<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
-
-<h3>How to debug engines?</h3>
-<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
-
-</div>
+{% include '__common__/about.html' %}
 {% endblock %}
 {% endblock %}

+ 0 - 28
searx/templates/legacy/opensearch.xml

@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
-  <ShortName>{{ instance_name }}</ShortName>
-  <Description>a privacy-respecting, hackable metasearch engine</Description>
-  <InputEncoding>UTF-8</InputEncoding>
-  <Image>{{ urljoin(host, url_for('static', filename='img/favicon.png')) }}</Image>
-  <LongName>searx metasearch</LongName>
-  {% if opensearch_method == 'get' %}
-    <Url type="text/html" method="get" template="{{ host }}search?q={searchTerms}"/>
-    {% if autocomplete %}
-    <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
-        <Param name="format" value="x-suggestions" />
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% endif %}
-  {% else %}
-    <Url type="text/html" method="post" template="{{ host }}">
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% if autocomplete %}
-    <!-- TODO, POST REQUEST doesn't work -->
-    <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
-        <Param name="format" value="x-suggestions" />
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% endif %}
-  {% endif %}
-</OpenSearchDescription>

+ 0 - 23
searx/templates/legacy/opensearch_response_rss.xml

@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<rss version="2.0"
-     xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
-     xmlns:atom="http://www.w3.org/2005/Atom">
-  <channel>
-    <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:startIndex>1</opensearch:startIndex>
-    <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
-    <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
-    <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
-    {% for r in results %}
-    <item>
-      <title>{{ r.title }}</title>
-      <link>{{ r.url }}</link>
-      <description>{{ r.content }}</description>
-      {% if r.pubdate %}<pubDate>{{ r.pubdate }}</pubDate>{% endif %}
-    </item>
-    {% endfor %}
-  </channel>
-</rss>

+ 1 - 62
searx/templates/oscar/about.html

@@ -1,66 +1,5 @@
 {% extends "oscar/base.html" %}
 {% extends "oscar/base.html" %}
 {% block title %}{{ _('about') }} - {% endblock %}
 {% block title %}{{ _('about') }} - {% endblock %}
 {% block content %}
 {% block content %}
-<div{% if rtl %} dir="ltr"{% endif %}>
-    <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
-
-    <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
-    </p>
-    <h2>Why use searx?</h2>
-    <ul>
-        <li>searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
-        <li>searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
-        <li>searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
-    </ul>
-    <p>If you do care about privacy, want to be a conscious user, or otherwise believe
-    in digital freedom, make searx your default search engine or run it on your own server</p>
-
-<h2>Technical details - How does it work?</h2>
-
-<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
-inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>.<br />
-It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, searx uses the search bar to perform GET requests.<br />
-Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
-</p>
-
-<h2>How can I make it my own?</h2>
-
-<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
-<br />The more decentralized the Internet is, the more freedom we have!</p>
-
-
-<h2>More about searx</h2>
-
-<ul>
-    <li><a href="https://github.com/asciimoo/searx">github</a></li>
-    <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
-    <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
-    <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
-    <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
-</ul>
-
-
-<hr />
-
-<h2 id="faq">FAQ</h2>
-
-<h3>How to add to firefox?</h3>
-<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
-
-<h2 id="dev_faq">Developer FAQ</h2>
-
-<h3>New engines?</h3>
-<ul>
-    <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
-    <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
-</ul>
-<p>Don't forget to restart searx after config edit!</p>
-
-<h3>Installation/WSGI support?</h3>
-<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
-
-<h3>How to debug engines?</h3>
-<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
-
-</div>
+{% include '__common__/about.html' %}
 {% endblock %}
 {% endblock %}

+ 9 - 3
searx/templates/oscar/advanced.html

@@ -4,7 +4,13 @@
     {{ _('Advanced settings') }}
     {{ _('Advanced settings') }}
 </label>
 </label>
 <div id="advanced-search-container">
 <div id="advanced-search-container">
-    {% include 'oscar/categories.html' %}
-    {% include 'oscar/time-range.html' %}
-    {% include 'oscar/languages.html' %}
+  {% include 'oscar/categories.html' %}
+  <div class="row">
+    <div class="col-xs-6">
+      {% include 'oscar/time-range.html' %}
+    </div>
+    <div class="col-xs-6">
+      {% include 'oscar/languages.html' %}
+    </div>
+  </div>
 </div>
 </div>

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

@@ -98,5 +98,10 @@
     {% for script in scripts %}
     {% for script in scripts %}
         <script src="{{ url_for('static', filename=script) }}"></script>
         <script src="{{ url_for('static', filename=script) }}"></script>
     {% endfor %}
     {% endfor %}
+    <noscript>
+      <style>
+        .glyphicon { display: none; }
+      </style>
+    </noscript>
 </body>
 </body>
 </html>
 </html>

+ 1 - 1
searx/templates/oscar/languages.html

@@ -1,7 +1,7 @@
 {% if preferences %}
 {% if preferences %}
 <select class="form-control" name='language'>
 <select class="form-control" name='language'>
 {% else %}
 {% else %}
-<select class="time_range" id='language' name='language'>
+<select class="time_range custom-select form-control" id='language' name='language'>
 {% endif %}
 {% endif %}
 	<option value="all" {% if current_language == 'all' %}selected="selected"{% endif %}>{{ _('Default language') }}</option>
 	<option value="all" {% if current_language == 'all' %}selected="selected"{% endif %}>{{ _('Default language') }}</option>
 		{% for lang_id,lang_name,country_name,english_name in language_codes | sort(attribute=1) %}
 		{% for lang_id,lang_name,country_name,english_name in language_codes | sort(attribute=1) %}

+ 3 - 3
searx/templates/oscar/macros.html

@@ -10,7 +10,7 @@
 {%- endmacro %}
 {%- endmacro %}
 
 
 {%- macro result_link(url, title, classes='') -%}
 {%- macro result_link(url, title, classes='') -%}
-<a href="{{ url }}" {% if classes %}class="{{ classes }} "{% endif %}{% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ title }}</a>
+<a href="{{ url }}" {% if classes %}class="{{ classes }}" {% endif %}{% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ title }}</a>
 {%- endmacro -%}
 {%- endmacro -%}
 
 
 <!-- Draw result header -->
 <!-- Draw result header -->
@@ -37,7 +37,7 @@
     <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
     <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
     {% endif %}
     {% endif %}
 </div>
 </div>
-    <div class="text-muted"><small>{{ result.pretty_url }}</small></div>
+<div class="external-link">{{ result.pretty_url }}</div>
 {%- endmacro %}
 {%- endmacro %}
 
 
 <!-- Draw result footer -->
 <!-- Draw result footer -->
@@ -50,7 +50,7 @@
     {% if proxify %}
     {% if proxify %}
     <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
     <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
     {% endif %}
     {% endif %}
-    <div class="text-muted"><small>{{ result.pretty_url }}</small></div>
+    <div class="external-link">{{ result.pretty_url }}</div>
 {%- endmacro %}
 {%- endmacro %}
 
 
 {% macro preferences_item_header(info, label, rtl) -%}
 {% macro preferences_item_header(info, label, rtl) -%}

+ 8 - 13
searx/templates/oscar/navbar.html

@@ -1,14 +1,9 @@
-<!-- Static navbar -->
-<div class="navbar navbar-default" role="navigation">
-    <div class="container-fluid">
-        <div class="navbar-header{% if rtl %} navbar-right{% endif %}">
-            <a class="navbar-brand" href="{{ url_for('index') }}">{{ instance_name }}</a>
-        </div>
-    </div><!--/.container-fluid -->
+<div class="searx-navbar">
+    <span class="instance {% if rtl %}pull-right{% else %}pull-left{% endif%}">
+        <a href="{{ url_for('index') }}">{{ instance_name }}</a>
+    </span>
+    <span class="{% if rtl %}pull-left{% else %}pull-right{% endif %}">
+        <a href="{{ url_for('about') }}">{{ _('about') }}</a>
+        <a href="{{ url_for('preferences') }}">{{ _('preferences') }}</a>
+    </span>
 </div>
 </div>
-<div class="menu menu-{% if rtl %}left{% else %}right{% endif %}">
-    <ul> <!-- results.html -->
-        <li{% if template_name == 'about.html' %} class="active"{% endif %}><a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a></li>
-        <li{% if template_name == 'preferences.html' %} class="active"{% endif %}><a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a></li>
-    </ul>
-</div><!--/.nav-collapse -->

+ 0 - 28
searx/templates/oscar/opensearch.xml

@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
-  <ShortName>{{ instance_name }}</ShortName>
-  <Description>a privacy-respecting, hackable metasearch engine</Description>
-  <InputEncoding>UTF-8</InputEncoding>
-  <Image>{{ urljoin(host, url_for('static', filename='img/favicon.png')) }}</Image>
-  <LongName>searx metasearch</LongName>
-  {% if opensearch_method == 'get' %}
-    <Url type="text/html" method="get" template="{{ host }}search?q={searchTerms}"/>
-    {% if autocomplete %}
-    <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
-        <Param name="format" value="x-suggestions" />
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% endif %}
-  {% else %}
-    <Url type="text/html" method="post" template="{{ host }}">
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% if autocomplete %}
-    <!-- TODO, POST REQUEST doesn't work -->
-    <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
-        <Param name="format" value="x-suggestions" />
-        <Param name="q" value="{searchTerms}" />
-    </Url>
-    {% endif %}
-  {% endif %}
-</OpenSearchDescription>

+ 0 - 23
searx/templates/oscar/opensearch_response_rss.xml

@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<rss version="2.0"
-     xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
-     xmlns:atom="http://www.w3.org/2005/Atom">
-  <channel>
-    <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:startIndex>1</opensearch:startIndex>
-    <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
-    <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
-    <opensearch:Query role="request" searchTerms="{{ q|e }}" startPage="1" />
-    {% for r in results %}
-    <item>
-      <title>{{ r.title }}</title>
-      <link>{{ r.url }}</link>
-      <description>{{ r.content }}</description>
-      {% if r.pubdate %}<pubDate>{{ r.pubdate }}</pubDate>{% endif %}
-    </item>
-    {% endfor %}
-  </channel>
-</rss>

+ 14 - 9
searx/templates/oscar/results.html

@@ -11,10 +11,22 @@
 {% block title %}{{ q|e }} - {% endblock %}
 {% block title %}{{ q|e }} - {% endblock %}
 {% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ search_url() }}&amp;format=rss">{% endblock %}
 {% block meta %}<link rel="alternate" type="application/rss+xml" title="Searx search: {{ q|e }}" href="{{ search_url() }}&amp;format=rss">{% endblock %}
 {% block content %}
 {% block content %}
+    {% include 'oscar/search.html' %}
     <div class="row">
     <div class="row">
         <div class="col-sm-8" id="main_results">
         <div class="col-sm-8" id="main_results">
             <h1 class="sr-only">{{ _('Search results') }}</h1>
             <h1 class="sr-only">{{ _('Search results') }}</h1>
-            {% include 'oscar/search.html' %}
+
+            {% if corrections %}
+            <div class="result">
+                <span class="result_header text-muted form-inline pull-left suggestion_item">{{ _('Try searching for:') }}</span>
+                {% for correction in corrections %}
+                    <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" role="navigation" class="form-inline pull-left suggestion_item">
+                        <input type="hidden" name="q" value="{{ correction }}">
+                        <button type="submit" class="btn btn-default btn-xs">{{ correction }}</button>
+                    </form>
+                {% endfor %}
+            </div>
+            {% endif %}
 
 
             {% if answers %}
             {% if answers %}
             {% for answer in answers %}
             {% for answer in answers %}
@@ -80,14 +92,7 @@
 
 
         <div class="col-sm-4" id="sidebar_results">
         <div class="col-sm-4" id="sidebar_results">
             {% if number_of_results != '0' %}
             {% if number_of_results != '0' %}
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <h4 class="panel-title">{{ _('Number of results') }}</h4>
-                </div>
-                <div class="panel-body">
-                    {{ number_of_results }}
-                </div>
-            </div>
+                <p><small>{{ _('Number of results') }}: {{ number_of_results }}</small></p>
             {% endif %}
             {% endif %}
             {% if infoboxes %}
             {% if infoboxes %}
                 {% for infobox in infoboxes %}
                 {% for infobox in infoboxes %}

+ 15 - 3
searx/templates/oscar/search.html

@@ -1,12 +1,24 @@
 {% from 'oscar/macros.html' import icon %}
 {% from 'oscar/macros.html' import icon %}
 <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
 <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form" role="search">
-    <div class="input-group col-sm-12">
+  <div class="row">
+    <div class="col-xs-12 col-md-8">
+      <div class="input-group search-margin">
         <input type="search" name="q" class="form-control" id="q" placeholder="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}">
         <input type="search" name="q" class="form-control" id="q" placeholder="{{ _('Search for...') }}" autocomplete="off" value="{{ q }}">
         <span class="input-group-btn">
         <span class="input-group-btn">
             <button type="submit" class="btn btn-default"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
             <button type="submit" class="btn btn-default"><span class="hide_if_nojs">{{ icon('search') }}</span><span class="hidden active_if_nojs">{{ _('Start search') }}</span></button>
         </span>
         </span>
+      </div>
     </div>
     </div>
-    <div class="input-group col-sm-12 advanced">
-        {% include 'oscar/advanced.html' %}
+    <div class="col-xs-6 col-md-2 search-margin">
+        {% include 'oscar/time-range.html' %}
     </div>
     </div>
+    <div class="col-xs-6 col-md-2 search-margin">
+        {% include 'oscar/languages.html' %}
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-sm-12">
+        {% include 'oscar/categories.html' %}
+    </div>
+  </div>
 </form><!-- / #search_form_full -->
 </form><!-- / #search_form_full -->

+ 1 - 1
searx/templates/oscar/time-range.html

@@ -1,4 +1,4 @@
-<select name="time_range" id="time-range">
+<select name="time_range" id="time-range" class="custom-select form-control">
     <option id="time-range-anytime" value="" {{ "selected" if time_range=="" or not time_range  else ""}}>
     <option id="time-range-anytime" value="" {{ "selected" if time_range=="" or not time_range  else ""}}>
         {{ _('Anytime') }}
         {{ _('Anytime') }}
     </option>
     </option>

+ 1 - 62
searx/templates/pix-art/about.html

@@ -1,65 +1,4 @@
 {% extends 'pix-art/base.html' %}
 {% extends 'pix-art/base.html' %}
 {% block content %}
 {% block content %}
-<div class="row"{% if rtl %} dir="ltr"{% endif %}>
-    <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
-
-    <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
-    </p>
-    <h2>Why use searx?</h2>
-    <ul>
-        <li>searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
-        <li>searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
-        <li>searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
-    </ul>
-    <p>If you do care about privacy, want to be a conscious user, or otherwise believe
-    in digital freedom, make searx your default search engine or run it on your own server</p>
-
-<h2>Technical details - How does it work?</h2>
-
-<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
-inspired by the <a href="https://beniz.github.io/seeks/">seeks project</a>.<br />
-It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, if searx used from the search bar it performs GET requests.<br />
-Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
-</p>
-
-<h2>How can I make it my own?</h2>
-
-<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
-<br />The more decentralized Internet is the more freedom we have!</p>
-
-
-<h2>More about searx</h2>
-
-<ul>
-    <li><a href="https://github.com/asciimoo/searx">github</a></li>
-    <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
-    <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
-    <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
-    <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
-</ul>
-
-
-<hr />
-
-<h2 id="faq">FAQ</h2>
-
-<h3>How to add to firefox?</h3>
-<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
-
-<h2 id="dev_faq">Developer FAQ</h2>
-
-<h3>New engines?</h3>
-<ul>
-    <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
-    <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
-</ul>
-<p>Don't forget to restart searx after config edit!</p>
-
-<h3>Installation/WSGI support?</h3>
-<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
-
-<h3>How to debug engines?</h3>
-<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
-
-</div>
+{% include '__common__/about.html' %}
 {% endblock %}
 {% endblock %}

+ 2 - 0
searx/utils.py

@@ -175,6 +175,8 @@ def get_themes(root):
     templates_path = os.path.join(root, 'templates')
     templates_path = os.path.join(root, 'templates')
 
 
     themes = os.listdir(os.path.join(static_path, 'themes'))
     themes = os.listdir(os.path.join(static_path, 'themes'))
+    if '__common__' in themes:
+        themes.remove('__common__')
     return static_path, templates_path, themes
     return static_path, templates_path, themes
 
 
 
 

+ 59 - 18
searx/webapp.py

@@ -52,6 +52,7 @@ from flask import (
 from flask_babel import Babel, gettext, format_date, format_decimal
 from flask_babel import Babel, gettext, format_date, format_decimal
 from flask.json import jsonify
 from flask.json import jsonify
 from searx import settings, searx_dir, searx_debug
 from searx import settings, searx_dir, searx_debug
+from searx.exceptions import SearxException, SearxParameterException
 from searx.engines import (
 from searx.engines import (
     categories, engines, engine_shortcuts, get_engines_stats, initialize_engines
     categories, engines, engine_shortcuts, get_engines_stats, initialize_engines
 )
 )
@@ -226,7 +227,7 @@ def get_current_theme_name(override=None):
     2. cookies
     2. cookies
     3. settings"""
     3. settings"""
 
 
-    if override and override in themes:
+    if override and (override in themes or override == '__common__'):
         return override
         return override
     theme_name = request.args.get('theme', request.preferences.get_value('theme'))
     theme_name = request.args.get('theme', request.preferences.get_value('theme'))
     if theme_name not in themes:
     if theme_name not in themes:
@@ -400,6 +401,33 @@ def pre_request():
             request.user_plugins.append(plugin)
             request.user_plugins.append(plugin)
 
 
 
 
+def index_error(output_format, error_message):
+    if output_format == 'json':
+        return Response(json.dumps({'error': error_message}),
+                        mimetype='application/json')
+    elif output_format == 'csv':
+        response = Response('', mimetype='application/csv')
+        cont_disp = 'attachment;Filename=searx.csv'
+        response.headers.add('Content-Disposition', cont_disp)
+        return response
+    elif output_format == 'rss':
+        response_rss = render(
+            'opensearch_response_rss.xml',
+            results=[],
+            q=request.form['q'] if 'q' in request.form else '',
+            number_of_results=0,
+            base_url=get_base_url(),
+            error_message=error_message
+        )
+        return Response(response_rss, mimetype='text/xml')
+    else:
+        # html
+        request.errors.append(gettext('search error'))
+        return render(
+            'index.html',
+        )
+
+
 @app.route('/search', methods=['GET', 'POST'])
 @app.route('/search', methods=['GET', 'POST'])
 @app.route('/', methods=['GET', 'POST'])
 @app.route('/', methods=['GET', 'POST'])
 def index():
 def index():
@@ -408,10 +436,19 @@ def index():
     Supported outputs: html, json, csv, rss.
     Supported outputs: html, json, csv, rss.
     """
     """
 
 
+    # output_format
+    output_format = request.form.get('format', 'html')
+    if output_format not in ['html', 'csv', 'json', 'rss']:
+        output_format = 'html'
+
+    # check if there is query
     if request.form.get('q') is None:
     if request.form.get('q') is None:
-        return render(
-            'index.html',
-        )
+        if output_format == 'html':
+            return render(
+                'index.html',
+            )
+        else:
+            return index_error(output_format, 'No query'), 400
 
 
     # search
     # search
     search_query = None
     search_query = None
@@ -421,20 +458,24 @@ def index():
         # search = Search(search_query) #  without plugins
         # search = Search(search_query) #  without plugins
         search = SearchWithPlugins(search_query, request.user_plugins, request)
         search = SearchWithPlugins(search_query, request.user_plugins, request)
         result_container = search.search()
         result_container = search.search()
-    except:
-        request.errors.append(gettext('search error'))
+    except Exception as e:
+        # log exception
         logger.exception('search error')
         logger.exception('search error')
-        return render(
-            'index.html',
-        )
 
 
+        # is it an invalid input parameter or something else ?
+        if (issubclass(e.__class__, SearxParameterException)):
+            return index_error(output_format, e.message), 400
+        else:
+            return index_error(output_format, gettext('search error')), 500
+
+    # results
     results = result_container.get_ordered_results()
     results = result_container.get_ordered_results()
+    number_of_results = result_container.results_number()
+    if number_of_results < result_container.results_length():
+        number_of_results = 0
 
 
     # UI
     # UI
     advanced_search = request.form.get('advanced_search', None)
     advanced_search = request.form.get('advanced_search', None)
-    output_format = request.form.get('format', 'html')
-    if output_format not in ['html', 'csv', 'json', 'rss']:
-        output_format = 'html'
 
 
     # output
     # output
     for result in results:
     for result in results:
@@ -470,15 +511,12 @@ def index():
                 else:
                 else:
                     result['publishedDate'] = format_date(result['publishedDate'])
                     result['publishedDate'] = format_date(result['publishedDate'])
 
 
-    number_of_results = result_container.results_number()
-    if number_of_results < result_container.results_length():
-        number_of_results = 0
-
     if output_format == 'json':
     if output_format == 'json':
         return Response(json.dumps({'query': search_query.query,
         return Response(json.dumps({'query': search_query.query,
                                     'number_of_results': number_of_results,
                                     'number_of_results': number_of_results,
                                     'results': results,
                                     'results': results,
                                     'answers': list(result_container.answers),
                                     'answers': list(result_container.answers),
+                                    'corrections': list(result_container.corrections),
                                     'infoboxes': result_container.infoboxes,
                                     'infoboxes': result_container.infoboxes,
                                     'suggestions': list(result_container.suggestions)}),
                                     'suggestions': list(result_container.suggestions)}),
                         mimetype='application/json')
                         mimetype='application/json')
@@ -500,7 +538,8 @@ def index():
             results=results,
             results=results,
             q=request.form['q'],
             q=request.form['q'],
             number_of_results=number_of_results,
             number_of_results=number_of_results,
-            base_url=get_base_url()
+            base_url=get_base_url(),
+            override_theme='__common__',
         )
         )
         return Response(response_rss, mimetype='text/xml')
         return Response(response_rss, mimetype='text/xml')
 
 
@@ -515,6 +554,7 @@ def index():
         advanced_search=advanced_search,
         advanced_search=advanced_search,
         suggestions=result_container.suggestions,
         suggestions=result_container.suggestions,
         answers=result_container.answers,
         answers=result_container.answers,
+        corrections=result_container.corrections,
         infoboxes=result_container.infoboxes,
         infoboxes=result_container.infoboxes,
         paging=result_container.paging,
         paging=result_container.paging,
         current_language=search_query.lang,
         current_language=search_query.lang,
@@ -720,7 +760,8 @@ def opensearch():
     ret = render('opensearch.xml',
     ret = render('opensearch.xml',
                  opensearch_method=method,
                  opensearch_method=method,
                  host=get_base_url(),
                  host=get_base_url(),
-                 urljoin=urljoin)
+                 urljoin=urljoin,
+                 override_theme='__common__')
 
 
     resp = Response(response=ret,
     resp = Response(response=ret,
                     status=200,
                     status=200,

+ 3 - 0
tests/unit/test_webapp.py

@@ -36,6 +36,7 @@ class ViewsTestCase(SearxTestCase):
         def search_mock(search_self, *args):
         def search_mock(search_self, *args):
             search_self.result_container = Mock(get_ordered_results=lambda: self.test_results,
             search_self.result_container = Mock(get_ordered_results=lambda: self.test_results,
                                                 answers=set(),
                                                 answers=set(),
+                                                corrections=set(),
                                                 suggestions=set(),
                                                 suggestions=set(),
                                                 infoboxes=[],
                                                 infoboxes=[],
                                                 results=self.test_results,
                                                 results=self.test_results,
@@ -45,6 +46,8 @@ class ViewsTestCase(SearxTestCase):
         Search.search = search_mock
         Search.search = search_mock
 
 
         def get_current_theme_name_mock(override=None):
         def get_current_theme_name_mock(override=None):
+            if override:
+                return override
             return 'legacy'
             return 'legacy'
 
 
         webapp.get_current_theme_name = get_current_theme_name_mock
         webapp.get_current_theme_name = get_current_theme_name_mock

Some files were not shown because too many files changed in this diff