mediawiki: Use drop-in config component for /etc files

- Don't ship /etc/mediawiki/FreedomBoxSettings.php anymore. Create the file on
first setup. Keep old file on update.

- Simplify and unify how the configuration settings are read and written.

Tests:

- Run unit and functional tests.

- All the drop-in config files in /etc/ are symlinks.

- Shipped configuration is effective.

- Upgrade from older version keeps old configuration.

- Config files are all symlinks in /etc/

- When upgrading from older version FreedomBoxSettings.php does not change.
  FreedomBoxStaticSettings.php becomes a symlink.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2023-05-16 11:53:49 -07:00 committed by James Valleroy
parent c326b35238
commit cd512bd24c
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
8 changed files with 106 additions and 174 deletions

View File

@ -117,3 +117,5 @@ rm_conffile /etc/letsencrypt/renewal-hooks/deploy/50-freedombox 23.10~
rm_conffile /etc/apache2/conf-available/matrix-synapse-plinth.conf 23.10~
rm_conffile /etc/fail2ban/jail.d/matrix-auth-freedombox.conf 23.10~
rm_conffile /etc/fail2ban/filter.d/matrix-auth-freedombox.conf 23.10~
rm_conffile /etc/apache2/conf-available/mediawiki-freedombox.conf 23.10~
rm_conffile /etc/mediawiki/FreedomBoxStaticSettings.php 23.10~

View File

@ -1,13 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to configure MediaWiki."""
import re
import pathlib
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
@ -31,7 +32,6 @@ _description = [
'logged in can make changes to the content.')
]
STATIC_CONFIG_FILE = '/etc/mediawiki/FreedomBoxStaticSettings.php'
USER_CONFIG_FILE = '/etc/mediawiki/FreedomBoxSettings.php'
@ -40,7 +40,7 @@ class MediaWikiApp(app_module.App):
app_id = 'mediawiki'
_version = 10
_version = 11
def __init__(self):
"""Create components for the app."""
@ -70,6 +70,12 @@ class MediaWikiApp(app_module.App):
['mediawiki', 'imagemagick', 'php-sqlite3'])
self.add(packages)
dropin_configs = DropinConfigs('dropin-configs-mediawiki', [
'/etc/apache2/conf-available/mediawiki-freedombox.conf',
'/etc/mediawiki/FreedomBoxStaticSettings.php',
])
self.add(dropin_configs)
firewall = Firewall('firewall-mediawiki', info.name,
ports=['http', 'https'], is_external=True)
self.add(firewall)
@ -109,7 +115,7 @@ class Shortcut(frontpage.Shortcut):
def enable(self):
"""When enabled, check if MediaWiki is in private mode."""
super().enable()
self.login_required = privileged.private_mode('status')
self.login_required = get_config()['enable_private_mode']
def _get_config_value_in_file(setting_name, config_file):
@ -117,26 +123,46 @@ def _get_config_value_in_file(setting_name, config_file):
with open(config_file, 'r', encoding='utf-8') as config:
for line in config:
if line.startswith(setting_name):
return re.findall(r'["\'][^"\']*["\']', line)[0].strip('"\'')
return line.partition('=')[2].strip('\n ;\'"')
return None
def _get_static_config_file():
"""Return the path for the file containing static settings."""
base_path = ('/usr/share/freedombox/etc/'
'mediawiki/FreedomBoxStaticSettings.php')
for path in [
pathlib.Path(base_path),
pathlib.Path(__file__).parent / 'data' / base_path.lstrip('/')
]:
if path.exists():
return path
raise RuntimeError('Unable to find static config file')
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)
_get_config_value_in_file(setting_name, _get_static_config_file())
def get_default_skin():
"""Return the value of the default skin."""
return _get_config_value('$wgDefaultSkin')
def get_server_url():
"""Return the value of the server URL."""
def get_config():
"""Return all the configuration settings."""
server_url = _get_config_value('$wgServer')
return urlparse(server_url).netloc
create_permission = _get_config_value(
"$wgGroupPermissions['*']['createaccount']")
read_permission = _get_config_value("$wgGroupPermissions['*']['read']")
print('=====', create_permission, read_permission)
return {
'default_skin': _get_config_value('$wgDefaultSkin'),
'domain': urlparse(server_url).netloc,
'site_name': _get_config_value('$wgSitename') or 'Wiki',
'default_lang': _get_config_value('$wgLanguageCode') or None,
'enable_public_registrations': 'true' in create_permission,
'enable_private_mode': 'false' in read_permission,
}
def set_server_url(domain):
@ -146,13 +172,3 @@ def set_server_url(domain):
protocol = 'http'
privileged.set_server_url(f'{protocol}://{domain}')
def get_site_name():
"""Return the value of MediaWiki's site name."""
return _get_config_value('$wgSitename') or 'Wiki'
def get_default_language():
"""Return the value of MediaWiki's default language"""
return _get_config_value('$wgLanguageCode') or None

View File

@ -1,26 +0,0 @@
<?php
# Default logo
$wgLogo = "$wgResourceBasePath/resources/assets/mediawiki.png";
# Enable file uploads
$wgEnableUploads = true;
# Public registrations
$wgGroupPermissions['*']['createaccount'] = false;
# Read/write permissions for anonymous users
$wgGroupPermissions['*']['edit'] = false;
$wgGroupPermissions['*']['read'] = true;
# Short urls
$wgArticlePath = "/mediawiki/$1";
$wgUsePathInfo = true;
# Instant Commons
$wgUseInstantCommons = true;
# SVG Enablement
$wgFileExtensions[] = 'svg';
$wgAllowTitlesInSVG = true;
$wgSVGConverter = 'ImageMagick';

View File

@ -3,9 +3,8 @@
This file is shipped by FreedomBox to manage static settings. Newer versions of
this file are shipped by FreedomBox and are expected to override this file
without any user prompts. It should not be modified by the system
administrator. Additional setting modified by FreedomBox at placed in
FreedomBoxSettings.php. No newer version of that file is ever shipped by
FreedomBox.
administrator. Additional setting modified by FreedomBox are placed in
FreedomBoxSettings.php.
*/
# Default logo

View File

@ -6,7 +6,6 @@ import pathlib
import shutil
import subprocess
import tempfile
from typing import Optional
from plinth.actions import privileged
from plinth.utils import generate_password
@ -60,6 +59,11 @@ def setup():
])
subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True)
subprocess.run(['chown', '-R', 'www-data:www-data', data_dir], check=True)
conf_file = pathlib.Path(CONF_FILE)
if not conf_file.exists():
conf_file.write_text('<?php\n')
_include_custom_config()
@ -109,68 +113,6 @@ def update():
subprocess.check_call([get_php_command(), update_script, '--quick'])
@privileged
def public_registrations(command: str) -> Optional[bool]:
"""Enable or Disable public registrations for MediaWiki."""
if command not in ('enable', 'disable', 'status'):
raise ValueError('Invalid command')
with open(CONF_FILE, 'r', encoding='utf-8') as conf_file:
lines = conf_file.readlines()
def is_pub_reg_line(line):
return line.startswith("$wgGroupPermissions['*']['createaccount']")
if command == 'status':
conf_lines = list(filter(is_pub_reg_line, lines))
return bool(conf_lines and 'true' in conf_lines[0])
with open(CONF_FILE, 'w', encoding='utf-8') as conf_file:
for line in lines:
if is_pub_reg_line(line):
words = line.split()
if command == 'enable':
words[-1] = 'true;'
else:
words[-1] = 'false;'
conf_file.write(" ".join(words) + '\n')
else:
conf_file.write(line)
return None
@privileged
def private_mode(command: str):
"""Enable or Disable Private mode for wiki."""
if command not in ('enable', 'disable', 'status'):
raise ValueError('Invalid command')
with open(CONF_FILE, 'r', encoding='utf-8') as conf_file:
lines = conf_file.readlines()
def is_read_line(line):
return line.startswith("$wgGroupPermissions['*']['read']")
read_conf_lines = list(filter(is_read_line, lines))
if command == 'status':
return (read_conf_lines and 'false' in read_conf_lines[0])
with open(CONF_FILE, 'w', encoding='utf-8') as conf_file:
conf_value = 'false;' if command == 'enable' else 'true;'
for line in lines:
if is_read_line(line):
words = line.split()
words[-1] = conf_value
conf_file.write(" ".join(words) + '\n')
else:
conf_file.write(line)
if not read_conf_lines:
conf_file.write("$wgGroupPermissions['*']['read'] = " +
conf_value + '\n')
def _update_setting(setting_name, setting_line):
"""Update the value of one setting in the config file."""
with open(CONF_FILE, 'r', encoding='utf-8') as conf_file:
@ -190,10 +132,26 @@ def _update_setting(setting_name, setting_line):
conf_file.writelines(lines)
@privileged
def set_public_registrations(should_enable: bool):
"""Enable or Disable public registrations for MediaWiki."""
setting = "$wgGroupPermissions['*']['createaccount']"
conf_value = 'true' if should_enable else 'false'
_update_setting(setting, f'{setting} = {conf_value};\n')
@privileged
def set_private_mode(should_enable: bool):
"""Enable or Disable Private mode for wiki."""
setting = "$wgGroupPermissions['*']['read']"
conf_value = 'false' if should_enable else 'true'
_update_setting(setting, f'{setting} = {conf_value};\n')
@privileged
def set_default_skin(skin: str):
"""Set a default skin."""
_update_setting('$wgDefaultSkin ', f'$wgDefaultSkin = "{skin}";\n')
_update_setting('$wgDefaultSkin', f'$wgDefaultSkin = "{skin}";\n')
@privileged
@ -228,3 +186,4 @@ def uninstall():
"""Remove Mediawiki's database and local config file."""
shutil.rmtree('/var/lib/mediawiki-db', ignore_errors=True)
pathlib.Path(LOCAL_SETTINGS_CONF).unlink(missing_ok=True)
pathlib.Path(CONF_FILE).unlink(missing_ok=True)

View File

@ -4,7 +4,6 @@ Test module for MediaWiki utility functions.
"""
import pathlib
import shutil
from unittest.mock import patch
import pytest
@ -17,60 +16,57 @@ current_directory = pathlib.Path(__file__).parent
privileged_modules_to_mock = ['plinth.modules.mediawiki.privileged']
@pytest.fixture(autouse=True)
def fixture_setup_configuration(conf_file):
"""Set configuration file path in actions module."""
privileged.CONF_FILE = conf_file
@pytest.fixture(name='test_configuration', autouse=True)
def fixture_test_configuration(tmp_path):
"""Use a separate MediaWiki configuration for tests.
@pytest.fixture(name='conf_file')
def fixture_conf_file(tmp_path):
"""Uses a dummy configuration file."""
FreedomBoxStaticSettings.php is used read-only from source code location.
"""
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)
@pytest.fixture(name='test_configuration', autouse=True)
def fixture_test_configuration(conf_file):
"""Use a separate MediaWiki configuration for tests.
Uses local FreedomBoxStaticSettings.php, a temp version of
FreedomBoxSettings.php
"""
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):
with (patch('plinth.modules.mediawiki.USER_CONFIG_FILE', conf_file),
patch('plinth.modules.mediawiki.privileged.CONF_FILE', conf_file)):
yield
def test_private_mode():
"""Test enabling/disabling private mode."""
assert not mediawiki.get_config()['enable_private_mode']
privileged.set_private_mode(True)
assert mediawiki.get_config()['enable_private_mode']
privileged.set_private_mode(False)
assert not mediawiki.get_config()['enable_private_mode']
def test_public_registrations():
"""Test enabling/disabling public registrations."""
assert not mediawiki.get_config()['enable_public_registrations']
privileged.set_public_registrations(True)
assert mediawiki.get_config()['enable_public_registrations']
privileged.set_public_registrations(False)
assert not mediawiki.get_config()['enable_public_registrations']
def test_default_skin():
"""Test getting and setting the default skin."""
assert mediawiki.get_default_skin() == 'timeless'
assert mediawiki.get_config()['default_skin'] == 'timeless'
new_skin = 'vector'
privileged.set_default_skin(new_skin)
assert mediawiki.get_default_skin() == new_skin
assert mediawiki.get_config()['default_skin'] == new_skin
def test_server_url():
def test_domain():
"""Test getting and setting $wgServer."""
assert mediawiki.get_server_url() == 'freedombox.local'
assert mediawiki.get_config()['domain'] == 'freedombox.local'
new_domain = 'mydomain.freedombox.rocks'
mediawiki.set_server_url(new_domain)
assert mediawiki.get_server_url() == new_domain
assert mediawiki.get_config()['domain'] == new_domain
def test_site_name():
"""Test getting and setting $wgSitename."""
assert mediawiki.get_site_name() == 'Wiki'
assert mediawiki.get_config()['site_name'] == 'Wiki'
new_site_name = 'My MediaWiki'
privileged.set_site_name(new_site_name)
assert mediawiki.get_site_name() == new_site_name
assert mediawiki.get_config()['site_name'] == new_site_name

View File

@ -10,8 +10,7 @@ from plinth import app as app_module
from plinth import views
from plinth.modules import mediawiki
from . import (get_default_skin,
get_server_url, get_site_name, get_default_language, privileged)
from . import privileged
from .forms import MediaWikiForm
logger = logging.getLogger(__name__)
@ -27,20 +26,7 @@ class MediaWikiAppView(views.AppView):
def get_initial(self):
"""Return the values to fill in the form."""
initial = super().get_initial()
initial.update({
'enable_public_registrations':
privileged.public_registrations('status'),
'enable_private_mode':
privileged.private_mode('status'),
'default_skin':
get_default_skin(),
'domain':
get_server_url(),
'site_name':
get_site_name(),
'default_lang':
get_default_language()
})
initial.update(mediawiki.get_config())
return initial
def form_valid(self, form):
@ -66,7 +52,7 @@ class MediaWikiAppView(views.AppView):
# note action public-registration restarts, if running now
if new_config['enable_public_registrations']:
if not new_config['enable_private_mode']:
privileged.public_registrations('enable')
privileged.set_public_registrations(True)
messages.success(self.request,
_('Public registrations enabled'))
else:
@ -74,19 +60,19 @@ class MediaWikiAppView(views.AppView):
self.request, 'Public registrations ' +
'cannot be enabled when private mode is enabled')
else:
privileged.public_registrations('disable')
privileged.set_public_registrations(False)
messages.success(self.request,
_('Public registrations disabled'))
if is_changed('enable_private_mode'):
if new_config['enable_private_mode']:
privileged.private_mode('enable')
privileged.set_private_mode(True)
messages.success(self.request, _('Private mode enabled'))
if new_config['enable_public_registrations']:
# If public registrations are enabled, then disable it
privileged.public_registrations('disable')
privileged.set_public_registrations(False)
else:
privileged.private_mode('disable')
privileged.set_private_mode(False)
messages.success(self.request, _('Private mode disabled'))
app = app_module.App.get('mediawiki')