diff --git a/actions/mediawiki b/actions/mediawiki index 44189ac50..08ad36e1c 100755 --- a/actions/mediawiki +++ b/actions/mediawiki @@ -48,6 +48,10 @@ def parse_arguments(): help='Set the default skin') default_skin.add_argument('skin', help='name of the skin') + server_url = subparsers.add_parser( + 'set-server-url', help='Set the value of $wgServer for this server') + server_url.add_argument('server_url', help='value of $wgServer') + subparsers.required = True return parser.parse_args() @@ -218,28 +222,37 @@ def subcommand_private_mode(arguments): conf_value + '\n') -def subcommand_set_default_skin(arguments): - """Set a default skin""" - skin = arguments.skin - skin_setting = f'$wgDefaultSkin = "{skin}";\n' - +def _update_setting(setting_name, setting_line): + """Update the value of one setting in the config file.""" with open(CONF_FILE, 'r') as conf_file: lines = conf_file.readlines() inserted = False for i, line in enumerate(lines): - if line.strip().startswith('$wgDefaultSkin'): - lines[i] = skin_setting + if line.strip().startswith(setting_name): + lines[i] = setting_line inserted = True break if not inserted: - lines.append(skin_setting) + lines.append(setting_line) with open(CONF_FILE, 'w') as conf_file: conf_file.writelines(lines) +def subcommand_set_default_skin(arguments): + """Set a default skin.""" + skin = arguments.skin + _update_setting('$wgDefaultSkin ', f'$wgDefaultSkin = "{skin}";\n') + + +def subcommand_set_server_url(arguments): + """Set the value of $wgServer for this MediaWiki server.""" + # This is a required setting from MediaWiki 1.34 + _update_setting('$wgServer', f'$wgServer = "{arguments.server_url}";\n') + + def main(): """Parse arguments and perform all duties.""" arguments = parse_arguments() diff --git a/plinth/modules/mediawiki/__init__.py b/plinth/modules/mediawiki/__init__.py index a588f9834..95925da93 100644 --- a/plinth/modules/mediawiki/__init__.py +++ b/plinth/modules/mediawiki/__init__.py @@ -4,6 +4,7 @@ FreedomBox app to configure MediaWiki. """ import re +from urllib.parse import urlparse from django.utils.translation import ugettext_lazy as _ @@ -16,7 +17,7 @@ from plinth.modules.firewall.components import Firewall from .manifest import backup, clients # noqa, pylint: disable=unused-import -version = 8 +version = 9 managed_packages = ['mediawiki', 'imagemagick', 'php-sqlite3'] @@ -39,6 +40,9 @@ _description = [ app = None +STATIC_CONFIG_FILE = '/etc/mediawiki/FreedomBoxStaticSettings.php' +USER_CONFIG_FILE = '/etc/mediawiki/FreedomBoxSettings.php' + class MediaWikiApp(app_module.App): """FreedomBox app for MediaWiki.""" @@ -109,24 +113,44 @@ def is_public_registration_enabled(): def is_private_mode_enabled(): - """ Return whether private mode is enabled or disabled""" + """Return whether private mode is enabled or disabled.""" output = actions.superuser_run('mediawiki', ['private-mode', 'status']) return output.strip() == 'enabled' -def get_default_skin(): - """Return the value of the default skin""" - - def _find_skin(config_file): - with open(config_file, 'r') as config: - for line in config: - if line.startswith('$wgDefaultSkin'): - return re.findall(r'["\'][^"\']*["\']', - line)[0].strip('"\'') +def _get_config_value_in_file(setting_name, config_file): + """Return the value of a setting from a config file.""" + with open(config_file, 'r') as config: + for line in config: + if line.startswith(setting_name): + return re.findall(r'["\'][^"\']*["\']', line)[0].strip('"\'') return None - user_config = '/etc/mediawiki/FreedomBoxSettings.php' - static_config = '/etc/mediawiki/FreedomBoxStaticSettings.php' - return _find_skin(user_config) or _find_skin(static_config) +def _get_config_value(setting_name): + """Return a configuration value from multiple configuration files.""" + return _get_config_value_in_file(setting_name, USER_CONFIG_FILE) or \ + _get_config_value_in_file(setting_name, STATIC_CONFIG_FILE) + + +def get_default_skin(): + """Return the value of the default skin.""" + return _get_config_value('$wgDefaultSkin') + + +def set_default_skin(skin): + """Set the value of the default skin.""" + actions.superuser_run('mediawiki', ['set-default-skin', skin]) + + +def get_server_url(): + """Return the value of the server URL.""" + server_url = _get_config_value('$wgServer') + return urlparse(server_url).netloc + + +def set_server_url(server_url): + """Set the value of $wgServer.""" + actions.superuser_run('mediawiki', + ['set-server-url', f'https://{server_url}']) diff --git a/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php b/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php index 70f33f2a1..53cb45859 100644 --- a/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php +++ b/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php @@ -38,3 +38,6 @@ $wgSessionCacheType = CACHE_DB; # Use the mobile-friendly skin Timeless by default $wgDefaultSkin = "timeless"; + +# Domain Name +$wgServer = "https://freedombox.local"; diff --git a/plinth/modules/mediawiki/forms.py b/plinth/modules/mediawiki/forms.py index 9c4823998..2c3f75812 100644 --- a/plinth/modules/mediawiki/forms.py +++ b/plinth/modules/mediawiki/forms.py @@ -6,6 +6,8 @@ FreedomBox app for configuring MediaWiki. import pathlib from django import forms +from django.forms import Widget +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -19,6 +21,29 @@ def get_skins(): if skin.is_dir()] +class PrependWidget(Widget): + """Widget to create input-groups with prepended text.""" + + def __init__(self, base_widget, data, *args, **kwargs): + """Initialize widget and get base instance""" + super(PrependWidget, self).__init__(*args, **kwargs) + self.base_widget = base_widget(*args, **kwargs) + self.data = data + + def render(self, name, value, attrs=None, renderer=None): + """Render base widget and add bootstrap spans.""" + attrs['class'] = 'form-control' + field = self.base_widget.render(name, value, attrs, renderer) + widget_html = ''' +
+ + %(data)s + + %(field)s +
''' + return mark_safe((widget_html) % {'field': field, 'data': self.data}) + + class MediaWikiForm(forms.Form): # pylint: disable=W0232 """MediaWiki configuration form.""" password = forms.CharField( @@ -27,6 +52,12 @@ class MediaWikiForm(forms.Form): # pylint: disable=W0232 '(admin). Leave this field blank to keep the current password.'), required=False, widget=forms.PasswordInput) + server_url = forms.CharField( + label=_('Server URL'), required=False, help_text=_( + 'Used by MediaWiki to generate URLs that point to the wiki ' + 'such as in footer, feeds and emails.'), + widget=PrependWidget(base_widget=forms.TextInput, data='https://')) + enable_public_registrations = forms.BooleanField( label=_('Enable public registrations'), required=False, help_text=_('If enabled, anyone on the internet will be able to ' diff --git a/plinth/modules/mediawiki/tests/conftest.py b/plinth/modules/mediawiki/tests/conftest.py new file mode 100644 index 000000000..72729133a --- /dev/null +++ b/plinth/modules/mediawiki/tests/conftest.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Common test fixtures for MediaWiki. +""" + +import shutil +import importlib +import pathlib +import types +from unittest.mock import patch + +import pytest + +current_directory = pathlib.Path(__file__).parent + + +def _load_actions_module(): + actions_file_path = str(current_directory / '..' / '..' / '..' / '..' / + 'actions' / 'mediawiki') + loader = importlib.machinery.SourceFileLoader('mediawiki', + actions_file_path) + module = types.ModuleType(loader.name) + loader.exec_module(module) + return module + + +actions = _load_actions_module() + + +@pytest.fixture(name='call_action') +def fixture_call_action(capsys, conf_file): + """Run actions with custom root path.""" + + def _call_action(module_name, args, **kwargs): + actions.CONF_FILE = conf_file + with patch('argparse._sys.argv', [module_name] + args): + actions.main() + captured = capsys.readouterr() + return captured.out + + return _call_action + + +@pytest.fixture(name='conf_file') +def fixture_conf_file(tmp_path): + """Uses a dummy configuration file.""" + settings_file_name = 'FreedomBoxSettings.php' + conf_file = tmp_path / settings_file_name + conf_file.touch() + shutil.copyfile( + str(current_directory / '..' / 'data' / 'etc' / 'mediawiki' / + settings_file_name), str(conf_file)) + return str(conf_file) diff --git a/plinth/modules/mediawiki/tests/mediawiki.feature b/plinth/modules/mediawiki/tests/mediawiki.feature index fd48e1297..fb2bc6f71 100644 --- a/plinth/modules/mediawiki/tests/mediawiki.feature +++ b/plinth/modules/mediawiki/tests/mediawiki.feature @@ -7,6 +7,7 @@ Feature: MediaWiki Wiki Engine Background: Given I'm a logged in user Given the mediawiki application is installed + Given the server url is set to test config url Scenario: Enable mediawiki application Given the mediawiki application is disabled diff --git a/plinth/modules/mediawiki/tests/test_functional.py b/plinth/modules/mediawiki/tests/test_functional.py index 626dfbd1b..48654a0ac 100644 --- a/plinth/modules/mediawiki/tests/test_functional.py +++ b/plinth/modules/mediawiki/tests/test_functional.py @@ -4,14 +4,21 @@ Functional, browser based tests for mediawiki app. """ import pathlib +from urllib.parse import urlparse -from pytest_bdd import parsers, scenarios, then, when +from pytest_bdd import given, parsers, scenarios, then, when from plinth.tests import functional +from plinth.tests.functional import config scenarios('mediawiki.feature') +@given(parsers.parse('the server url is set to test config url')) +def set_server_url(session_browser): + _set_server_url(session_browser) + + @when(parsers.parse('I enable mediawiki public registrations')) def enable_mediawiki_public_registrations(session_browser): _enable_public_registrations(session_browser) @@ -57,8 +64,7 @@ def mediawiki_allows_anonymous_reads_edits(session_browser): @then( parsers.parse( 'the mediawiki site should not allow anonymous reads and writes')) -def mediawiki_does_not_allow__account_creation_anonymous_reads_edits( - session_browser): +def mediawiki_does_not_allow_anonymous_reads_edits(session_browser): _verify_no_anonymous_reads_edits_link(session_browser) @@ -216,3 +222,11 @@ def __has_main_page(browser): functional.visit(browser, '/mediawiki/Main_Page') content = browser.find_by_id('mw-content-text').first return 'This page has been deleted.' not in content.text + + +def _set_server_url(browser): + """Set the value of server url to the value in the given env_var.""" + functional.nav_to_module(browser, 'mediawiki') + server_url = urlparse(config['DEFAULT']['url']).netloc + browser.find_by_id('id_server_url').fill(server_url) + functional.submit(browser, form_class='form-configuration') diff --git a/plinth/modules/mediawiki/tests/test_settings.py b/plinth/modules/mediawiki/tests/test_settings.py new file mode 100644 index 000000000..b129590e9 --- /dev/null +++ b/plinth/modules/mediawiki/tests/test_settings.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Test module for MediaWiki utility functions. +""" + +import pathlib +from unittest.mock import patch + +import pytest + +from plinth.modules import mediawiki + + +@pytest.fixture(name='test_configuration', autouse=True) +def fixture_test_configuration(call_action, conf_file): + """Use a separate MediaWiki configuration for tests. + + Uses local FreedomBoxStaticSettings.php, a temp version of + FreedomBoxSettings.php and patches actions.superuser_run with the fixture + call_action + + """ + data_directory = pathlib.Path(__file__).parent.parent / 'data' + static_config_file = str(data_directory / 'etc' / 'mediawiki' / + mediawiki.STATIC_CONFIG_FILE.split('/')[-1]) + with patch('plinth.modules.mediawiki.STATIC_CONFIG_FILE', + static_config_file), \ + patch('plinth.modules.mediawiki.USER_CONFIG_FILE', conf_file), \ + patch('plinth.actions.superuser_run', call_action): + yield + + +def test_default_skin(): + """Test getting and setting the default skin.""" + assert mediawiki.get_default_skin() == 'timeless' + new_skin = 'vector' + mediawiki.set_default_skin(new_skin) + assert mediawiki.get_default_skin() == new_skin + + +def test_server_url(): + """Test getting and setting $wgServer.""" + assert mediawiki.get_server_url() == 'freedombox.local' + new_server_url = 'mydomain.freedombox.rocks' + mediawiki.set_server_url(new_server_url) + assert mediawiki.get_server_url() == new_server_url diff --git a/plinth/modules/mediawiki/views.py b/plinth/modules/mediawiki/views.py index 0abacb969..f26d15650 100644 --- a/plinth/modules/mediawiki/views.py +++ b/plinth/modules/mediawiki/views.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _ from plinth import actions, views from plinth.modules import mediawiki -from . import (get_default_skin, is_private_mode_enabled, +from . import (get_default_skin, get_server_url, is_private_mode_enabled, is_public_registration_enabled) from .forms import MediaWikiForm @@ -30,7 +30,8 @@ class MediaWikiAppView(views.AppView): initial.update({ 'enable_public_registrations': is_public_registration_enabled(), 'enable_private_mode': is_private_mode_enabled(), - 'default_skin': get_default_skin() + 'default_skin': get_default_skin(), + 'server_url': get_server_url() }) return initial @@ -39,15 +40,15 @@ class MediaWikiAppView(views.AppView): old_config = self.get_initial() new_config = form.cleaned_data - def is_unchanged(key): - return old_config[key] == new_config[key] + def is_changed(key): + return old_config.get(key) != new_config.get(key) if new_config['password']: actions.superuser_run('mediawiki', ['change-password'], input=new_config['password'].encode()) messages.success(self.request, _('Password updated')) - if not is_unchanged('enable_public_registrations'): + if is_changed('enable_public_registrations'): # note action public-registration restarts, if running now if new_config['enable_public_registrations']: if not new_config['enable_private_mode']: @@ -65,7 +66,7 @@ class MediaWikiAppView(views.AppView): messages.success(self.request, _('Public registrations disabled')) - if not is_unchanged('enable_private_mode'): + if is_changed('enable_private_mode'): if new_config['enable_private_mode']: actions.superuser_run('mediawiki', ['private-mode', 'enable']) messages.success(self.request, _('Private mode enabled')) @@ -80,9 +81,12 @@ class MediaWikiAppView(views.AppView): shortcut = mediawiki.app.get_component('shortcut-mediawiki') shortcut.login_required = new_config['enable_private_mode'] - if not is_unchanged('default_skin'): - actions.superuser_run( - 'mediawiki', ['set-default-skin', new_config['default_skin']]) + if is_changed('default_skin'): + mediawiki.set_default_skin(new_config['default_skin']) messages.success(self.request, _('Default skin changed')) + if is_changed('server_url'): + mediawiki.set_server_url(new_config['server_url']) + messages.success(self.request, _('Server URL updated')) + return super().form_valid(form) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index e5c6f8c78..03e01a8c9 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -152,7 +152,10 @@ def is_available(browser, site_name): not_404 = '404' not in browser.title # The site might have a default path after the sitename, # e.g /mediawiki/Main_Page - no_redirect = browser.url.startswith(url_to_visit.strip('/')) + print('URL =', browser.url, url_to_visit, browser.title) + browser_url = browser.url.partition('://')[2] + url_to_visit_without_proto = url_to_visit.strip('/').partition('://')[2] + no_redirect = browser_url.startswith(url_to_visit_without_proto) return not_404 and no_redirect