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.
"""
import os
from django.utils.translation import ugettext_lazy as _
@ -11,7 +12,7 @@ from plinth import cfg
from plinth.daemon import Daemon
from plinth.modules.firewall.components import Firewall
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
@ -19,8 +20,9 @@ is_essential = True
managed_services = ['apache2', 'uwsgi']
managed_packages = ['apache2', 'php-fpm', 'ssl-cert', 'uwsgi',
'uwsgi-plugin-python3']
managed_packages = [
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
]
app = None
@ -63,3 +65,81 @@ def setup(helper, old_version=None):
actions.superuser_run(
'apache',
['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')
def test_webserver_diagnose(diagnose_url_on_all, diagnose_url):
"""Test running diagnostics."""
def on_all_side_effect(url, check_certificate):
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 app as app_module
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.signals import domain_added
@ -81,13 +83,56 @@ def get_hostname():
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."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
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.load()
@ -103,35 +148,20 @@ def _get_home_page_url():
def get_home_page():
"""Return the shortcut ID that is set as current home page."""
url = _get_home_page_url()
if url in ['/plinth/', '/plinth', 'plinth']:
return 'plinth'
CONF_FILE = APACHE_HOMEPAGE_CONFIG if os.path.exists(
APACHE_HOMEPAGE_CONFIG) else FREEDOMBOX_APACHE_CONFIG
if url == '/index.html':
return 'apache-default'
shortcuts = frontpage.Shortcut.list()
for shortcut in shortcuts:
if shortcut.url == url:
return shortcut.component_id
return None
url = _get_home_page_url(CONF_FILE)
return home_page_url2scid(url)
def change_home_page(shortcut_id):
"""Change the FreedomBox's default redirect to URL of the shortcut
specified.
"""
if shortcut_id == 'plinth':
url = '/plinth/'
elif shortcut_id == 'apache-default':
url = '/index.html'
else:
shortcuts = frontpage.Shortcut.list()
url = [
shortcut.url for shortcut in shortcuts
if shortcut.component_id == shortcut_id
][0]
url = _home_page_scid2url(shortcut_id)
if url is None:
url = '/plinth/' # fall back to default url if scid is unknown.
# URL may be a reverse_lazy() proxy
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.utils import format_lazy
from plinth.modules.apache import get_users_with_website
from . import home_page_url2scid
logger = logging.getLogger(__name__)
@ -32,9 +35,13 @@ def get_homepage_choices():
shortcuts = frontpage.Shortcut.list(web_apps_only=True)
shortcut_choices = [(shortcut.component_id, shortcut.name)
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'))
plinth = ('plinth', _('FreedomBox Service (Plinth)'))
return [apache_default, plinth] + shortcut_choices
return [apache_default, plinth] + uws_choices + shortcut_choices
class ConfigurationForm(forms.Form):

View File

@ -3,11 +3,16 @@
Tests for config module.
"""
import pytest
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():
@ -64,6 +69,111 @@ def test_domainname_field():
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():
"""
Test that the 'locale' directory is in the same folder as __main__.py.

View File

@ -10,11 +10,26 @@ import pytest
import ruamel.yaml
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:
"""Test class for is_user_admin utility."""
@staticmethod
@pytest.fixture(name='web_request')
def fixture_web_request():

View File

@ -58,6 +58,24 @@ def user_group_view(func, group_name):
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):
"""Return whether user is an administrator."""
if not request.user.is_authenticated: