From 823c2968f0dc029e057bf81f8d0635b2550c3344 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Mon, 11 Dec 2017 17:56:43 +0530 Subject: [PATCH] searx: New app for Searx metasearch engine Signed-off-by: Joseph Nuthalapati Reviewed-by: Sunil Mohan Adapa --- LICENSES | 1 + actions/searx | 99 +++++++++++++++ .../apache2/conf-available/searx-plinth.conf | 5 + data/etc/plinth/modules-enabled/searx | 1 + plinth/modules/searx/__init__.py | 119 ++++++++++++++++++ plinth/modules/searx/manifest.py | 28 +++++ plinth/modules/searx/urls.py | 32 +++++ plinth/utils.py | 19 +++ static/themes/default/icons/searx.png | Bin 0 -> 8390 bytes 9 files changed, 304 insertions(+) create mode 100755 actions/searx create mode 100644 data/etc/apache2/conf-available/searx-plinth.conf create mode 100644 data/etc/plinth/modules-enabled/searx create mode 100644 plinth/modules/searx/__init__.py create mode 100644 plinth/modules/searx/manifest.py create mode 100644 plinth/modules/searx/urls.py create mode 100644 static/themes/default/icons/searx.png diff --git a/LICENSES b/LICENSES index 84f71d4f9..94fdf6903 100644 --- a/LICENSES +++ b/LICENSES @@ -70,3 +70,4 @@ otherwise. - static/themes/default/icons/windows.png :: [[https://thenounproject.com/icon/1206946/download/color/000000/png][CC BY 3.0 US]] - static/themes/default/icons/gnu-linux.png :: [[https://upload.wikimedia.org/wikipedia/commons/9/95/Tux-icon-mono.svg][Public Domain]] - static/themes/default/icons/mediawiki.svg :: [[http://tango.freedesktop.org/][Public Domain]] +- static/themes/default/icons/searx.png :: [[https://github.com/asciimoo/searx/blob/master/searx/static/themes/simple/img/logo_searx_a.png][GPLv3+]] diff --git a/actions/searx b/actions/searx new file mode 100755 index 000000000..25fc2f07d --- /dev/null +++ b/actions/searx @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +# -*- mode: python -*- +# +# This file is part of Plinth. +# +# This program 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. +# +# This program 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 this program. If not, see . +# +""" +Configuration helper for searx. +""" + +import argparse +import os + +from plinth import action_utils +from plinth.utils import YAMLFile, gunzip + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser('enable', help='Enable searx') + subparsers.add_parser('disable', help='Disable searx') + subparsers.add_parser( + 'setup', help="Perform post-installation operations for Searx") + + subparsers.required = True + return parser.parse_args() + + +def _copy_uwsgi_configuration(): + """Copy the example uwsgi configuration shipped with Searx to the + appropriate uwsgi directory.""" + example_config = ('/usr/share/doc/searx/examples/' + 'uwsgi/apps-available/searx.ini') + destination = '/etc/uwsgi/apps-enabled/searx.ini' + + if not os.path.exists(destination): + os.symlink(example_config, destination) + action_utils.webserver_enable('uwsgi', kind='module') + + +def _generate_secret_key(): + """ Generate a secret key for the Searx installation.""" + settings_file = '/etc/searx/settings.yml' + + # Create settings file if not exists + if not os.path.exists(settings_file): + example_settings_file = '/usr/share/doc/searx/examples/settings.yml.gz' + gunzip(example_settings_file, settings_file) + + # Generate and set a secret key + with YAMLFile(settings_file) as settings: + secret_key = os.urandom(16).hex() + settings['server']['secret_key'] = secret_key + + action_utils.service_restart('uwsgi') + + +def subcommand_setup(_): + """Post installation actions for Searx""" + _copy_uwsgi_configuration() + _generate_secret_key() + + +def subcommand_enable(_): + """Enable web configuration and reload.""" + action_utils.webserver_enable('searx-plinth') + + +def subcommand_disable(_): + """Disable web configuration and reload.""" + action_utils.webserver_disable('searx-plinth') + + +def main(): + """Parse arguments and perform all duties.""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + + +if __name__ == '__main__': + main() diff --git a/data/etc/apache2/conf-available/searx-plinth.conf b/data/etc/apache2/conf-available/searx-plinth.conf new file mode 100644 index 000000000..69d92392f --- /dev/null +++ b/data/etc/apache2/conf-available/searx-plinth.conf @@ -0,0 +1,5 @@ + + Options FollowSymLinks Indexes + SetHandler uwsgi-handler + uWSGISocket /run/uwsgi/app/searx/socket + \ No newline at end of file diff --git a/data/etc/plinth/modules-enabled/searx b/data/etc/plinth/modules-enabled/searx new file mode 100644 index 000000000..d4cc7b9d2 --- /dev/null +++ b/data/etc/plinth/modules-enabled/searx @@ -0,0 +1 @@ +plinth.modules.searx diff --git a/plinth/modules/searx/__init__.py b/plinth/modules/searx/__init__.py new file mode 100644 index 000000000..6577a7bf9 --- /dev/null +++ b/plinth/modules/searx/__init__.py @@ -0,0 +1,119 @@ +# +# This file is part of Plinth. +# +# This program 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. +# +# This program 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 this program. If not, see . +# +""" +Plinth module to configure Searx +""" + +from django.utils.translation import ugettext_lazy as _ + +from plinth import service as service_module +from plinth import action_utils, actions, frontpage +from plinth.menu import main_menu + +from .manifest import clients + +clients = clients + +version = 1 + +managed_services = ['searx'] + +managed_packages = [ + 'searx', 'uwsgi', 'uwsgi-plugin-python3', 'libapache2-mod-uwsgi' +] + +name = _('Searx') + +short_description = _('Web Search') + +description = [ + _('Searx is a privacy-respecting internet metasearch engine. ' + 'It aggregrates and displays results from multiple search engines.'), + _('Searx can be used to avoid tracking and profiling by search engines. ' + 'It stores no cookies by default. Additionally, Searx can be used over ' + 'Tor for online anonymity.'), + _('When enabled, Searx\'s web interface will be available from ' + '/searx.'), +] + +service = None + + +def init(): + """Intialize the module.""" + menu = main_menu.get('apps') + menu.add_urlname(name, 'glyphicon-search', 'searx:index', + short_description) + + global service + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() != 'needs-setup': + service = service_module.Service(managed_services[0], name, ports=[ + 'http', 'https' + ], is_external=True, is_enabled=is_enabled, enable=enable, + disable=disable) + + if is_enabled(): + add_shortcut() + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(managed_packages) + helper.call('setup', actions.superuser_run, 'searx', ['setup']) + helper.call('post', actions.superuser_run, 'searx', ['enable']) + global service + if service is None: + service = service_module.Service(managed_services[0], name, ports=[ + 'http', 'https' + ], is_external=True, is_enabled=is_enabled, enable=enable, + disable=disable) + helper.call('post', add_shortcut) + + +def add_shortcut(): + """Helper method to add a shortcut to the frontpage.""" + frontpage.add_shortcut('searx', name, short_description=short_description, + url='/searx', login_required=True) + + +def is_enabled(): + """Return whether the module is enabled.""" + return action_utils.webserver_is_enabled('searx-plinth') + + +def enable(): + """Enable the module.""" + actions.superuser_run('searx', ['enable']) + add_shortcut() + + +def disable(): + """Disable the module.""" + actions.superuser_run('searx', ['disable']) + frontpage.remove_shortcut('searx') + + +def diagnose(): + """Run diagnostics and return the results.""" + results = [] + + results.extend( + action_utils.diagnose_url_on_all('https://{host}/searx/', + check_certificate=False)) + + return results diff --git a/plinth/modules/searx/manifest.py b/plinth/modules/searx/manifest.py new file mode 100644 index 000000000..8cffa5df2 --- /dev/null +++ b/plinth/modules/searx/manifest.py @@ -0,0 +1,28 @@ +# +# This file is part of Plinth. +# +# This program 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. +# +# This program 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 this program. If not, see . +# + +from django.utils.translation import ugettext_lazy as _ + +from plinth.clients import validate + +clients = validate([{ + 'name': _('Searx'), + 'platforms': [{ + 'type': 'web', + 'url': '/searx' + }] +}]) diff --git a/plinth/modules/searx/urls.py b/plinth/modules/searx/urls.py new file mode 100644 index 000000000..a8bcc4f9b --- /dev/null +++ b/plinth/modules/searx/urls.py @@ -0,0 +1,32 @@ +# +# This file is part of Plinth. +# +# This program 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. +# +# This program 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 this program. If not, see . +# +""" +URLs for the Searx module. +""" + +from django.conf.urls import url + +from plinth.modules import searx +from plinth.views import ServiceView + +urlpatterns = [ + url(r'^apps/searx/$', + ServiceView.as_view( + service_id='searx', diagnostics_module_name='searx', + description=searx.description, clients=searx.clients, + show_status_block=False), name='index'), +] diff --git a/plinth/utils.py b/plinth/utils.py index 1f971ab43..918548457 100644 --- a/plinth/utils.py +++ b/plinth/utils.py @@ -18,6 +18,7 @@ Miscellaneous utility methods. """ +import gzip import importlib import os import random @@ -136,3 +137,21 @@ def grep(pattern, file_name): return [ line.rstrip() for line in open(file_name) if re.search(pattern, line) ] + + +def gunzip(gzip_file, output_file): + """Utility to unzip a given gzip file and write it to an output file + + gzip_file: string path to a gzip file + output_file: string path to the output file + mode: an octal number to specify file permissions + """ + output_dir = os.path.dirname(output_file) + if not os.path.exists(output_dir): + os.makedirs(output_dir, mode=0o755) + + with gzip.open(gzip_file, 'rb') as f: + contents = f.read() + with open(output_file, 'wb') as f: + f.write(contents) + os.chmod(output_file, 0o644) diff --git a/static/themes/default/icons/searx.png b/static/themes/default/icons/searx.png new file mode 100644 index 0000000000000000000000000000000000000000..2db08171010c34e83b3e32897534d4b244109c96 GIT binary patch literal 8390 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 wQcW}!=