diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py index dbc54d3d8..524f55892 100644 --- a/plinth/modules/config/__init__.py +++ b/plinth/modules/config/__init__.py @@ -1,8 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """FreedomBox app for basic system configuration.""" -import socket - import augeas from django.utils.translation import gettext_lazy as _ @@ -11,16 +9,14 @@ from plinth import frontpage, menu from plinth.daemon import RelatedDaemon from plinth.modules.apache import (get_users_with_website, user_of_uws_url, uws_url_of_user) -from plinth.modules.names.components import DomainType from plinth.package import Packages from plinth.privileged import service as service_privileged -from plinth.signals import domain_added from . import privileged _description = [ _('Here you can set some general configuration options ' - 'like domain name, webserver home page etc.') + 'like webserver home page etc.') ] ADVANCED_MODE_KEY = 'advanced_mode' @@ -60,20 +56,6 @@ class ConfigApp(app_module.App): daemon2 = RelatedDaemon('related-daemon-config2', 'rsyslog') self.add(daemon2) - domain_type = DomainType('domain-type-static', _('Domain Name'), - 'config:index', can_have_certificate=True) - self.add(domain_type) - - @staticmethod - def post_init(): - """Perform post initialization operations.""" - # Register domain with Name Services module. - domainname = get_domainname() - if domainname: - domain_added.send_robust(sender='config', - domain_type='domain-type-static', - name=domainname, services='__all__') - def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) @@ -95,12 +77,6 @@ class ConfigApp(app_module.App): service_privileged.mask('rsyslog') -def get_domainname(): - """Return the domainname.""" - fqdn = socket.getfqdn() - return '.'.join(fqdn.split('.')[1:]) - - def home_page_url2scid(url): """Return the shortcut ID of the given home page url.""" # url is None when the freedombox-apache-homepage configuration file does diff --git a/plinth/modules/config/forms.py b/plinth/modules/config/forms.py index 599ad7c82..e79d9aa11 100644 --- a/plinth/modules/config/forms.py +++ b/plinth/modules/config/forms.py @@ -3,12 +3,7 @@ Forms for basic system configuration """ -import logging -import re - from django import forms -from django.core import validators -from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy @@ -18,17 +13,6 @@ from plinth.utils import format_lazy from . import home_page_url2scid -logger = logging.getLogger(__name__) - -HOSTNAME_REGEX = r'^[a-zA-Z0-9]([-a-zA-Z0-9]{,61}[a-zA-Z0-9])?$' - - -def domain_label_validator(domainname): - """Validate domain name labels.""" - for label in domainname.split('.'): - if not re.match(HOSTNAME_REGEX, label): - raise ValidationError(_('Invalid domain name')) - def get_homepage_choices(): """Return list of drop down choices for home page.""" @@ -47,23 +31,6 @@ def get_homepage_choices(): class ConfigurationForm(forms.Form): """Main system configuration form""" - domainname = forms.CharField( - label=gettext_lazy('Domain Name'), help_text=format_lazy( - gettext_lazy( - 'Domain name is the global name by which other devices on the ' - 'Internet can reach your {box_name}. It must consist of ' - 'labels separated by dots. Each label must start and end ' - 'with an alphabet or a digit and have as interior characters ' - 'only alphabets, digits and hyphens. Length of each label ' - 'must be 63 characters or less. Total length of domain name ' - 'must be 253 characters or less.'), - box_name=gettext_lazy(cfg.box_name)), required=False, validators=[ - validators.RegexValidator( - r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$', - gettext_lazy('Invalid domain name')), - domain_label_validator - ], strip=True) - homepage = forms.ChoiceField( label=gettext_lazy('Webserver Home Page'), help_text=format_lazy( gettext_lazy( diff --git a/plinth/modules/config/privileged.py b/plinth/modules/config/privileged.py index 17654562c..fd9cba671 100644 --- a/plinth/modules/config/privileged.py +++ b/plinth/modules/config/privileged.py @@ -3,7 +3,6 @@ import os import pathlib -import subprocess import augeas @@ -18,32 +17,6 @@ APACHE_HOMEPAGE_CONFIG = os.path.join(APACHE_CONF_ENABLED_DIR, JOURNALD_FILE = pathlib.Path('/etc/systemd/journald.conf.d/50-freedombox.conf') -@privileged -def set_domainname(domainname: str | None = None): - """Set system domainname in /etc/hosts.""" - hostname = subprocess.check_output(['hostname']).decode().strip() - hosts_path = pathlib.Path('/etc/hosts') - if domainname: - insert_line = f'127.0.1.1 {hostname}.{domainname} {hostname}\n' - else: - insert_line = f'127.0.1.1 {hostname}\n' - - lines = hosts_path.read_text(encoding='utf-8').splitlines(keepends=True) - new_lines = [] - found = False - for line in lines: - if '127.0.1.1' in line: - new_lines.append(insert_line) - found = True - else: - new_lines.append(line) - - if not found: - new_lines.append(insert_line) - - hosts_path.write_text(''.join(new_lines), encoding='utf-8') - - def load_augeas(): """Initialize Augeas.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + diff --git a/plinth/modules/config/tests/test_config.py b/plinth/modules/config/tests/test_config.py index 6ffb11832..d0bb834be 100644 --- a/plinth/modules/config/tests/test_config.py +++ b/plinth/modules/config/tests/test_config.py @@ -12,37 +12,6 @@ from plinth import __main__ as plinth_main from plinth.modules.apache import uws_directory_of_user, uws_url_of_user from plinth.modules.config import (_home_page_scid2url, change_home_page, get_home_page, home_page_url2scid) -from plinth.modules.config.forms import ConfigurationForm - - -def test_domainname_field(): - """Test that domainname field accepts only valid domainnames.""" - valid_domainnames = [ - '', 'a', '0a', 'a0', 'AAA', '00', '0-0', 'example-hostname', 'example', - 'example.org', 'a.b.c.d', 'a-0.b-0.c-0', - '012345678901234567890123456789012345678901234567890123456789012', - ((('x' * 63) + '.') * 3) + 'x' * 61 - ] - invalid_domainnames = [ - '-', '-a', 'a-', '.a', 'a.', '?', 'a?a', 'a..a', 'a.-a', '.', - ((('x' * 63) + '.') * 3) + 'x' * 62, 'x' * 64 - ] - - for domainname in valid_domainnames: - form = ConfigurationForm({ - 'hostname': 'example', - 'domainname': domainname, - 'logging_mode': 'volatile' - }) - assert form.is_valid() - - for domainname in invalid_domainnames: - form = ConfigurationForm({ - 'hostname': 'example', - 'domainname': domainname, - 'logging_mode': 'volatile' - }) - assert not form.is_valid() def test_homepage_mapping(): diff --git a/plinth/modules/config/tests/test_functional.py b/plinth/modules/config/tests/test_functional.py index 1b1c949e4..241858386 100644 --- a/plinth/modules/config/tests/test_functional.py +++ b/plinth/modules/config/tests/test_functional.py @@ -7,10 +7,7 @@ import pytest from plinth.tests import functional -pytestmark = [ - pytest.mark.system, pytest.mark.essential, pytest.mark.domain, - pytest.mark.config -] +pytestmark = [pytest.mark.system, pytest.mark.essential, pytest.mark.config] @pytest.fixture(scope='module', autouse=True) @@ -19,16 +16,6 @@ def fixture_background(session_browser): functional.login(session_browser) -def test_change_domain_name(session_browser): - """Test changing the domain name.""" - functional.set_domain_name(session_browser, 'mydomain.example') - assert _get_domain_name(session_browser) == 'mydomain.example' - - # Capitalization is ignored. - functional.set_domain_name(session_browser, 'Mydomain.example') - assert _get_domain_name(session_browser) == 'mydomain.example' - - def test_change_home_page(session_browser): """Test changing webserver home page.""" functional.install(session_browser, 'syncthing') @@ -39,11 +26,6 @@ def test_change_home_page(session_browser): assert _check_home_page_redirect(session_browser, 'plinth') -def _get_domain_name(browser): - functional.nav_to_module(browser, 'config') - return browser.find_by_id('id_domainname').value - - def _set_home_page(browser, home_page): if 'plinth' not in home_page and 'apache' not in home_page: home_page = 'shortcut-' + home_page diff --git a/plinth/modules/config/views.py b/plinth/modules/config/views.py index a94b76f8b..6c07c3008 100644 --- a/plinth/modules/config/views.py +++ b/plinth/modules/config/views.py @@ -1,20 +1,15 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """FreedomBox views for basic system configuration.""" -import logging - from django.contrib import messages from django.utils.translation import gettext as _ from plinth import views from plinth.modules import config -from plinth.signals import domain_added, domain_removed from . import privileged from .forms import ConfigurationForm -LOGGER = logging.getLogger(__name__) - class ConfigAppView(views.AppView): """Serve configuration page.""" @@ -25,7 +20,6 @@ class ConfigAppView(views.AppView): def get_initial(self): """Return the current status.""" return { - 'domainname': config.get_domainname(), 'homepage': config.get_home_page(), 'advanced_mode': config.get_advanced_mode(), 'logging_mode': privileged.get_logging_mode(), @@ -38,18 +32,6 @@ class ConfigAppView(views.AppView): is_changed = False - if old_status['domainname'] != new_status['domainname']: - try: - set_domainname(new_status['domainname'], - old_status['domainname']) - except Exception as exception: - messages.error( - self.request, - _('Error setting domain name: {exception}').format( - exception=exception)) - else: - messages.success(self.request, _('Domain name set')) - if old_status['homepage'] != new_status['homepage']: try: config.change_home_page(new_status['homepage']) @@ -85,26 +67,3 @@ class ConfigAppView(views.AppView): messages.success(self.request, _('Configuration updated')) return super().form_valid(form) - - -def set_domainname(domainname, old_domainname): - """Set machine domain name to domainname.""" - old_domainname = config.get_domainname() - - # Domain name is not case sensitive, but Let's Encrypt certificate - # paths use lower-case domain name. - domainname = domainname.lower() - - LOGGER.info('Changing domain name to - %s', domainname) - privileged.set_domainname(domainname) - - # Update domain registered with Name Services module. - if old_domainname: - domain_removed.send_robust(sender='config', - domain_type='domain-type-static', - name=old_domainname) - - if domainname: - domain_added.send_robust(sender='config', - domain_type='domain-type-static', - name=domainname, services='__all__') diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index d11fe1983..e8240e14f 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -11,7 +11,7 @@ from plinth import app as app_module from plinth import cfg, frontpage, menu from plinth.config import DropinConfigs from plinth.daemon import Daemon -from plinth.modules import config +from plinth.modules import names from plinth.modules.apache.components import Webserver from plinth.modules.backups.components import BackupRestore from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer @@ -136,15 +136,15 @@ class EjabberdApp(app_module.App): def setup(self, old_version): """Install and configure the app.""" - domainname = config.get_domainname() - logger.info('ejabberd service domainname - %s', domainname) + domain_name = names.get_domain_name() + logger.info('ejabberd service domain name - %s', domain_name) - privileged.pre_install(domainname) + privileged.pre_install(domain_name) # XXX: Configure all other domain names super().setup(old_version) self.get_component('letsencrypt-ejabberd').setup_certificates( - [domainname]) - privileged.setup(domainname) + [domain_name]) + privileged.setup(domain_name) if not old_version: self.enable() diff --git a/plinth/modules/ejabberd/privileged.py b/plinth/modules/ejabberd/privileged.py index 11851c734..6f177c4c6 100644 --- a/plinth/modules/ejabberd/privileged.py +++ b/plinth/modules/ejabberd/privileged.py @@ -34,18 +34,18 @@ TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})(?:\?transport=(tcp|udp))?' @privileged -def pre_install(domainname: str): +def pre_install(domain_name: str): """Preseed debconf values before packages are installed.""" - if not domainname: - # If new domainname is blank, use hostname instead. - domainname = socket.gethostname() + if not domain_name: + # If new domain_name is blank, use hostname instead. + domain_name = socket.gethostname() action_utils.debconf_set_selections( - ['ejabberd ejabberd/hostname string ' + domainname]) + ['ejabberd ejabberd/hostname string ' + domain_name]) @privileged -def setup(domainname: str): +def setup(domain_name: str): """Enable LDAP authentication.""" with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle: conf = yaml.load(file_handle) @@ -86,7 +86,7 @@ def setup(domainname: str): with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle: yaml.dump(conf, file_handle) - _upgrade_config(domainname) + _upgrade_config(domain_name) try: subprocess.check_output(['ejabberdctl', 'restart']) @@ -195,8 +195,8 @@ def get_domains() -> list[str]: @privileged -def add_domain(domainname: str): - """Update ejabberd with new domainname. +def add_domain(domain_name: str): + """Update ejabberd with new domain name. Restarting ejabberd is handled by letsencrypt-ejabberd component. """ @@ -204,11 +204,11 @@ def add_domain(domainname: str): logger.info('ejabberdctl not found') return - # Add updated domainname to ejabberd hosts list. + # Add updated domain name to ejabberd hosts list. with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle: conf = yaml.load(file_handle) - conf['hosts'].append(scalarstring.DoubleQuotedScalarString(domainname)) + conf['hosts'].append(scalarstring.DoubleQuotedScalarString(domain_name)) conf['hosts'] = list(set(conf['hosts'])) diff --git a/plinth/modules/ejabberd/templates/ejabberd.html b/plinth/modules/ejabberd/templates/ejabberd.html index 106e19a2e..4e9c6333b 100644 --- a/plinth/modules/ejabberd/templates/ejabberd.html +++ b/plinth/modules/ejabberd/templates/ejabberd.html @@ -13,17 +13,19 @@
- {% url 'config:index' as index_url %} - {% if domainname %} - {% blocktrans trimmed with domainname=domainname %} - Your XMPP server domain is set to {{ domainname }}. User - IDs will look like username@{{ domainname }}. You + {% url 'names:index' as names_url %} + {% if domain_name %} + {% blocktrans trimmed with domain_name=domain_name %} + Your XMPP server domain is set to {{ domain_name }}. User + IDs will look like username@{{ domain_name }}. You can setup your domain on the system - Configure page. + Name Services page. {% endblocktrans %} {% else %} - Your XMPP server domain is not set. You can setup your domain on - the system Configure page. + {% blocktrans trimmed %} + Your XMPP server domain is not set. You can setup your domain on the + system Name Services page. + {% endblocktrans %} {% endif %}
diff --git a/plinth/modules/ejabberd/views.py b/plinth/modules/ejabberd/views.py index eb01d2b1f..56aae022e 100644 --- a/plinth/modules/ejabberd/views.py +++ b/plinth/modules/ejabberd/views.py @@ -34,7 +34,7 @@ class EjabberdAppView(AppView): """Add service to the context data.""" context = super().get_context_data(*args, **kwargs) domains = ejabberd.get_domains() - context['domainname'] = domains[0] if domains else None + context['domain_name'] = domains[0] if domains else None return context @staticmethod diff --git a/plinth/modules/email/__init__.py b/plinth/modules/email/__init__.py index 5c28538da..09ba154d4 100644 --- a/plinth/modules/email/__init__.py +++ b/plinth/modules/email/__init__.py @@ -12,7 +12,6 @@ from plinth.config import DropinConfigs from plinth.daemon import Daemon from plinth.modules.apache.components import Webserver from plinth.modules.backups.components import BackupRestore -from plinth.modules.config import get_domainname from plinth.modules.firewall.components import (Firewall, FirewallLocalProtection) from plinth.modules.letsencrypt.components import LetsEncrypt @@ -223,12 +222,6 @@ class EmailApp(plinth.app.App): self.enable() -def get_domains(): - """Return the list of domains configured.""" - default_domain = get_domainname() - return [default_domain] if default_domain else [] - - def _get_first_admin(): """Return an admin user in the system or None if non exist.""" from django.contrib.auth.models import User diff --git a/plinth/modules/email/privileged/domain.py b/plinth/modules/email/privileged/domain.py index d8a792956..b24d98933 100644 --- a/plinth/modules/email/privileged/domain.py +++ b/plinth/modules/email/privileged/domain.py @@ -12,7 +12,7 @@ import re from plinth.actions import privileged from plinth.app import App -from plinth.modules import config +from plinth.modules import names from plinth.modules.email import postfix from plinth.modules.names.components import DomainName @@ -34,7 +34,7 @@ def set_all_domains(primary_domain=None): if not primary_domain: primary_domain = get_domains()['primary_domain'] if primary_domain not in all_domains: - primary_domain = config.get_domainname() or list(all_domains)[0] + primary_domain = names.get_domain_name() or list(all_domains)[0] # Update configuration and don't restart daemons set_domains(primary_domain, list(all_domains)) diff --git a/plinth/modules/jsxc/templates/jsxc_launch.html b/plinth/modules/jsxc/templates/jsxc_launch.html index 8635a8f01..140254c03 100644 --- a/plinth/modules/jsxc/templates/jsxc_launch.html +++ b/plinth/modules/jsxc/templates/jsxc_launch.html @@ -57,7 +57,7 @@ -{% blocktrans trimmed %} No domain(s) are available. Configure diff --git a/plinth/modules/names/__init__.py b/plinth/modules/names/__init__.py index b86eb087e..adb63c6f6 100644 --- a/plinth/modules/names/__init__.py +++ b/plinth/modules/names/__init__.py @@ -16,6 +16,7 @@ from plinth.daemon import Daemon from plinth.diagnostic_check import (DiagnosticCheck, DiagnosticCheckParameters, Result) from plinth.modules.backups.components import BackupRestore +from plinth.modules.names.components import DomainType from plinth.package import Packages from plinth.privileged import service as service_privileged from plinth.signals import (domain_added, domain_removed, post_hostname_change, @@ -64,6 +65,10 @@ class NamesApp(app_module.App): ['systemd-resolved', 'libnss-resolve', 'iproute2']) self.add(packages) + domain_type = DomainType('domain-type-static', _('Domain Name'), + 'names:domains', can_have_certificate=True) + self.add(domain_type) + daemon = Daemon('daemon-names', 'systemd-resolved') self.add(daemon) @@ -77,6 +82,13 @@ class NamesApp(app_module.App): domain_added.connect(on_domain_added) domain_removed.connect(on_domain_removed) + # Register domain with Name Services module. + domain_name = get_domain_name() + if domain_name: + domain_added.send_robust(sender='names', + domain_type='domain-type-static', + name=domain_name, services='__all__') + def diagnose(self) -> list[DiagnosticCheck]: """Run diagnostics and return the results.""" results = super().diagnose() @@ -160,6 +172,12 @@ def on_domain_removed(sender, domain_type, name='', **kwargs): ###################################################### +def get_domain_name(): + """Return the currently set static domain name.""" + fqdn = socket.getfqdn() + return '.'.join(fqdn.split('.')[1:]) + + def get_hostname(): """Return the hostname.""" return socket.gethostname() @@ -167,11 +185,8 @@ def get_hostname(): def set_hostname(hostname): """Set machine hostname and send signals before and after.""" - from plinth.modules import config - from plinth.modules.config import privileged as config_privileged - old_hostname = get_hostname() - domainname = config.get_domainname() + domain_name = get_domain_name() # Hostname should be ASCII. If it's unicode but passed our # valid_hostname check, convert @@ -183,8 +198,8 @@ def set_hostname(hostname): logger.info('Changing hostname to - %s', hostname) privileged.set_hostname(hostname) - logger.info('Setting domain name after hostname change - %s', domainname) - config_privileged.set_domainname(domainname) + logger.info('Setting domain name after hostname change - %s', domain_name) + privileged.set_domain_name(domain_name) post_hostname_change.send_robust(sender='names', old_hostname=old_hostname, new_hostname=hostname) diff --git a/plinth/modules/names/forms.py b/plinth/modules/names/forms.py index 5337a5099..3aa99bbf5 100644 --- a/plinth/modules/names/forms.py +++ b/plinth/modules/names/forms.py @@ -1,8 +1,11 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Forms for the names app.""" +import re + from django import forms from django.core import validators +from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from plinth import cfg @@ -87,3 +90,29 @@ class HostnameForm(forms.Form): validators.RegexValidator(HOSTNAME_REGEX, _('Invalid hostname')) ], strip=True) + + +def _domain_label_validator(domain_name): + """Validate domain name labels.""" + for label in domain_name.split('.'): + if not re.match(HOSTNAME_REGEX, label): + raise ValidationError(_('Invalid domain name')) + + +class DomainNameForm(forms.Form): + """Form to update system's static domain name.""" + + domain_name = forms.CharField( + label=_('Domain Name'), help_text=format_lazy( + _('Domain name is the global name by which other devices on the ' + 'Internet can reach your {box_name}. It must consist of ' + 'labels separated by dots. Each label must start and end ' + 'with an alphabet or a digit and have as interior characters ' + 'only alphabets, digits and hyphens. Length of each label ' + 'must be 63 characters or less. Total length of domain name ' + 'must be 253 characters or less.'), box_name=_(cfg.box_name)), + required=False, validators=[ + validators.RegexValidator( + r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$', + _('Invalid domain name')), _domain_label_validator + ], strip=True) diff --git a/plinth/modules/names/privileged.py b/plinth/modules/names/privileged.py index 96deb95a7..8d492085e 100644 --- a/plinth/modules/names/privileged.py +++ b/plinth/modules/names/privileged.py @@ -26,6 +26,32 @@ def set_hostname(hostname: str): action_utils.service_restart('avahi-daemon') +@privileged +def set_domain_name(domain_name: str | None = None): + """Set system's static domain name in /etc/hosts.""" + hostname = subprocess.check_output(['hostname']).decode().strip() + hosts_path = pathlib.Path('/etc/hosts') + if domain_name: + insert_line = f'127.0.1.1 {hostname}.{domain_name} {hostname}\n' + else: + insert_line = f'127.0.1.1 {hostname}\n' + + lines = hosts_path.read_text(encoding='utf-8').splitlines(keepends=True) + new_lines = [] + found = False + for line in lines: + if '127.0.1.1' in line: + new_lines.append(insert_line) + found = True + else: + new_lines.append(line) + + if not found: + new_lines.append(insert_line) + + hosts_path.write_text(''.join(new_lines), encoding='utf-8') + + @privileged def set_resolved_configuration(dns_fallback: bool | None = None, dns_over_tls: str | None = None, diff --git a/plinth/modules/names/tests/test_forms.py b/plinth/modules/names/tests/test_forms.py index 7faaa98ad..03c633698 100644 --- a/plinth/modules/names/tests/test_forms.py +++ b/plinth/modules/names/tests/test_forms.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Tests for forms in names app.""" -from ..forms import HostnameForm +from ..forms import DomainNameForm, HostnameForm def test_hostname_field(): @@ -22,3 +22,25 @@ def test_hostname_field(): for hostname in invalid_hostnames: form = HostnameForm({'hostname': hostname}) assert not form.is_valid() + + +def test_domain_name_field(): + """Test that domain name field accepts only valid domain names.""" + valid_domain_names = [ + '', 'a', '0a', 'a0', 'AAA', '00', '0-0', 'example-hostname', 'example', + 'example.org', 'a.b.c.d', 'a-0.b-0.c-0', + '012345678901234567890123456789012345678901234567890123456789012', + ((('x' * 63) + '.') * 3) + 'x' * 61 + ] + invalid_domain_names = [ + '-', '-a', 'a-', '.a', 'a.', '?', 'a?a', 'a..a', 'a.-a', '.', + ((('x' * 63) + '.') * 3) + 'x' * 62, 'x' * 64 + ] + + for domain_name in valid_domain_names: + form = DomainNameForm({'domain_name': domain_name}) + assert form.is_valid() + + for domain_name in invalid_domain_names: + form = DomainNameForm({'domain_name': domain_name}) + assert not form.is_valid() diff --git a/plinth/modules/names/tests/test_functional.py b/plinth/modules/names/tests/test_functional.py index 2e716de96..f29328646 100644 --- a/plinth/modules/names/tests/test_functional.py +++ b/plinth/modules/names/tests/test_functional.py @@ -26,3 +26,18 @@ def test_change_hostname(session_browser): def _get_hostname(browser): functional.visit(browser, '/plinth/sys/names/hostname/') return browser.find_by_id('id_hostname-hostname').value + + +def test_change_domain_name(session_browser): + """Test changing the domain name.""" + functional.set_domain_name(session_browser, 'mydomain.example') + assert _get_domain_name(session_browser) == 'mydomain.example' + + # Capitalization is ignored. + functional.set_domain_name(session_browser, 'Mydomain.example') + assert _get_domain_name(session_browser) == 'mydomain.example' + + +def _get_domain_name(browser): + functional.visit(browser, '/plinth/sys/names/domains/') + return browser.find_by_id('id_domain-name-domain_name').value diff --git a/plinth/modules/names/tests/test_names.py b/plinth/modules/names/tests/test_names.py index 4d42ae7d6..c58e4c8a1 100644 --- a/plinth/modules/names/tests/test_names.py +++ b/plinth/modules/names/tests/test_names.py @@ -40,4 +40,4 @@ def test_on_domain_removed(): # try to remove things that don't exist on_domain_removed('', '') with pytest.raises(KeyError): - on_domain_removed('', 'domainname', 'iiiii') + on_domain_removed('', 'some-domain-type', 'x-unknown-name') diff --git a/plinth/modules/names/urls.py b/plinth/modules/names/urls.py index ad92b66f5..7f8fe3b17 100644 --- a/plinth/modules/names/urls.py +++ b/plinth/modules/names/urls.py @@ -11,4 +11,6 @@ urlpatterns = [ re_path(r'^sys/names/$', views.NamesAppView.as_view(), name='index'), re_path(r'^sys/names/hostname/$', views.HostnameView.as_view(), name='hostname'), + re_path(r'^sys/names/domains/$', views.DomainNameView.as_view(), + name='domains'), ] diff --git a/plinth/modules/names/views.py b/plinth/modules/names/views.py index 75bc75169..b5e94dcd1 100644 --- a/plinth/modules/names/views.py +++ b/plinth/modules/names/views.py @@ -3,16 +3,21 @@ FreedomBox app for name services. """ +import logging + from django.contrib import messages from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic.edit import FormView from plinth.modules import names +from plinth.signals import domain_added, domain_removed from plinth.views import AppView from . import components, privileged, resolved -from .forms import HostnameForm, NamesConfigurationForm +from .forms import DomainNameForm, HostnameForm, NamesConfigurationForm + +logger = logging.getLogger(__name__) class NamesAppView(AppView): @@ -93,6 +98,40 @@ class HostnameView(FormView): return super().form_valid(form) +class DomainNameView(FormView): + """View to update system's static domain name.""" + template_name = 'form.html' + form_class = DomainNameForm + prefix = 'domain-name' + success_url = reverse_lazy('names:index') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Set Domain Name') + return context + + def get_initial(self): + """Return the values to fill in the form.""" + initial = super().get_initial() + initial['domain_name'] = names.get_domain_name() + return initial + + def form_valid(self, form): + """Apply the form changes.""" + if form.initial['domain_name'] != form.cleaned_data['domain_name']: + try: + set_domain_name(form.cleaned_data['domain_name']) + messages.success(self.request, _('Configuration updated')) + except Exception as exception: + messages.error( + self.request, + _('Error setting domain name: {exception}').format( + exception=exception)) + + return super().form_valid(form) + + def get_status(): """Get configured services per name.""" domains = components.DomainName.list() @@ -103,3 +142,26 @@ def get_status(): ] return {'domains': domains, 'unused_domain_types': unused_domain_types} + + +def set_domain_name(domain_name): + """Set system's static domain name to domain_name.""" + old_domain_name = names.get_domain_name() + + # Domain name is not case sensitive, but Let's Encrypt certificate + # paths use lower-case domain name. + domain_name = domain_name.lower() + + logger.info('Changing domain name to - %s', domain_name) + privileged.set_domain_name(domain_name) + + # Update domain registered with Name Services module. + if old_domain_name: + domain_removed.send_robust(sender='names', + domain_type='domain-type-static', + name=old_domain_name) + + if domain_name: + domain_added.send_robust(sender='names', + domain_type='domain-type-static', + name=domain_name, services='__all__') diff --git a/plinth/modules/openvpn/views.py b/plinth/modules/openvpn/views.py index 3d62d2c36..1c83bd998 100644 --- a/plinth/modules/openvpn/views.py +++ b/plinth/modules/openvpn/views.py @@ -5,7 +5,7 @@ import logging from django.http import HttpResponse -from plinth.modules import config, names +from plinth.modules import names from plinth.views import AppView from . import privileged @@ -23,12 +23,12 @@ class OpenVPNAppView(AppView): def profile(request): """Provide the user's profile for download.""" username = request.user.username - domainname = config.get_domainname() + domain_name = names.get_domain_name() - if not config.get_domainname(): - domainname = names.get_hostname() + if not domain_name: + domain_name = names.get_hostname() - profile_string = privileged.get_profile(username, domainname) + profile_string = privileged.get_profile(username, domain_name) response = HttpResponse(profile_string, content_type='application/x-openvpn-profile') response['Content-Disposition'] = \ diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 8072ed4fe..49d212b1c 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -461,15 +461,6 @@ def app_can_be_disabled(browser, app_name): return bool(button) -######################### -# Domain name utilities # -######################### -def set_domain_name(browser, domain_name): - nav_to_module(browser, 'config') - browser.find_by_id('id_domainname').fill(domain_name) - submit(browser, form_class='form-configuration') - - ######################## # Front page utilities # ######################## @@ -521,6 +512,12 @@ def set_hostname(browser, hostname): submit(browser, form_class='form-hostname') +def set_domain_name(browser, domain_name): + visit(browser, '/plinth/sys/names/domains/') + browser.find_by_id('id_domain-name-domain_name').fill(domain_name) + submit(browser, form_class='form-domain-name') + + ############################## # System -> Config utilities # ##############################