[enh] add settings option to enable/disable search formats
Access to formats can be denied by settings configuration::
    search:
        formats: [html, csv, json, rss]
Closes: https://github.com/searxng/searxng/issues/95
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
			
			
This commit is contained in:
		
							parent
							
								
									e1f244b2d5
								
							
						
					
					
						commit
						6ed4616da9
					
				| @ -18,6 +18,7 @@ search: | |||||||
|     default_lang : "" # Default search language - leave blank to detect from browser information or use codes from 'languages.py' |     default_lang : "" # Default search language - leave blank to detect from browser information or use codes from 'languages.py' | ||||||
|     ban_time_on_fail : 5 # ban time in seconds after engine errors |     ban_time_on_fail : 5 # ban time in seconds after engine errors | ||||||
|     max_ban_time_on_fail : 120 # max ban time in seconds after engine errors |     max_ban_time_on_fail : 120 # max ban time in seconds after engine errors | ||||||
|  |     formats: [html, csv, json, rss]  # remove format to deny access, use lower case. | ||||||
| 
 | 
 | ||||||
| server: | server: | ||||||
|     port : 8888 |     port : 8888 | ||||||
|  | |||||||
| @ -80,9 +80,10 @@ | |||||||
|                             <input id="search_url" type="url" class="form-control select-all-on-click cursor-text" name="search_url" value="{{ search_url() }}" readonly>{{- "" -}} |                             <input id="search_url" type="url" class="form-control select-all-on-click cursor-text" name="search_url" value="{{ search_url() }}" readonly>{{- "" -}} | ||||||
|                         </div>{{- "" -}} |                         </div>{{- "" -}} | ||||||
|                     </form> |                     </form> | ||||||
|  |                     {% if search_formats %} | ||||||
|                     <label>{{ _('Download results') }}</label> |                     <label>{{ _('Download results') }}</label> | ||||||
|                     <div class="clearfix"></div> |                     <div class="clearfix"></div> | ||||||
|                     {% for output_type in ('csv', 'json', 'rss') %} |                     {% for output_type in search_formats %} | ||||||
|                     <form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download"> |                     <form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" class="form-inline pull-{% if rtl %}right{% else %}left{% endif %} result_download"> | ||||||
|                         {{- search_form_attrs(pageno) -}} |                         {{- search_form_attrs(pageno) -}} | ||||||
|                         <input type="hidden" name="format" value="{{ output_type }}">{{- "" -}} |                         <input type="hidden" name="format" value="{{ output_type }}">{{- "" -}} | ||||||
| @ -90,8 +91,11 @@ | |||||||
|                     </form> |                     </form> | ||||||
|                     {% endfor %} |                     {% endfor %} | ||||||
|                     <div class="clearfix"></div> |                     <div class="clearfix"></div> | ||||||
|  |                     {% if 'rss' in search_formats %} | ||||||
|                     <br /><label><a href="{{ search_url() }}&format=rss">{{ _('RSS subscription') }}</a></label> |                     <br /><label><a href="{{ search_url() }}&format=rss">{{ _('RSS subscription') }}</a></label> | ||||||
|  |                     {% endif %} | ||||||
|                     <div class="clearfix"></div> |                     <div class="clearfix"></div> | ||||||
|  |                     {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|         </div><!-- /#sidebar_results --> |         </div><!-- /#sidebar_results --> | ||||||
|  | |||||||
| @ -85,8 +85,9 @@ | |||||||
|             <div class="selectable_url"><pre>{{ url_for('search', _external=True) }}?q={{ q|urlencode }}&language={{ current_language }}&time_range={{ time_range }}&safesearch={{ safesearch }}{% if pageno > 1 %}&pageno={{ pageno }}{% endif %}{% if selected_categories %}&categories={{ selected_categories|join(",") | replace(' ','+') }}{% endif %}{% if timeout_limit %}&timeout_limit={{ timeout_limit|urlencode }}{% endif %}</pre></div> |             <div class="selectable_url"><pre>{{ url_for('search', _external=True) }}?q={{ q|urlencode }}&language={{ current_language }}&time_range={{ time_range }}&safesearch={{ safesearch }}{% if pageno > 1 %}&pageno={{ pageno }}{% endif %}{% if selected_categories %}&categories={{ selected_categories|join(",") | replace(' ','+') }}{% endif %}{% if timeout_limit %}&timeout_limit={{ timeout_limit|urlencode }}{% endif %}</pre></div> | ||||||
|         </div> |         </div> | ||||||
|         <div id="apis"> |         <div id="apis"> | ||||||
|  |           {% if search_formats %} | ||||||
|           <h4 class="title">{{ _('Download results') }}</h4> |           <h4 class="title">{{ _('Download results') }}</h4> | ||||||
|           {% for output_type in ('csv', 'json', 'rss') %} |           {% for output_type in search_formats %} | ||||||
| 	  <div class="left"> | 	  <div class="left"> | ||||||
|             <form method="{{ method or 'POST' }}" action="{{ url_for('search') }}"> |             <form method="{{ method or 'POST' }}" action="{{ url_for('search') }}"> | ||||||
|               <input type="hidden" name="q" value="{{ q|e }}"> |               <input type="hidden" name="q" value="{{ q|e }}"> | ||||||
| @ -103,6 +104,7 @@ | |||||||
|             </form> |             </form> | ||||||
| 	  </div> | 	  </div> | ||||||
|           {% endfor %} |           {% endfor %} | ||||||
|  |           {% endif %} | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from os.path import splitext, join | |||||||
| from random import choice | from random import choice | ||||||
| from html.parser import HTMLParser | from html.parser import HTMLParser | ||||||
| from urllib.parse import urljoin, urlparse | from urllib.parse import urljoin, urlparse | ||||||
|  | from collections.abc import Mapping | ||||||
| 
 | 
 | ||||||
| from lxml import html | from lxml import html | ||||||
| from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult | from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult | ||||||
| @ -500,6 +501,62 @@ def get_engine_from_settings(name): | |||||||
|     return {} |     return {} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | NOT_EXISTS = object() | ||||||
|  | """Singleton used by :py:obj:`get_value` if a key does not exists.""" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_value(dictionary, keyword, *keys, default=NOT_EXISTS): | ||||||
|  |     """Return the value from a *deep* mapping type (e.g. the ``settings`` object | ||||||
|  |     from yaml).  If the path to the *key* does not exists a :py:obj:`NOT_EXISTS` | ||||||
|  |     is returned (non ``KeyError`` exception is raised). | ||||||
|  | 
 | ||||||
|  |     .. code: python | ||||||
|  | 
 | ||||||
|  |        >>> from searx.utils import get_value, NOT_EXISTS | ||||||
|  |        >>> get_value(settings, 'checker', 'additional_tests', 'rosebud', 'result_container') | ||||||
|  |        ['not_empty', ['one_title_contains', 'citizen kane']] | ||||||
|  | 
 | ||||||
|  |        >>> get_value(settings, 'search', 'xxx') is NOT_EXISTS | ||||||
|  |        True | ||||||
|  |        >>> get_value(settings, 'search', 'formats') | ||||||
|  |        ['csv', 'json', 'rss'] | ||||||
|  | 
 | ||||||
|  |     The list returned from the ``search.format`` key is not a mapping type, you | ||||||
|  |     can't traverse along non-mapping types.  If you try it, you will get a | ||||||
|  |     :py:ref:`NOT_EXISTS`: | ||||||
|  | 
 | ||||||
|  |     .. code: python | ||||||
|  | 
 | ||||||
|  |        >>> get_value(settings, 'search', 'format', 'csv') is NOT_EXISTS | ||||||
|  |        True | ||||||
|  |        >>> get_value(settings, 'search', 'formats')[0] | ||||||
|  |        'csv' | ||||||
|  | 
 | ||||||
|  |     For convenience you can replace :py:ref:`NOT_EXISTS` by a default value of | ||||||
|  |     your choice: | ||||||
|  | 
 | ||||||
|  |     .. code: python | ||||||
|  | 
 | ||||||
|  |        if 'csv' in get_value(settings, 'search', 'formats', default=[]): | ||||||
|  |            print("csv format is denied") | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     if not isinstance(dictionary, Mapping): | ||||||
|  |         raise TypeError("expected mapping type, got %s" % type(dictionary)) | ||||||
|  | 
 | ||||||
|  |     ret_val = dictionary.get(keyword, default) | ||||||
|  | 
 | ||||||
|  |     if ret_val is default: | ||||||
|  |         return ret_val | ||||||
|  | 
 | ||||||
|  |     if len(keys): | ||||||
|  |         if not isinstance(ret_val, Mapping): | ||||||
|  |             ret_val = default | ||||||
|  |         else: | ||||||
|  |             ret_val = get_value(ret_val, *keys, default=default) | ||||||
|  |     return ret_val | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_xpath(xpath_spec): | def get_xpath(xpath_spec): | ||||||
|     """Return cached compiled XPath |     """Return cached compiled XPath | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -31,6 +31,8 @@ from pygments.formatters import HtmlFormatter  # pylint: disable=no-name-in-modu | |||||||
| from werkzeug.middleware.proxy_fix import ProxyFix | from werkzeug.middleware.proxy_fix import ProxyFix | ||||||
| from werkzeug.serving import WSGIRequestHandler | from werkzeug.serving import WSGIRequestHandler | ||||||
| 
 | 
 | ||||||
|  | import flask | ||||||
|  | 
 | ||||||
| from flask import ( | from flask import ( | ||||||
|     Flask, |     Flask, | ||||||
|     request, |     request, | ||||||
| @ -86,6 +88,7 @@ from searx.utils import ( | |||||||
|     gen_useragent, |     gen_useragent, | ||||||
|     dict_subset, |     dict_subset, | ||||||
|     match_language, |     match_language, | ||||||
|  |     get_value, | ||||||
| ) | ) | ||||||
| from searx.version import VERSION_STRING | from searx.version import VERSION_STRING | ||||||
| from searx.query import RawTextQuery | from searx.query import RawTextQuery | ||||||
| @ -161,6 +164,8 @@ for indice, theme in enumerate(themes): | |||||||
|     for (dirpath, dirnames, filenames) in os.walk(theme_img_path): |     for (dirpath, dirnames, filenames) in os.walk(theme_img_path): | ||||||
|         global_favicons[indice].extend(filenames) |         global_favicons[indice].extend(filenames) | ||||||
| 
 | 
 | ||||||
|  | OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss'] | ||||||
|  | 
 | ||||||
| STATS_SORT_PARAMETERS = { | STATS_SORT_PARAMETERS = { | ||||||
|     'name': (False, 'name', ''), |     'name': (False, 'name', ''), | ||||||
|     'score': (True, 'score', 0), |     'score': (True, 'score', 0), | ||||||
| @ -511,6 +516,11 @@ def render(template_name, override_theme=None, **kwargs): | |||||||
| 
 | 
 | ||||||
|     kwargs['preferences'] = request.preferences |     kwargs['preferences'] = request.preferences | ||||||
| 
 | 
 | ||||||
|  |     kwargs['search_formats'] = [ | ||||||
|  |         x for x in get_value( | ||||||
|  |             settings, 'search', 'formats', default=OUTPUT_FORMATS) | ||||||
|  |         if x != 'html'] | ||||||
|  | 
 | ||||||
|     kwargs['brand'] = brand |     kwargs['brand'] = brand | ||||||
| 
 | 
 | ||||||
|     kwargs['translations'] = json.dumps(get_translations(), separators=(',', ':')) |     kwargs['translations'] = json.dumps(get_translations(), separators=(',', ':')) | ||||||
| @ -683,9 +693,12 @@ def search(): | |||||||
| 
 | 
 | ||||||
|     # output_format |     # output_format | ||||||
|     output_format = request.form.get('format', 'html') |     output_format = request.form.get('format', 'html') | ||||||
|     if output_format not in ['html', 'csv', 'json', 'rss']: |     if output_format not in OUTPUT_FORMATS: | ||||||
|         output_format = 'html' |         output_format = 'html' | ||||||
| 
 | 
 | ||||||
|  |     if output_format not in get_value(settings, 'search', 'formats', default=OUTPUT_FORMATS): | ||||||
|  |         flask.abort(403) | ||||||
|  | 
 | ||||||
|     # check if there is query (not None and not an empty string) |     # check if there is query (not None and not an empty string) | ||||||
|     if not request.form.get('q'): |     if not request.form.get('q'): | ||||||
|         if output_format == 'html': |         if output_format == 'html': | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ 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", "swisscows", "qwant", "wikipedia" - leave blank to turn it off by default |     autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "swisscows", "qwant", "wikipedia" - leave blank to turn it off by default | ||||||
|     default_lang : "" # Default search language - leave blank to detect from browser information or use codes from 'languages.py' |     default_lang : "" # Default search language - leave blank to detect from browser information or use codes from 'languages.py' | ||||||
|  |     formats: [html, csv, json, rss] | ||||||
| 
 | 
 | ||||||
| server: | server: | ||||||
|     port : 8888 |     port : 8888 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user