diff --git a/actions/sharing b/actions/sharing
old mode 100644
new mode 100755
index a3dd88669..baac364a7
--- a/actions/sharing
+++ b/actions/sharing
@@ -1,7 +1,6 @@
#!/usr/bin/python3
-# -*- mode: python -*-
#
-# This file is part of Plinth.
+# 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
@@ -16,77 +15,177 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
-
"""
-Configuration helper for the sharing module
+Configuration helper for the sharing app.
"""
import argparse
+import json
import os
+import pathlib
+import re
import augeas
+from plinth import action_utils
+
+APACHE_CONFIGURATION = '/etc/apache2/conf-available/sharing-freedombox.conf'
+
def parse_arguments():
parser = argparse.ArgumentParser()
- subparsers = parser.add_subparsers(help='Sub command')
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- subparsers.add_parser('add', help='Add a new share')
- subparsers.add_parser('remove', help='Remove an existing share')
- subparsers.add_parser('list', help='Remove an existing share')
+ subparsers.add_parser('list', help='List all existing shares')
+
+ add_parser = subparsers.add_parser('add', help='Add a new share')
+ add_parser.add_argument('--name', required=True, help='Name of the share')
+ add_parser.add_argument('--path', required=True, help='Disk path to share')
+ add_parser.add_argument('--groups', nargs='*',
+ help='List of groups that can access the share')
+
+ remove_parser = subparsers.add_parser('remove',
+ help='Remove an existing share')
+ remove_parser.add_argument('--name', required=True,
+ help='Name of the share to remove')
subparsers.required = True
return parser.parse_args()
-def load_augeas(conf_file):
- aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
- augeas.Augeas.NO_MODL_AUTOLOAD)
+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.set('/augeas/load/Httpd/lens', 'Httpd.lns')
- aug.set('/augeas/load/Httpd/incl[last() + 1]', conf_file)
+ aug.set('/augeas/load/Httpd/incl[last() + 1]', APACHE_CONFIGURATION)
aug.load()
+ aug.defvar('conf', '/files' + APACHE_CONFIGURATION)
+
return aug
-# TODO: Handle the error case scenarios
def subcommand_add(arguments):
- share_url = arguments.url
- share_path = arguments.path
- share_user = arguments.user
+ """Add a share to Apache configuration."""
+ name = arguments.name
+ path = arguments.path
+ groups = arguments.groups
+ url = '/share/' + name
- aug = load_augeas('/etc/apache2/sites-available/sharing.conf')
- aug.defvar('conf', '/files/etc/apache2/sites-available/sharing.conf')
+ if not os.path.exists(APACHE_CONFIGURATION):
+ pathlib.Path(APACHE_CONFIGURATION).touch()
+
+ aug = load_augeas()
+ shares = _list(aug)
+ if any([share for share in shares if share['name'] == name]):
+ raise Exception('Share already present')
+
+ aug.set('$conf/directive[last() + 1]', 'Alias')
+ aug.set('$conf/directive[last()]/arg[1]', url)
+ aug.set('$conf/directive[last()]/arg[2]', path)
+
+ aug.set('$conf/Location[last() + 1]/arg', url)
+
+ aug.set('$conf/Location[last()]/directive[last() + 1]', 'Include')
+ aug.set('$conf/Location[last()]/directive[last()]/arg',
+ 'includes/freedombox-sharing.conf')
+ aug.set('$conf/Location[last()]/directive[last() + 1]', 'Include')
+ aug.set('$conf/Location[last()]/directive[last()]/arg',
+ 'includes/freedombox-single-sign-on.conf')
+
+ aug.set('$conf/Location[last()]/IfModule/arg', 'mod_auth_pubtkt.c')
+ aug.set('$conf/Location[last()]/IfModule/directive[1]', 'TKTAuthToken')
+ for group_name in groups:
+ aug.set('$conf/Location[last()]/IfModule/directive[1]/arg[last() + 1]',
+ group_name)
+
+ aug.save()
+
+ with action_utils.WebserverChange() as webserver_change:
+ webserver_change.enable('sharing-freedombox')
+
+
+def subcommand_remove(arguments):
+ """Remove a share from Apache configuration."""
+ url_to_remove = '/share/' + arguments.name
+
+ aug = load_augeas()
+
+ for directive in aug.match('$conf/directive'):
+ if aug.get(directive) != 'Alias':
+ continue
+
+ url = aug.get(directive + '/arg[1]')
+ if url == url_to_remove:
+ aug.remove(directive)
+
+ for location in aug.match('$conf/Location'):
+ url = aug.get(location + '/arg')
+ if url == url_to_remove:
+ aug.remove(location)
+
+ aug.save()
+
+ with action_utils.WebserverChange() as webserver_change:
+ webserver_change.enable('sharing-freedombox')
+
+
+def _get_name_from_url(url):
+ """Return the name of the share given the URL for it."""
+ matches = re.match(r'/share/([a-z0-9\-]*)', url)
+ if not matches:
+ raise ValueError
+
+ return matches[1]
+
+
+def _list(aug=None):
+ """List all Apache configuration shares."""
+ if not aug:
+ aug = load_augeas()
+
+ shares = []
+
+ for match in aug.match('$conf/directive'):
+ if aug.get(match) != 'Alias':
+ continue
+
+ url = aug.get(match + '/arg[1]')
+ path = aug.get(match + '/arg[2]')
- if os.path.exists(share_path):
try:
- aug.set('$conf/directive[last() + 1]', 'Alias')
- aug.set('$conf/directive[last()]/arg[last() + 1]', share_url)
- aug.set('$conf/directive[last()]/arg[last() + 1]', share_path)
+ name = _get_name_from_url(url)
+ shares.append({
+ 'name': name,
+ 'path': path,
+ 'url': '/share/' + name
+ })
+ except ValueError:
+ continue
- aug.set('$conf/Directory[last() + 1]')
- aug.set('$conf/Directory[last()]/arg', share_path)
- aug.set('$conf/Directory[last()]/directive[last() + 1]', 'Include')
- aug.set('$conf/Directory[last()]/directive[last()]/arg', 'includes/freedombox-sharing.conf')
- aug.set('$conf/Directory[last()]/directive[last() + 1]', 'Require')
- aug.set('$conf/Directory[last()]/directive[last()]/arg', share_user)
- except Exception:
- pass
+ for location in aug.match('$conf/Location'):
+ url = aug.get(location + '/arg')
+
+ try:
+ name = _get_name_from_url(url)
+ except ValueError:
+ continue
+
+ groups = []
+ for group in aug.match(location + '//directive["TKTAuthToken"]/arg'):
+ groups.append(aug.get(group))
+
+ for share in shares:
+ if share['name'] == name:
+ share['groups'] = groups
+
+ return shares
-def subcommand_list():
- aug = load_augeas('/etc/apache2/sites-available/sharing.conf')
- aug.defvar('conf', '/files/etc/apache2/sites-available/sharing.conf')
-
- path = '/files/etc/apache2/conf-available/sharing.conf/Directory'
- list_of_shares = []
- for match in aug.match(path):
- path = aug.get(match + '/arg')
- for directive_name in aug.match(match + '/directive'):
- if directive_name == 'Require':
- user = aug.get(directive_name + '/arg')
- list_of_shares.append(dict(path=path, user_group=user))
- return list_of_shares
+def subcommand_list(_):
+ """List all Apache configuration shares and print as JSON."""
+ print(json.dumps({'shares': _list()}))
def main():
diff --git a/data/etc/apache2/conf-available/sharing.conf b/data/etc/apache2/conf-available/sharing.conf
deleted file mode 100644
index e69de29bb..000000000
diff --git a/data/etc/apache2/includes/freedombox-sharing.conf b/data/etc/apache2/includes/freedombox-sharing.conf
index 388435715..c8d6f5b25 100644
--- a/data/etc/apache2/includes/freedombox-sharing.conf
+++ b/data/etc/apache2/includes/freedombox-sharing.conf
@@ -1 +1,2 @@
-Options -FollowSymLinks
\ No newline at end of file
+Options +Indexes -FollowSymLinks -ExecCGI -Includes -IncludesNOEXEC
+AllowOverride None
diff --git a/plinth/modules/sharing/__init__.py b/plinth/modules/sharing/__init__.py
index 597b24c2b..ed7527a32 100644
--- a/plinth/modules/sharing/__init__.py
+++ b/plinth/modules/sharing/__init__.py
@@ -1,5 +1,5 @@
#
-# This file is part of Plinth.
+# 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
@@ -14,94 +14,46 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
-
"""
-Plinth module to configure sharing.
+FreedomBox app to configure sharing.
"""
-import os
-from django import forms
-from django.template.response import TemplateResponse
-from django.urls import reverse_lazy
-from django.utils.translation import ugettext_lazy as _, ugettext_lazy
+import json
+
+from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth.menu import main_menu
-from plinth.modules.users import groups
version = 1
name = _('Sharing')
-short_description = _('File Sharing')
-
description = [
- _('Sharing allows you to share your content over web with a group of '
- 'users. Add the content you would like to share in the sharing app.'),
-
- _('Sharing app will be available from '
- '/sharing path on the web server.'),
+ _('Sharing allows you to share your content over web with chosen groups'
+ 'of users. Add the content you would like to share in the sharing app.'),
]
-subsubmenu = [{'url': reverse_lazy('sharing:about'),
- 'text': ugettext_lazy('About')},
- {'url': reverse_lazy('sharing:add_share'),
- 'text': ugettext_lazy('Add share')}]
-
def init():
"""Initialize the module."""
menu = main_menu.get('apps')
- menu.add_urlname(name, 'glyphicon-share', 'sharing:about')
+ menu.add_urlname(name, 'glyphicon-share', 'sharing:index')
-def index(request):
- return TemplateResponse(request, 'about.html',
- {'title': name,
- 'description': description,
- 'subsubmenu': subsubmenu})
+def list_shares():
+ """Return a list of shares."""
+ output = actions.superuser_run('sharing', ['list'])
+ return json.loads(output)['shares']
-# TODO: handle the error case
-def add_path_to_share(url, path, user_group):
- if os.path.exists(path):
- actions.superuser_run('sharing', options=['add', url, path, user_group])
- else:
- pass
+def add_share(name, path, groups):
+ """Add a new share by called the action script."""
+ actions.superuser_run(
+ 'sharing',
+ ['add', '--name', name, '--path', path, '--groups'] + groups)
-def share(request):
- if request.method == 'POST':
- form = AddShareForm(request.POST)
-
- if form.is_valid():
- path = form.cleaned_data['share_path']
- user_group = form.cleaned_data['user_group']
- share_url = 'share_' + path.split("/")[len(path.split("/")) - 1]
- add_path_to_share(share_url, path, user_group)
-
- form = AddShareForm()
-
- else:
- form = AddShareForm()
-
- return TemplateResponse(request, 'share.html',
- {'title': name,
- 'subsubmenu': subsubmenu,
- 'form': form})
-
-
-class AddShareForm(forms.Form):
- share_path = forms.CharField(
- label=_('Add path'),
- help_text=_('Add the path to the folder you want to share'))
-
- user_group = forms.ChoiceField(
- required=False,
- choices=groups,
- label=_('User-group'),
- initial=None)
-
- def __init__(self, *args, **kwargs):
- super(forms.Form, self).__init__(*args, **kwargs)
- return
+def remove_share(name):
+ """Remove a share by calling the action script."""
+ actions.superuser_run('sharing', ['remove', '--name', name])
diff --git a/plinth/modules/sharing/forms.py b/plinth/modules/sharing/forms.py
new file mode 100644
index 000000000..e36aa371c
--- /dev/null
+++ b/plinth/modules/sharing/forms.py
@@ -0,0 +1,64 @@
+#
+# 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 .
+#
+"""
+Django forms for sharing app.
+"""
+
+from django import forms
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.modules.users.forms import get_group_choices
+from plinth.modules import sharing
+
+
+class AddShareForm(forms.Form):
+ """Form to add a new share."""
+
+ name = forms.RegexField(
+ label=_('Name of the share'), strip=True, regex=r'^[a-z0-9]+$',
+ help_text=_(
+ 'A lowercase alpha-numeric string that uniquely identifies a '
+ 'share. Example: media.'))
+
+ path = forms.CharField(
+ label=_('Path to share'), strip=True, help_text=_(
+ 'Disk path to a folder on this server that you intend to share.'))
+
+ groups = forms.MultipleChoiceField(
+ widget=forms.CheckboxSelectMultiple,
+ label=_('User groups who can read the files in the share'),
+ help_text=_(
+ 'Users who have these permissions will also be able to read the '
+ 'files in the share.'))
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the form with extra request argument."""
+ super().__init__(*args, **kwargs)
+ self.fields['groups'].choices = get_group_choices()
+
+ def clean_name(self):
+ """Check if the name is valid."""
+ name = self.cleaned_data['name']
+ if 'name' in self.initial and name == self.initial['name']:
+ return name
+
+ if any((share for share in sharing.list_shares()
+ if name == share['name'])):
+ raise ValidationError(_('A share with this name already exists.'))
+
+ return name
diff --git a/plinth/modules/sharing/templates/about.html b/plinth/modules/sharing/templates/about.html
deleted file mode 100644
index 91e4bff4d..000000000
--- a/plinth/modules/sharing/templates/about.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "simple_service.html" %}
-{% comment %}
-#
-# This file is part of Plinth.
-#
-# 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 .
-#
-{% endcomment %}
-
-{% load i18n %}
-
-{% block configuration %}
-
-{% endblock %}
diff --git a/plinth/modules/sharing/templates/sharing.html b/plinth/modules/sharing/templates/sharing.html
new file mode 100644
index 000000000..c0264b7af
--- /dev/null
+++ b/plinth/modules/sharing/templates/sharing.html
@@ -0,0 +1,83 @@
+{% extends "base.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 .
+#
+{% endcomment %}
+
+{% load i18n %}
+
+{% block content %}
+
+
+ {% endif %}
+
+{% endblock %}
diff --git a/plinth/modules/sharing/templates/share.html b/plinth/modules/sharing/templates/sharing_add_edit.html
similarity index 64%
rename from plinth/modules/sharing/templates/share.html
rename to plinth/modules/sharing/templates/sharing_add_edit.html
index ceb22ac11..d2b752c95 100644
--- a/plinth/modules/sharing/templates/share.html
+++ b/plinth/modules/sharing/templates/sharing_add_edit.html
@@ -1,7 +1,7 @@
-{% extends "simple_service.html" %}
+{% extends "base.html" %}
{% comment %}
#
-# This file is part of Plinth.
+# 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
@@ -21,16 +21,23 @@
{% load bootstrap %}
{% load i18n %}
-{% block configuration %}
+{% block content %}
-
+ {{ form|bootstrap }}
+
+
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
+{% block page_js %}
+
+{% endblock %}
diff --git a/plinth/modules/sharing/urls.py b/plinth/modules/sharing/urls.py
index 4a85e3fe1..853f56490 100644
--- a/plinth/modules/sharing/urls.py
+++ b/plinth/modules/sharing/urls.py
@@ -1,5 +1,5 @@
#
-# This file is part of Plinth.
+# 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
@@ -14,16 +14,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
-
"""
-URLs for the sharing module.
+URLs for the sharing app.
"""
from django.conf.urls import url
-from . import index, share
+from .views import AddShareView, EditShareView, IndexView, remove
urlpatterns = [
- url(r'^apps/sharing/$', index, name='about'),
- url(r'^apps/sharing/add_share$', share, name='add_share'),
-]
\ No newline at end of file
+ url(r'^apps/sharing/$', IndexView.as_view(), name='index'),
+ url(r'^apps/sharing/add/$', AddShareView.as_view(), name='add'),
+ url(r'^apps/sharing/(?P[a-z0-9]+)/edit/$', EditShareView.as_view(),
+ name='edit'),
+ url(r'^apps/sharing/(?P[a-z0-9]+)/remove/$', remove, name='remove'),
+]
diff --git a/plinth/modules/sharing/views.py b/plinth/modules/sharing/views.py
new file mode 100644
index 000000000..ee6efbc38
--- /dev/null
+++ b/plinth/modules/sharing/views.py
@@ -0,0 +1,109 @@
+#
+# 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 .
+#
+"""
+Views for the sharing app.
+"""
+
+from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
+from django.http import Http404
+from django.shortcuts import redirect
+from django.urls import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+from django.views.decorators.http import require_POST
+from django.views.generic import FormView, TemplateView
+
+from plinth.modules import sharing
+
+from .forms import AddShareForm
+
+
+class IndexView(TemplateView):
+ """View to show list of shares."""
+ template_name = 'sharing.html'
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = sharing.name
+ context['description'] = sharing.description
+ context['shares'] = sharing.list_shares()
+ return context
+
+
+class AddShareView(SuccessMessageMixin, FormView):
+ """View to add a new share."""
+ form_class = AddShareForm
+ prefix = 'sharing'
+ template_name = 'sharing_add_edit.html'
+ success_url = reverse_lazy('sharing:index')
+ success_message = _('Share added.')
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = _('Add Share')
+ return context
+
+ def form_valid(self, form):
+ """Add the share on valid form submission."""
+ sharing.add_share(form.cleaned_data['name'], form.cleaned_data['path'],
+ form.cleaned_data['groups'])
+ return super().form_valid(form)
+
+
+class EditShareView(SuccessMessageMixin, FormView):
+ """View to edit an existing share."""
+ form_class = AddShareForm
+ prefix = 'sharing'
+ template_name = 'sharing_add_edit.html'
+ success_url = reverse_lazy('sharing:index')
+ success_message = _('Share edited.')
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = _('Edit Share')
+ return context
+
+ def get_initial(self):
+ """Load information about share being edited."""
+ try:
+ return [
+ share for share in sharing.list_shares()
+ if share['name'] == self.kwargs['name']
+ ][0]
+ except IndexError:
+ raise Http404
+
+ def form_valid(self, form):
+ """Add the share on valid form submission."""
+ if form.initial != form.cleaned_data:
+ sharing.remove_share(form.initial['name'])
+ sharing.add_share(form.cleaned_data['name'],
+ form.cleaned_data['path'],
+ form.cleaned_data['groups'])
+
+ return super().form_valid(form)
+
+
+@require_POST
+def remove(request, name):
+ """View to remove a share."""
+ sharing.remove_share(name)
+ messages.success(request, _('Share deleted.'))
+ return redirect(reverse_lazy('sharing:index'))