diff --git a/actions/ikiwiki b/actions/ikiwiki deleted file mode 100755 index 4080f9bef..000000000 --- a/actions/ikiwiki +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Configuration helper for ikiwiki -""" - -import argparse -import os -import re -import shutil -import subprocess -import sys - -SETUP_WIKI = '/etc/ikiwiki/plinth-wiki.setup' -SETUP_BLOG = '/etc/ikiwiki/plinth-blog.setup' -SITE_PATH = '/var/www/ikiwiki' -WIKI_PATH = '/var/lib/ikiwiki' - - -def parse_arguments(): - """Return parsed command line arguments as dictionary.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - - # Setup ikiwiki site - subparsers.add_parser('setup', help='Perform first time setup operations') - - # Get wikis and blogs - subparsers.add_parser('get-sites', help='Get wikis and blogs') - - # Create a wiki - create_wiki = subparsers.add_parser('create-wiki', help='Create a wiki') - create_wiki.add_argument('--wiki_name', help='Name of new wiki') - create_wiki.add_argument('--admin_name', help='Administrator account name') - - # Create a blog - create_blog = subparsers.add_parser('create-blog', help='Create a blog') - create_blog.add_argument('--blog_name', help='Name of new blog') - create_blog.add_argument('--admin_name', help='Administrator account name') - - # Delete a wiki or blog - delete = subparsers.add_parser('delete', help='Delete a wiki or blog.') - delete.add_argument('--name', help='Name of wiki or blog to delete.') - - subparsers.required = True - return parser.parse_args() - - -def _is_safe_path(basedir, path): - """Return whether a path is safe.""" - return os.path.realpath(path).startswith(basedir) - - -def subcommand_setup(_): - """Perform first time setup operations.""" - setup() - - -def get_title(site): - """Get blog or wiki title""" - try: - with open(os.path.join(SITE_PATH, site, 'index.html'), - encoding='utf-8') as index_file: - match = re.search(r'(.*)', index_file.read()) - if match: - return match[1] - except FileNotFoundError: - pass - - return site - - -def subcommand_get_sites(_): - """Get wikis and blogs.""" - if os.path.exists(SITE_PATH): - for site in os.listdir(SITE_PATH): - if not os.path.isdir(os.path.join(SITE_PATH, site)): - continue - - title = get_title(site) - print(site, title) - - -def subcommand_create_wiki(arguments): - """Create a wiki.""" - pw_bytes = sys.stdin.read().encode() - proc = subprocess.Popen([ - 'ikiwiki', '-setup', SETUP_WIKI, arguments.wiki_name, - arguments.admin_name - ], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, - env=dict(os.environ, PERL_UNICODE='AS')) - outs, errs = proc.communicate(input=pw_bytes + b'\n' + pw_bytes) - print(outs) - print(errs) - - -def subcommand_create_blog(arguments): - """Create a blog.""" - pw_bytes = sys.stdin.read().encode() - proc = subprocess.Popen([ - 'ikiwiki', '-setup', SETUP_BLOG, arguments.blog_name, - arguments.admin_name - ], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, - env=dict(os.environ, PERL_UNICODE='AS')) - outs, errs = proc.communicate(input=pw_bytes + b'\n' + pw_bytes) - print(outs) - print(errs) - - -def subcommand_delete(arguments): - """Delete a wiki or blog.""" - html_folder = os.path.join(SITE_PATH, arguments.name) - wiki_folder = os.path.join(WIKI_PATH, arguments.name) - - if not (_is_safe_path(SITE_PATH, html_folder) - and _is_safe_path(WIKI_PATH, wiki_folder)): - print('Error: {0} is not a correct name.'.format(arguments.name)) - exit(1) - - try: - shutil.rmtree(html_folder) - shutil.rmtree(wiki_folder) - shutil.rmtree(wiki_folder + '.git') - os.remove(wiki_folder + '.setup') - print('Deleted {0}'.format(arguments.name)) - except FileNotFoundError: - print('Error: {0} not found.'.format(arguments.name)) - exit(1) - - -def setup(): - """Write Apache configuration and wiki/blog setup scripts.""" - if not os.path.exists(SITE_PATH): - os.makedirs(SITE_PATH) - - -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() diff --git a/plinth/modules/ikiwiki/__init__.py b/plinth/modules/ikiwiki/__init__.py index f237a6657..72ab46354 100644 --- a/plinth/modules/ikiwiki/__init__.py +++ b/plinth/modules/ikiwiki/__init__.py @@ -1,12 +1,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox app to configure ikiwiki. -""" +"""FreedomBox app to configure ikiwiki.""" from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from plinth import actions from plinth import app as app_module from plinth import cfg, frontpage, menu from plinth.modules.apache.components import Webserver @@ -16,7 +13,7 @@ from plinth.modules.users.components import UsersAndGroups from plinth.package import Packages from plinth.utils import format_lazy -from . import manifest +from . import manifest, privileged _description = [ _('ikiwiki is a simple wiki and blog application. It supports ' @@ -100,9 +97,8 @@ class IkiwikiApp(app_module.App): component.remove() # Remove from global list. def refresh_sites(self): - """Refresh blog and wiki list""" - sites = actions.run('ikiwiki', ['get-sites']).split('\n') - sites = [name.split(' ', 1) for name in sites if name != ''] + """Refresh blog and wiki list.""" + sites = privileged.get_sites() for site in sites: if not 'shortcut-ikiwiki-' + site[0] in self.components: @@ -113,5 +109,5 @@ class IkiwikiApp(app_module.App): def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) - actions.superuser_run('ikiwiki', ['setup']) + privileged.setup() self.enable() diff --git a/plinth/modules/ikiwiki/privileged.py b/plinth/modules/ikiwiki/privileged.py new file mode 100644 index 000000000..2b61e9e5c --- /dev/null +++ b/plinth/modules/ikiwiki/privileged.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Configure ikiwiki.""" + +import os +import re +import shutil +import subprocess +from typing import Tuple + +from plinth.actions import privileged + +SETUP_WIKI = '/etc/ikiwiki/plinth-wiki.setup' +SETUP_BLOG = '/etc/ikiwiki/plinth-blog.setup' +SITE_PATH = '/var/www/ikiwiki' +WIKI_PATH = '/var/lib/ikiwiki' + + +def _is_safe_path(basedir, path): + """Return whether a path is safe.""" + return os.path.realpath(path).startswith(basedir) + + +@privileged +def setup(): + """Write Apache configuration and wiki/blog setup scripts.""" + if not os.path.exists(SITE_PATH): + os.makedirs(SITE_PATH) + + +def _get_title(site): + """Get blog or wiki title.""" + try: + with open(os.path.join(SITE_PATH, site, 'index.html'), + encoding='utf-8') as index_file: + match = re.search(r'(.*)', index_file.read()) + if match: + return match[1] + except FileNotFoundError: + pass + + return site + + +@privileged +def get_sites() -> list[Tuple[str, str]]: + """Get wikis and blogs.""" + sites = [] + if os.path.exists(SITE_PATH): + for site in os.listdir(SITE_PATH): + if not os.path.isdir(os.path.join(SITE_PATH, site)): + continue + + title = _get_title(site) + sites.append((site, title)) + + return sites + + +@privileged +def create_wiki(wiki_name: str, admin_name: str, admin_password: str): + """Create a wiki.""" + pw_bytes = admin_password.encode() + input_ = pw_bytes + b'\n' + pw_bytes + subprocess.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], + stdout=subprocess.PIPE, input=input_, + stderr=subprocess.PIPE, + env=dict(os.environ, PERL_UNICODE='AS'), check=True) + + +@privileged +def create_blog(blog_name: str, admin_name: str, admin_password: str): + """Create a blog.""" + pw_bytes = admin_password.encode() + input_ = pw_bytes + b'\n' + pw_bytes + subprocess.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], + stdout=subprocess.PIPE, input=input_, + stderr=subprocess.PIPE, env=dict(os.environ, + PERL_UNICODE='AS')) + + +@privileged +def delete(name: str): + """Delete a wiki or blog.""" + html_folder = os.path.join(SITE_PATH, name) + wiki_folder = os.path.join(WIKI_PATH, name) + + if not (_is_safe_path(SITE_PATH, html_folder) + and _is_safe_path(WIKI_PATH, wiki_folder)): + raise ValueError( + 'Error: {0} is not a correct wiki/blog name.'.format(name)) + + try: + shutil.rmtree(html_folder) + shutil.rmtree(wiki_folder) + shutil.rmtree(wiki_folder + '.git') + os.remove(wiki_folder + '.setup') + except FileNotFoundError: + raise RuntimeError('Unable to delete wiki/blog') diff --git a/plinth/modules/ikiwiki/views.py b/plinth/modules/ikiwiki/views.py index b7f7c4e20..4635cae8a 100644 --- a/plinth/modules/ikiwiki/views.py +++ b/plinth/modules/ikiwiki/views.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox app for configuring ikiwiki. -""" +"""FreedomBox app for configuring ikiwiki.""" from django.contrib import messages from django.shortcuts import redirect @@ -9,10 +7,10 @@ from django.template.response import TemplateResponse from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from plinth import actions from plinth import app as app_module from plinth import views +from . import privileged from .forms import IkiwikiCreateForm @@ -67,12 +65,9 @@ def create(request): def _create_wiki(request, name, admin_name, admin_password): """Create wiki.""" try: - actions.superuser_run( - 'ikiwiki', - ['create-wiki', '--wiki_name', name, '--admin_name', admin_name], - input=admin_password.encode()) + privileged.create_wiki(name, admin_name, admin_password) messages.success(request, _('Created wiki {name}.').format(name=name)) - except actions.ActionError as error: + except Exception as error: messages.error(request, _('Could not create wiki: {error}').format(error=error)) @@ -80,12 +75,9 @@ def _create_wiki(request, name, admin_name, admin_password): def _create_blog(request, name, admin_name, admin_password): """Create blog.""" try: - actions.superuser_run( - 'ikiwiki', - ['create-blog', '--blog_name', name, '--admin_name', admin_name], - input=admin_password.encode()) + privileged.create_blog(name, admin_name, admin_password) messages.success(request, _('Created blog {name}.').format(name=name)) - except actions.ActionError as error: + except Exception as error: messages.error(request, _('Could not create blog: {error}').format(error=error)) @@ -100,11 +92,11 @@ def delete(request, name): title = app.components['shortcut-ikiwiki-' + name].name if request.method == 'POST': try: - actions.superuser_run('ikiwiki', ['delete', '--name', name]) + privileged.delete(name) app.remove_shortcut(name) messages.success(request, _('{title} deleted.').format(title=title)) - except actions.ActionError as error: + except Exception as error: messages.error( request, _('Could not delete {title}: {error}').format(