diff --git a/INSTALL b/INSTALL index a08401cc5..3259610fe 100644 --- a/INSTALL +++ b/INSTALL @@ -29,6 +29,7 @@ python3-django-stronghold \ python3-gi \ python3-psutil \ + python3-requests \ python3-setuptools \ python3-yaml \ xmlto diff --git a/plinth/errors.py b/plinth/errors.py index 325413dfa..097095bcb 100644 --- a/plinth/errors.py +++ b/plinth/errors.py @@ -28,3 +28,8 @@ class PlinthError(Exception): class ActionError(PlinthError): """Use this error for exceptions when executing an action.""" pass + + +class DomainRegistrationError(PlinthError): + """Domain registration failed""" + pass diff --git a/plinth/modules/first_boot/forms.py b/plinth/modules/first_boot/forms.py index 78360bfd2..61101f232 100644 --- a/plinth/modules/first_boot/forms.py +++ b/plinth/modules/first_boot/forms.py @@ -19,13 +19,23 @@ Forms for first boot module. """ +import json +import logging +import requests + +from django import forms from django.contrib import auth from django.contrib import messages +from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from plinth import actions -from plinth.errors import ActionError +from plinth import cfg +from plinth.errors import ActionError, DomainRegistrationError +from plinth.modules.pagekite.utils import PREDEFINED_SERVICES, run from plinth.modules.users.forms import GROUP_CHOICES +from plinth.utils import format_lazy +LOGGER = logging.getLogger(__name__) class State1Form(auth.forms.UserCreationForm): @@ -77,3 +87,99 @@ class State1Form(auth.forms.UserCreationForm): else: message = _('User account created, you are now logged in') messages.success(self.request, message) + + +class SubdomainWidget(forms.widgets.TextInput): + """Append the domain to the subdomain bootstrap input field""" + def render(self, *args, **kwargs): + inputfield = super(SubdomainWidget, self).render(*args, **kwargs) + domain = State5Form.DOMAIN_APPENDIX + return """
+ {0} + {1} +
""".format(inputfield, domain) + + +class State5Form(forms.Form): + """Set up freedombox.me pagekite subdomain""" + DOMAIN_APPENDIX = ".freedombox.me" + # webservice url for domain validation and registration + service_url = "http://freedombox.me/cgi-bin/freedomkite.pl" + code_help_text = _("The voucher you received with your {box_name} Danube " + "Edition") + code = forms.CharField(help_text=format_lazy(code_help_text, + box_name=_(cfg.box_name))) + domain = forms.SlugField(label=_("Subdomain"), + widget=SubdomainWidget, + help_text=_("The subdomain you want to register")) + + def clean_domain(self): + """Append the domain to the users' subdomain""" + return self.cleaned_data['domain'] + self.DOMAIN_APPENDIX + + def clean(self): + """Validate user input (subdomain and code)""" + cleaned_data = super(State5Form, self).clean() + # if the subdomain is wrong don't look if the domain is available + if self.errors: + return cleaned_data + + self.domain_already_registered = False + code = cleaned_data.get("code") + domain = cleaned_data.get("domain") + + response = requests.get(self.service_url, params={'code': code}).json() + # The validation response looks like: + # 1. code invalid: {} + if 'domain' not in response: + raise ValidationError(_('This code is not valid'), code='invalid') + # 2. code valid, domain registered: {'domain': 'xx.freedombox.me'} + elif response['domain']: + if response['domain'] == domain: + self.domain_already_registered = True + else: + msg = _('This code is bound to the domain %s' % + response['domain']) + raise ValidationError(msg, code='invalid') + # 3. code valid, no domain registered: {'domain': None} + elif response['domain'] is None: + # make sure that the desired domain is available + data = {'domain': domain} + domain_response = requests.get(self.service_url, params=data) + registered_domain = domain_response.json()['domain'] + if registered_domain is not None: + msg = _('The requested Domain is already registered') + raise ValidationError(msg, code='invalid') + + return cleaned_data + + def register_domain(self): + """Register a domain (only if it's not already registered)""" + if not self.domain_already_registered: + data = {'domain': self.cleaned_data['domain'], + 'code': self.cleaned_data['code']} + response = requests.post(self.service_url, data) + if not response.ok: + msg = "Domain registration failed: %s" % response.text + LOGGER.error(msg) + raise DomainRegistrationError(msg) + + def setup_pagekite(self): + """Configure pagekite and enable the pagekite service""" + # set kite name and secret + run(['set-kite', '--kite-name', self.cleaned_data['domain']], + input=self.cleaned_data['code'].encode()) + + # set frontend + run(['set-frontend', '%s:80' % self.cleaned_data['domain']]) + + # enable pagekite http+https service + for service_name in ['http', 'https']: + service = PREDEFINED_SERVICES[service_name]['params'] + try: + run(['add-service', '--service', json.dumps(service)]) + except ActionError as err: + if 'already exists' not in str(err): + raise + + run(['start-and-enable']) diff --git a/plinth/modules/first_boot/templates/base_firstboot.html b/plinth/modules/first_boot/templates/base_firstboot.html new file mode 100644 index 000000000..1c523481c --- /dev/null +++ b/plinth/modules/first_boot/templates/base_firstboot.html @@ -0,0 +1,33 @@ +{% extends "base.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 static %} + +{% block page_head %} + +{% endblock %} + +{% block mainmenu_right %} + {% include "firstboot_navbar.html" %} +{% endblock %} diff --git a/plinth/modules/first_boot/templates/firstboot_state0.html b/plinth/modules/first_boot/templates/firstboot_state0.html index 670a901cc..bcb3bb76e 100644 --- a/plinth/modules/first_boot/templates/firstboot_state0.html +++ b/plinth/modules/first_boot/templates/firstboot_state0.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "base_firstboot.html" %} {% comment %} # # This file is part of Plinth. @@ -30,10 +30,6 @@ {% endblock %} -{% block mainmenu_right %} - {% include "firstboot_navbar.html" %} -{% endblock %} - {% block content_row %}

- a.navbar-brand { - display: none; - } - -{% endblock %} - -{% block mainmenu_right %} - {% include "firstboot_navbar.html" %} -{% endblock %} - {% block content_row %}

{% include 'messages.html' %} diff --git a/plinth/modules/first_boot/templates/firstboot_state5.html b/plinth/modules/first_boot/templates/firstboot_state5.html new file mode 100644 index 000000000..088cf24ba --- /dev/null +++ b/plinth/modules/first_boot/templates/firstboot_state5.html @@ -0,0 +1,70 @@ +{% extends "base_firstboot.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 bootstrap %} +{% load i18n %} +{% load static %} + +{% block content %} + +

{% trans "Set up a freedombox.me subdomain with your voucher" %}

+ +

+{% url 'first_boot:state10' as finish_firstboot_url %} +{% blocktrans trimmed %} + Skip the setup if you do not have a + voucher or want to configure pagekite without using a freedombox.me subdomain. +{% endblocktrans %} +

+ +

+{% blocktrans trimmed %} + You can use a redeemed voucher but it will only work with the initially + registered subdomain. +{% endblocktrans %} +

+ +
+
+
+ {% csrf_token %} + {{ form|bootstrap_horizontal:'col-lg-3' }} +
+
+ + + {% trans "Skip Registration" %} + +
+
+ +
+
+
+{% endblock %} + +{% block page_js %} + +{% endblock %} diff --git a/plinth/modules/first_boot/urls.py b/plinth/modules/first_boot/urls.py index 5356b4685..c4c87bc03 100644 --- a/plinth/modules/first_boot/urls.py +++ b/plinth/modules/first_boot/urls.py @@ -22,7 +22,7 @@ URLs for the First Boot module from django.conf.urls import url from stronghold.decorators import public -from .views import State0View, State1View, state10 +from .views import State0View, State1View, State5View, state10 urlpatterns = [ @@ -30,5 +30,6 @@ urlpatterns = [ url(r'^firstboot/$', public(State0View.as_view()), name='index'), url(r'^firstboot/state0/$', public(State0View.as_view()), name='state0'), url(r'^firstboot/state1/$', public(State1View.as_view()), name='state1'), + url(r'^firstboot/state5/$', State5View.as_view(), name='state5'), url(r'^firstboot/state10/$', state10, name='state10'), ] diff --git a/plinth/modules/first_boot/views.py b/plinth/modules/first_boot/views.py index 9d456f98d..36af8be46 100644 --- a/plinth/modules/first_boot/views.py +++ b/plinth/modules/first_boot/views.py @@ -15,16 +15,20 @@ # along with this program. If not, see . # +from django.contrib import messages from django.contrib.auth.models import User from django.core.urlresolvers import reverse_lazy +from django.http.response import HttpResponseRedirect from django.shortcuts import render_to_response from django.template import RequestContext from django.utils.translation import ugettext as _ -from django.views.generic import CreateView, TemplateView +from django.views.generic import CreateView, FormView, TemplateView +from plinth import cfg from plinth import kvstore from plinth import network -from .forms import State1Form +from plinth.errors import DomainRegistrationError +from .forms import State1Form, State5Form class State0View(TemplateView): @@ -38,6 +42,11 @@ class State1View(CreateView): form_class = State1Form success_url = reverse_lazy('first_boot:state10') + def __init__(self, *args, **kwargs): + if cfg.danube_edition: + self.success_url = reverse_lazy('first_boot:state5') + return super(State1View, self).__init__(*args, **kwargs) + def get_form_kwargs(self): """Make request available to the form (to insert messages)""" kwargs = super(State1View, self).get_form_kwargs() @@ -60,3 +69,29 @@ def state10(request): {'title': _('Setup Complete'), 'connections': connections}, context_instance=RequestContext(request)) + + +class State5View(FormView): + """ + State 5 is is the (optional) setup of the pagekite freedombox.me subdomain + """ + template_name = 'firstboot_state5.html' + form_class = State5Form + success_url = reverse_lazy('first_boot:state10') + + def get(self, *args, **kwargs): + kvstore.set('firstboot_state', 5) + return super(State5View, self).get(*args, **kwargs) + + def form_valid(self, form): + try: + form.register_domain() + except DomainRegistrationError as err: + messages.error(self.request, err) + return HttpResponseRedirect(reverse_lazy('first_boot:state5')) + else: + form.setup_pagekite() + msg = _("Pagekite setup finished. The HTTP and HTTPS services \ + are activated now.") + messages.success(self.request, msg) + return super(State5View, self).form_valid(form) diff --git a/setup.py b/setup.py index c56b01c4c..361e845c7 100755 --- a/setup.py +++ b/setup.py @@ -185,6 +185,7 @@ setuptools.setup( 'django-stronghold', 'psutil', 'python-augeas', + 'requests', 'pyyaml', ], tests_require=['coverage >= 3.7'],