diff --git a/LICENSES b/LICENSES index d0e7cd9b5..84f71d4f9 100644 --- a/LICENSES +++ b/LICENSES @@ -69,3 +69,4 @@ otherwise. - static/themes/default/icons/apple.png :: [[https://thenounproject.com/icon/1203053/download/color/000000/png][CC BY 3.0 US]] - 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]] diff --git a/actions/mediawiki b/actions/mediawiki new file mode 100755 index 000000000..7f322d01f --- /dev/null +++ b/actions/mediawiki @@ -0,0 +1,143 @@ +#!/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 MediaWiki. +""" + +import argparse +import os +import re +import subprocess +import sys +import tempfile + +from plinth import action_utils +from plinth.utils import generate_password, grep + +MAINTENANCE_SCRIPTS_DIR = "/usr/share/mediawiki/maintenance" +CONF_FILE = '/var/lib/mediawiki/LocalSettings.php' + + +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 MediaWiki') + subparsers.add_parser('disable', help='Disable MediaWiki') + subparsers.add_parser('setup', help='Setup MediaWiki') + change_password = subparsers.add_parser('change-password', + help='Change user password') + change_password.add_argument('--username', default='admin', + help='name of the MediaWiki user') + change_password.add_argument('--password', + help='new password for the MediaWiki user') + + subparsers.required = True + return parser.parse_args() + + +def subcommand_setup(_): + """Run the installer script to create database and configuration file.""" + data_dir = '/var/lib/mediawiki-db/' + if not os.path.exists(data_dir): + os.mkdir(data_dir) + + if not os.path.exists(os.path.join(data_dir, 'my_wiki.sqlite')): + install_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'install.php') + password = generate_password() + with tempfile.NamedTemporaryFile() as password_file_handle: + password_file_handle.write(password.encode()) + subprocess.check_call([ + 'php', install_script, '--confpath=/etc/mediawiki', + '--dbtype=sqlite', '--dbpath=' + data_dir, + '--scriptpath=/mediawiki', '--passfile', + password_file_handle.name, 'Wiki', 'admin' + ]) + subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True) + subprocess.run(['chown', '-R', 'www-data:www-data', data_dir], check=True) + _disable_public_registrations() + _disable_anonymous_editing() + _change_logo() + + +def _disable_public_registrations(): + """Edit MediaWiki configuration to disable public registrations.""" + if not grep(r'\$wgGroupPermissions.*createaccount', CONF_FILE): + with open(CONF_FILE, 'a') as file_handle: + file_handle.write( + "$wgGroupPermissions['*']['createaccount'] = false;\n") + + +def _disable_anonymous_editing(): + """Edit MediaWiki configuration to allow anonymous users from editing. + + MediaWiki instances get a lot of spam bot typically. + """ + if not grep(r'\$wgGroupPermissions.*edit', CONF_FILE): + with open(CONF_FILE, 'a') as file_handle: + file_handle.write("$wgGroupPermissions['*']['edit'] = false;\n") + + +def _change_logo(): + """Change the placeholder logo to MediaWiki's official logo""" + lines = open(CONF_FILE, 'r').readlines() + with open(CONF_FILE, 'w') as file_handle: + for line in lines: + if re.match('^\s*\$wgLogo', line): + line = line.replace('assets/wiki.png', 'assets/mediawiki.png') + + file_handle.write(line) + + +def subcommand_change_password(arguments): + """Change the password for a given user""" + new_password = ''.join(sys.stdin) + change_password_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, + 'changePassword.php') + + subprocess.check_call([ + 'php', change_password_script, '--user', arguments.username, + '--password', new_password + ]) + + +def subcommand_enable(_): + """Enable web configuration and reload.""" + action_utils.service_enable('mediawiki-jobrunner') + action_utils.webserver_enable('mediawiki') + + +def subcommand_disable(_): + """Disable web configuration and reload.""" + action_utils.webserver_disable('mediawiki') + action_utils.service_disable('mediawiki-jobrunner') + + +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/plinth/modules-enabled/mediawiki b/data/etc/plinth/modules-enabled/mediawiki new file mode 100644 index 000000000..261fc06e2 --- /dev/null +++ b/data/etc/plinth/modules-enabled/mediawiki @@ -0,0 +1 @@ +plinth.modules.mediawiki diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 4d1e5b446..dbf94cd0c 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -24,8 +24,8 @@ import shutil import socket import subprocess import tempfile -import psutil +import psutil from django.utils.translation import ugettext as _ logger = logging.getLogger(__name__) @@ -43,15 +43,11 @@ def service_is_running(servicename): """ try: if is_systemd_running(): - subprocess.run( - ['systemctl', 'status', servicename], - check=True, - stdout=subprocess.DEVNULL) + subprocess.run(['systemctl', 'status', servicename], check=True, + stdout=subprocess.DEVNULL) else: - subprocess.run( - ['service', servicename, 'status'], - check=True, - stdout=subprocess.DEVNULL) + subprocess.run(['service', servicename, 'status'], check=True, + stdout=subprocess.DEVNULL) return True except subprocess.CalledProcessError: @@ -63,11 +59,8 @@ def service_is_running(servicename): def service_is_enabled(service_name): """Check if service is enabled in systemd.""" try: - subprocess.run( - ['systemctl', 'is-enabled', service_name], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.run(['systemctl', 'is-enabled', service_name], check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError: return False @@ -96,41 +89,41 @@ def service_unmask(service_name): def service_start(service_name): """Start a service with systemd or sysvinit.""" if is_systemd_running(): - subprocess.run( - ['systemctl', 'start', service_name], stdout=subprocess.DEVNULL) + subprocess.run(['systemctl', 'start', service_name], + stdout=subprocess.DEVNULL) else: - subprocess.run( - ['service', service_name, 'start'], stdout=subprocess.DEVNULL) + subprocess.run(['service', service_name, 'start'], + stdout=subprocess.DEVNULL) def service_stop(service_name): """Stop a service with systemd or sysvinit.""" if is_systemd_running(): - subprocess.run( - ['systemctl', 'stop', service_name], stdout=subprocess.DEVNULL) + subprocess.run(['systemctl', 'stop', service_name], + stdout=subprocess.DEVNULL) else: - subprocess.run( - ['service', service_name, 'stop'], stdout=subprocess.DEVNULL) + subprocess.run(['service', service_name, 'stop'], + stdout=subprocess.DEVNULL) def service_restart(service_name): """Restart a service with systemd or sysvinit.""" if is_systemd_running(): - subprocess.run( - ['systemctl', 'restart', service_name], stdout=subprocess.DEVNULL) + subprocess.run(['systemctl', 'restart', service_name], + stdout=subprocess.DEVNULL) else: - subprocess.run( - ['service', service_name, 'restart'], stdout=subprocess.DEVNULL) + subprocess.run(['service', service_name, 'restart'], + stdout=subprocess.DEVNULL) def service_reload(service_name): """Reload a service with systemd or sysvinit.""" if is_systemd_running(): - subprocess.run( - ['systemctl', 'reload', service_name], stdout=subprocess.DEVNULL) + subprocess.run(['systemctl', 'reload', service_name], + stdout=subprocess.DEVNULL) else: - subprocess.run( - ['service', service_name, 'reload'], stdout=subprocess.DEVNULL) + subprocess.run(['service', service_name, 'reload'], + stdout=subprocess.DEVNULL) def webserver_is_enabled(name, kind='config'): @@ -141,8 +134,8 @@ def webserver_is_enabled(name, kind='config'): option_map = {'config': '-c', 'site': '-s', 'module': '-m'} try: # Don't print anything on the terminal - subprocess.check_output( - ['a2query', option_map[kind], name], stderr=subprocess.STDOUT) + subprocess.check_output(['a2query', option_map[kind], name], + stderr=subprocess.STDOUT) return True except subprocess.CalledProcessError: return False @@ -309,13 +302,8 @@ def _check_port(port, kind='tcp', listen_address=None): return False -def diagnose_url(url, - kind=None, - env=None, - check_certificate=True, - extra_options=None, - wrapper=None, - expected_output=None): +def diagnose_url(url, kind=None, env=None, check_certificate=True, + extra_options=None, wrapper=None, expected_output=None): """Run a diagnostic on whether a URL is accessible. Kind can be '4' for IPv4 or '6' for IPv6. @@ -335,12 +323,9 @@ def diagnose_url(url, command.append({'4': '-4', '6': '-6'}[kind]) try: - process = subprocess.run( - command, - env=env, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + process = subprocess.run(command, env=env, check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) result = 'passed' if expected_output and expected_output not in process.stdout.decode(): result = 'failed' @@ -376,10 +361,8 @@ def diagnose_netcat(host, port, input='', negate=False): """Run a diagnostic using netcat.""" try: process = subprocess.Popen( - ['nc', host, str(port)], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + ['nc', host, str(port)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.communicate(input=input.encode()) if process.returncode != 0: result = 'failed' @@ -473,8 +456,8 @@ Owners: {package} ''' override_data = '' for key, value in config.items(): - override_data += override_template.format( - package=package, key=key, value=value) + override_data += override_template.format(package=package, key=key, + value=value) with tempfile.NamedTemporaryFile(mode='w', delete=False) as override_file: override_file.write(override_data) diff --git a/plinth/modules/mediawiki/__init__.py b/plinth/modules/mediawiki/__init__.py new file mode 100644 index 000000000..980700d91 --- /dev/null +++ b/plinth/modules/mediawiki/__init__.py @@ -0,0 +1,125 @@ +# +# 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 MediaWiki. +""" + +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 + +version = 1 + +managed_packages = ['mediawiki', 'imagemagick', 'php-sqlite3'] + +name = _('MediaWiki') + +short_description = _('Wiki') + +description = [ + _('MediaWiki is the wiki engine that powers Wikipedia and other WikiMedia ' + 'projects. A wiki engine is a program for creating a collaboratively ' + 'edited website. You can use MediaWiki to host a wiki-like website, ' + 'take notes or collaborate with friends on projects.'), + _('This MediaWiki instance comes with a randomly generated administrator ' + 'password. You can set a new password in the Configuration section and ' + 'login using the "admin" account. You can then create more user ' + 'accounts from MediaWiki itself by going to the ' + 'Special:CreateAccount page'), + _('Anyone with a link to this Wiki can read it. Only users that are ' + 'logged in can make changes to the content.') +] + +service = None + +clients = clients + + +def init(): + """Intialize the module.""" + menu = main_menu.get('apps') + menu.add_urlname(name, 'glyphicon-edit', 'mediawiki:index', + short_description) + + global service + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() != 'needs-setup': + service = service_module.Service( + 'mediawiki', 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, 'mediawiki', ['setup']) + helper.call('enable', actions.superuser_run, 'mediawiki', ['enable']) + global service + if service is None: + service = service_module.Service( + 'mediawiki', + name, + is_external=True, + is_enabled=is_enabled, + enable=enable, + disable=disable, + ports=['http', 'https'], ) + helper.call('post', service.notify_enabled, None, True) + helper.call('post', add_shortcut) + + +def add_shortcut(): + """Helper method to add a shortcut to the frontpage.""" + frontpage.add_shortcut('mediawiki', name, + short_description=short_description, + url='/mediawiki', login_required=True) + + +def is_enabled(): + """Return whether the module is enabled.""" + return action_utils.webserver_is_enabled('mediawiki') + + +def enable(): + """Enable the module.""" + actions.superuser_run('mediawiki', ['enable']) + add_shortcut() + + +def disable(): + """Enable the module.""" + actions.superuser_run('mediawiki', ['disable']) + frontpage.remove_shortcut('mediawiki') + + +def diagnose(): + """Run diagnostics and return the results.""" + results = [] + + results.extend( + action_utils.diagnose_url_on_all('https://{host}/mediawiki', + check_certificate=False)) + + return results diff --git a/plinth/modules/mediawiki/forms.py b/plinth/modules/mediawiki/forms.py new file mode 100644 index 000000000..4367d75ab --- /dev/null +++ b/plinth/modules/mediawiki/forms.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 . +# +""" +Plinth module for configuring MediaWiki. +""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from plinth.forms import ServiceForm + + +class MediaWikiForm(ServiceForm): # pylint: disable=W0232 + """MediaWiki configuration form.""" + password = forms.CharField(label=_('Administrator Password'), help_text=_( + 'Set a new password for MediaWiki\'s administrator account (admin). ' + 'Leave this field blank to keep the current password.'), + required=False, widget=forms.PasswordInput) diff --git a/plinth/modules/mediawiki/manifest.py b/plinth/modules/mediawiki/manifest.py new file mode 100644 index 000000000..352e38b28 --- /dev/null +++ b/plinth/modules/mediawiki/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': _('MediaWiki'), + 'platforms': [{ + 'type': 'web', + 'url': '/mediawiki' + }] +}]) diff --git a/plinth/modules/mediawiki/urls.py b/plinth/modules/mediawiki/urls.py new file mode 100644 index 000000000..27763878e --- /dev/null +++ b/plinth/modules/mediawiki/urls.py @@ -0,0 +1,27 @@ +# +# 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 mediawiki module. +""" + +from django.conf.urls import url + +from .views import MediaWikiServiceView + +urlpatterns = [ + url(r'^apps/mediawiki/$', MediaWikiServiceView.as_view(), name='index'), +] diff --git a/plinth/modules/mediawiki/views.py b/plinth/modules/mediawiki/views.py new file mode 100644 index 000000000..c9d403f58 --- /dev/null +++ b/plinth/modules/mediawiki/views.py @@ -0,0 +1,52 @@ +# +# 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 for configuring MediaWiki. +""" + +import logging + +from django.contrib import messages +from django.utils.translation import ugettext as _ + +from plinth import actions, views +from plinth.modules import mediawiki + +from .forms import MediaWikiForm + +logger = logging.getLogger(__name__) + + +class MediaWikiServiceView(views.ServiceView): + """Serve configuration page.""" + clients = mediawiki.clients + description = mediawiki.description + diagnostics_module_name = 'mediawiki' + service_id = 'mediawiki' + form_class = MediaWikiForm + show_status_block = False + + def form_valid(self, form): + """Apply the changes submitted in the form.""" + form_data = form.cleaned_data + + if form_data['password']: + actions.superuser_run('mediawiki', ['change-password'], + input=form_data['password'].encode()) + messages.success(self.request, _('Password updated')) + + return super().form_valid(form) diff --git a/plinth/utils.py b/plinth/utils.py index b56aff1ec..edf5e3118 100644 --- a/plinth/utils.py +++ b/plinth/utils.py @@ -20,6 +20,9 @@ Miscellaneous utility methods. import importlib import os +import random +import re +import string from django.utils.functional import lazy @@ -119,3 +122,17 @@ class YAMLFile(object): def yes_or_no(cond): return 'yes' if cond else 'no' + + +def generate_password(size=32): + """Generate a random password using ascii alphabet and digits.""" + chars = (random.SystemRandom().choice(string.ascii_letters + string.digits) + for _ in range(size)) + return ''.join(chars) + + +def grep(pattern, file_name): + """Return lines of a file matching a pattern.""" + return [ + line.rstrip() for line in open(file_name) if re.search(pattern, line) + ] diff --git a/static/themes/default/icons/mediawiki.png b/static/themes/default/icons/mediawiki.png new file mode 100644 index 000000000..6d2706cf9 Binary files /dev/null and b/static/themes/default/icons/mediawiki.png differ