Merge tag 'v19.23' into debian/buster-backports

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
James Valleroy 2019-12-19 10:03:55 -05:00
commit 06fe129bfb
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
243 changed files with 20305 additions and 15130 deletions

View File

@ -109,8 +109,9 @@ def subcommand_generate_ticket(arguments):
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, fil.read().encode())
valid_until = minutes_from_now(12 * 60)
grace_period = minutes_from_now(11 * 60)
print(create_ticket(pkey, uid, valid_until, tokens=tokens,
graceperiod=grace_period))
print(
create_ticket(pkey, uid, valid_until, tokens=tokens,
graceperiod=grace_period))
def minutes_from_now(minutes):
@ -120,8 +121,8 @@ def minutes_from_now(minutes):
def seconds_from_now(seconds):
"""Return a timestamp at the given number of seconds from now."""
return (
datetime.datetime.now() + datetime.timedelta(0, seconds)).timestamp()
return (datetime.datetime.now() +
datetime.timedelta(0, seconds)).timestamp()
def main():

View File

@ -66,8 +66,8 @@ def subcommand_reset_home_page(_):
config_file = FREEDOMBOX_APACHE_CONFIG
default_path = 'plinth'
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Httpd/lens', 'Httpd.lns')
aug.set('/augeas/load/Httpd/incl[last() + 1]', config_file)
aug.load()

View File

@ -72,8 +72,8 @@ def set_domain_name(domain_name):
# {'url': domain_name})
# Manually changing the domain name in the conf files.
conf_file = '/etc/diaspora.conf'
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# lens for shell-script config file
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')

View File

@ -62,8 +62,9 @@ def parse_arguments():
# Add a service
add_service = subparsers.add_parser('add-service', help='Add a service')
add_service.add_argument('service', help='Name of the service to add')
add_service.add_argument(
'--zone', help='Zone to which service is to be added', required=True)
add_service.add_argument('--zone',
help='Zone to which service is to be added',
required=True)
# Remove a service status
remove_service = subparsers.add_parser('remove-service',
@ -111,8 +112,8 @@ def _flush_iptables_rules():
def set_firewall_backend(backend):
"""Set FirewallBackend attribute to the specified string."""
conf_file = '/etc/firewalld/firewalld.conf'
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# lens for shell-script config file
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')

View File

@ -38,7 +38,6 @@ logger = logging.getLogger(__name__)
class ValidateRepoName(argparse.Action):
"""Validate a repository name and add .git extension if necessary."""
def __call__(self, parser, namespace, values, option_string=None):
RepositoryValidator()(values)
if not values.endswith('.git'):
@ -48,7 +47,6 @@ class ValidateRepoName(argparse.Action):
class ValidateRepoUrl(argparse.Action):
"""Validate a repository URL."""
def __call__(self, parser, namespace, values, option_string=None):
RepositoryValidator(input_should_be='url')(values)
setattr(namespace, self.dest, values)
@ -359,8 +357,9 @@ def subcommand_repo_info(arguments):
print(
json.dumps(
dict(name=arguments.name[:-4], description=_get_repo_description(
arguments.name), owner=_get_repo_owner(arguments.name),
dict(name=arguments.name[:-4],
description=_get_repo_description(arguments.name),
owner=_get_repo_owner(arguments.name),
access=_get_access_status(arguments.name))))

View File

@ -48,8 +48,8 @@ def parse_arguments():
help_private_mode = 'Enable/Disable/Status private mode.'
private_mode = subparsers.add_parser('private-mode',
help=help_private_mode)
private_mode.add_argument('command', choices=('enable', 'disable',
'status'),
private_mode.add_argument('command',
choices=('enable', 'disable', 'status'),
help=help_private_mode)
change_password = subparsers.add_parser('change-password',

View File

@ -15,7 +15,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Configuration helper for Minetest server.
"""
@ -25,7 +24,6 @@ import augeas
from plinth import action_utils
CONFIG_FILE = '/etc/minetest/minetest.conf'
AUG_PATH = '/files' + CONFIG_FILE + '/.anon'

114
actions/minidlna Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/python3
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Configuration actions for the minidlna server.
"""
import argparse
from tempfile import mkstemp
from shutil import move
import subprocess
from os import fdopen, remove, chmod, stat
import augeas
from plinth import action_utils
from plinth.utils import grep
CONFIG_PATH = '/etc/minidlna.conf'
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('setup', help='Setup SSH server')
subparsers.add_parser('get-media-dir', help='Get media directory')
set_media_dir = subparsers.add_parser('set-media-dir',
help='Set custom media directory')
set_media_dir.add_argument('--dir')
subparsers.required = True
return parser.parse_args()
def subcommand_setup(arguments):
"""
Increase inotify watches per folder to allow minidlna to
monitor changes in large media-dirs.
"""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Sysctl/lens', 'Sysctl.lns')
aug.set('/augeas/load/Sysctl/incl[last() + 1]', '/etc/sysctl.conf')
aug.load()
aug.set('/files/etc/sysctl.conf/fs.inotify.max_user_watches', '100000')
aug.save()
subprocess.run(['sysctl', '--system'], check=True)
def subcommand_get_media_dir(arguments):
"""Retrieve media directory from minidlna.conf"""
line = grep('^media_dir=', CONFIG_PATH)
print(line[0].split("=")[1])
def subcommand_set_media_dir(arguments):
"""Set media directory in minidlna.conf"""
line = grep('^media_dir=', CONFIG_PATH)[0]
new_line = 'media_dir=%s\n' % arguments.dir
replace_in_config_file(CONFIG_PATH, line, new_line)
if action_utils.service_is_running('minidlna'):
action_utils.service_restart('minidlna')
def replace_in_config_file(file_path, pattern, subst):
"""
Create a temporary minidlna.conf file,
replace the media dir config,
remove original one and move the temporary file.
Preserve permissions as the original file.
"""
temp_file, temp_file_path = mkstemp()
with fdopen(temp_file, 'w') as new_file:
with open(file_path) as old_file:
for line in old_file:
new_file.write(line.replace(pattern, subst))
old_st_mode = stat(file_path).st_mode
remove(file_path)
move(temp_file_path, file_path)
chmod(file_path, old_st_mode)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()

View File

@ -150,9 +150,8 @@ def get_monkeysphere_keys(key_id=None):
"""Return the list of keys imported into monkeysphere."""
try:
key_ids = [] if not key_id else [key_id]
output = subprocess.check_output(
['monkeysphere-host', 'show-keys'] + key_ids,
stderr=subprocess.DEVNULL)
output = subprocess.check_output(['monkeysphere-host', 'show-keys'] +
key_ids, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
# no keys available
return {}
@ -288,9 +287,10 @@ def subcommand_host_publish_key(arguments):
# setting TMPDIR as workaround for Debian bug #656750
proc = subprocess.Popen(
['monkeysphere-host', 'publish-keys'] + arguments.key_ids,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=dict(
os.environ, TMPDIR='/var/lib/monkeysphere/authentication/tmp/',
MONKEYSPHERE_PROMPT='false'))
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=dict(os.environ,
TMPDIR='/var/lib/monkeysphere/authentication/tmp/',
MONKEYSPHERE_PROMPT='false'))
output, error = proc.communicate()
output, error = output.decode(), error.decode()
if proc.returncode != 0:

79
actions/mumble Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/python3
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Configure Mumble server.
"""
import argparse
import sys
from subprocess import Popen, PIPE
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('create-password',
help='Setup mumble superuser password')
return parser.parse_args()
def read_from_stdin():
"""Read password from stdin"""
return (''.join(sys.stdin)).strip()
def subcommand_create_password(arguments):
"""Save superuser password with murmurd command"""
password = read_from_stdin()
cmd = ['murmurd', '-ini', '/etc/mumble-server.ini', '-readsupw']
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=False)
# The exit code of the command above seems to be 1 when successful!
# checking if the 'phrase' is included in the error message which
# shows that the password is successfully set.
out, err = proc.communicate(input=password.encode())
out, err = out.decode(), err.decode()
phrase = "Superuser password set on server"
if phrase not in err:
print(
"Error occured while saving password: %s" % err
)
sys.exit(1)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()

View File

@ -177,8 +177,8 @@ def subcommand_upgrade(_):
'renewed/private_by_serial', 'renewed/reqs_by_serial'
]
for dir_name in directories_to_create:
os.makedirs(
os.path.join(pki_dir, dir_name), mode=0o700, exist_ok=True)
os.makedirs(os.path.join(pki_dir, dir_name), mode=0o700,
exist_ok=True)
def _move_by_file_extension(file_extension, directory, excluded=None):
excluded = excluded or []
@ -217,7 +217,6 @@ def _write_server_config():
def _setup_firewall():
"""Add TUN device to internal zone in firewalld."""
def _configure_interface(interface, operation):
"""Add or remove an interface into internal zone."""
command = [
@ -299,9 +298,10 @@ def subcommand_get_profile(arguments):
user_key_string = _read_file(user_key)
ca_string = _read_file(CA_CERTIFICATE_PATH)
profile = CLIENT_CONFIGURATION.format(
ca=ca_string, cert=user_certificate_string, key=user_key_string,
remote=remote_server)
profile = CLIENT_CONFIGURATION.format(ca=ca_string,
cert=user_certificate_string,
key=user_key_string,
remote=remote_server)
print(profile)
@ -326,8 +326,8 @@ def _is_non_empty_file(filepath):
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# shell-script config file lens
aug.set('/augeas/load/Simplevars/lens', 'Simplevars.lns')

View File

@ -349,8 +349,9 @@ def _download_packages(packages):
return downloaded_files
def _get_conffile_hashes_from_downloaded_files(
packages, downloaded_files, status_hashes, mismatched_hashes):
def _get_conffile_hashes_from_downloaded_files(packages, downloaded_files,
status_hashes,
mismatched_hashes):
"""Retrieve the conffile hashes from downloaded .deb files."""
new_hashes = defaultdict(dict)
new_versions = defaultdict(lambda: None)
@ -369,8 +370,9 @@ def _get_conffile_hashes_from_downloaded_files(
return new_hashes, new_versions
def _get_conffile_hashes_from_downloaded_file(
packages, downloaded_file, status_hashes, mismatched_hashes):
def _get_conffile_hashes_from_downloaded_file(packages, downloaded_file,
status_hashes,
mismatched_hashes):
"""Retrieve the conffile hashes from a single downloaded .deb file."""
deb_file = apt_inst.DebFile(downloaded_file)

View File

@ -31,13 +31,18 @@ from plinth.modules.pagekite import utils
aug = None
PATHS = {
'service_on': os.path.join(utils.CONF_PATH, '*', 'service_on', '*'),
'kitename': os.path.join(utils.CONF_PATH, '10_account.rc', 'kitename'),
'kitesecret': os.path.join(utils.CONF_PATH, '10_account.rc', 'kitesecret'),
'abort_not_configured': os.path.join(utils.CONF_PATH, '10_account.rc',
'abort_not_configured'),
'defaults': os.path.join(utils.CONF_PATH, '20_frontends.rc', 'defaults'),
'frontend': os.path.join(utils.CONF_PATH, '20_frontends.rc', 'frontend'),
'service_on':
os.path.join(utils.CONF_PATH, '*', 'service_on', '*'),
'kitename':
os.path.join(utils.CONF_PATH, '10_account.rc', 'kitename'),
'kitesecret':
os.path.join(utils.CONF_PATH, '10_account.rc', 'kitesecret'),
'abort_not_configured':
os.path.join(utils.CONF_PATH, '10_account.rc', 'abort_not_configured'),
'defaults':
os.path.join(utils.CONF_PATH, '20_frontends.rc', 'defaults'),
'frontend':
os.path.join(utils.CONF_PATH, '20_frontends.rc', 'frontend'),
}
@ -50,9 +55,9 @@ def parse_arguments():
subparsers.add_parser('start-and-enable', help='Enable PageKite service')
subparsers.add_parser('stop-and-disable', help='Disable PageKite service')
subparsers.add_parser('restart', help='Restart PageKite service')
subparsers.add_parser('is-disabled',
help=('Whether PageKite is disabled in the file '
'/etc/pagekite.d/10_accounts.rc'))
subparsers.add_parser(
'is-disabled', help=('Whether PageKite is disabled in the file '
'/etc/pagekite.d/10_accounts.rc'))
# Frontend
subparsers.add_parser('get-frontend', help='Get pagekite frontend')

View File

@ -15,7 +15,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Configuration helper for power controls.
"""

View File

@ -132,8 +132,8 @@ def subcommand_fix_collections(_):
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# shell-script config file lens
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')

View File

@ -114,10 +114,7 @@ def _create_share(mount_point, windows_filesystem=False):
# FAT and NTFS partitions don't support setting permissions
if not windows_filesystem:
shutil.chown(open_share_path, group='freedombox-share')
os.chmod(open_share_path, 0o2775)
subprocess.check_call(['setfacl', '-Rm', 'g::rwx', open_share_path])
subprocess.check_call(['setfacl', '-Rdm', 'g::rwx', open_share_path])
_set_open_share_permissions(open_share_path)
share_name = _create_share_name(mount_point)
_define_open_share(share_name, open_share_path, windows_filesystem)
@ -166,9 +163,10 @@ def _get_shares():
config = configparser.ConfigParser()
config.read_string(output.decode())
for name in config.sections():
mount_point = _get_mount_point(config[name]['path'])
path = config[name]['path']
mount_point = _get_mount_point(path)
mount_point = os.path.normpath(mount_point)
shares.append(dict(name=name, mount_point=mount_point))
shares.append(dict(name=name, mount_point=mount_point, path=path))
return shares
@ -193,6 +191,23 @@ def _make_mounts_readable_by_others(mount_point):
os.chmod(dirname, stats.st_mode | stat.S_IROTH | stat.S_IXOTH)
def _set_open_share_permissions(directory):
"""Set file and directory permissions for open share."""
shutil.chown(directory, group='freedombox-share')
os.chmod(directory, 0o2775)
for root, dirs, files in os.walk(directory):
for subdir in dirs:
subdir_path = os.path.join(root, subdir)
shutil.chown(subdir_path, group='freedombox-share')
os.chmod(subdir_path, 0o2775)
for file in files:
file_path = os.path.join(root, file)
shutil.chown(file_path, group='freedombox-share')
os.chmod(file_path, 0o0664)
subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory])
subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory])
def _use_config_file(conf_file):
"""Set samba configuration file location."""
aug = augeas.Augeas(

View File

@ -81,8 +81,8 @@ def _update_uwsgi_configuration():
uwsgi 2.0.15-debian crashes when trying to autoload.
"""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/inifile/lens', 'Puppet.lns')
aug.set('/augeas/load/inifile/incl[last() + 1]', UWSGI_FILE)
aug.load()

View File

@ -35,12 +35,11 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('setup',
help='Perform initial setup steps')
subparsers.add_parser(
'get-config', help='Read and print JSON config to stdout')
subparsers.add_parser(
'merge-config', help='Merge JSON config from stdin with existing')
subparsers.add_parser('setup', help='Perform initial setup steps')
subparsers.add_parser('get-config',
help='Read and print JSON config to stdout')
subparsers.add_parser('merge-config',
help='Merge JSON config from stdin with existing')
subparsers.required = True
return parser.parse_args()

View File

@ -58,8 +58,8 @@ def parse_arguments():
def load_augeas():
"""Initialize augeas for this app's configuration file."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Httpd/lens', 'Httpd.lns')
aug.set('/augeas/load/Httpd/incl[last() + 1]', APACHE_CONFIGURATION)
aug.load()
@ -191,8 +191,9 @@ def _list(aug=None):
"""Must contain the line 'Require all granted'."""
require = location + '//directive["Require"]'
return bool(aug.match(require)) and aug.get(
require + '/arg[1]') == 'all' and aug.get(
require + '/arg[2]') == 'granted'
require +
'/arg[1]') == 'all' and aug.get(require +
'/arg[2]') == 'granted'
for share in shares:
if share['name'] == name:

View File

@ -117,8 +117,8 @@ def subcommand_set_keys(arguments):
def _load_augeas():
"""Initialize augeas for this app's configuration file."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Sshd/lens', 'Sshd.lns')
aug.set('/augeas/load/Sshd/incl[last() + 1]', '/etc/ssh/sshd_config')
aug.load()

View File

@ -96,11 +96,11 @@ def validate_mountpoint(mountpoint):
"""Check that the folder is empty, and create it if it doesn't exist"""
if os.path.exists(mountpoint):
if _is_mounted(mountpoint):
raise AlreadyMountedError(
'Mountpoint %s already mounted' % mountpoint)
raise AlreadyMountedError('Mountpoint %s already mounted' %
mountpoint)
if os.listdir(mountpoint) or not os.path.isdir(mountpoint):
raise ValueError(
'Mountpoint %s is not an empty directory' % mountpoint)
raise ValueError('Mountpoint %s is not an empty directory' %
mountpoint)
else:
os.makedirs(mountpoint)

View File

@ -21,6 +21,7 @@ Configuration helper for disks manager.
import argparse
import json
import os
import re
import subprocess
import sys
@ -51,6 +52,14 @@ def parse_arguments():
subparsers.add_parser('usage-info',
help='Get information about disk space usage')
subparser = subparsers.add_parser('validate-directory',
help='Validate a directory')
subparser.add_argument('--path', help='Path of the directory',
required=True)
subparser.add_argument('--check-writable', required=False, default=False,
action='store_true',
help='Check that the directory is writable')
subparsers.required = True
return parser.parse_args()
@ -316,6 +325,19 @@ def subcommand_usage_info(_):
subprocess.run(command, check=True)
def subcommand_validate_directory(arguments):
"""Validate a directory"""
directory = arguments.path
if not os.path.exists(directory):
print('ValidationError: 1')
if not os.path.isdir(directory):
print('ValidationError: 2')
if not os.access(directory, os.R_OK):
print('ValidationError: 3')
if arguments.check_writable and not os.access(directory, os.W_OK):
print('ValidationError: 4')
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()

View File

@ -114,8 +114,8 @@ def subcommand_setup(arguments):
def subcommand_autostart(_):
"""Automatically start all introducers and storage nodes on system startup.
"""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', DEFAULT_FILE)
aug.load()
@ -157,8 +157,9 @@ def subcommand_create_storage_node(_):
if not os.path.exists(os.path.join(tahoe_home, storage_node_name)):
subprocess.check_call([
'tahoe', 'create-node', '--nickname=\"storage_node\"',
'--webport=1234', '--hostname={}'.format(
get_configured_domain_name()), storage_node_name
'--webport=1234',
'--hostname={}'.format(get_configured_domain_name()),
storage_node_name
])
with open(
os.path.join(tahoe_home, introducer_name, 'private',

View File

@ -15,7 +15,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Set time zones with timedatectl (requires root permission).
"""

View File

@ -529,8 +529,8 @@ def _update_ports():
def augeas_load():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Tor/lens', 'Tor.lns')
aug.set('/augeas/load/Tor/incl[last() + 1]',
'/etc/tor/instances/plinth/torrc')

View File

@ -133,8 +133,8 @@ def _run_as_postgres(command, stdin=None, stdout=None):
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', DEFAULT_FILE)
aug.set('/augeas/load/Phpvars/lens', 'Phpvars.lns')

View File

@ -210,8 +210,8 @@ def configure_ldapscripts():
# modify a copy of the config file
shutil.copy('/etc/ldapscripts/ldapscripts.conf', LDAPSCRIPTS_CONF)
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', LDAPSCRIPTS_CONF)
aug.load()

65
debian/changelog vendored
View File

@ -1,3 +1,68 @@
plinth (19.23) unstable; urgency=medium
[ Thomas Vincent ]
* Translated using Weblate (French)
* Translated using Weblate (French)
[ Fred ]
* Translated using Weblate (French)
[ Alice Kile ]
* show app icons in apps page
* use single variable for referencing icon filename
* fix formatting issues
* fix formatting and template-related issues
* properly implement header in app and setup pages
* implement responsive layout for app page
* fix toggle button html layout and responsive design css
* config: fix minor syntax error
* fix: implement requested changes
[ James Valleroy ]
* themes: css whitespace minor fixes
* samba: Add icon to app page
* minidlna: Add managed service and Daemon component
* minidlna: Use single action to set media dir and restart
* minidlna: Show icon on app page
* minidlna: Fix webserver config name
* minidlna: Only show shortcut to users in group
* mumble: Keep icon_filename in moved view
* cockpit: Filter out localhost URLs from displayed access list
* users: Use service action to restart share group service
* locale: Update translation strings
* doc: Fetch latest manual
[ Veiko Aasa ]
* samba: recursively set open share directory permissions
* users: Fix functional tests changing the language feature
* app: Fix app checkbox status change functional tests
* storage: Directory selection form and validator
* transmission: New directory selection form
[ Nektarios Katakis ]
* feature: minidlna app
* fix: minidlna.conf file permissions after editing
* update minidlna svg
* run sysctl after installation
* mumble: Add option to set SuperUser password
* cockpit: extend apps description with access info
* cockpit: add list of valid urls to access the app.
[ /rgb ]
* Translated using Weblate (German)
* Translated using Weblate (German)
[ Luis A. Arizmendi ]
* Translated using Weblate (Spanish)
[ adaragao ]
* Translated using Weblate (Portuguese)
[ Michael Breidenbach ]
* Translated using Weblate (Swedish)
-- James Valleroy <jvalleroy@mailbox.org> Mon, 16 Dec 2019 18:38:46 -0500
plinth (19.22~bpo10+1) buster-backports; urgency=medium
* Rebuild for buster-backports.

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,12 @@
<articleinfo>
<title>FreedomBox/Manual</title>
<revhistory>
<revision>
<revnumber>78</revnumber>
<date>2019-12-15 19:26:31</date>
<authorinitials>Drahtseil</authorinitials>
<revremark>Added Gitweb (to the end of the manual, but I have no clue how this is sorted)</revremark>
</revision>
<revision>
<revnumber>77</revnumber>
<date>2019-04-30 00:45:49</date>
@ -3113,6 +3119,34 @@ echo "newpassword" | su mumble-server -s /bin/sh -c "/usr/sbin/murmurd -ini /etc
</itemizedlist>
</section>
</section>
<section>
<title>Simple Git Hosting (GitWeb)</title>
<para>Git is a distributed version-control system for tracking changes in source code during software development. GitWeb provides a web interface to Git repositories. You can browse history and content of source code, use search to find relevant commits and code. You can also clone repositories and upload code changes with a command-line Git client or with multiple available graphical clients. And you can share your code with people around the world. </para>
<para>To learn more on how to use Git visit <ulink url="https://git-scm.com/docs/gittutorial">Git tutorial</ulink>. </para>
<para><emphasis role="strong">Available since version:</emphasis> 19.19 </para>
<section>
<title>Managing the repositories</title>
<para>After installation of GitWeb, a new repository can be created. It can be marked as <emphasis>private</emphasis> to limit access. </para>
</section>
<section>
<title>Access</title>
<para>GitWeb can be accessed after installation e.g. by the web client through <ulink url="https://&lt;my"/> freedombox name&gt;/gitweb </para>
</section>
<section>
<title>HTTP basic auth</title>
<para>GitWeb on FreedomBox currently supports HTTP remotes only. To avoid having to enter the password each time you pull/push to the repository, you can edit your remote to include the credentials. </para>
<para>
<emphasis>Example:</emphasis>
<ulink url="https://username@password:my.freedombox.rocks/gitweb/myrepo"/>
</para>
<para>Your username and password will be encrypted. Someone monitoring the network traffic will notice the domain name only.</para>
<para><emphasis role="strong">Note:</emphasis> If using this method, your password will be stored in plain text in the local repository's .git/config file. </para>
</section>
<section>
<title>Mirroring</title>
<para>Though your repositories are primarily hosted on your own FreedomBox, you can configure a repository on another Git hosting system like <ulink url="https://wiki.debian.org/FreedomBox/Manual/GitLab#">GitLab</ulink> as a mirror. </para>
</section>
</section>
</section>
<section>
<title>System</title>
@ -9956,6 +9990,38 @@ wget https://www.thinkpenguin.com/files/ath9k_firmware_free-version/htc_7010.fw]
<section>
<title>Release Notes</title>
<para>The following are the release notes for each FreedomBox version. </para>
<section>
<title>FreedomBox 19.23 (2019-12-16)</title>
<itemizedlist>
<listitem>
<para>minidlna: New app for MiniDLNA (Simple Media Server) </para>
</listitem>
<listitem>
<para>apps: Show app icons in app pages </para>
</listitem>
<listitem>
<para>apps: Implement responsive layout for app pages </para>
</listitem>
<listitem>
<para>samba: Recursively set open share directory permissions </para>
</listitem>
<listitem>
<para>transmission: Add directory selection form </para>
</listitem>
<listitem>
<para>mumble: Add option to set SuperUser password </para>
</listitem>
<listitem>
<para>cockpit: Extend apps description with access info </para>
</listitem>
<listitem>
<para>cockpit: Add list of valid urls to access the app </para>
</listitem>
<listitem>
<para>Update translations for French, German, Spanish, Portuguese, and Swedish </para>
</listitem>
</itemizedlist>
</section>
<section>
<title>FreedomBox 19.22 (2019-12-02)</title>
<itemizedlist>

View File

@ -9806,6 +9806,38 @@ wget https://www.thinkpenguin.com/files/ath9k_firmware_free-version/htc_7010.fw]
<section>
<title>Release Notes</title>
<para>The following are the release notes for each FreedomBox version. </para>
<section>
<title>FreedomBox 19.23 (2019-12-16)</title>
<itemizedlist>
<listitem>
<para>minidlna: New app for MiniDLNA (Simple Media Server) </para>
</listitem>
<listitem>
<para>apps: Show app icons in app pages </para>
</listitem>
<listitem>
<para>apps: Implement responsive layout for app pages </para>
</listitem>
<listitem>
<para>samba: Recursively set open share directory permissions </para>
</listitem>
<listitem>
<para>transmission: Add directory selection form </para>
</listitem>
<listitem>
<para>mumble: Add option to set SuperUser password </para>
</listitem>
<listitem>
<para>cockpit: Extend apps description with access info </para>
</listitem>
<listitem>
<para>cockpit: Add list of valid urls to access the app </para>
</listitem>
<listitem>
<para>Update translations for French, German, Spanish, Portuguese, and Swedish </para>
</listitem>
</itemizedlist>
</section>
<section>
<title>FreedomBox 19.22 (2019-12-02)</title>
<itemizedlist>

View File

@ -107,8 +107,7 @@ def select_domain_name(browser, app_name, domain_name):
@given('the shadowsocks application is configured')
def configure_shadowsocks(browser):
application.configure_shadowsocks(browser, 'example.com',
'fakepassword')
application.configure_shadowsocks(browser, 'example.com', 'fakepassword')
@when(
@ -128,8 +127,8 @@ def assert_shadowsocks_configuration(browser, server, password):
password) == application.shadowsocks_get_configuration(browser)
@when(
parsers.parse('I modify the maximum file size of coquelicot to {size:d}'))
@when(parsers.parse('I modify the maximum file size of coquelicot to {size:d}')
)
def modify_max_file_size(browser, size):
application.modify_max_file_size(browser, size)
@ -372,38 +371,55 @@ def tahoe_given_add_introducer(browser, domain):
def tahoe_remove_introducer(browser, domain):
application.tahoe_remove_introducer(browser, domain)
@given('the access rights are set to "only the owner can view or make changes"')
@given('the access rights are set to "only the owner can view or make changes"'
)
def radicale_given_owner_only(browser):
application.radicale_set_access_rights(browser, 'owner_only')
@given('the access rights are set to "any user can view, but only the owner can make changes"')
@given(
'the access rights are set to "any user can view, but only the owner can make changes"'
)
def radicale_given_owner_write(browser):
application.radicale_set_access_rights(browser, 'owner_write')
@given('the access rights are set to "any user can view or make changes"')
def radicale_given_authenticated(browser):
application.radicale_set_access_rights(browser, 'authenticated')
@when('I change the access rights to "only the owner can view or make changes"')
@when('I change the access rights to "only the owner can view or make changes"'
)
def radicale_set_owner_only(browser):
application.radicale_set_access_rights(browser, 'owner_only')
@when('I change the access rights to "any user can view, but only the owner can make changes"')
@when(
'I change the access rights to "any user can view, but only the owner can make changes"'
)
def radicale_set_owner_write(browser):
application.radicale_set_access_rights(browser, 'owner_write')
@when('I change the access rights to "any user can view or make changes"')
def radicale_set_authenticated(browser):
application.radicale_set_access_rights(browser, 'authenticated')
@then('the access rights should be "only the owner can view or make changes"')
def radicale_check_owner_only(browser):
assert application.radicale_get_access_rights(browser) == 'owner_only'
@then('the access rights should be "any user can view, but only the owner can make changes"')
@then(
'the access rights should be "any user can view, but only the owner can make changes"'
)
def radicale_check_owner_write(browser):
assert application.radicale_get_access_rights(browser) == 'owner_write'
@then('the access rights should be "any user can view or make changes"')
def radicale_check_authenticated(browser):
assert application.radicale_get_access_rights(browser) == 'authenticated'
@ -451,8 +467,8 @@ def app_visible_on_front_page(browser, app_name):
assert len(shortcuts) == 1
@then(
parsers.parse('{app_name:w} app should not be visible on the front page'))
@then(parsers.parse('{app_name:w} app should not be visible on the front page')
)
def app_not_visible_on_front_page(browser, app_name):
shortcuts = application.find_on_front_page(browser, app_name)
assert len(shortcuts) == 0

View File

@ -19,7 +19,6 @@ from pytest_bdd import given, parsers, then, when
from support import config, interface
default_url = config['DEFAULT']['url']
@ -52,8 +51,8 @@ def new_user_does_not_exist(browser, name):
@given(parsers.parse('the user {name:w} exists'))
def test_user_exists(browser, name):
interface.nav_to_module(browser, 'users')
user_link = browser.find_link_by_href(
'/plinth/sys/users/' + name + '/edit/')
user_link = browser.find_link_by_href('/plinth/sys/users/' + name +
'/edit/')
if not user_link:
create_user(browser, name, 'secret123')

View File

@ -61,8 +61,9 @@ def verify_upload_password(browser, password):
'I upload the sample local file to coquelicot with password {password:w}'
))
def coquelicot_upload_file(browser, sample_local_file, password):
url = site.upload_file_to_coquelicot(
browser, sample_local_file['file_path'], password)
url = site.upload_file_to_coquelicot(browser,
sample_local_file['file_path'],
password)
sample_local_file['upload_url'] = url
@ -214,7 +215,8 @@ def syncthing_folder_not_present(browser, folder_name):
@given(
parsers.parse(
'folder {folder_path:S} is present as syncthing folder {folder_name:w}'))
'folder {folder_path:S} is present as syncthing folder {folder_name:w}'
))
def syncthing_folder_present(browser, folder_name, folder_path):
if not site.syncthing_folder_is_present(browser, folder_name):
site.syncthing_add_folder(browser, folder_name, folder_path)

View File

@ -98,30 +98,40 @@ def is_installed(browser, app_name):
return not bool(install_button)
def _change_status(browser, app_name, change_status_to='enabled',
checkbox_id=None):
interface.nav_to_module(browser, get_app_module(app_name))
checkbox_id = checkbox_id or get_app_checkbox_id(app_name)
checkbox = browser.find_by_id(checkbox_id)
def _change_app_status(browser, app_name, change_status_to='enabled'):
"""Enable or disable application."""
button = browser.find_by_id('app-toggle-button')
checkbox_id = get_app_checkbox_id(app_name)
checkbox = browser.find_by_id(checkbox_id)
if button:
if checkbox.checked and change_status_to == 'disabled' or (
not checkbox.checked and change_status_to == 'enabled'):
interface.submit(browser, element=button)
else:
checkbox.check(
) if change_status_to == 'enabled' else checkbox.uncheck()
interface.submit(browser, form_class='form-configuration')
_change_status(browser, app_name, checkbox_id, change_status_to)
if app_name in apps_with_loaders:
wait_for_config_update(browser, app_name)
def _change_status(browser, app_name, checkbox_id, change_status_to='enabled'):
"""Change checkbox status."""
checkbox = browser.find_by_id(checkbox_id)
checkbox.check() if change_status_to == 'enabled' else checkbox.uncheck()
interface.submit(browser, form_class='form-configuration')
if app_name in apps_with_loaders:
wait_for_config_update(browser, app_name)
def enable(browser, app_name):
_change_status(browser, app_name, 'enabled')
interface.nav_to_module(browser, get_app_module(app_name))
_change_app_status(browser, app_name, 'enabled')
def disable(browser, app_name):
_change_status(browser, app_name, 'disabled')
interface.nav_to_module(browser, get_app_module(app_name))
_change_app_status(browser, app_name, 'disabled')
def wait_for_config_update(browser, app_name):
@ -270,29 +280,27 @@ def verify_inaccessible_share(browser, name):
def enable_mediawiki_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'enabled',
checkbox_id='id_enable_public_registrations')
_change_status(browser, 'mediawiki', 'id_enable_public_registrations',
'enabled')
def disable_mediawiki_public_registrations(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'disabled',
checkbox_id='id_enable_public_registrations')
_change_status(browser, 'mediawiki', 'id_enable_public_registrations',
'disabled')
def enable_mediawiki_private_mode(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'enabled',
checkbox_id='id_enable_private_mode')
_change_status(browser, 'mediawiki', 'id_enable_private_mode', 'enabled')
def disable_mediawiki_private_mode(browser):
"""Enable public registrations in MediaWiki."""
interface.nav_to_module(browser, 'mediawiki')
_change_status(browser, 'mediawiki', 'disabled',
checkbox_id='id_enable_private_mode')
_change_status(browser, 'mediawiki', 'id_enable_private_mode', 'disabled')
def set_mediawiki_admin_password(browser, password):
@ -305,15 +313,13 @@ def set_mediawiki_admin_password(browser, password):
def enable_ejabberd_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
interface.nav_to_module(browser, 'ejabberd')
_change_status(browser, 'ejabberd', 'enabled',
checkbox_id='id_MAM_enabled')
_change_status(browser, 'ejabberd', 'id_MAM_enabled', 'enabled')
def disable_ejabberd_message_archive_management(browser):
"""Enable Message Archive Management in Ejabberd."""
interface.nav_to_module(browser, 'ejabberd')
_change_status(browser, 'ejabberd', 'disabled',
checkbox_id='id_MAM_enabled')
_change_status(browser, 'ejabberd', 'id_MAM_enabled', 'disabled')
def ejabberd_add_contact(browser):
@ -401,9 +407,8 @@ def _gitweb_get_repo_url(repo, with_auth):
if with_auth:
password = config['DEFAULT']['password']
return '{0}://{1}:{2}@{3}/gitweb/{4}'.format(scheme,
config['DEFAULT']['username'],
password, url, repo)
return '{0}://{1}:{2}@{3}/gitweb/{4}'.format(
scheme, config['DEFAULT']['username'], password, url, repo)
@contextlib.contextmanager

View File

@ -294,8 +294,8 @@ def transmission_remove_all_torrents(browser):
def transmission_upload_sample_torrent(browser):
"""Upload a sample torrent into transmission."""
browser.visit(config['DEFAULT']['url'] + '/transmission')
file_path = os.path.join(
os.path.dirname(__file__), '..', 'data', 'sample.torrent')
file_path = os.path.join(os.path.dirname(__file__), '..', 'data',
'sample.torrent')
browser.click_link_by_id('toolbar-open')
eventually(browser.is_element_not_present_by_css,
args=['#upload-container[style="display: none;"]'])
@ -396,9 +396,8 @@ def deluge_remove_all_torrents(browser):
browser.find_by_id('remove').first.click()
# Remove window shows up
assert eventually(
lambda: _deluge_get_active_window_title(browser) == 'Remove Torrent'
)
assert eventually(lambda: _deluge_get_active_window_title(browser) ==
'Remove Torrent')
_deluge_click_active_window_button(browser, 'Remove With Data')
@ -434,8 +433,8 @@ def deluge_upload_sample_torrent(browser):
eventually(
lambda: _deluge_get_active_window_title(browser) == 'Add Torrents')
file_path = os.path.join(
os.path.dirname(__file__), '..', 'data', 'sample.torrent')
file_path = os.path.join(os.path.dirname(__file__), '..', 'data',
'sample.torrent')
if browser.find_by_id('fileUploadForm'): # deluge-web 2.x
browser.attach_file('file', file_path)
@ -443,9 +442,8 @@ def deluge_upload_sample_torrent(browser):
browser.find_by_css('button.x-deluge-add-file').first.click()
# Add from file window appears
eventually(
lambda: _deluge_get_active_window_title(browser) == 'Add from File'
)
eventually(lambda: _deluge_get_active_window_title(browser) ==
'Add from File')
# Attach file
browser.attach_file('file', file_path)

View File

@ -95,7 +95,8 @@ def set_language(browser, language_code):
def check_language(browser, language_code):
nav_to_module(browser, 'config')
return browser.title == config_page_title_language_map[language_code]
return browser.find_by_css('.header-bar').first.find_by_tag(
'h2').first.value == config_page_title_language_map[language_code]
def delete_all_snapshots(browser):

View File

@ -18,4 +18,4 @@
Package init file.
"""
__version__ = '19.22'
__version__ = '19.23'

View File

@ -211,7 +211,6 @@ def webserver_disable(name, kind='config', apply_changes=True):
class WebserverChange(object):
"""Context to restart/reload Apache after configuration changes."""
def __init__(self):
"""Initialize the context object state."""
self.actions_required = set()
@ -425,9 +424,10 @@ def diagnose_url_on_all(url, **kwargs):
def diagnose_netcat(host, port, input='', negate=False):
"""Run a diagnostic using netcat."""
try:
process = subprocess.Popen(
['nc', host, str(port)], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process = subprocess.Popen(['nc', host, str(port)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.communicate(input=input.encode())
if process.returncode != 0:
result = 'failed'

View File

@ -114,7 +114,6 @@ class Component:
def enable(self):
"""Run operations to enable the component."""
def disable(self):
"""Run operations to disable the component."""

View File

@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Django context processors to provide common data to templates.
"""

View File

@ -23,7 +23,6 @@ from plinth import action_utils, actions, app
class Daemon(app.LeaderComponent):
"""Component to manage a background daemon or any systemd unit."""
def __init__(self, component_id, unit, strict_check=False):
"""Initialize a new daemon component.

View File

@ -83,7 +83,6 @@ class PackageHandler():
class DBusServer():
"""Abstraction over a connection to D-Bus."""
def __init__(self):
"""Initialize the server object."""
self.package_handler = None

View File

@ -43,7 +43,6 @@ class DomainSelectionForm(forms.Form):
"""Form for selecting a domain name to be used for
distributed federated applications
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -84,9 +83,9 @@ class LanguageSelectionFormMixin:
plinth_dir = os.path.dirname(plinth.__file__)
if language_code == 'en' or os.path.exists(
os.path.join(plinth_dir, 'locale', locale_code)):
supported_languages.append((language_code,
_get_local_name(
language_code, language_name)))
supported_languages.append(
(language_code,
_get_local_name(language_code, language_name)))
self.fields['language'].choices = supported_languages
@ -106,7 +105,6 @@ class CheckboxSelectMultipleWithReadOnly(forms.widgets.CheckboxSelectMultiple):
Derived from https://djangosnippets.org/snippets/2786/
"""
def render(self, name, value, attrs=None, choices=(), renderer=None):
if value is None:
value = []
@ -114,8 +112,8 @@ class CheckboxSelectMultipleWithReadOnly(forms.widgets.CheckboxSelectMultiple):
output = [u'<ul>']
global_readonly = 'readonly' in final_attrs
str_values = set([v for v in value])
for i, (option_value, option_label) in enumerate(
chain(self.choices, choices)):
for i, (option_value,
option_label) in enumerate(chain(self.choices, choices)):
if not global_readonly and 'readonly' in final_attrs:
# If the entire group is readonly keep all options readonly
del final_attrs['readonly']

View File

@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Simple key/value store using Django models
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,6 @@ logger = logging.getLogger(__name__)
class SetupMiddleware(MiddlewareMixin):
"""Django middleware to show pre-setup message and setup progress."""
@staticmethod
def process_view(request, view_func, view_args, view_kwargs):
"""Handle a request as Django middleware request handler."""
@ -77,8 +76,8 @@ class SetupMiddleware(MiddlewareMixin):
str(exception))
error_details = getattr(exception, 'error_details', '')
message = _('Error installing application: {string} '
'{details}').format(
string=error_string, details=error_details)
'{details}').format(string=error_string,
details=error_details)
else:
message = _('Error installing application: {error}') \
.format(error=exception)
@ -96,7 +95,6 @@ class SetupMiddleware(MiddlewareMixin):
class AdminRequiredMiddleware(MiddlewareMixin):
"""Django middleware for authenticating requests for admin areas."""
@staticmethod
def process_view(request, view_func, view_args, view_kwargs):
"""Reject non-admin access to views that are private and not marked."""
@ -110,7 +108,6 @@ class AdminRequiredMiddleware(MiddlewareMixin):
class FirstSetupMiddleware(MiddlewareMixin):
"""Django middleware to block all interactions before first setup."""
@staticmethod
def process_view(request, view_func, view_args, view_kwargs):
"""Block all user interactions when first setup is pending."""

View File

@ -18,7 +18,6 @@
#
# Generated by Django 1.9 on 2015-12-04 07:27
#
"""
Initial Django migration for FreedomBox to create database tables.
"""
@ -33,8 +32,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(

View File

@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Remove the deprecated KVStore entries 'setup_state' and 'firstboot_state',
and only use the new entry 'firstboot_completed' instead.
@ -63,8 +62,8 @@ def merge_firstboot_finished_fields(apps, schema_editor):
firstboot_completed = _object.value
# Set new 'firstboot_completed' if needed
new_firstboot_completed = bool(firstboot_completed or setup_state or
firstboot_state)
new_firstboot_completed = bool(firstboot_completed or setup_state
or firstboot_state)
if new_firstboot_completed and not firstboot_completed:
obj, created = KVStore.objects.get_or_create(key='firstboot_completed')
obj.value = 1

View File

@ -30,14 +30,17 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
('language', models.CharField(default=None, max_length=32,
null=True)),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL)),
], ),
('id',
models.AutoField(auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
('language',
models.CharField(default=None, max_length=32, null=True)),
('user',
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(code=insert_users,
reverse_code=truncate_user_profile),
]

View File

@ -54,9 +54,9 @@ class ApacheApp(app_module.App):
freedombox_ports = Firewall(
'firewall-plinth',
format_lazy(
_('{box_name} Web Interface (Plinth)'), box_name=_(
cfg.box_name)), ports=['http', 'https'], is_external=True)
format_lazy(_('{box_name} Web Interface (Plinth)'),
box_name=_(cfg.box_name)), ports=['http', 'https'],
is_external=True)
self.add(freedombox_ports)
letsencrypt = LetsEncrypt('letsencrypt-apache', domains='*',

View File

@ -23,7 +23,6 @@ from plinth import action_utils, actions, app
class Webserver(app.LeaderComponent):
"""Component to enable/disable Apache configuration."""
def __init__(self, component_id, web_name, kind='config'):
"""Initialize the web server component.
@ -62,7 +61,6 @@ class Webserver(app.LeaderComponent):
class Uwsgi(app.LeaderComponent):
"""Component to enable/disable uWSGI configuration."""
def __init__(self, component_id, uwsgi_name):
"""Initialize the uWSGI component.

View File

@ -14,10 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Apache module.
"""
urlpatterns = [
]
urlpatterns = []

View File

@ -45,9 +45,8 @@ def shortcuts(request, **kwargs):
# XXX: Get the module (or module name) from shortcut properly.
username = str(request.user) if request.user.is_authenticated else None
response = get_shortcuts_as_json(username)
return HttpResponse(
json.dumps(response, cls=DjangoJSONEncoder),
content_type='application/json')
return HttpResponse(json.dumps(response, cls=DjangoJSONEncoder),
content_type='application/json')
def get_shortcuts_as_json(username=None):

View File

@ -91,9 +91,10 @@ def init():
global app
app = AvahiApp()
if app.is_enabled():
domain_added.send_robust(
sender='avahi', domain_type='domain-type-local',
name=get_hostname() + '.local', services='__all__')
domain_added.send_robust(sender='avahi',
domain_type='domain-type-local',
name=get_hostname() + '.local',
services='__all__')
app.set_enabled(True)
post_hostname_change.connect(on_post_hostname_change)

View File

@ -108,9 +108,8 @@ def _backup_handler(packet, encryption_passphrase=None):
arguments = ['create-archive', '--path', packet.path, '--paths'] + paths
input_data = ''
if encryption_passphrase:
input_data = json.dumps({
'encryption_passphrase': encryption_passphrase
})
input_data = json.dumps(
{'encryption_passphrase': encryption_passphrase})
actions.superuser_run('backups', arguments, input=input_data.encode())

View File

@ -82,7 +82,6 @@ def _validate_service(service):
class BackupError:
"""Represent an backup/restore operation error."""
def __init__(self, error_type, app, hook=None):
"""Initialize the error object."""
self.error_type = error_type
@ -98,7 +97,6 @@ class BackupError:
class Packet:
"""Information passed to a handlers for backup/restore operations."""
def __init__(self, operation, scope, root, apps=None, path=None):
"""Initialize the packet.
@ -240,7 +238,6 @@ def _install_apps_before_restore(apps):
class BackupApp:
"""A application that can be backed up and its manifest."""
def __init__(self, name, app):
"""Initialize object and load manfiest."""
self.name = name
@ -365,7 +362,6 @@ def _switch_to_subvolume(subvolume):
class ServiceHandler:
"""Abstraction to help with service shutdown/restart."""
@staticmethod
def create(backup_app, service):
service_type = 'system'
@ -400,7 +396,6 @@ class ServiceHandler:
class SystemServiceHandler(ServiceHandler):
"""Handle starting and stopping of system services for backup."""
def __init__(self, backup_app, service):
"""Initialize the object."""
super().__init__(backup_app, service)
@ -420,7 +415,6 @@ class SystemServiceHandler(ServiceHandler):
class ApacheServiceHandler(ServiceHandler):
"""Handle starting and stopping of Apache services for backup."""
def __init__(self, backup_app, service):
"""Initialize the object."""
super().__init__(backup_app, service)

View File

@ -30,7 +30,6 @@ def delete_tmp_backup_file(function):
XXX: Implement a better way to delete uploaded files.
"""
@functools.wraps(function)
def wrapper(request, *args, **kwargs):
path = request.session.get(SESSION_PATH_VARIABLE, None)

View File

@ -59,10 +59,8 @@ KNOWN_ERRORS = [
'errors': [
'not a valid repository', 'does not exist', 'FileNotFoundError'
],
'message':
_('Repository not found'),
'raise_as':
errors.BorgRepositoryDoesNotExistError,
'message': _('Repository not found'),
'raise_as': errors.BorgRepositoryDoesNotExistError,
},
{
'errors': ['passphrase supplied in .* is incorrect'],
@ -176,7 +174,6 @@ class BaseBorgRepository(abc.ABC):
def remove(self):
"""Remove a borg repository"""
def list_archives(self):
"""Return list of archives in this repository."""
output = self.run(['list-repo', '--path', self.borg_path])
@ -241,7 +238,6 @@ class BaseBorgRepository(abc.ABC):
def get_download_stream(self, archive_name):
"""Return an stream of .tar.gz binary data for a backup archive."""
class BufferedReader(io.BufferedReader):
"""Improve performance of buffered binary streaming.
@ -255,7 +251,6 @@ class BaseBorgRepository(abc.ABC):
binary data.
"""
def __next__(self):
"""Override to call read() instead of readline()."""
chunk = self.read(io.DEFAULT_BUFFER_SIZE)

View File

@ -57,7 +57,6 @@ def _get_backup_app(name):
class TestBackupApp:
"""Test the BackupApp class."""
@staticmethod
def test_run_hook():
"""Test running a hook on an application."""
@ -82,7 +81,6 @@ class TestBackupApp:
@pytest.mark.usefixtures('load_cfg')
class TestBackupProcesses:
"""Test cases for backup processes"""
@staticmethod
def test_packet_process_manifests():
"""Test that directories/files are collected from manifests."""
@ -239,7 +237,6 @@ class TestBackupProcesses:
class TestBackupModule:
"""Tests of the backups django module, like views or forms."""
@staticmethod
def test_file_upload():
# posting a video should fail

View File

@ -44,6 +44,8 @@ managed_packages = ['cockpit']
name = _('Cockpit')
icon_filename = 'cockpit'
short_description = _('Server Administration')
description = [
@ -56,7 +58,11 @@ description = [
format_lazy(
_('It can be accessed by <a href="{users_url}">any user</a> on '
'{box_name} belonging to the admin group.'),
box_name=_(cfg.box_name), users_url=reverse_lazy('users:index'))
box_name=_(cfg.box_name), users_url=reverse_lazy('users:index')),
format_lazy(
_('Cockpit requires that you access it through a domain name. '
'It will not work when accessed using an IP address as part'
' of the URL.')),
]
manual_page = 'Cockpit'

View File

@ -0,0 +1,39 @@
{% extends "app.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block status %}
{{ block.super }}
<h3>{% trans "Access" %}</h3>
<p>
{% blocktrans trimmed %}
Cockpit will only work when accessed using the following URLs.
{% endblocktrans %}
</p>
<ol>
{% for url_ in urls %}
<li>{{ url_ }}</li>
{% endfor %}
</ol>
{% endblock %}

View File

@ -20,15 +20,8 @@ URLs for Cockpit module.
from django.conf.urls import url
from plinth.modules import cockpit
from plinth.views import AppView
from plinth.modules.cockpit.views import CockpitAppView
urlpatterns = [
url(
r'^sys/cockpit/$',
AppView.as_view(app_id='cockpit', name=cockpit.name,
diagnostics_module_name='cockpit',
description=cockpit.description,
show_status_block=True, clients=cockpit.clients,
manual_page=cockpit.manual_page), name='index'),
url(r'^sys/cockpit/$', CockpitAppView.as_view(), name='index'),
]

View File

@ -27,8 +27,8 @@ CONFIG_FILE = '/etc/cockpit/cockpit.conf'
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/inifile/lens', 'Puppet.lns')
aug.set('/augeas/load/inifile/incl[last() + 1]', CONFIG_FILE)
aug.load()

View File

@ -0,0 +1,47 @@
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Views for the Cockpit module
"""
from plinth.views import AppView
from plinth.modules.cockpit import (
name,
description,
clients,
manual_page,
icon_filename,
)
from plinth.modules.cockpit.utils import get_origin_domains, load_augeas
class CockpitAppView(AppView):
app_id = 'cockpit'
name = name
description = description
diagnostics_module_name = 'cockpit'
show_status_block = True
clients = clients
manual_page = manual_page
template_name = 'cockpit.html'
icon_filename = icon_filename
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
urls = get_origin_domains(load_augeas())
context['urls'] = [url for url in urls if 'localhost' not in url]
return context

View File

@ -36,6 +36,11 @@ is_essential = True
name = _('General Configuration')
description = [
_('Here you can set some general configuration options '
'like hostname, domain name, webserver home page etc.')
]
depends = ['firewall', 'names']
manual_page = 'Configure'
@ -81,8 +86,8 @@ def get_hostname():
def _get_home_page_url():
"""Get the default application for the domain."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
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

View File

@ -67,11 +67,10 @@ class ConfigurationForm(forms.Form):
'end with an alphabet or a digit and have as interior '
'characters only alphabets, digits and hyphens. Total '
'length must be 63 characters or less.'),
box_name=ugettext_lazy(cfg.box_name)),
validators=[
validators.RegexValidator(HOSTNAME_REGEX,
ugettext_lazy('Invalid hostname'))
], strip=True)
box_name=ugettext_lazy(cfg.box_name)), validators=[
validators.RegexValidator(HOSTNAME_REGEX,
ugettext_lazy('Invalid hostname'))
], strip=True)
domainname = forms.CharField(
label=ugettext_lazy('Domain Name'), help_text=format_lazy(
@ -83,12 +82,12 @@ class ConfigurationForm(forms.Form):
'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=ugettext_lazy(cfg.box_name)),
required=False, validators=[
validators.RegexValidator(
r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$',
ugettext_lazy('Invalid domain name')), domain_label_validator
], strip=True)
box_name=ugettext_lazy(cfg.box_name)), required=False, validators=[
validators.RegexValidator(
r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$',
ugettext_lazy('Invalid domain name')),
domain_label_validator
], strip=True)
homepage = forms.ChoiceField(
label=ugettext_lazy('Webserver Home Page'), help_text=format_lazy(

View File

@ -36,6 +36,7 @@ LOGGER = logging.getLogger(__name__)
class ConfigAppView(views.AppView):
"""Serve configuration page."""
name = config.name
description = config.description
form_class = ConfigurationForm
app_id = 'config'
manual_page = config.manual_page

View File

@ -38,6 +38,8 @@ managed_packages = ['deluged', 'deluge-web']
name = _('Deluge')
icon_filename = 'deluge'
short_description = _('BitTorrent Web Client')
description = [
@ -71,7 +73,7 @@ class DelugeApp(app_module.App):
shortcut = frontpage.Shortcut('shortcut-deluge', name,
short_description=short_description,
url='/deluge', icon='deluge',
url='/deluge', icon=icon_filename,
clients=clients, login_required=True,
allowed_groups=[group[0]])
self.add(shortcut)

Some files were not shown because too many files have changed in this diff Show More