webapp.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. #!/usr/bin/env python
  2. '''
  3. searx is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. searx is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with searx. If not, see < http://www.gnu.org/licenses/ >.
  13. (C) 2013- by Adam Tauber, <asciimoo@gmail.com>
  14. '''
  15. if __name__ == '__main__':
  16. from sys import path
  17. from os.path import realpath, dirname
  18. path.append(realpath(dirname(realpath(__file__)) + '/../'))
  19. import json
  20. import cStringIO
  21. import os
  22. import hashlib
  23. import requests
  24. from searx import logger
  25. logger = logger.getChild('webapp')
  26. try:
  27. from pygments import highlight
  28. from pygments.lexers import get_lexer_by_name
  29. from pygments.formatters import HtmlFormatter
  30. except:
  31. logger.critical("cannot import dependency: pygments")
  32. from sys import exit
  33. exit(1)
  34. from datetime import datetime, timedelta
  35. from urllib import urlencode
  36. from urlparse import urlparse
  37. from werkzeug.contrib.fixers import ProxyFix
  38. from flask import (
  39. Flask, request, render_template, url_for, Response, make_response,
  40. redirect, send_from_directory
  41. )
  42. from flask.ext.babel import Babel, gettext, format_date
  43. from searx import settings, searx_dir
  44. from searx.engines import (
  45. categories, engines, get_engines_stats, engine_shortcuts
  46. )
  47. from searx.utils import (
  48. UnicodeWriter, highlight_content, html_to_text, get_themes,
  49. get_static_files, get_result_templates, gen_useragent, dict_subset,
  50. prettify_url, get_blocked_engines
  51. )
  52. from searx.version import VERSION_STRING
  53. from searx.languages import language_codes
  54. from searx.search import Search
  55. from searx.query import Query
  56. from searx.autocomplete import searx_bang, backends as autocomplete_backends
  57. from searx.plugins import plugins
  58. static_path, templates_path, themes =\
  59. get_themes(settings['themes_path']
  60. if settings.get('themes_path')
  61. else searx_dir)
  62. default_theme = settings['server'].get('default_theme', 'default')
  63. static_files = get_static_files(searx_dir)
  64. result_templates = get_result_templates(searx_dir)
  65. app = Flask(
  66. __name__,
  67. static_folder=static_path,
  68. template_folder=templates_path
  69. )
  70. app.jinja_env.trim_blocks = True
  71. app.jinja_env.lstrip_blocks = True
  72. app.secret_key = settings['server']['secret_key']
  73. babel = Babel(app)
  74. rtl_locales = ['ar', 'arc', 'bcc', 'bqi', 'ckb', 'dv', 'fa', 'glk', 'he',
  75. 'ku', 'mzn', 'pnb'', ''ps', 'sd', 'ug', 'ur', 'yi']
  76. global_favicons = []
  77. for indice, theme in enumerate(themes):
  78. global_favicons.append([])
  79. theme_img_path = searx_dir + "/static/themes/" + theme + "/img/icons/"
  80. for (dirpath, dirnames, filenames) in os.walk(theme_img_path):
  81. global_favicons[indice].extend(filenames)
  82. cookie_max_age = 60 * 60 * 24 * 365 * 5 # 5 years
  83. _category_names = (gettext('files'),
  84. gettext('general'),
  85. gettext('music'),
  86. gettext('social media'),
  87. gettext('images'),
  88. gettext('videos'),
  89. gettext('it'),
  90. gettext('news'),
  91. gettext('map'))
  92. outgoing_proxies = settings.get('outgoing_proxies', None)
  93. @babel.localeselector
  94. def get_locale():
  95. locale = request.accept_languages.best_match(settings['locales'].keys())
  96. if settings['server'].get('default_locale'):
  97. locale = settings['server']['default_locale']
  98. if request.cookies.get('locale', '') in settings['locales']:
  99. locale = request.cookies.get('locale', '')
  100. if 'locale' in request.args\
  101. and request.args['locale'] in settings['locales']:
  102. locale = request.args['locale']
  103. if 'locale' in request.form\
  104. and request.form['locale'] in settings['locales']:
  105. locale = request.form['locale']
  106. return locale
  107. # code-highlighter
  108. @app.template_filter('code_highlighter')
  109. def code_highlighter(codelines, language=None):
  110. if not language:
  111. language = 'text'
  112. try:
  113. # find lexer by programing language
  114. lexer = get_lexer_by_name(language, stripall=True)
  115. except:
  116. # if lexer is not found, using default one
  117. logger.debug('highlighter cannot find lexer for {0}'.format(language))
  118. lexer = get_lexer_by_name('text', stripall=True)
  119. html_code = ''
  120. tmp_code = ''
  121. last_line = None
  122. # parse lines
  123. for line, code in codelines:
  124. if not last_line:
  125. line_code_start = line
  126. # new codeblock is detected
  127. if last_line is not None and\
  128. last_line + 1 != line:
  129. # highlight last codepart
  130. formatter = HtmlFormatter(linenos='inline',
  131. linenostart=line_code_start)
  132. html_code = html_code + highlight(tmp_code, lexer, formatter)
  133. # reset conditions for next codepart
  134. tmp_code = ''
  135. line_code_start = line
  136. # add codepart
  137. tmp_code += code + '\n'
  138. # update line
  139. last_line = line
  140. # highlight last codepart
  141. formatter = HtmlFormatter(linenos='inline', linenostart=line_code_start)
  142. html_code = html_code + highlight(tmp_code, lexer, formatter)
  143. return html_code
  144. # Extract domain from url
  145. @app.template_filter('extract_domain')
  146. def extract_domain(url):
  147. return urlparse(url)[1]
  148. def get_base_url():
  149. if settings['server']['base_url']:
  150. hostname = settings['server']['base_url']
  151. else:
  152. scheme = 'http'
  153. if request.is_secure:
  154. scheme = 'https'
  155. hostname = url_for('index', _external=True, _scheme=scheme)
  156. return hostname
  157. def get_current_theme_name(override=None):
  158. """Returns theme name.
  159. Checks in this order:
  160. 1. override
  161. 2. cookies
  162. 3. settings"""
  163. if override and override in themes:
  164. return override
  165. theme_name = request.args.get('theme',
  166. request.cookies.get('theme',
  167. default_theme))
  168. if theme_name not in themes:
  169. theme_name = default_theme
  170. return theme_name
  171. def get_result_template(theme, template_name):
  172. themed_path = theme + '/result_templates/' + template_name
  173. if themed_path in result_templates:
  174. return themed_path
  175. return 'result_templates/' + template_name
  176. def url_for_theme(endpoint, override_theme=None, **values):
  177. if endpoint == 'static' and values.get('filename'):
  178. theme_name = get_current_theme_name(override=override_theme)
  179. filename_with_theme = "themes/{}/{}".format(theme_name, values['filename'])
  180. if filename_with_theme in static_files:
  181. values['filename'] = filename_with_theme
  182. return url_for(endpoint, **values)
  183. def image_proxify(url):
  184. if url.startswith('//'):
  185. url = 'https:' + url
  186. if not settings['server'].get('image_proxy') and not request.cookies.get('image_proxy'):
  187. return url
  188. hash_string = url + settings['server']['secret_key']
  189. h = hashlib.sha256(hash_string.encode('utf-8')).hexdigest()
  190. return '{0}?{1}'.format(url_for('image_proxy'),
  191. urlencode(dict(url=url.encode('utf-8'), h=h)))
  192. def render(template_name, override_theme=None, **kwargs):
  193. blocked_engines = get_blocked_engines(engines, request.cookies)
  194. autocomplete = request.cookies.get('autocomplete')
  195. if autocomplete not in autocomplete_backends:
  196. autocomplete = None
  197. nonblocked_categories = set(category for engine_name in engines
  198. for category in engines[engine_name].categories
  199. if (engine_name, category) not in blocked_engines)
  200. if 'categories' not in kwargs:
  201. kwargs['categories'] = ['general']
  202. kwargs['categories'].extend(x for x in
  203. sorted(categories.keys())
  204. if x != 'general'
  205. and x in nonblocked_categories)
  206. if 'selected_categories' not in kwargs:
  207. kwargs['selected_categories'] = []
  208. for arg in request.args:
  209. if arg.startswith('category_'):
  210. c = arg.split('_', 1)[1]
  211. if c in categories:
  212. kwargs['selected_categories'].append(c)
  213. if not kwargs['selected_categories']:
  214. cookie_categories = request.cookies.get('categories', '').split(',')
  215. for ccateg in cookie_categories:
  216. if ccateg in categories:
  217. kwargs['selected_categories'].append(ccateg)
  218. if not kwargs['selected_categories']:
  219. kwargs['selected_categories'] = ['general']
  220. if 'autocomplete' not in kwargs:
  221. kwargs['autocomplete'] = autocomplete
  222. if get_locale() in rtl_locales and 'rtl' not in kwargs:
  223. kwargs['rtl'] = True
  224. kwargs['searx_version'] = VERSION_STRING
  225. kwargs['method'] = request.cookies.get('method', 'POST')
  226. kwargs['safesearch'] = request.cookies.get('safesearch', '1')
  227. # override url_for function in templates
  228. kwargs['url_for'] = url_for_theme
  229. kwargs['image_proxify'] = image_proxify
  230. kwargs['get_result_template'] = get_result_template
  231. kwargs['theme'] = get_current_theme_name(override=override_theme)
  232. kwargs['template_name'] = template_name
  233. kwargs['cookies'] = request.cookies
  234. kwargs['scripts'] = set()
  235. for plugin in request.user_plugins:
  236. for script in plugin.js_dependencies:
  237. kwargs['scripts'].add(script)
  238. kwargs['styles'] = set()
  239. for plugin in request.user_plugins:
  240. for css in plugin.css_dependencies:
  241. kwargs['styles'].add(css)
  242. return render_template(
  243. '{}/{}'.format(kwargs['theme'], template_name), **kwargs)
  244. @app.before_request
  245. def pre_request():
  246. # merge GET, POST vars
  247. request.form = dict(request.form.items())
  248. for k, v in request.args.items():
  249. if k not in request.form:
  250. request.form[k] = v
  251. request.user_plugins = []
  252. allowed_plugins = request.cookies.get('allowed_plugins', '').split(',')
  253. disabled_plugins = request.cookies.get('disabled_plugins', '').split(',')
  254. for plugin in plugins:
  255. if ((plugin.default_on and plugin.id not in disabled_plugins)
  256. or plugin.id in allowed_plugins):
  257. request.user_plugins.append(plugin)
  258. @app.route('/search', methods=['GET', 'POST'])
  259. @app.route('/', methods=['GET', 'POST'])
  260. def index():
  261. """Render index page.
  262. Supported outputs: html, json, csv, rss.
  263. """
  264. if not request.args and not request.form:
  265. return render(
  266. 'index.html',
  267. )
  268. try:
  269. search = Search(request)
  270. except:
  271. return render(
  272. 'index.html',
  273. )
  274. if plugins.call('pre_search', request, locals()):
  275. search.search(request)
  276. plugins.call('post_search', request, locals())
  277. for result in search.results:
  278. plugins.call('on_result', request, locals())
  279. if not search.paging and engines[result['engine']].paging:
  280. search.paging = True
  281. if search.request_data.get('format', 'html') == 'html':
  282. if 'content' in result:
  283. result['content'] = highlight_content(result['content'],
  284. search.query.encode('utf-8')) # noqa
  285. result['title'] = highlight_content(result['title'],
  286. search.query.encode('utf-8'))
  287. else:
  288. if result.get('content'):
  289. result['content'] = html_to_text(result['content']).strip()
  290. # removing html content and whitespace duplications
  291. result['title'] = ' '.join(html_to_text(result['title']).strip().split())
  292. result['pretty_url'] = prettify_url(result['url'])
  293. # TODO, check if timezone is calculated right
  294. if 'publishedDate' in result:
  295. result['pubdate'] = result['publishedDate'].strftime('%Y-%m-%d %H:%M:%S%z')
  296. if result['publishedDate'].replace(tzinfo=None) >= datetime.now() - timedelta(days=1):
  297. timedifference = datetime.now() - result['publishedDate'].replace(tzinfo=None)
  298. minutes = int((timedifference.seconds / 60) % 60)
  299. hours = int(timedifference.seconds / 60 / 60)
  300. if hours == 0:
  301. result['publishedDate'] = gettext(u'{minutes} minute(s) ago').format(minutes=minutes) # noqa
  302. else:
  303. result['publishedDate'] = gettext(u'{hours} hour(s), {minutes} minute(s) ago').format(hours=hours, minutes=minutes) # noqa
  304. else:
  305. result['publishedDate'] = format_date(result['publishedDate'])
  306. if search.request_data.get('format') == 'json':
  307. return Response(json.dumps({'query': search.query,
  308. 'results': search.results}),
  309. mimetype='application/json')
  310. elif search.request_data.get('format') == 'csv':
  311. csv = UnicodeWriter(cStringIO.StringIO())
  312. keys = ('title', 'url', 'content', 'host', 'engine', 'score')
  313. if search.results:
  314. csv.writerow(keys)
  315. for row in search.results:
  316. row['host'] = row['parsed_url'].netloc
  317. csv.writerow([row.get(key, '') for key in keys])
  318. csv.stream.seek(0)
  319. response = Response(csv.stream.read(), mimetype='application/csv')
  320. cont_disp = 'attachment;Filename=searx_-_{0}.csv'.format(search.query)
  321. response.headers.add('Content-Disposition', cont_disp)
  322. return response
  323. elif search.request_data.get('format') == 'rss':
  324. response_rss = render(
  325. 'opensearch_response_rss.xml',
  326. results=search.results,
  327. q=search.request_data['q'],
  328. number_of_results=len(search.results),
  329. base_url=get_base_url()
  330. )
  331. return Response(response_rss, mimetype='text/xml')
  332. return render(
  333. 'results.html',
  334. results=search.results,
  335. q=search.request_data['q'],
  336. selected_categories=search.categories,
  337. paging=search.paging,
  338. pageno=search.pageno,
  339. base_url=get_base_url(),
  340. suggestions=search.suggestions,
  341. answers=search.answers,
  342. infoboxes=search.infoboxes,
  343. theme=get_current_theme_name(),
  344. favicons=global_favicons[themes.index(get_current_theme_name())]
  345. )
  346. @app.route('/about', methods=['GET'])
  347. def about():
  348. """Render about page"""
  349. return render(
  350. 'about.html',
  351. )
  352. @app.route('/autocompleter', methods=['GET', 'POST'])
  353. def autocompleter():
  354. """Return autocompleter results"""
  355. request_data = {}
  356. # select request method
  357. if request.method == 'POST':
  358. request_data = request.form
  359. else:
  360. request_data = request.args
  361. # set blocked engines
  362. blocked_engines = get_blocked_engines(engines, request.cookies)
  363. # parse query
  364. query = Query(request_data.get('q', '').encode('utf-8'), blocked_engines)
  365. query.parse_query()
  366. # check if search query is set
  367. if not query.getSearchQuery():
  368. return '', 400
  369. # run autocompleter
  370. completer = autocomplete_backends.get(request.cookies.get('autocomplete'))
  371. # parse searx specific autocompleter results like !bang
  372. raw_results = searx_bang(query)
  373. # normal autocompletion results only appear if max 3 inner results returned
  374. if len(raw_results) <= 3 and completer:
  375. # run autocompletion
  376. raw_results.extend(completer(query.getSearchQuery()))
  377. # parse results (write :language and !engine back to result string)
  378. results = []
  379. for result in raw_results:
  380. query.changeSearchQuery(result)
  381. # add parsed result
  382. results.append(query.getFullQuery())
  383. # return autocompleter results
  384. if request_data.get('format') == 'x-suggestions':
  385. return Response(json.dumps([query.query, results]),
  386. mimetype='application/json')
  387. return Response(json.dumps(results),
  388. mimetype='application/json')
  389. @app.route('/preferences', methods=['GET', 'POST'])
  390. def preferences():
  391. """Render preferences page.
  392. Settings that are going to be saved as cookies."""
  393. lang = None
  394. image_proxy = request.cookies.get('image_proxy', settings['server'].get('image_proxy'))
  395. if request.cookies.get('language')\
  396. and request.cookies['language'] in (x[0] for x in language_codes):
  397. lang = request.cookies['language']
  398. blocked_engines = []
  399. resp = make_response(redirect(url_for('index')))
  400. if request.method == 'GET':
  401. blocked_engines = get_blocked_engines(engines, request.cookies)
  402. else: # on save
  403. selected_categories = []
  404. post_disabled_plugins = []
  405. locale = None
  406. autocomplete = ''
  407. method = 'POST'
  408. safesearch = '1'
  409. for pd_name, pd in request.form.items():
  410. if pd_name.startswith('category_'):
  411. category = pd_name[9:]
  412. if category not in categories:
  413. continue
  414. selected_categories.append(category)
  415. elif pd_name == 'locale' and pd in settings['locales']:
  416. locale = pd
  417. elif pd_name == 'image_proxy':
  418. image_proxy = pd
  419. elif pd_name == 'autocomplete':
  420. autocomplete = pd
  421. elif pd_name == 'language' and (pd == 'all' or
  422. pd in (x[0] for
  423. x in language_codes)):
  424. lang = pd
  425. elif pd_name == 'method':
  426. method = pd
  427. elif pd_name == 'safesearch':
  428. safesearch = pd
  429. elif pd_name.startswith('engine_'):
  430. if pd_name.find('__') > -1:
  431. # TODO fix underscore vs space
  432. engine_name, category = [x.replace('_', ' ') for x in
  433. pd_name.replace('engine_', '', 1).split('__', 1)]
  434. if engine_name in engines and category in engines[engine_name].categories:
  435. blocked_engines.append((engine_name, category))
  436. elif pd_name == 'theme':
  437. theme = pd if pd in themes else default_theme
  438. elif pd_name.startswith('plugin_'):
  439. plugin_id = pd_name.replace('plugin_', '', 1)
  440. if not any(plugin.id == plugin_id for plugin in plugins):
  441. continue
  442. post_disabled_plugins.append(plugin_id)
  443. else:
  444. resp.set_cookie(pd_name, pd, max_age=cookie_max_age)
  445. disabled_plugins = []
  446. allowed_plugins = []
  447. for plugin in plugins:
  448. if plugin.default_on:
  449. if plugin.id in post_disabled_plugins:
  450. disabled_plugins.append(plugin.id)
  451. elif plugin.id not in post_disabled_plugins:
  452. allowed_plugins.append(plugin.id)
  453. resp.set_cookie('disabled_plugins', ','.join(disabled_plugins), max_age=cookie_max_age)
  454. resp.set_cookie('allowed_plugins', ','.join(allowed_plugins), max_age=cookie_max_age)
  455. resp.set_cookie(
  456. 'blocked_engines', ','.join('__'.join(e) for e in blocked_engines),
  457. max_age=cookie_max_age
  458. )
  459. if locale:
  460. resp.set_cookie(
  461. 'locale', locale,
  462. max_age=cookie_max_age
  463. )
  464. if lang:
  465. resp.set_cookie(
  466. 'language', lang,
  467. max_age=cookie_max_age
  468. )
  469. if selected_categories:
  470. # cookie max age: 4 weeks
  471. resp.set_cookie(
  472. 'categories', ','.join(selected_categories),
  473. max_age=cookie_max_age
  474. )
  475. resp.set_cookie(
  476. 'autocomplete', autocomplete,
  477. max_age=cookie_max_age
  478. )
  479. resp.set_cookie('method', method, max_age=cookie_max_age)
  480. resp.set_cookie('safesearch', safesearch, max_age=cookie_max_age)
  481. resp.set_cookie('image_proxy', image_proxy, max_age=cookie_max_age)
  482. resp.set_cookie('theme', theme, max_age=cookie_max_age)
  483. return resp
  484. return render('preferences.html',
  485. locales=settings['locales'],
  486. current_locale=get_locale(),
  487. current_language=lang or 'all',
  488. image_proxy=image_proxy,
  489. language_codes=language_codes,
  490. engines_by_category=categories,
  491. blocked_engines=blocked_engines,
  492. autocomplete_backends=autocomplete_backends,
  493. shortcuts={y: x for x, y in engine_shortcuts.items()},
  494. themes=themes,
  495. plugins=plugins,
  496. allowed_plugins=[plugin.id for plugin in request.user_plugins],
  497. theme=get_current_theme_name())
  498. @app.route('/image_proxy', methods=['GET'])
  499. def image_proxy():
  500. url = request.args.get('url').encode('utf-8')
  501. if not url:
  502. return '', 400
  503. h = hashlib.sha256(url + settings['server']['secret_key'].encode('utf-8')).hexdigest()
  504. if h != request.args.get('h'):
  505. return '', 400
  506. headers = dict_subset(request.headers, {'If-Modified-Since', 'If-None-Match'})
  507. headers['User-Agent'] = gen_useragent()
  508. resp = requests.get(url,
  509. stream=True,
  510. timeout=settings['server'].get('request_timeout', 2),
  511. headers=headers,
  512. proxies=outgoing_proxies)
  513. if resp.status_code == 304:
  514. return '', resp.status_code
  515. if resp.status_code != 200:
  516. logger.debug('image-proxy: wrong response code: {0}'.format(resp.status_code))
  517. if resp.status_code >= 400:
  518. return '', resp.status_code
  519. return '', 400
  520. if not resp.headers.get('content-type', '').startswith('image/'):
  521. logger.debug('image-proxy: wrong content-type: {0}'.format(resp.get('content-type')))
  522. return '', 400
  523. img = ''
  524. chunk_counter = 0
  525. for chunk in resp.iter_content(1024 * 1024):
  526. chunk_counter += 1
  527. if chunk_counter > 5:
  528. return '', 502 # Bad gateway - file is too big (>5M)
  529. img += chunk
  530. headers = dict_subset(resp.headers, {'Content-Length', 'Length', 'Date', 'Last-Modified', 'Expires', 'Etag'})
  531. return Response(img, mimetype=resp.headers['content-type'], headers=headers)
  532. @app.route('/stats', methods=['GET'])
  533. def stats():
  534. """Render engine statistics page."""
  535. stats = get_engines_stats()
  536. return render(
  537. 'stats.html',
  538. stats=stats,
  539. )
  540. @app.route('/robots.txt', methods=['GET'])
  541. def robots():
  542. return Response("""User-agent: *
  543. Allow: /
  544. Allow: /about
  545. Disallow: /stats
  546. Disallow: /preferences
  547. """, mimetype='text/plain')
  548. @app.route('/opensearch.xml', methods=['GET'])
  549. def opensearch():
  550. method = 'post'
  551. if request.cookies.get('method', 'POST') == 'GET':
  552. method = 'get'
  553. # chrome/chromium only supports HTTP GET....
  554. if request.headers.get('User-Agent', '').lower().find('webkit') >= 0:
  555. method = 'get'
  556. ret = render('opensearch.xml',
  557. opensearch_method=method,
  558. host=get_base_url())
  559. resp = Response(response=ret,
  560. status=200,
  561. mimetype="text/xml")
  562. return resp
  563. @app.route('/favicon.ico')
  564. def favicon():
  565. return send_from_directory(os.path.join(app.root_path,
  566. 'static/themes',
  567. get_current_theme_name(),
  568. 'img'),
  569. 'favicon.png',
  570. mimetype='image/vnd.microsoft.icon')
  571. @app.route('/clear_cookies')
  572. def clear_cookies():
  573. resp = make_response(redirect(url_for('index')))
  574. for cookie_name in request.cookies:
  575. resp.delete_cookie(cookie_name)
  576. return resp
  577. def run():
  578. app.run(
  579. debug=settings['server']['debug'],
  580. use_debugger=settings['server']['debug'],
  581. port=settings['server']['port']
  582. )
  583. application = app
  584. app.wsgi_app = ProxyFix(application.wsgi_app)
  585. if __name__ == "__main__":
  586. run()