config: Add user websites as choices for homepage config.

Closes: #1981
Closes also most of threads in !1952.

Signed-off-by: Fioddor Superconcentrado <fioddor@gmail.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Fioddor Superconcentrado 2020-12-11 17:19:38 +01:00 committed by James Valleroy
parent f527d9af83
commit 337e8c28dd
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
8 changed files with 323 additions and 32 deletions

View File

@ -2,6 +2,7 @@
""" """
FreedomBox app for Apache server. FreedomBox app for Apache server.
""" """
import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -11,7 +12,7 @@ from plinth import cfg
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.utils import format_lazy from plinth.utils import format_lazy, is_valid_user_name
version = 8 version = 8
@ -19,8 +20,9 @@ is_essential = True
managed_services = ['apache2', 'uwsgi'] managed_services = ['apache2', 'uwsgi']
managed_packages = ['apache2', 'php-fpm', 'ssl-cert', 'uwsgi', managed_packages = [
'uwsgi-plugin-python3'] 'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
]
app = None app = None
@ -63,3 +65,81 @@ def setup(helper, old_version=None):
actions.superuser_run( actions.superuser_run(
'apache', 'apache',
['setup', '--old-version', str(old_version)]) ['setup', '--old-version', str(old_version)])
# (U)ser (W)eb (S)ites
def uws_usr2dir(user):
"""Returns the directory of the given user's website."""
return '/home/{}/public_html'.format(user)
def uws_usr2url(user):
"""Returns the url path of the given user's website."""
return '/~{}/'.format(user)
def uws_dir2usr(directory):
"""Returns the user of a given user website directory."""
if directory.startswith('/home/'):
pos_ini = 6
elif directory.startswith('home/'):
pos_ini = 5
else:
return None
pos_end = directory.find('/public_html')
if pos_end == -1:
return None
user = directory[pos_ini:pos_end]
return user if is_valid_user_name(user) else None
def uws_url2usr(url):
"""Returns the user of a given user website url path."""
MISSING = -1
pos_ini = url.find('~')
if pos_ini == MISSING:
return None
pos_end = url.find('/', pos_ini)
if pos_end == MISSING:
pos_end = len(url)
user = url[pos_ini + 1:pos_end]
return user if is_valid_user_name(user) else None
def uws_url2dir(url):
"""Returns the directory of the user's website for the given url path.
Note: It doesn't return the full OS file path to the url path!
"""
return uws_usr2dir(uws_url2usr(url))
def uws_dir2url(directory):
"""Returns the url base path of the user's website for the given OS path.
Note: It doesn't return the url path for the file!
"""
return uws_usr2url(uws_dir2usr(directory))
def get_users_with_website():
"""Returns a dictionary of users with actual website subdirectory."""
def lst_sub_dirs(directory):
"""Returns the list of subdirectories of the given directory."""
return [
name for name in os.listdir(directory)
if os.path.isdir(os.path.join(directory, name))
]
return {
name: uws_usr2url(name)
for name in lst_sub_dirs('/home') if os.path.isdir(uws_usr2dir(name))
}

View File

@ -72,6 +72,7 @@ def test_webserver_disable(superuser_run):
@patch('plinth.modules.apache.components.diagnose_url_on_all') @patch('plinth.modules.apache.components.diagnose_url_on_all')
def test_webserver_diagnose(diagnose_url_on_all, diagnose_url): def test_webserver_diagnose(diagnose_url_on_all, diagnose_url):
"""Test running diagnostics.""" """Test running diagnostics."""
def on_all_side_effect(url, check_certificate): def on_all_side_effect(url, check_certificate):
return [('test-result-' + url, 'success')] return [('test-result-' + url, 'success')]

View File

@ -0,0 +1,30 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Test module for (U)ser (Web) (S)ites.
"""
from plinth.modules.apache import (uws_usr2dir, uws_dir2url, uws_url2usr,
uws_usr2url, uws_url2dir, uws_dir2usr)
def test_uws_namings():
"""Test name solvers for user, url and directory of UWS."""
assert '/home/usr/public_html' == uws_usr2dir('usr')
assert '/~usr/' == uws_usr2url('usr')
f = uws_dir2usr
assert f('/home/usr/lacks/the/UWS/directory') is None
assert 'usr' == f('/home/usr/public_html/is/a/normal/UWS/file')
assert 'usr' == f('/home/usr/public_html/is/a/normal/UWS/path/')
assert '€.;#@|' == f('/home/€.;#@|/public_html/is/stange/but/valid/')
f = uws_url2usr
assert f('/usr/is/not/a/valid/UWS/url/due/to/missing/tilde') is None
assert 'usr' == f('whatever/~usr/is/considered/a/valid/UWS/path')
assert 'usr' == f('~usr')
assert 'usr' == f('~usr/')
assert 'usr' == f('/~usr/')
assert '/home/usr/public_html' == uws_url2dir('~usr/any/file')
assert '/~usr/' == uws_dir2url('/home/usr/public_html/path/to/file')

View File

@ -12,6 +12,8 @@ from django.utils.translation import ugettext_lazy as _
from plinth import actions from plinth import actions
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.modules.apache import (uws_url2usr, uws_usr2url,
get_users_with_website)
from plinth.modules.names.components import DomainType from plinth.modules.names.components import DomainType
from plinth.signals import domain_added from plinth.signals import domain_added
@ -81,13 +83,56 @@ def get_hostname():
return socket.gethostname() return socket.gethostname()
def _get_home_page_url(): def home_page_url2scid(url):
"""Returns the shortcut ID of the given home page url."""
if url in ('/plinth/', '/plinth', 'plinth'):
return 'plinth'
if url == '/index.html':
return 'apache-default'
if url and url.startswith('/~'):
return 'uws-{}'.format(uws_url2usr(url))
shortcuts = frontpage.Shortcut.list()
for shortcut in shortcuts:
if shortcut.url == url:
return shortcut.component_id
return None
def _home_page_scid2url(shortcut_id):
"""Returns the url for the given home page shortcut ID."""
if shortcut_id is None:
url = None
elif shortcut_id == 'plinth':
url = '/plinth/'
elif shortcut_id == 'apache-default':
url = '/index.html'
elif shortcut_id.startswith('uws-'):
user = shortcut_id[4:]
if user in get_users_with_website():
url = uws_usr2url(user)
else:
url = None
else:
shortcuts = frontpage.Shortcut.list()
aux = [
shortcut.url for shortcut in shortcuts
if shortcut_id == shortcut.component_id
]
url = aux[0] if 1 == len(aux) else None
return url
def _get_home_page_url(conf_file):
"""Get the default application for the domain.""" """Get the default application for the domain."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD) augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Httpd/lens', 'Httpd.lns') aug.set('/augeas/load/Httpd/lens', 'Httpd.lns')
conf_file = APACHE_HOMEPAGE_CONFIG if os.path.exists(
APACHE_HOMEPAGE_CONFIG) else FREEDOMBOX_APACHE_CONFIG
aug.set('/augeas/load/Httpd/incl[last() + 1]', conf_file) aug.set('/augeas/load/Httpd/incl[last() + 1]', conf_file)
aug.load() aug.load()
@ -103,35 +148,20 @@ def _get_home_page_url():
def get_home_page(): def get_home_page():
"""Return the shortcut ID that is set as current home page.""" """Return the shortcut ID that is set as current home page."""
url = _get_home_page_url() CONF_FILE = APACHE_HOMEPAGE_CONFIG if os.path.exists(
if url in ['/plinth/', '/plinth', 'plinth']: APACHE_HOMEPAGE_CONFIG) else FREEDOMBOX_APACHE_CONFIG
return 'plinth'
if url == '/index.html': url = _get_home_page_url(CONF_FILE)
return 'apache-default' return home_page_url2scid(url)
shortcuts = frontpage.Shortcut.list()
for shortcut in shortcuts:
if shortcut.url == url:
return shortcut.component_id
return None
def change_home_page(shortcut_id): def change_home_page(shortcut_id):
"""Change the FreedomBox's default redirect to URL of the shortcut """Change the FreedomBox's default redirect to URL of the shortcut
specified. specified.
""" """
if shortcut_id == 'plinth': url = _home_page_scid2url(shortcut_id)
url = '/plinth/' if url is None:
elif shortcut_id == 'apache-default': url = '/plinth/' # fall back to default url if scid is unknown.
url = '/index.html'
else:
shortcuts = frontpage.Shortcut.list()
url = [
shortcut.url for shortcut in shortcuts
if shortcut.component_id == shortcut_id
][0]
# URL may be a reverse_lazy() proxy # URL may be a reverse_lazy() proxy
actions.superuser_run('config', ['set-home-page', str(url)]) actions.superuser_run('config', ['set-home-page', str(url)])

View File

@ -14,6 +14,9 @@ from django.utils.translation import ugettext_lazy
from plinth import cfg, frontpage from plinth import cfg, frontpage
from plinth.utils import format_lazy from plinth.utils import format_lazy
from plinth.modules.apache import get_users_with_website
from . import home_page_url2scid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -32,9 +35,13 @@ def get_homepage_choices():
shortcuts = frontpage.Shortcut.list(web_apps_only=True) shortcuts = frontpage.Shortcut.list(web_apps_only=True)
shortcut_choices = [(shortcut.component_id, shortcut.name) shortcut_choices = [(shortcut.component_id, shortcut.name)
for shortcut in shortcuts if shortcut.is_enabled()] for shortcut in shortcuts if shortcut.is_enabled()]
uws_choices = \
[(home_page_url2scid(url),
format_lazy(ugettext_lazy("{user}'s website"), user=user))
for user, url in get_users_with_website().items()]
apache_default = ('apache-default', _('Apache Default')) apache_default = ('apache-default', _('Apache Default'))
plinth = ('plinth', _('FreedomBox Service (Plinth)')) plinth = ('plinth', _('FreedomBox Service (Plinth)'))
return [apache_default, plinth] + shortcut_choices return [apache_default, plinth] + uws_choices + shortcut_choices
class ConfigurationForm(forms.Form): class ConfigurationForm(forms.Form):

View File

@ -3,11 +3,16 @@
Tests for config module. Tests for config module.
""" """
import pytest
import os import os
from plinth import __main__ as plinth_main from unittest.mock import (patch, MagicMock)
from ..forms import ConfigurationForm from plinth import __main__ as plinth_main
from plinth.modules.apache import uws_usr2dir
from plinth.modules.config import (home_page_url2scid, get_home_page,
_home_page_scid2url, change_home_page)
from plinth.modules.config.forms import ConfigurationForm
def test_hostname_field(): def test_hostname_field():
@ -64,6 +69,111 @@ def test_domainname_field():
assert not form.is_valid() assert not form.is_valid()
def test_homepage_mapping():
"""Basic tests for homepage functions."""
f = home_page_url2scid
assert f(None) is None
assert f('/unknown/url') is None
assert 'plinth' == f('/plinth/')
assert 'plinth' == f('/plinth')
assert 'plinth' == f('plinth')
assert 'apache-default' == f('/index.html')
assert 'uws-user' == f('/~user')
assert 'uws-user' == f('/~user/whatever/else')
# assert 'config' == f('/plinth/apps/sharing/')
f = _home_page_scid2url
assert f(None) is None
assert '/plinth/' == f('plinth')
assert '/index.html' == f('apache-default')
def test_homepage_mapping_skip_ci():
"""Special tests for homepage functions."""
try:
UWS_DIRECTORY = uws_usr2dir(os.getlogin())
except OSError:
reason = "Needs access to ~/ directory. " \
+ "CI sandboxed workspace doesn't provide it."
pytest.skip(reason)
if os.path.exists(UWS_DIRECTORY):
reason = "UWS dir {} exists already.".format(UWS_DIRECTORY)
pytest.skip(reason)
f = _home_page_scid2url
os.mkdir(UWS_DIRECTORY)
assert '/~fbx/' == f('uws-fbx')
os.rmdir(UWS_DIRECTORY)
assert f('uws-fbx') is None
class Dict2Obj(object):
"""Mock object made out of any dict."""
def __init__(self, a_dict):
self.__dict__ = a_dict
@patch('plinth.frontpage.Shortcut.list', MagicMock(return_value=[
Dict2Obj({'url': 'url/for/'+id, 'component_id': id})
for id in ('a', 'b')
]))
def test_homepage_field():
"""Test homepage changes.
Test Cases:
1) FreedomBox Homepage (default),
2) Apache default,
3) A user's website of an...
3.1) unexisting user
3.2) existing user without a page
3.3) existing user page.
4) A FreedomBox App.
4.1) unknown app
4.2) uninstalled app
4.3) disabled app
4.4) enabled app
Note: If run on a pristine unconfigured FreedomBox, this test will leave
the homepage default-configured. (Imperfect cleanup in such case).
Pending: Specific test cases to distiguish 4.1,2,3.
Currently they share the same test case.
"""
try:
UWS_DIRECTORY = uws_usr2dir(os.getlogin())
except OSError:
reason = "Needs access to ~/ directory, etc. " \
+ "CI sandboxed workspace doesn't provide it."
pytest.skip(reason)
DEFAULT_HOME_PAGE = 'plinth'
ORIGINAL_HOME_PAGE = get_home_page() or DEFAULT_HOME_PAGE
if ORIGINAL_HOME_PAGE not in (DEFAULT_HOME_PAGE, None):
reason = "Unexpected home page {}.".format(ORIGINAL_HOME_PAGE)
pytest.skip(reason)
# invalid changes fall back to default:
for scid in ('uws-unexisting', 'uws-fbx', 'missing_app'):
change_home_page(scid)
assert get_home_page() == DEFAULT_HOME_PAGE
os.mkdir(UWS_DIRECTORY)
# valid changes actually happen:
for scid in ('b', 'a', 'uws-fbx', 'apache-default', 'plinth'):
change_home_page(scid)
assert get_home_page() == scid
# cleanup:
change_home_page(ORIGINAL_HOME_PAGE)
os.rmdir(UWS_DIRECTORY)
assert get_home_page() == ORIGINAL_HOME_PAGE
def test_locale_path(): def test_locale_path():
""" """
Test that the 'locale' directory is in the same folder as __main__.py. Test that the 'locale' directory is in the same folder as __main__.py.

View File

@ -10,11 +10,26 @@ import pytest
import ruamel.yaml import ruamel.yaml
from django.test.client import RequestFactory from django.test.client import RequestFactory
from plinth.utils import YAMLFile, is_user_admin from plinth.utils import YAMLFile, is_user_admin, is_valid_user_name
def test_is_valid_user_name():
"""Test valid user names in Debian."""
f = is_valid_user_name
assert not f('this_user_name_is_too_long_to_be_valid')
assert not f('-invalid')
assert not f('not\tvalid')
assert not f('not\nvalid')
assert not f('not valid')
assert not f('not:valid')
assert not f('not/valid')
assert not f('not\\valid')
assert f('€.;#@|')
class TestIsAdminUser: class TestIsAdminUser:
"""Test class for is_user_admin utility.""" """Test class for is_user_admin utility."""
@staticmethod @staticmethod
@pytest.fixture(name='web_request') @pytest.fixture(name='web_request')
def fixture_web_request(): def fixture_web_request():

View File

@ -58,6 +58,24 @@ def user_group_view(func, group_name):
return func return func
def is_valid_user_name(user_name):
"""Check if the given username is valid.
Note: Debian is VERY flexible with user names.
"""
if 32 < len(user_name):
return False
if user_name.startswith('-'):
return False
for forbidden in (' \n\t/\\:'):
if forbidden in user_name:
return False
return True
def is_user_admin(request, cached=False): def is_user_admin(request, cached=False):
"""Return whether user is an administrator.""" """Return whether user is an administrator."""
if not request.user.is_authenticated: if not request.user.is_authenticated: