We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
/preferences: use simple layout for the oscar theme
Suggestion: change how the options are displayed in the /preferences page in the oscar theme :
* General
  * Default categories
  * Search languages
  * Autocomplete
  * SafeSearch
  * Open Access DOI rewrite (plugin)
  * Open Access DOI resolver
* Engines
* User Interface
  * Interface language
  * Themes
  * Results on new tabs
  * Infinite scroll (plugin)
  * Search on category select (plugin)
  * Hotkeys (plugins)
* Privacy
  * HTTP method
  * Image proxy
  * Tracker URL remover (plugin)
* Cookies
* Query syntax (new tab, a mix between documentation and auto documentation from the current settings)
  * Documentation about the query syntax (bang, external bangs, ...)
  * Answers (list of configured answers)
  * Special engines: currency, translations (list the engines that ).
  * External bangs

/preferences: use simple layout for the oscar theme
Suggestion: change how the options are displayed in the /preferences page in the oscar theme :
* General
  * Default categories
  * Search languages
  * Autocomplete
  * SafeSearch
  * Open Access DOI rewrite (plugin)
  * Open Access DOI resolver
* Engines
* User Interface
  * Interface language
  * Themes
  * Results on new tabs
  * Infinite scroll (plugin)
  * Search on category select (plugin)
  * Hotkeys (plugins)
* Privacy
  * HTTP method
  * Image proxy
  * Tracker URL remover (plugin)
* Cookies
* Query syntax (new tab, a mix between documentation and auto documentation from the current settings)
  * Documentation about the query syntax (bang, external bangs, ...)
  * Answers (list of configured answers)
  * Special engines: currency, translations (list the engines that ).
  * External bangs


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `searx/plugins/hash_plugin.py`
Content:
```
1 '''
2 searx is free software: you can redistribute it and/or modify
3 it under the terms of the GNU Affero General Public License as published by
4 the Free Software Foundation, either version 3 of the License, or
5 (at your option) any later version.
6 
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 
12 You should have received a copy of the GNU Affero General Public License
13 along with searx. If not, see < http://www.gnu.org/licenses/ >.
14 
15 (C) 2015 by Adam Tauber, <asciimoo@gmail.com>
16 (C) 2018, 2020 by Vaclav Zouzalik
17 '''
18 
19 from flask_babel import gettext
20 import hashlib
21 import re
22 
23 name = "Hash plugin"
24 description = gettext("Converts strings to different hash digests.")
25 default_on = True
26 
27 parser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)
28 
29 
30 def post_search(request, search):
31     # process only on first page
32     if search.search_query.pageno > 1:
33         return True
34     m = parser_re.match(search.search_query.query)
35     if not m:
36         # wrong query
37         return True
38 
39     function, string = m.groups()
40     if string.strip().__len__() == 0:
41         # end if the string is empty
42         return True
43 
44     # select hash function
45     f = hashlib.new(function.lower())
46 
47     # make digest from the given string
48     f.update(string.encode('utf-8').strip())
49     answer = function + " " + gettext('hash digest') + ": " + f.hexdigest()
50 
51     # print result
52     search.result_container.answers.clear()
53     search.result_container.answers['hash'] = {'answer': answer}
54     return True
55
```

Path: `searx/plugins/__init__.py`
Content:
```
1 '''
2 searx is free software: you can redistribute it and/or modify
3 it under the terms of the GNU Affero General Public License as published by
4 the Free Software Foundation, either version 3 of the License, or
5 (at your option) any later version.
6 
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 
12 You should have received a copy of the GNU Affero General Public License
13 along with searx. If not, see < http://www.gnu.org/licenses/ >.
14 
15 (C) 2015 by Adam Tauber, <asciimoo@gmail.com>
16 '''
17 
18 from hashlib import sha256
19 from importlib import import_module
20 from os import listdir, makedirs, remove, stat, utime
21 from os.path import abspath, basename, dirname, exists, join
22 from shutil import copyfile
23 
24 from searx import logger, settings
25 
26 
27 logger = logger.getChild('plugins')
28 
29 from searx.plugins import (oa_doi_rewrite,
30                            ahmia_filter,
31                            hash_plugin,
32                            infinite_scroll,
33                            self_info,
34                            search_on_category_select,
35                            tracker_url_remover,
36                            vim_hotkeys)
37 
38 required_attrs = (('name', str),
39                   ('description', str),
40                   ('default_on', bool))
41 
42 optional_attrs = (('js_dependencies', tuple),
43                   ('css_dependencies', tuple))
44 
45 
46 class Plugin():
47     default_on = False
48     name = 'Default plugin'
49     description = 'Default plugin description'
50 
51 
52 class PluginStore():
53 
54     def __init__(self):
55         self.plugins = []
56 
57     def __iter__(self):
58         for plugin in self.plugins:
59             yield plugin
60 
61     def register(self, *plugins, external=False):
62         if external:
63             plugins = load_external_plugins(plugins)
64         for plugin in plugins:
65             for plugin_attr, plugin_attr_type in required_attrs:
66                 if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):
67                     logger.critical('missing attribute "{0}", cannot load plugin: {1}'.format(plugin_attr, plugin))
68                     exit(3)
69             for plugin_attr, plugin_attr_type in optional_attrs:
70                 if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):
71                     setattr(plugin, plugin_attr, plugin_attr_type())
72             plugin.id = plugin.name.replace(' ', '_')
73             self.plugins.append(plugin)
74 
75     def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs):
76         ret = True
77         for plugin in ordered_plugin_list:
78             if hasattr(plugin, plugin_type):
79                 ret = getattr(plugin, plugin_type)(request, *args, **kwargs)
80                 if not ret:
81                     break
82 
83         return ret
84 
85 
86 def load_external_plugins(plugin_names):
87     plugins = []
88     for name in plugin_names:
89         logger.debug('loading plugin: {0}'.format(name))
90         try:
91             pkg = import_module(name)
92         except Exception as e:
93             logger.critical('failed to load plugin module {0}: {1}'.format(name, e))
94             exit(3)
95 
96         pkg.__base_path = dirname(abspath(pkg.__file__))
97 
98         prepare_package_resources(pkg, name)
99 
100         plugins.append(pkg)
101         logger.debug('plugin "{0}" loaded'.format(name))
102     return plugins
103 
104 
105 def sync_resource(base_path, resource_path, name, target_dir, plugin_dir):
106     dep_path = join(base_path, resource_path)
107     file_name = basename(dep_path)
108     resource_path = join(target_dir, file_name)
109     if not exists(resource_path) or sha_sum(dep_path) != sha_sum(resource_path):
110         try:
111             copyfile(dep_path, resource_path)
112             # copy atime_ns and mtime_ns, so the weak ETags (generated by
113             # the HTTP server) do not change
114             dep_stat = stat(dep_path)
115             utime(resource_path, ns=(dep_stat.st_atime_ns, dep_stat.st_mtime_ns))
116         except:
117             logger.critical('failed to copy plugin resource {0} for plugin {1}'.format(file_name, name))
118             exit(3)
119 
120     # returning with the web path of the resource
121     return join('plugins/external_plugins', plugin_dir, file_name)
122 
123 
124 def prepare_package_resources(pkg, name):
125     plugin_dir = 'plugin_' + name
126     target_dir = join(settings['ui']['static_path'], 'plugins/external_plugins', plugin_dir)
127     try:
128         makedirs(target_dir, exist_ok=True)
129     except:
130         logger.critical('failed to create resource directory {0} for plugin {1}'.format(target_dir, name))
131         exit(3)
132 
133     resources = []
134 
135     if hasattr(pkg, 'js_dependencies'):
136         resources.extend(map(basename, pkg.js_dependencies))
137         pkg.js_dependencies = tuple([
138             sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)
139             for x in pkg.js_dependencies
140         ])
141     if hasattr(pkg, 'css_dependencies'):
142         resources.extend(map(basename, pkg.css_dependencies))
143         pkg.css_dependencies = tuple([
144             sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)
145             for x in pkg.css_dependencies
146         ])
147 
148     for f in listdir(target_dir):
149         if basename(f) not in resources:
150             resource_path = join(target_dir, basename(f))
151             try:
152                 remove(resource_path)
153             except:
154                 logger.critical('failed to remove unused resource file {0} for plugin {1}'.format(resource_path, name))
155                 exit(3)
156 
157 
158 def sha_sum(filename):
159     with open(filename, "rb") as f:
160         file_content_bytes = f.read()
161         return sha256(file_content_bytes).hexdigest()
162 
163 
164 plugins = PluginStore()
165 plugins.register(oa_doi_rewrite)
166 plugins.register(hash_plugin)
167 plugins.register(infinite_scroll)
168 plugins.register(self_info)
169 plugins.register(search_on_category_select)
170 plugins.register(tracker_url_remover)
171 plugins.register(vim_hotkeys)
172 # load external plugins
173 if settings['plugins']:
174     plugins.register(*settings['plugins'], external=True)
175 
176 if settings['enabled_plugins']:
177     for plugin in plugins:
178         if plugin.name in settings['enabled_plugins']:
179             plugin.default_on = True
180         else:
181             plugin.default_on = False
182 
183 # load tor specific plugins
184 if settings['outgoing']['using_tor_proxy']:
185     plugins.register(ahmia_filter)
186
```

Path: `searx/plugins/self_info.py`
Content:
```
1 '''
2 searx is free software: you can redistribute it and/or modify
3 it under the terms of the GNU Affero General Public License as published by
4 the Free Software Foundation, either version 3 of the License, or
5 (at your option) any later version.
6 
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 
12 You should have received a copy of the GNU Affero General Public License
13 along with searx. If not, see < http://www.gnu.org/licenses/ >.
14 
15 (C) 2015 by Adam Tauber, <asciimoo@gmail.com>
16 '''
17 from flask_babel import gettext
18 import re
19 name = gettext('Self Informations')
20 description = gettext('Displays your IP if the query is "ip" and your user agent if the query contains "user agent".')
21 default_on = True
22 
23 
24 # Self User Agent regex
25 p = re.compile('.*user[ -]agent.*', re.IGNORECASE)
26 
27 
28 # attach callback to the post search hook
29 #  request: flask request object
30 #  ctx: the whole local context of the pre search hook
31 def post_search(request, search):
32     if search.search_query.pageno > 1:
33         return True
34     if search.search_query.query == 'ip':
35         x_forwarded_for = request.headers.getlist("X-Forwarded-For")
36         if x_forwarded_for:
37             ip = x_forwarded_for[0]
38         else:
39             ip = request.remote_addr
40         search.result_container.answers['ip'] = {'answer': ip}
41     elif p.match(search.search_query.query):
42         ua = request.user_agent
43         search.result_container.answers['user-agent'] = {'answer': ua}
44     return True
45
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/searx/plugins/__init__.py b/searx/plugins/__init__.py
--- a/searx/plugins/__init__.py
+++ b/searx/plugins/__init__.py
@@ -70,6 +70,13 @@
                 if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):
                     setattr(plugin, plugin_attr, plugin_attr_type())
             plugin.id = plugin.name.replace(' ', '_')
+            if not hasattr(plugin, 'preference_section'):
+                plugin.preference_section = 'general'
+            if plugin.preference_section == 'query':
+                for plugin_attr in ('query_keywords', 'query_examples'):
+                    if not hasattr(plugin, plugin_attr):
+                        logger.critical('missing attribute "{0}", cannot load plugin: {1}'.format(plugin_attr, plugin))
+                        exit(3)
             self.plugins.append(plugin)
 
     def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs):
diff --git a/searx/plugins/hash_plugin.py b/searx/plugins/hash_plugin.py
--- a/searx/plugins/hash_plugin.py
+++ b/searx/plugins/hash_plugin.py
@@ -23,6 +23,9 @@
 name = "Hash plugin"
 description = gettext("Converts strings to different hash digests.")
 default_on = True
+preference_section = 'query'
+query_keywords = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
+query_examples = 'sha512 The quick brown fox jumps over the lazy dog'
 
 parser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)
 
diff --git a/searx/plugins/self_info.py b/searx/plugins/self_info.py
--- a/searx/plugins/self_info.py
+++ b/searx/plugins/self_info.py
@@ -19,7 +19,9 @@
 name = gettext('Self Informations')
 description = gettext('Displays your IP if the query is "ip" and your user agent if the query contains "user agent".')
 default_on = True
-
+preference_section = 'query'
+query_keywords = ['user-agent']
+query_examples = ''
 
 # Self User Agent regex
 p = re.compile('.*user[ -]agent.*', re.IGNORECASE)

{"golden_diff": "diff --git a/searx/plugins/__init__.py b/searx/plugins/__init__.py\n--- a/searx/plugins/__init__.py\n+++ b/searx/plugins/__init__.py\n@@ -70,6 +70,13 @@\n                 if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):\n                     setattr(plugin, plugin_attr, plugin_attr_type())\n             plugin.id = plugin.name.replace(' ', '_')\n+            if not hasattr(plugin, 'preference_section'):\n+                plugin.preference_section = 'general'\n+            if plugin.preference_section == 'query':\n+                for plugin_attr in ('query_keywords', 'query_examples'):\n+                    if not hasattr(plugin, plugin_attr):\n+                        logger.critical('missing attribute \"{0}\", cannot load plugin: {1}'.format(plugin_attr, plugin))\n+                        exit(3)\n             self.plugins.append(plugin)\n \n     def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs):\ndiff --git a/searx/plugins/hash_plugin.py b/searx/plugins/hash_plugin.py\n--- a/searx/plugins/hash_plugin.py\n+++ b/searx/plugins/hash_plugin.py\n@@ -23,6 +23,9 @@\n name = \"Hash plugin\"\n description = gettext(\"Converts strings to different hash digests.\")\n default_on = True\n+preference_section = 'query'\n+query_keywords = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']\n+query_examples = 'sha512 The quick brown fox jumps over the lazy dog'\n \n parser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)\n \ndiff --git a/searx/plugins/self_info.py b/searx/plugins/self_info.py\n--- a/searx/plugins/self_info.py\n+++ b/searx/plugins/self_info.py\n@@ -19,7 +19,9 @@\n name = gettext('Self Informations')\n description = gettext('Displays your IP if the query is \"ip\" and your user agent if the query contains \"user agent\".')\n default_on = True\n-\n+preference_section = 'query'\n+query_keywords = ['user-agent']\n+query_examples = ''\n \n # Self User Agent regex\n p = re.compile('.*user[ -]agent.*', re.IGNORECASE)\n", "issue": "/preferences: use simple layout for the oscar theme\nSuggestion: change how the options are displayed in the /preferences page in the oscar theme :\r\n* General\r\n  * Default categories\r\n  * Search languages\r\n  * Autocomplete\r\n  * SafeSearch\r\n  * Open Access DOI rewrite (plugin)\r\n  * Open Access DOI resolver\r\n* Engines\r\n* User Interface\r\n  * Interface language\r\n  * Themes\r\n  * Results on new tabs\r\n  * Infinite scroll (plugin)\r\n  * Search on category select (plugin)\r\n  * Hotkeys (plugins)\r\n* Privacy\r\n  * HTTP method\r\n  * Image proxy\r\n  * Tracker URL remover (plugin)\r\n* Cookies\r\n* Query syntax (new tab, a mix between documentation and auto documentation from the current settings)\r\n  * Documentation about the query syntax (bang, external bangs, ...)\r\n  * Answers (list of configured answers)\r\n  * Special engines: currency, translations (list the engines that ).\r\n  * External bangs\r\n\n/preferences: use simple layout for the oscar theme\nSuggestion: change how the options are displayed in the /preferences page in the oscar theme :\r\n* General\r\n  * Default categories\r\n  * Search languages\r\n  * Autocomplete\r\n  * SafeSearch\r\n  * Open Access DOI rewrite (plugin)\r\n  * Open Access DOI resolver\r\n* Engines\r\n* User Interface\r\n  * Interface language\r\n  * Themes\r\n  * Results on new tabs\r\n  * Infinite scroll (plugin)\r\n  * Search on category select (plugin)\r\n  * Hotkeys (plugins)\r\n* Privacy\r\n  * HTTP method\r\n  * Image proxy\r\n  * Tracker URL remover (plugin)\r\n* Cookies\r\n* Query syntax (new tab, a mix between documentation and auto documentation from the current settings)\r\n  * Documentation about the query syntax (bang, external bangs, ...)\r\n  * Answers (list of configured answers)\r\n  * Special engines: currency, translations (list the engines that ).\r\n  * External bangs\r\n\n", "before_files": [{"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n(C) 2018, 2020 by Vaclav Zouzalik\n'''\n\nfrom flask_babel import gettext\nimport hashlib\nimport re\n\nname = \"Hash plugin\"\ndescription = gettext(\"Converts strings to different hash digests.\")\ndefault_on = True\n\nparser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)\n\n\ndef post_search(request, search):\n    # process only on first page\n    if search.search_query.pageno > 1:\n        return True\n    m = parser_re.match(search.search_query.query)\n    if not m:\n        # wrong query\n        return True\n\n    function, string = m.groups()\n    if string.strip().__len__() == 0:\n        # end if the string is empty\n        return True\n\n    # select hash function\n    f = hashlib.new(function.lower())\n\n    # make digest from the given string\n    f.update(string.encode('utf-8').strip())\n    answer = function + \" \" + gettext('hash digest') + \": \" + f.hexdigest()\n\n    # print result\n    search.result_container.answers.clear()\n    search.result_container.answers['hash'] = {'answer': answer}\n    return True\n", "path": "searx/plugins/hash_plugin.py"}, {"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n'''\n\nfrom hashlib import sha256\nfrom importlib import import_module\nfrom os import listdir, makedirs, remove, stat, utime\nfrom os.path import abspath, basename, dirname, exists, join\nfrom shutil import copyfile\n\nfrom searx import logger, settings\n\n\nlogger = logger.getChild('plugins')\n\nfrom searx.plugins import (oa_doi_rewrite,\n                           ahmia_filter,\n                           hash_plugin,\n                           infinite_scroll,\n                           self_info,\n                           search_on_category_select,\n                           tracker_url_remover,\n                           vim_hotkeys)\n\nrequired_attrs = (('name', str),\n                  ('description', str),\n                  ('default_on', bool))\n\noptional_attrs = (('js_dependencies', tuple),\n                  ('css_dependencies', tuple))\n\n\nclass Plugin():\n    default_on = False\n    name = 'Default plugin'\n    description = 'Default plugin description'\n\n\nclass PluginStore():\n\n    def __init__(self):\n        self.plugins = []\n\n    def __iter__(self):\n        for plugin in self.plugins:\n            yield plugin\n\n    def register(self, *plugins, external=False):\n        if external:\n            plugins = load_external_plugins(plugins)\n        for plugin in plugins:\n            for plugin_attr, plugin_attr_type in required_attrs:\n                if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):\n                    logger.critical('missing attribute \"{0}\", cannot load plugin: {1}'.format(plugin_attr, plugin))\n                    exit(3)\n            for plugin_attr, plugin_attr_type in optional_attrs:\n                if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):\n                    setattr(plugin, plugin_attr, plugin_attr_type())\n            plugin.id = plugin.name.replace(' ', '_')\n            self.plugins.append(plugin)\n\n    def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs):\n        ret = True\n        for plugin in ordered_plugin_list:\n            if hasattr(plugin, plugin_type):\n                ret = getattr(plugin, plugin_type)(request, *args, **kwargs)\n                if not ret:\n                    break\n\n        return ret\n\n\ndef load_external_plugins(plugin_names):\n    plugins = []\n    for name in plugin_names:\n        logger.debug('loading plugin: {0}'.format(name))\n        try:\n            pkg = import_module(name)\n        except Exception as e:\n            logger.critical('failed to load plugin module {0}: {1}'.format(name, e))\n            exit(3)\n\n        pkg.__base_path = dirname(abspath(pkg.__file__))\n\n        prepare_package_resources(pkg, name)\n\n        plugins.append(pkg)\n        logger.debug('plugin \"{0}\" loaded'.format(name))\n    return plugins\n\n\ndef sync_resource(base_path, resource_path, name, target_dir, plugin_dir):\n    dep_path = join(base_path, resource_path)\n    file_name = basename(dep_path)\n    resource_path = join(target_dir, file_name)\n    if not exists(resource_path) or sha_sum(dep_path) != sha_sum(resource_path):\n        try:\n            copyfile(dep_path, resource_path)\n            # copy atime_ns and mtime_ns, so the weak ETags (generated by\n            # the HTTP server) do not change\n            dep_stat = stat(dep_path)\n            utime(resource_path, ns=(dep_stat.st_atime_ns, dep_stat.st_mtime_ns))\n        except:\n            logger.critical('failed to copy plugin resource {0} for plugin {1}'.format(file_name, name))\n            exit(3)\n\n    # returning with the web path of the resource\n    return join('plugins/external_plugins', plugin_dir, file_name)\n\n\ndef prepare_package_resources(pkg, name):\n    plugin_dir = 'plugin_' + name\n    target_dir = join(settings['ui']['static_path'], 'plugins/external_plugins', plugin_dir)\n    try:\n        makedirs(target_dir, exist_ok=True)\n    except:\n        logger.critical('failed to create resource directory {0} for plugin {1}'.format(target_dir, name))\n        exit(3)\n\n    resources = []\n\n    if hasattr(pkg, 'js_dependencies'):\n        resources.extend(map(basename, pkg.js_dependencies))\n        pkg.js_dependencies = tuple([\n            sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)\n            for x in pkg.js_dependencies\n        ])\n    if hasattr(pkg, 'css_dependencies'):\n        resources.extend(map(basename, pkg.css_dependencies))\n        pkg.css_dependencies = tuple([\n            sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)\n            for x in pkg.css_dependencies\n        ])\n\n    for f in listdir(target_dir):\n        if basename(f) not in resources:\n            resource_path = join(target_dir, basename(f))\n            try:\n                remove(resource_path)\n            except:\n                logger.critical('failed to remove unused resource file {0} for plugin {1}'.format(resource_path, name))\n                exit(3)\n\n\ndef sha_sum(filename):\n    with open(filename, \"rb\") as f:\n        file_content_bytes = f.read()\n        return sha256(file_content_bytes).hexdigest()\n\n\nplugins = PluginStore()\nplugins.register(oa_doi_rewrite)\nplugins.register(hash_plugin)\nplugins.register(infinite_scroll)\nplugins.register(self_info)\nplugins.register(search_on_category_select)\nplugins.register(tracker_url_remover)\nplugins.register(vim_hotkeys)\n# load external plugins\nif settings['plugins']:\n    plugins.register(*settings['plugins'], external=True)\n\nif settings['enabled_plugins']:\n    for plugin in plugins:\n        if plugin.name in settings['enabled_plugins']:\n            plugin.default_on = True\n        else:\n            plugin.default_on = False\n\n# load tor specific plugins\nif settings['outgoing']['using_tor_proxy']:\n    plugins.register(ahmia_filter)\n", "path": "searx/plugins/__init__.py"}, {"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n'''\nfrom flask_babel import gettext\nimport re\nname = gettext('Self Informations')\ndescription = gettext('Displays your IP if the query is \"ip\" and your user agent if the query contains \"user agent\".')\ndefault_on = True\n\n\n# Self User Agent regex\np = re.compile('.*user[ -]agent.*', re.IGNORECASE)\n\n\n# attach callback to the post search hook\n#  request: flask request object\n#  ctx: the whole local context of the pre search hook\ndef post_search(request, search):\n    if search.search_query.pageno > 1:\n        return True\n    if search.search_query.query == 'ip':\n        x_forwarded_for = request.headers.getlist(\"X-Forwarded-For\")\n        if x_forwarded_for:\n            ip = x_forwarded_for[0]\n        else:\n            ip = request.remote_addr\n        search.result_container.answers['ip'] = {'answer': ip}\n    elif p.match(search.search_query.query):\n        ua = request.user_agent\n        search.result_container.answers['user-agent'] = {'answer': ua}\n    return True\n", "path": "searx/plugins/self_info.py"}], "after_files": [{"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n(C) 2018, 2020 by Vaclav Zouzalik\n'''\n\nfrom flask_babel import gettext\nimport hashlib\nimport re\n\nname = \"Hash plugin\"\ndescription = gettext(\"Converts strings to different hash digests.\")\ndefault_on = True\npreference_section = 'query'\nquery_keywords = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']\nquery_examples = 'sha512 The quick brown fox jumps over the lazy dog'\n\nparser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)\n\n\ndef post_search(request, search):\n    # process only on first page\n    if search.search_query.pageno > 1:\n        return True\n    m = parser_re.match(search.search_query.query)\n    if not m:\n        # wrong query\n        return True\n\n    function, string = m.groups()\n    if string.strip().__len__() == 0:\n        # end if the string is empty\n        return True\n\n    # select hash function\n    f = hashlib.new(function.lower())\n\n    # make digest from the given string\n    f.update(string.encode('utf-8').strip())\n    answer = function + \" \" + gettext('hash digest') + \": \" + f.hexdigest()\n\n    # print result\n    search.result_container.answers.clear()\n    search.result_container.answers['hash'] = {'answer': answer}\n    return True\n", "path": "searx/plugins/hash_plugin.py"}, {"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n'''\n\nfrom hashlib import sha256\nfrom importlib import import_module\nfrom os import listdir, makedirs, remove, stat, utime\nfrom os.path import abspath, basename, dirname, exists, join\nfrom shutil import copyfile\n\nfrom searx import logger, settings\n\n\nlogger = logger.getChild('plugins')\n\nfrom searx.plugins import (oa_doi_rewrite,\n                           ahmia_filter,\n                           hash_plugin,\n                           infinite_scroll,\n                           self_info,\n                           search_on_category_select,\n                           tracker_url_remover,\n                           vim_hotkeys)\n\nrequired_attrs = (('name', str),\n                  ('description', str),\n                  ('default_on', bool))\n\noptional_attrs = (('js_dependencies', tuple),\n                  ('css_dependencies', tuple))\n\n\nclass Plugin():\n    default_on = False\n    name = 'Default plugin'\n    description = 'Default plugin description'\n\n\nclass PluginStore():\n\n    def __init__(self):\n        self.plugins = []\n\n    def __iter__(self):\n        for plugin in self.plugins:\n            yield plugin\n\n    def register(self, *plugins, external=False):\n        if external:\n            plugins = load_external_plugins(plugins)\n        for plugin in plugins:\n            for plugin_attr, plugin_attr_type in required_attrs:\n                if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):\n                    logger.critical('missing attribute \"{0}\", cannot load plugin: {1}'.format(plugin_attr, plugin))\n                    exit(3)\n            for plugin_attr, plugin_attr_type in optional_attrs:\n                if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):\n                    setattr(plugin, plugin_attr, plugin_attr_type())\n            plugin.id = plugin.name.replace(' ', '_')\n            if not hasattr(plugin, 'preference_section'):\n                plugin.preference_section = 'general'\n            if plugin.preference_section == 'query':\n                for plugin_attr in ('query_keywords', 'query_examples'):\n                    if not hasattr(plugin, plugin_attr):\n                        logger.critical('missing attribute \"{0}\", cannot load plugin: {1}'.format(plugin_attr, plugin))\n                        exit(3)\n            self.plugins.append(plugin)\n\n    def call(self, ordered_plugin_list, plugin_type, request, *args, **kwargs):\n        ret = True\n        for plugin in ordered_plugin_list:\n            if hasattr(plugin, plugin_type):\n                ret = getattr(plugin, plugin_type)(request, *args, **kwargs)\n                if not ret:\n                    break\n\n        return ret\n\n\ndef load_external_plugins(plugin_names):\n    plugins = []\n    for name in plugin_names:\n        logger.debug('loading plugin: {0}'.format(name))\n        try:\n            pkg = import_module(name)\n        except Exception as e:\n            logger.critical('failed to load plugin module {0}: {1}'.format(name, e))\n            exit(3)\n\n        pkg.__base_path = dirname(abspath(pkg.__file__))\n\n        prepare_package_resources(pkg, name)\n\n        plugins.append(pkg)\n        logger.debug('plugin \"{0}\" loaded'.format(name))\n    return plugins\n\n\ndef sync_resource(base_path, resource_path, name, target_dir, plugin_dir):\n    dep_path = join(base_path, resource_path)\n    file_name = basename(dep_path)\n    resource_path = join(target_dir, file_name)\n    if not exists(resource_path) or sha_sum(dep_path) != sha_sum(resource_path):\n        try:\n            copyfile(dep_path, resource_path)\n            # copy atime_ns and mtime_ns, so the weak ETags (generated by\n            # the HTTP server) do not change\n            dep_stat = stat(dep_path)\n            utime(resource_path, ns=(dep_stat.st_atime_ns, dep_stat.st_mtime_ns))\n        except:\n            logger.critical('failed to copy plugin resource {0} for plugin {1}'.format(file_name, name))\n            exit(3)\n\n    # returning with the web path of the resource\n    return join('plugins/external_plugins', plugin_dir, file_name)\n\n\ndef prepare_package_resources(pkg, name):\n    plugin_dir = 'plugin_' + name\n    target_dir = join(settings['ui']['static_path'], 'plugins/external_plugins', plugin_dir)\n    try:\n        makedirs(target_dir, exist_ok=True)\n    except:\n        logger.critical('failed to create resource directory {0} for plugin {1}'.format(target_dir, name))\n        exit(3)\n\n    resources = []\n\n    if hasattr(pkg, 'js_dependencies'):\n        resources.extend(map(basename, pkg.js_dependencies))\n        pkg.js_dependencies = tuple([\n            sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)\n            for x in pkg.js_dependencies\n        ])\n    if hasattr(pkg, 'css_dependencies'):\n        resources.extend(map(basename, pkg.css_dependencies))\n        pkg.css_dependencies = tuple([\n            sync_resource(pkg.__base_path, x, name, target_dir, plugin_dir)\n            for x in pkg.css_dependencies\n        ])\n\n    for f in listdir(target_dir):\n        if basename(f) not in resources:\n            resource_path = join(target_dir, basename(f))\n            try:\n                remove(resource_path)\n            except:\n                logger.critical('failed to remove unused resource file {0} for plugin {1}'.format(resource_path, name))\n                exit(3)\n\n\ndef sha_sum(filename):\n    with open(filename, \"rb\") as f:\n        file_content_bytes = f.read()\n        return sha256(file_content_bytes).hexdigest()\n\n\nplugins = PluginStore()\nplugins.register(oa_doi_rewrite)\nplugins.register(hash_plugin)\nplugins.register(infinite_scroll)\nplugins.register(self_info)\nplugins.register(search_on_category_select)\nplugins.register(tracker_url_remover)\nplugins.register(vim_hotkeys)\n# load external plugins\nif settings['plugins']:\n    plugins.register(*settings['plugins'], external=True)\n\nif settings['enabled_plugins']:\n    for plugin in plugins:\n        if plugin.name in settings['enabled_plugins']:\n            plugin.default_on = True\n        else:\n            plugin.default_on = False\n\n# load tor specific plugins\nif settings['outgoing']['using_tor_proxy']:\n    plugins.register(ahmia_filter)\n", "path": "searx/plugins/__init__.py"}, {"content": "'''\nsearx is free software: you can redistribute it and/or modify\nit under the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nsearx is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Affero General Public License for more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with searx. If not, see < http://www.gnu.org/licenses/ >.\n\n(C) 2015 by Adam Tauber, <asciimoo@gmail.com>\n'''\nfrom flask_babel import gettext\nimport re\nname = gettext('Self Informations')\ndescription = gettext('Displays your IP if the query is \"ip\" and your user agent if the query contains \"user agent\".')\ndefault_on = True\npreference_section = 'query'\nquery_keywords = ['user-agent']\nquery_examples = ''\n\n# Self User Agent regex\np = re.compile('.*user[ -]agent.*', re.IGNORECASE)\n\n\n# attach callback to the post search hook\n#  request: flask request object\n#  ctx: the whole local context of the pre search hook\ndef post_search(request, search):\n    if search.search_query.pageno > 1:\n        return True\n    if search.search_query.query == 'ip':\n        x_forwarded_for = request.headers.getlist(\"X-Forwarded-For\")\n        if x_forwarded_for:\n            ip = x_forwarded_for[0]\n        else:\n            ip = request.remote_addr\n        search.result_container.answers['ip'] = {'answer': ip}\n    elif p.match(search.search_query.query):\n        ua = request.user_agent\n        search.result_container.answers['user-agent'] = {'answer': ua}\n    return True\n", "path": "searx/plugins/self_info.py"}]}