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/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/jail.d/matrix-auth-freedombox.conf 23.10~
rm_conffile /etc/fail2ban/filter.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 # SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to configure MediaWiki.""" """FreedomBox app to configure MediaWiki."""
import re import pathlib
from urllib.parse import urlparse from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import app as app_module from plinth import app as app_module
from plinth import frontpage, menu from plinth import frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore from plinth.modules.backups.components import BackupRestore
@ -31,7 +32,6 @@ _description = [
'logged in can make changes to the content.') 'logged in can make changes to the content.')
] ]
STATIC_CONFIG_FILE = '/etc/mediawiki/FreedomBoxStaticSettings.php'
USER_CONFIG_FILE = '/etc/mediawiki/FreedomBoxSettings.php' USER_CONFIG_FILE = '/etc/mediawiki/FreedomBoxSettings.php'
@ -40,7 +40,7 @@ class MediaWikiApp(app_module.App):
app_id = 'mediawiki' app_id = 'mediawiki'
_version = 10 _version = 11
def __init__(self): def __init__(self):
"""Create components for the app.""" """Create components for the app."""
@ -70,6 +70,12 @@ class MediaWikiApp(app_module.App):
['mediawiki', 'imagemagick', 'php-sqlite3']) ['mediawiki', 'imagemagick', 'php-sqlite3'])
self.add(packages) 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, firewall = Firewall('firewall-mediawiki', info.name,
ports=['http', 'https'], is_external=True) ports=['http', 'https'], is_external=True)
self.add(firewall) self.add(firewall)
@ -109,7 +115,7 @@ class Shortcut(frontpage.Shortcut):
def enable(self): def enable(self):
"""When enabled, check if MediaWiki is in private mode.""" """When enabled, check if MediaWiki is in private mode."""
super().enable() 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): 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: with open(config_file, 'r', encoding='utf-8') as config:
for line in config: for line in config:
if line.startswith(setting_name): if line.startswith(setting_name):
return re.findall(r'["\'][^"\']*["\']', line)[0].strip('"\'') return line.partition('=')[2].strip('\n ;\'"')
return None 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): def _get_config_value(setting_name):
"""Return a configuration value from multiple configuration files.""" """Return a configuration value from multiple configuration files."""
return _get_config_value_in_file(setting_name, USER_CONFIG_FILE) or \ 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(): def get_config():
"""Return the value of the default skin.""" """Return all the configuration settings."""
return _get_config_value('$wgDefaultSkin')
def get_server_url():
"""Return the value of the server URL."""
server_url = _get_config_value('$wgServer') 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): def set_server_url(domain):
@ -146,13 +172,3 @@ def set_server_url(domain):
protocol = 'http' protocol = 'http'
privileged.set_server_url(f'{protocol}://{domain}') 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 is shipped by FreedomBox to manage static settings. Newer versions of
this file are shipped by FreedomBox and are expected to override this file 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 without any user prompts. It should not be modified by the system
administrator. Additional setting modified by FreedomBox at placed in administrator. Additional setting modified by FreedomBox are placed in
FreedomBoxSettings.php. No newer version of that file is ever shipped by FreedomBoxSettings.php.
FreedomBox.
*/ */
# Default logo # Default logo

View File

@ -6,7 +6,6 @@ import pathlib
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
from typing import Optional
from plinth.actions import privileged from plinth.actions import privileged
from plinth.utils import generate_password from plinth.utils import generate_password
@ -60,6 +59,11 @@ def setup():
]) ])
subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True) subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True)
subprocess.run(['chown', '-R', 'www-data:www-data', 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() _include_custom_config()
@ -109,68 +113,6 @@ def update():
subprocess.check_call([get_php_command(), update_script, '--quick']) 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): def _update_setting(setting_name, setting_line):
"""Update the value of one setting in the config file.""" """Update the value of one setting in the config file."""
with open(CONF_FILE, 'r', encoding='utf-8') as conf_file: with open(CONF_FILE, 'r', encoding='utf-8') as conf_file:
@ -190,6 +132,22 @@ def _update_setting(setting_name, setting_line):
conf_file.writelines(lines) 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 @privileged
def set_default_skin(skin: str): def set_default_skin(skin: str):
"""Set a default skin.""" """Set a default skin."""
@ -228,3 +186,4 @@ def uninstall():
"""Remove Mediawiki's database and local config file.""" """Remove Mediawiki's database and local config file."""
shutil.rmtree('/var/lib/mediawiki-db', ignore_errors=True) shutil.rmtree('/var/lib/mediawiki-db', ignore_errors=True)
pathlib.Path(LOCAL_SETTINGS_CONF).unlink(missing_ok=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 pathlib
import shutil
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -17,60 +16,57 @@ current_directory = pathlib.Path(__file__).parent
privileged_modules_to_mock = ['plinth.modules.mediawiki.privileged'] privileged_modules_to_mock = ['plinth.modules.mediawiki.privileged']
@pytest.fixture(autouse=True) @pytest.fixture(name='test_configuration', autouse=True)
def fixture_setup_configuration(conf_file): def fixture_test_configuration(tmp_path):
"""Set configuration file path in actions module.""" """Use a separate MediaWiki configuration for tests.
privileged.CONF_FILE = conf_file
FreedomBoxStaticSettings.php is used read-only from source code location.
@pytest.fixture(name='conf_file') """
def fixture_conf_file(tmp_path):
"""Uses a dummy configuration file."""
settings_file_name = 'FreedomBoxSettings.php' settings_file_name = 'FreedomBoxSettings.php'
conf_file = tmp_path / settings_file_name conf_file = tmp_path / settings_file_name
conf_file.touch() conf_file.touch()
shutil.copyfile( with (patch('plinth.modules.mediawiki.USER_CONFIG_FILE', conf_file),
str(current_directory / '..' / 'data' / 'etc' / 'mediawiki' / patch('plinth.modules.mediawiki.privileged.CONF_FILE', conf_file)):
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):
yield 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(): def test_default_skin():
"""Test getting and setting the 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' new_skin = 'vector'
privileged.set_default_skin(new_skin) 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.""" """Test getting and setting $wgServer."""
assert mediawiki.get_server_url() == 'freedombox.local' assert mediawiki.get_config()['domain'] == 'freedombox.local'
new_domain = 'mydomain.freedombox.rocks' new_domain = 'mydomain.freedombox.rocks'
mediawiki.set_server_url(new_domain) mediawiki.set_server_url(new_domain)
assert mediawiki.get_server_url() == new_domain assert mediawiki.get_config()['domain'] == new_domain
def test_site_name(): def test_site_name():
"""Test getting and setting $wgSitename.""" """Test getting and setting $wgSitename."""
assert mediawiki.get_site_name() == 'Wiki' assert mediawiki.get_config()['site_name'] == 'Wiki'
new_site_name = 'My MediaWiki' new_site_name = 'My MediaWiki'
privileged.set_site_name(new_site_name) 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 import views
from plinth.modules import mediawiki from plinth.modules import mediawiki
from . import (get_default_skin, from . import privileged
get_server_url, get_site_name, get_default_language, privileged)
from .forms import MediaWikiForm from .forms import MediaWikiForm
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -27,20 +26,7 @@ class MediaWikiAppView(views.AppView):
def get_initial(self): def get_initial(self):
"""Return the values to fill in the form.""" """Return the values to fill in the form."""
initial = super().get_initial() initial = super().get_initial()
initial.update({ initial.update(mediawiki.get_config())
'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()
})
return initial return initial
def form_valid(self, form): def form_valid(self, form):
@ -66,7 +52,7 @@ class MediaWikiAppView(views.AppView):
# note action public-registration restarts, if running now # note action public-registration restarts, if running now
if new_config['enable_public_registrations']: if new_config['enable_public_registrations']:
if not new_config['enable_private_mode']: if not new_config['enable_private_mode']:
privileged.public_registrations('enable') privileged.set_public_registrations(True)
messages.success(self.request, messages.success(self.request,
_('Public registrations enabled')) _('Public registrations enabled'))
else: else:
@ -74,19 +60,19 @@ class MediaWikiAppView(views.AppView):
self.request, 'Public registrations ' + self.request, 'Public registrations ' +
'cannot be enabled when private mode is enabled') 'cannot be enabled when private mode is enabled')
else: else:
privileged.public_registrations('disable') privileged.set_public_registrations(False)
messages.success(self.request, messages.success(self.request,
_('Public registrations disabled')) _('Public registrations disabled'))
if is_changed('enable_private_mode'): if is_changed('enable_private_mode'):
if new_config['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')) messages.success(self.request, _('Private mode enabled'))
if new_config['enable_public_registrations']: if new_config['enable_public_registrations']:
# If public registrations are enabled, then disable it # If public registrations are enabled, then disable it
privileged.public_registrations('disable') privileged.set_public_registrations(False)
else: else:
privileged.private_mode('disable') privileged.set_private_mode(False)
messages.success(self.request, _('Private mode disabled')) messages.success(self.request, _('Private mode disabled'))
app = app_module.App.get('mediawiki') app = app_module.App.get('mediawiki')