mediawiki: Add action to set domain name

- Set domain name during app setup

- Improve tests for settings. Prefer to call functions in plinth which invoke
actions than test actions directly.

- Also, '$wgServer' is not a domain name since it also includes the protocol.

- Add domain selection form. Make server url a text input field.

- Added a functional test to set the value of server url to the value provided
by FREEDOMBOX_URL before doing running any other tests.

- Make server url setting a pre-requisite.

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
[sunil: Squash commits as they were fixing themselves]
[sunil: Simplify configuration reading]
[sunil: Use 'server_url' terminology consistently]
[sunil: cosmetic: Minor styling]
[sunil: Update test_settings.py to use fixture pattern]
[sunil: Remove seemingly incorrectly used aria-describedby attribute]
[sunil: Don't rely solely on env variable value in functional tests]
[sunil: Fix issue with http/https mismatch when checking site availability]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Joseph Nuthalapati 2020-09-30 21:52:56 +05:30 committed by Sunil Mohan Adapa
parent 04617cbf7f
commit 658e260d23
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
10 changed files with 227 additions and 35 deletions

View File

@ -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()

View File

@ -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}'])

View File

@ -38,3 +38,6 @@ $wgSessionCacheType = CACHE_DB;
# Use the mobile-friendly skin Timeless by default
$wgDefaultSkin = "timeless";
# Domain Name
$wgServer = "https://freedombox.local";

View File

@ -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 = '''
<div class="input-group">
<span class="input-group-addon">
%(data)s
</span>
%(field)s
</div>'''
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 '

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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