diff --git a/LICENSES b/LICENSES index 7581e1686..55f212f99 100644 --- a/LICENSES +++ b/LICENSES @@ -52,7 +52,7 @@ specified and linked otherwise. - modules/first_boot/first_boot.py :: - - modules/help/help.py :: - - modules/lib/auth_page.py :: - -- modules/lib/auth.py :: Derived from [[http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions][Arnar Birisson's CherryPy wiki code]]. +- modules/lib/auth.py :: - - modules/lib/user_store.py :: - - modules/owncloud/owncloud.py :: - - modules/packages/packages.py :: - diff --git a/Makefile b/Makefile index bf6f8c021..59c53dab5 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ install: default apache-install freedombox-setup-install cp share/init.d/plinth $(DESTDIR)/etc/init.d cp -a lib/* $(DESTDIR)/usr/lib install plinth $(DESTDIR)/usr/bin/ - mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run + mkdir -p $(DESTDIR)/var/lib/plinth/sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run mkdir -p $(DESTDIR)/var/lib/plinth/data rm -f $(DESTDIR)/var/lib/plinth/users/sqlite3.distrib @@ -46,7 +46,7 @@ uninstall: $(DESTDIR)/usr/share/man/man1/plinth.1.gz $(DESTDIR)/var/run/plinth.pid dirs: - @mkdir -p data/cherrypy_sessions + @mkdir -p data/sessions config: Makefile @test -f plinth.config || cp plinth.sample.config plinth.config @@ -59,7 +59,7 @@ html: @$(MAKE) -s -C doc html clean: - @rm -f cherrypy.config data/cherrypy_sessions/* + @rm -f cherrypy.config data/sessions/* @find . -name "*~" -exec rm {} \; @find . -name ".#*" -exec rm {} \; @find . -name "#*" -exec rm {} \; diff --git a/cfg.py b/cfg.py index 01aeee2d7..124bb0ebc 100644 --- a/cfg.py +++ b/cfg.py @@ -32,7 +32,6 @@ pidfile = get_item(parser, 'Path', 'pidfile') host = get_item(parser, 'Network', 'host') port = int(get_item(parser, 'Network', 'port')) -html_root = None main_menu = Menu() if store_file.endswith(".sqlite3"): diff --git a/filedict.py b/filedict.py index 94f779759..955e0e09f 100644 --- a/filedict.py +++ b/filedict.py @@ -10,12 +10,15 @@ import json import UserDict import sqlite3 + -class DefaultArg: +class DefaultArg(object): pass -class Solutions: + +class Solutions(object): Sqlite3 = 0 + class FileDict(UserDict.DictMixin): "A dictionary that stores its data persistantly in a file" @@ -134,7 +137,7 @@ class FileDict(UserDict.DictMixin): def batch(self): return self._Batch(self) - class _Batch: + class _Batch(object): def __init__(self, d): self.__d = d diff --git a/logger.py b/logger.py index 6fd8171fa..9a753948e 100644 --- a/logger.py +++ b/logger.py @@ -6,7 +6,8 @@ cherrypy.log.error_file = cfg.status_log_file cherrypy.log.access_file = cfg.access_log_file cherrypy.log.screen = False -class Logger(): + +class Logger(object): """By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL.""" def log(self, msg, level="DEBUG"): try: diff --git a/menu.py b/menu.py index 84b26c19d..8688e7a9d 100644 --- a/menu.py +++ b/menu.py @@ -1,9 +1,8 @@ from urlparse import urlparse -import cherrypy import cfg -class Menu(): +class Menu(object): """One menu item.""" def __init__(self, label="", icon="", url="#", order=50): """label is the text that is displayed on the menu. @@ -29,6 +28,17 @@ class Menu(): self.order = order self.items = [] + def find(self, url, basehref=True): + """Return a menu item with given URL""" + if basehref and url.startswith('/'): + url = cfg.server_dir + url + + for item in self.items: + if item.url == url: + return item + + raise KeyError('Menu item not found') + def sort_items(self): """Sort the items in self.items by order.""" self.items = sorted(self.items, key=lambda x: x.order, reverse=False) @@ -48,16 +58,17 @@ class Menu(): self.sort_items() return item - def active_p(self): - """Returns True if this menu item is active, otherwise False. + def is_active(self, request_path): + """ + Returns True if this menu item is active, otherwise False. We can tell if a menu is active if the menu item points - anywhere above url we are visiting in the url tree.""" - return urlparse(cherrypy.url()).path.startswith(self.url) + anywhere above url we are visiting in the url tree. + """ + return request_path.startswith(self.url) - def active_item(self): + def active_item(self, request): """Return item list (e.g. submenu) of active menu item.""" - path = urlparse(cherrypy.url()).path for item in self.items: - if path.startswith(item.url): + if request.path.startswith(item.url): return item diff --git a/model.py b/model.py index 6ad424e23..f92952221 100644 --- a/model.py +++ b/model.py @@ -3,6 +3,8 @@ class User(dict): is a bcrypt hash of the password), salt, groups, and an email address. They can be blank or None, but the keys must exist. """ def __init__(self, dict=None): + super(User, self).__init__() + for key in ['username', 'name', 'passphrase', 'salt', 'email']: self[key] = '' for key in ['groups']: diff --git a/module_loader.py b/module_loader.py new file mode 100644 index 000000000..0c7165ac6 --- /dev/null +++ b/module_loader.py @@ -0,0 +1,137 @@ +# +# 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 . +# + +""" +Discover, load and manage Plinth modules +""" + +import django +import importlib +import os + +import cfg +import urls + + +def load_modules(): + """ + Read names of enabled modules in modules/enabled directory and + import them from modules directory. + """ + module_names = [] + modules = {} + for name in os.listdir('modules/enabled'): + full_name = 'modules.{module}'.format(module=name) + + cfg.log.info('Importing {full_name}'.format(full_name=full_name)) + try: + module = importlib.import_module(full_name) + modules[name] = module + module_names.append(name) + except ImportError as exception: + cfg.log.error( + 'Could not import modules/{module}: {exception}' + .format(module=name, exception=exception)) + + _include_module_urls(full_name) + + ordered_modules = [] + remaining_modules = dict(modules) + for module_name in modules: + if module_name not in remaining_modules: + continue + + module = remaining_modules.pop(module_name) + try: + _insert_modules(module_name, module, remaining_modules, + ordered_modules) + except KeyError: + cfg.log.error('Unsatified dependency for module - %s' % + (module_name,)) + + cfg.log.debug('Module load order - %s' % ordered_modules) + + for module_name in ordered_modules: + _initialize_module(modules[module_name]) + + +def _insert_modules(module_name, module, remaining_modules, ordered_modules): + """Insert modules into a list based on dependency order""" + if module_name in ordered_modules: + return + + dependencies = [] + try: + dependencies = module.DEPENDS + except AttributeError: + pass + + for dependency in dependencies: + if dependency in ordered_modules: + continue + + try: + module = remaining_modules.pop(dependency) + except KeyError: + cfg.log.error('Not found or circular dependency - %s, %s' % + (module_name, dependency)) + raise + + _insert_modules(dependency, module, remaining_modules, ordered_modules) + + ordered_modules.append(module_name) + + +def _include_module_urls(module_name): + """Include the module's URLs in global project URLs list""" + url_module = module_name + '.urls' + try: + urls.urlpatterns += django.conf.urls.patterns( + '', django.conf.urls.url( + r'', django.conf.urls.include(url_module))) + except ImportError: + cfg.log.debug('No URLs for {module}'.format(module=module_name)) + + +def _initialize_module(module): + """Call initialization method in the module if it exists""" + try: + init = module.init + except AttributeError: + cfg.log.debug('No init() for module - {module}' + .format(module=module.__name__)) + return + + try: + init() + except Exception as exception: + cfg.log.error('Exception while running init for {module}: {exception}' + .format(module=module, exception=exception)) + + +def get_template_directories(): + """Return the list of template directories""" + directory = os.path.dirname(os.path.abspath(__file__)) + core_directory = os.path.join(directory, 'templates') + + directories = set((core_directory,)) + for name in os.listdir('modules/enabled'): + directories.add(os.path.join('modules', name, 'templates')) + + cfg.log.info('Template directories - %s' % directories) + + return directories diff --git a/modules/apps/__init__.py b/modules/apps/__init__.py index bf5f66418..28d22ea0b 100644 --- a/modules/apps/__init__.py +++ b/modules/apps/__init__.py @@ -16,10 +16,10 @@ # """ -Plinth module for apps section page +Plinth module for Apps section page """ from . import apps +from .apps import init - -__all__ = ['apps'] +__all__ = ['apps', 'init'] diff --git a/modules/apps/apps.py b/modules/apps/apps.py index 05bb7fad3..a5ae5596f 100644 --- a/modules/apps/apps.py +++ b/modules/apps/apps.py @@ -1,18 +1,14 @@ -import cherrypy +from django.template.response import TemplateResponse from gettext import gettext as _ -from plugin_mount import PagePlugin + import cfg -import util -class Apps(PagePlugin): - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("apps") - self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80) - self.menu.add_item("Chat", "icon-comment", "/../jwchat", 30) +def init(): + """Initailize the apps module""" + cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80) - @cherrypy.expose - def index(self): - return util.render_template(template='apps', - title=_('User Applications')) + +def index(request): + """Serve the apps index page""" + return TemplateResponse(request, 'apps.html', {'title': _('Applications')}) diff --git a/modules/apps/urls.py b/modules/apps/urls.py new file mode 100644 index 000000000..a65383f51 --- /dev/null +++ b/modules/apps/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Apps module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.apps.apps', + url(r'^apps/$', 'index') + ) diff --git a/modules/config/__init__.py b/modules/config/__init__.py index aface4101..9e8ba0667 100644 --- a/modules/config/__init__.py +++ b/modules/config/__init__.py @@ -20,6 +20,8 @@ Plinth module for basic system configuration """ from . import config +from .config import init +__all__ = ['config', 'init'] -__all__ = ['config'] +DEPENDS = ['system'] diff --git a/modules/config/config.py b/modules/config/config.py index bda9dd2de..fe87dee70 100644 --- a/modules/config/config.py +++ b/modules/config/config.py @@ -19,17 +19,16 @@ Plinth module for configuring timezone, hostname etc. """ -import cherrypy from django import forms from django.core import validators +from django.template.response import TemplateResponse from gettext import gettext as _ import re import socket import actions import cfg -from ..lib.auth import require -from plugin_mount import PagePlugin +from ..lib.auth import login_required import util @@ -89,67 +88,66 @@ and must not be greater than 63 characters in length.'), return time_zones -class Configuration(PagePlugin): - """System configuration page""" - def __init__(self, *args, **kwargs): - del args # Unused - del kwargs # Unused +def init(): + """Initialize the module""" + menu = cfg.main_menu.find('/sys') + menu.add_item(_('Configure'), 'icon-cog', '/sys/config', 10) - self.register_page('sys.config') - self.menu = cfg.html_root.sys.menu.add_item(_('Configure'), 'icon-cog', - '/sys/config', 10) +@login_required +def index(request): + """Serve the configuration form""" + status = get_status() - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the configuration form""" - status = self.get_status() + form = None + messages = [] - form = None - messages = [] + is_expert = cfg.users.expert(request=request) + if request.method == 'POST' and is_expert: + form = ConfigurationForm(request.POST, prefix='configuration') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(status, form.cleaned_data, messages) + status = get_status() + form = ConfigurationForm(initial=status, + prefix='configuration') + else: + form = ConfigurationForm(initial=status, prefix='configuration') - if kwargs and cfg.users.expert(): - form = ConfigurationForm(kwargs, prefix='configuration') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(status, form.cleaned_data, messages) - status = self.get_status() - form = ConfigurationForm(initial=status, - prefix='configuration') + return TemplateResponse(request, 'config.html', + {'title': _('General Configuration'), + 'form': form, + 'messages_': messages, + 'is_expert': is_expert}) + + +def get_status(): + """Return the current status""" + return {'hostname': get_hostname(), + 'time_zone': util.slurp('/etc/timezone').rstrip()} + + +def _apply_changes(old_status, new_status, messages): + """Apply the form changes""" + if old_status['hostname'] != new_status['hostname']: + if not set_hostname(new_status['hostname']): + messages.append(('error', _('Setting hostname failed'))) else: - form = ConfigurationForm(initial=status, prefix='configuration') - - return util.render_template(template='config', - title=_('General Configuration'), - form=form, messages=messages) - - @staticmethod - def get_status(): - """Return the current status""" - return {'hostname': get_hostname(), - 'time_zone': util.slurp('/etc/timezone').rstrip()} - - @staticmethod - def _apply_changes(old_status, new_status, messages): - """Apply the form changes""" - if old_status['hostname'] != new_status['hostname']: - set_hostname(new_status['hostname']) messages.append(('success', _('Hostname set'))) - else: - messages.append(('info', _('Hostname is unchanged'))) + else: + messages.append(('info', _('Hostname is unchanged'))) - if old_status['time_zone'] != new_status['time_zone']: - output, error = actions.superuser_run('timezone-change', - [new_status['time_zone']]) - del output # Unused - if error: - messages.append(('error', - _('Error setting time zone - %s') % error)) - else: - messages.append(('success', _('Time zone set'))) + if old_status['time_zone'] != new_status['time_zone']: + output, error = actions.superuser_run('timezone-change', + [new_status['time_zone']]) + del output # Unused + if error: + messages.append(('error', + _('Error setting time zone - %s') % error)) else: - messages.append(('info', _('Time zone is unchanged'))) + messages.append(('success', _('Time zone set'))) + else: + messages.append(('info', _('Time zone is unchanged'))) def set_hostname(hostname): @@ -166,6 +164,7 @@ def set_hostname(hostname): # don't persist/cache change unless it was saved successfuly sys_store = util.filedict_con(cfg.store_file, 'sys') sys_store['hostname'] = hostname - except OSError as exception: - raise cherrypy.HTTPError(500, - 'Updating hostname failed: %s' % exception) + except OSError: + return False + + return True diff --git a/modules/config/templates/config.html b/modules/config/templates/config.html index 5e5310481..591ad5e5b 100644 --- a/modules/config/templates/config.html +++ b/modules/config/templates/config.html @@ -22,7 +22,7 @@ {% block main_block %} -{% if cfg.users.expert %} +{% if is_expert %} {% include 'messages.html' %} diff --git a/modules/config/urls.py b/modules/config/urls.py new file mode 100644 index 000000000..e40bdf15a --- /dev/null +++ b/modules/config/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Configuration module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.config.config', + url(r'^sys/config/$', 'index'), + ) diff --git a/modules/diagnostics/__init__.py b/modules/diagnostics/__init__.py index 8c01bd990..5346b6ae7 100644 --- a/modules/diagnostics/__init__.py +++ b/modules/diagnostics/__init__.py @@ -16,10 +16,12 @@ # """ -Plinth module to system diagnostics +Plinth module for system diagnostics """ from . import diagnostics +from .diagnostics import init +__all__ = ['diagnostics', 'init'] -__all__ = ['diagnostics'] +DEPENDS = ['system'] diff --git a/modules/diagnostics/diagnostics.py b/modules/diagnostics/diagnostics.py index e82de7d11..2639f969c 100644 --- a/modules/diagnostics/diagnostics.py +++ b/modules/diagnostics/diagnostics.py @@ -19,39 +19,32 @@ Plinth module for running diagnostics """ -import cherrypy from gettext import gettext as _ -from ..lib.auth import require -from plugin_mount import PagePlugin +from django.template.response import TemplateResponse + import actions import cfg -import util +from ..lib.auth import login_required -class diagnostics(PagePlugin): - order = 30 - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("sys.diagnostics") - cfg.html_root.sys.menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30) +def init(): + """Initialize the module""" + menu = cfg.main_menu.find('/sys') + menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30) - @cherrypy.expose - @require() - def index(self): - return util.render_template(template='diagnostics', - title=_('System Diagnostics')) -class test(PagePlugin): - order = 31 - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("sys.diagnostics.test") +@login_required +def index(request): + """Serve the index page""" + return TemplateResponse(request, 'diagnostics.html', + {'title': _('System Diagnostics')}) - @cherrypy.expose - @require() - def index(self): - output, error = actions.superuser_run("diagnostic-test") - return util.render_template(template='diagnostics_test', - title=_('Diagnostic Test'), - diagnostics_output=output, - diagnostics_error=error) + +@login_required +def test(request): + """Run diagnostics and the output page""" + output, error = actions.superuser_run("diagnostic-test") + return TemplateResponse(request, 'diagnostics_test.html', + {'title': _('Diagnostic Test'), + 'diagnostics_output': output, + 'diagnostics_error': error}) diff --git a/modules/diagnostics/urls.py b/modules/diagnostics/urls.py new file mode 100644 index 000000000..5b3d94653 --- /dev/null +++ b/modules/diagnostics/urls.py @@ -0,0 +1,29 @@ +# +# 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 . +# + +""" +URLs for the Diagnostics module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.diagnostics.diagnostics', + url(r'^sys/diagnostics/$', 'index'), + url(r'^sys/diagnostics/test/$', 'test'), + ) diff --git a/modules/expert_mode/__init__.py b/modules/expert_mode/__init__.py index c638fd55e..071bd92a6 100644 --- a/modules/expert_mode/__init__.py +++ b/modules/expert_mode/__init__.py @@ -20,6 +20,8 @@ Plinth module for expert mode configuration """ from . import expert_mode +from .expert_mode import init +__all__ = ['expert_mode', 'init'] -__all__ = ['expert_mode'] +DEPENDS = ['system'] diff --git a/modules/expert_mode/expert_mode.py b/modules/expert_mode/expert_mode.py index 50c2811e6..389aa8fe0 100644 --- a/modules/expert_mode/expert_mode.py +++ b/modules/expert_mode/expert_mode.py @@ -1,79 +1,66 @@ -import cherrypy from django import forms +from django.template.response import TemplateResponse from gettext import gettext as _ -from ..lib.auth import require -from plugin_mount import PagePlugin + import cfg -import util +from ..lib.auth import login_required class ExpertsForm(forms.Form): # pylint: disable-msg=W0232 """Form to configure expert mode""" - expert_mode = forms.BooleanField( label=_('Expert Mode'), required=False) - # XXX: Only present due to issue with submitting empty form - dummy = forms.CharField(label='Dummy', initial='dummy', - widget=forms.HiddenInput()) + +def init(): + """Initialize the module""" + menu = cfg.main_menu.find('/sys') + menu.add_item(_('Expert Mode'), 'icon-cog', '/sys/expert', 10) -class Experts(PagePlugin): - """Expert forms page""" - order = 60 +@login_required +def index(request): + """Serve the configuration form""" + status = get_status(request) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('sys.expert') + form = None + messages = [] - cfg.html_root.sys.menu.add_item(_('Expert Mode'), 'icon-cog', - '/sys/expert', 10) - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the configuration form""" - status = self.get_status() - - cfg.log.info('Args - %s' % kwargs) - - form = None - messages = [] - - if kwargs: - form = ExpertsForm(kwargs, prefix='experts') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(form.cleaned_data, messages) - status = self.get_status() - form = ExpertsForm(initial=status, prefix='experts') - else: + if request.method == 'POST': + form = ExpertsForm(request.POST, prefix='experts') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(request, form.cleaned_data, messages) + status = get_status(request) form = ExpertsForm(initial=status, prefix='experts') + else: + form = ExpertsForm(initial=status, prefix='experts') - return util.render_template(template='expert_mode', - title=_('Expert Mode'), form=form, - messages=messages) + return TemplateResponse(request, 'expert_mode.html', + {'title': _('Expert Mode'), + 'form': form, + 'messages_': messages}) - @staticmethod - def get_status(): - """Return the current status""" - return {'expert_mode': cfg.users.expert()} - @staticmethod - def _apply_changes(new_status, messages): - """Apply expert mode configuration""" - message = ('info', _('Settings unchanged')) +def get_status(request): + """Return the current status""" + return {'expert_mode': cfg.users.expert(request=request)} - user = cfg.users.current() - if new_status['expert_mode']: - if not 'expert' in user['groups']: - user['groups'].append('expert') - message = ('success', _('Expert mode enabled')) - else: - if 'expert' in user['groups']: - user['groups'].remove('expert') - message = ('success', _('Expert mode disabled')) +def _apply_changes(request, new_status, messages): + """Apply expert mode configuration""" + message = ('info', _('Settings unchanged')) - cfg.users.set(user['username'], user) - messages.append(message) + user = cfg.users.current(request=request) + + if new_status['expert_mode']: + if not 'expert' in user['groups']: + user['groups'].append('expert') + message = ('success', _('Expert mode enabled')) + else: + if 'expert' in user['groups']: + user['groups'].remove('expert') + message = ('success', _('Expert mode disabled')) + + cfg.users.set(user['username'], user) + messages.append(message) diff --git a/modules/expert_mode/urls.py b/modules/expert_mode/urls.py new file mode 100644 index 000000000..83ed46428 --- /dev/null +++ b/modules/expert_mode/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Expert Mode module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.expert_mode.expert_mode', + url(r'^sys/expert/$', 'index'), + ) diff --git a/modules/firewall/__init__.py b/modules/firewall/__init__.py index 33eb5d302..3e3718c6c 100644 --- a/modules/firewall/__init__.py +++ b/modules/firewall/__init__.py @@ -20,6 +20,8 @@ Plinth module to configure a firewall """ from . import firewall +from .firewall import init +__all__ = ['firewall', 'init'] -__all__ = ['firewall'] +DEPENDS = ['system'] diff --git a/modules/firewall/firewall.py b/modules/firewall/firewall.py index ebdbf67a7..ae0c61d0f 100644 --- a/modules/firewall/firewall.py +++ b/modules/firewall/firewall.py @@ -19,138 +19,134 @@ Plinth module to configure a firewall """ -import cherrypy +from django.template.response import TemplateResponse from gettext import gettext as _ import actions import cfg -from ..lib.auth import require -from plugin_mount import PagePlugin +from ..lib.auth import login_required import service as service_module -import util -class Firewall(PagePlugin): - """Firewall menu entry and introduction page""" - order = 40 +def init(): + """Initailze firewall module""" + menu = cfg.main_menu.find('/sys') + menu.add_item(_('Firewall'), 'icon-flag', '/sys/firewall', 50) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) + service_module.ENABLED.connect(on_service_enabled) - self.register_page('sys.firewall') - cfg.html_root.sys.menu.add_item(_('Firewall'), 'icon-flag', - '/sys/firewall', 50) - service_module.ENABLED.connect(self.on_service_enabled) +@login_required +def index(request): + """Serve introcution page""" + if not get_installed_status(): + return TemplateResponse(request, 'firewall.html', + {'title': _('Firewall'), + 'firewall_status': 'not_installed'}) - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve introcution page""" - del kwargs # Unused + if not get_enabled_status(): + return TemplateResponse(request, 'firewall.html', + {'title': _('Firewall'), + 'firewall_status': 'not_running'}) - if not self.get_installed_status(): - return util.render_template(template='firewall', - title=_("Firewall"), - firewall_status='not_installed') + internal_enabled_services = get_enabled_services(zone='internal') + external_enabled_services = get_enabled_services(zone='external') - if not self.get_enabled_status(): - return util.render_template(template='firewall', - title=_("Firewall"), - firewall_status='not_running') + return TemplateResponse( + request, 'firewall.html', + {'title': _('Firewall'), + 'services': service_module.SERVICES.values(), + 'internal_enabled_services': internal_enabled_services, + 'external_enabled_services': external_enabled_services}) - internal_enabled_services = self.get_enabled_services(zone='internal') - external_enabled_services = self.get_enabled_services(zone='external') - return util.render_template( - template='firewall', title=_('Firewall'), - services=service_module.SERVICES.values(), - internal_enabled_services=internal_enabled_services, - external_enabled_services=external_enabled_services) +def get_installed_status(): + """Return whether firewall is installed""" + output = _run(['get-installed'], superuser=True) + return output.split()[0] == 'installed' - def get_installed_status(self): - """Return whether firewall is installed""" - output = self._run(['get-installed'], superuser=True) - return output.split()[0] == 'installed' - def get_enabled_status(self): - """Return whether firewall is installed""" - output = self._run(['get-status'], superuser=True) - return output.split()[0] == 'running' +def get_enabled_status(): + """Return whether firewall is installed""" + output = _run(['get-status'], superuser=True) + return output.split()[0] == 'running' - def get_enabled_services(self, zone): - """Return the status of various services currently enabled""" - output = self._run(['get-enabled-services', '--zone', zone], - superuser=True) - return output.split() - def add_service(self, port, zone): - """Enable a service in firewall""" - self._run(['add-service', port, '--zone', zone], superuser=True) +def get_enabled_services(zone): + """Return the status of various services currently enabled""" + output = _run(['get-enabled-services', '--zone', zone], superuser=True) + return output.split() - def remove_service(self, port, zone): - """Remove a service in firewall""" - self._run(['remove-service', port, '--zone', zone], superuser=True) - def on_service_enabled(self, sender, service_id, enabled, **kwargs): - """ - Enable/disable firewall ports when a service is - enabled/disabled. - """ - del sender # Unused - del kwargs # Unused +def add_service(port, zone): + """Enable a service in firewall""" + _run(['add-service', port, '--zone', zone], superuser=True) - internal_enabled_services = self.get_enabled_services(zone='internal') - external_enabled_services = self.get_enabled_services(zone='external') - cfg.log.info('Service enabled - %s, %s' % (service_id, enabled)) - service = service_module.SERVICES[service_id] - for port in service.ports: - if enabled: - if port not in internal_enabled_services: - self.add_service(port, zone='internal') +def remove_service(port, zone): + """Remove a service in firewall""" + _run(['remove-service', port, '--zone', zone], superuser=True) - if (service.is_external and - port not in external_enabled_services): - self.add_service(port, zone='external') - else: - # service already configured. - pass + +def on_service_enabled(sender, service_id, enabled, **kwargs): + """ + Enable/disable firewall ports when a service is + enabled/disabled. + """ + del sender # Unused + del kwargs # Unused + + internal_enabled_services = get_enabled_services(zone='internal') + external_enabled_services = get_enabled_services(zone='external') + + cfg.log.info('Service enabled - %s, %s' % (service_id, enabled)) + service = service_module.SERVICES[service_id] + for port in service.ports: + if enabled: + if port not in internal_enabled_services: + add_service(port, zone='internal') + + if (service.is_external and + port not in external_enabled_services): + add_service(port, zone='external') else: - if port in internal_enabled_services: - enabled_services_on_port = [ - service_.is_enabled() - for service_ in service_module.SERVICES.values() - if port in service_.ports and - service_id != service_.service_id] - if not any(enabled_services_on_port): - self.remove_service(port, zone='internal') - - if port in external_enabled_services: - enabled_services_on_port = [ - service_.is_enabled() - for service_ in service_module.SERVICES.values() - if port in service_.ports and - service_id != service_.service_id and - service_.is_external] - if not any(enabled_services_on_port): - self.remove_service(port, zone='external') - - @staticmethod - def _run(arguments, superuser=False): - """Run an given command and raise exception if there was an error""" - command = 'firewall' - - cfg.log.info('Running command - %s, %s, %s' % (command, arguments, - superuser)) - - if superuser: - output, error = actions.superuser_run(command, arguments) + # service already configured. + pass else: - output, error = actions.run(command, arguments) + if port in internal_enabled_services: + enabled_services_on_port = [ + service_.is_enabled() + for service_ in service_module.SERVICES.values() + if port in service_.ports and + service_id != service_.service_id] + if not any(enabled_services_on_port): + remove_service(port, zone='internal') - if error: - raise Exception('Error setting/getting firewalld confguration - %s' - % error) + if port in external_enabled_services: + enabled_services_on_port = [ + service_.is_enabled() + for service_ in service_module.SERVICES.values() + if port in service_.ports and + service_id != service_.service_id and + service_.is_external] + if not any(enabled_services_on_port): + remove_service(port, zone='external') - return output + +def _run(arguments, superuser=False): + """Run an given command and raise exception if there was an error""" + command = 'firewall' + + cfg.log.info('Running command - %s, %s, %s' % (command, arguments, + superuser)) + + if superuser: + output, error = actions.superuser_run(command, arguments) + else: + output, error = actions.run(command, arguments) + + if error: + raise Exception('Error setting/getting firewalld confguration - %s' + % error) + + return output diff --git a/modules/firewall/templates/firewall.html b/modules/firewall/templates/firewall.html index 19c165a46..560169a2b 100644 --- a/modules/firewall/templates/firewall.html +++ b/modules/firewall/templates/firewall.html @@ -25,7 +25,7 @@ and outgoing network traffic on your {{ cfg.box_name }}. Keeping a firewall enabled and properly configured reduces risk of security threat from the Internet.

-

The following the current status:

+

The following is the current status:

{% if firewall_status = 'not_installed' %}

Firewall is not installed. Please install it. Firewall comes diff --git a/modules/firewall/urls.py b/modules/firewall/urls.py new file mode 100644 index 000000000..62ae413e7 --- /dev/null +++ b/modules/firewall/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Firewall module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.firewall.firewall', + url(r'^sys/firewall/$', 'index') + ) diff --git a/modules/first_boot/__init__.py b/modules/first_boot/__init__.py index 20e972dae..b9a2a74f5 100644 --- a/modules/first_boot/__init__.py +++ b/modules/first_boot/__init__.py @@ -21,5 +21,4 @@ Plinth module for first boot wizard from . import first_boot - __all__ = ['first_boot'] diff --git a/modules/first_boot/first_boot.py b/modules/first_boot/first_boot.py index 7bcf98e27..d38f042a5 100644 --- a/modules/first_boot/first_boot.py +++ b/modules/first_boot/first_boot.py @@ -18,16 +18,16 @@ The Plinth first-connection process has several stages: 4. The user interacts with the box normally. """ -import cherrypy from django import forms from django.core import validators +from django.http.response import HttpResponseRedirect +from django.template.response import TemplateResponse from gettext import gettext as _ -from plugin_mount import PagePlugin + from ..lib.auth import add_user from ..config import config from withsqlite.withsqlite import sqlite_db import cfg -import util ## TODO: flesh out these tests values @@ -66,132 +66,124 @@ below. This key should not be the same as your key because you are not your \ FreedomBox!')) -class FirstBoot(PagePlugin): - """First boot wizard""" +def index(request): + """Serve the index first boot page""" + return state0(request) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - # this is the url this page will hang off of (/firstboot) - self.register_page('firstboot') - self.register_page('firstboot/state0') - self.register_page('firstboot/state1') +def generate_box_key(): + """Generate a box key""" + return "fake key" - @cherrypy.expose - def index(self, *args, **kwargs): - return self.state0(*args, **kwargs) - def generate_box_key(self): - return "fake key" +def state0(request): + """ + In this state, we do time config over HTTP, name the box and + server key selection. - @cherrypy.expose - def state0(self, **kwargs): - """ - In this state, we do time config over HTTP, name the box and - server key selection. + All the parameters are form inputs. They get passed in when + the form is submitted. This method checks the inputs and if + they validate, uses them to take action. If they do not + validate, it displays the form to give the user a chance to + input correct values. It might display an error message (in + the message parameter). - All the parameters are form inputs. They get passed in when - the form is submitted. This method checks the inputs and if - they validate, uses them to take action. If they do not - validate, it displays the form to give the user a chance to - input correct values. It might display an error message (in - the message parameter). + message is an optional string that we can display to the + user. It's a good place to put error messages. + """ + try: + if _read_state() >= 5: + return HttpResponseRedirect(cfg.server_dir) + except KeyError: + pass - message is an optional string that we can display to the - user. It's a good place to put error messages. - """ - try: - if FirstBoot._read_state() >= 5: - raise cherrypy.HTTPRedirect(cfg.server_dir, 302) - except KeyError: - pass + ## Until LDAP is in place, we'll put the box key in the cfg.store_file + status = get_state0() - ## Until LDAP is in place, we'll put the box key in the cfg.store_file - status = self.get_state0() + form = None + messages = [] - form = None - messages = [] + if request.method == 'POST': + form = State0Form(request.POST, prefix='firstboot') + # pylint: disable-msg=E1101 + if form.is_valid(): + success = _apply_state0(status, form.cleaned_data, messages) - if kwargs: - form = State0Form(kwargs, prefix='firstboot') - # pylint: disable-msg=E1101 - if form.is_valid(): - success = self._apply_state0(status, form.cleaned_data, - messages) + if success: + # Everything is good, permanently mark and move to page 2 + _write_state(1) + return HttpResponseRedirect( + cfg.server_dir + '/firstboot/state1') + else: + form = State0Form(initial=status, prefix='firstboot') - if success: - # Everything is good, permanently mark and move to page 2 - FirstBoot._write_state(1) - raise cherrypy.HTTPRedirect('state1', 302) + return TemplateResponse(request, 'firstboot_state0.html', + {'title': _('First Boot!'), + 'form': form, + 'messages_': messages}) + + +def get_state0(): + """Return the state for form state 0""" + with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \ + database: + return {'hostname': config.get_hostname(), + 'box_key': database.get('box_key', None)} + + +def _apply_state0(old_state, new_state, messages): + """Apply changes in state 0 form""" + success = True + with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \ + database: + database['about'] = 'Information about this FreedomBox' + + if new_state['box_key']: + database['box_key'] = new_state['box_key'] + elif not old_state['box_key']: + database['box_key'] = generate_box_key() + + if old_state['hostname'] != new_state['hostname']: + config.set_hostname(new_state['hostname']) + + error = add_user(new_state['username'], new_state['password'], + 'First user, please change', '', True) + if error: + messages.append( + ('error', _('User account creation failed: %s') % error)) + success = False else: - form = State0Form(initial=status, prefix='firstboot') + messages.append(('success', _('User account created'))) - return util.render_template(template='firstboot_state0', - title=_('First Boot!'), form=form, - messages=messages) + return success - @staticmethod - def get_state0(): - """Return the state for form state 0""" - with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \ - database: - return {'hostname': config.get_hostname(), - 'box_key': database.get('box_key', None)} - def _apply_state0(self, old_state, new_state, messages): - """Apply changes in state 0 form""" - success = True - with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \ - database: - database['about'] = 'Information about this FreedomBox' +def state1(request): + """ + State 1 is when we have a box name and key. In this state, + our task is to provide a certificate and maybe to guide the + user through installing it. We automatically move to State 2, + which is an HTTPS connection. - if new_state['box_key']: - database['box_key'] = new_state['box_key'] - elif not old_state['box_key']: - database['box_key'] = self.generate_box_key() + TODO: HTTPS failure in State 2 should returns to state 1. + """ + # TODO complete first_boot handling + # Make sure the user is not stuck on a dead end for now. + _write_state(5) - if old_state['hostname'] != new_state['hostname']: - config.set_hostname(new_state['hostname']) + return TemplateResponse(request, 'firstboot_state1.html', + {'title': _('Installing the Certificate')}) - error = add_user(new_state['username'], new_state['password'], - 'First user, please change', '', True) - if error: - messages.append( - ('error', _('User account creation failed: %s') % error)) - success = False - else: - messages.append(('success', _('User account created'))) - return success +def _read_state(): + """Read the current state from database""" + with sqlite_db(cfg.store_file, table='firstboot', + autocommit=True) as database: + return database['state'] - @staticmethod - @cherrypy.expose - def state1(): - """ - State 1 is when we have a box name and key. In this state, - our task is to provide a certificate and maybe to guide the - user through installing it. We automatically move to State 2, - which is an HTTPS connection. - TODO: HTTPS failure in State 2 should returns to state 1. - """ - # TODO complete first_boot handling - # Make sure the user is not stuck on a dead end for now. - FirstBoot._write_state(5) - - return util.render_template(template='firstboot_state1', - title=_('Installing the Certificate')) - - @staticmethod - def _read_state(): - """Read the current state from database""" - with sqlite_db(cfg.store_file, table='firstboot', - autocommit=True) as database: - return database['state'] - - @staticmethod - def _write_state(state): - """Write state to database""" - with sqlite_db(cfg.store_file, table='firstboot', - autocommit=True) as database: - database['state'] = state +def _write_state(state): + """Write state to database""" + with sqlite_db(cfg.store_file, table='firstboot', + autocommit=True) as database: + database['state'] = state diff --git a/modules/first_boot/urls.py b/modules/first_boot/urls.py new file mode 100644 index 000000000..6fa4aadc4 --- /dev/null +++ b/modules/first_boot/urls.py @@ -0,0 +1,30 @@ +# +# 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 . +# + +""" +URLs for the First Boot module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.first_boot.first_boot', + url(r'^firstboot/$', 'index'), + url(r'^firstboot/state0/$', 'state0'), + url(r'^firstboot/state1/$', 'state1') + ) diff --git a/modules/help/__init__.py b/modules/help/__init__.py index 9ef609e2d..27adfedfb 100644 --- a/modules/help/__init__.py +++ b/modules/help/__init__.py @@ -20,6 +20,6 @@ Plinth module for help pages """ from . import help # pylint: disable-msg=W0622 +from .help import init - -__all__ = ['help'] +__all__ = ['help', 'init'] diff --git a/modules/help/help.py b/modules/help/help.py index 927f9e6c6..4dadd45d5 100644 --- a/modules/help/help.py +++ b/modules/help/help.py @@ -1,46 +1,40 @@ import os -import cherrypy from gettext import gettext as _ -from plugin_mount import PagePlugin +from django.template.response import TemplateResponse + import cfg -import util -class Help(PagePlugin): - order = 20 # order of running init in PagePlugins - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("help") - self.menu = cfg.main_menu.add_item(_("Documentation"), "icon-book", "/help", 101) - self.menu.add_item(_("Where to Get Help"), "icon-search", "/help/index", 5) - self.menu.add_item(_("Developer's Manual"), "icon-info-sign", "/help/view/plinth", 10) - self.menu.add_item(_("FAQ"), "icon-question-sign", "/help/view/faq", 20) - self.menu.add_item(_("%s Wiki" % cfg.box_name), "icon-pencil", "http://wiki.debian.org/FreedomBox", 30) - self.menu.add_item(_("About"), "icon-star", "/help/about", 100) - - @cherrypy.expose - def index(self): - return util.render_template(template='help', - title=_('Documentation and FAQ')) - - @cherrypy.expose - def about(self): - return util.render_template( - template='about', - title=_('About the {box_name}').format(box_name=cfg.box_name)) +def init(): + """Initialize the Help module""" + menu = cfg.main_menu.add_item(_('Documentation'), 'icon-book', '/help', + 101) + menu.add_item(_("Where to Get Help"), "icon-search", "/help/index", 5) + menu.add_item(_('Developer\'s Manual'), 'icon-info-sign', + '/help/view/plinth', 10) + menu.add_item(_('FAQ'), 'icon-question-sign', '/help/view/faq', 20) + menu.add_item(_('%s Wiki' % cfg.box_name), 'icon-pencil', + 'http://wiki.debian.org/FreedomBox', 30) + menu.add_item(_('About'), 'icon-star', '/help/about', 100) -class View(PagePlugin): - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("help.view") +def index(request): + """Serve the index page""" + return TemplateResponse(request, 'help.html', + {'title': _('Documentation and FAQ')}) - @cherrypy.expose - def default(self, page=''): - if page not in ['design', 'plinth', 'hacking', 'faq']: - raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page) - with open(os.path.join("doc", "%s.part.html" % page), 'r') as IF: - main = IF.read() - return util.render_template(title=_("%s Documentation" % - cfg.product_name), main=main) +def about(request): + """Serve the about page""" + title = _('About the {box_name}').format(box_name=cfg.box_name) + return TemplateResponse(request, 'about.html', {'title': title}) + + +def default(request, page=''): + """Serve the documentation pages""" + with open(os.path.join('doc', '%s.part.html' % page), 'r') as input_file: + main = input_file.read() + + title = _('%s Documentation') % cfg.product_name + return TemplateResponse(request, 'login_nav.html', + {'title': title, 'main': main}) diff --git a/modules/help/urls.py b/modules/help/urls.py new file mode 100644 index 000000000..3c623ebcc --- /dev/null +++ b/modules/help/urls.py @@ -0,0 +1,34 @@ +# +# 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 . +# + +""" +URLs for the Help module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.help.help', + url(r'^help/$', 'index'), + url(r'^help/index/$', 'index'), + url(r'^help/about/$', 'about'), + url(r'^help/view/(?Pdesign)/$', 'default'), + url(r'^help/view/(?Pplinth)/$', 'default'), + url(r'^help/view/(?Phacking)/$', 'default'), + url(r'^help/view/(?Pfaq)/$', 'default'), + ) diff --git a/modules/lib/__init__.py b/modules/lib/__init__.py index ab399cd3f..3b0136b8c 100644 --- a/modules/lib/__init__.py +++ b/modules/lib/__init__.py @@ -23,7 +23,6 @@ from . import auth from . import auth_page from . import user_store - __all__ = ['auth', 'auth_page', 'user_store'] diff --git a/modules/lib/auth.py b/modules/lib/auth.py index 974ecef93..61f5e0702 100644 --- a/modules/lib/auth.py +++ b/modules/lib/auth.py @@ -1,20 +1,13 @@ -# Form based authentication for CherryPy. Requires the -# Session tool to be loaded. -# -# Thanks for this code is owed to Arnar Birgisson -at - gmail.com. It -# is based on code he wrote that was retrieved from -# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions -# on 1 February 2011. - -import cherrypy -import urllib +from django.http.response import HttpResponseRedirect +import functools from passlib.hash import bcrypt from passlib.exc import PasswordSizeError + import cfg -import random from model import User -cfg.session_key = '_cp_username' +cfg.session_key = '_username' + def add_user(username, passphrase, name='', email='', expert=False): """Add a new user with specified username and passphrase. @@ -49,6 +42,7 @@ def add_user(username, passphrase, name='', email='', expert=False): cfg.log(error) return error + def check_credentials(username, passphrase): """Verifies credentials for username and passphrase. @@ -87,79 +81,20 @@ def check_credentials(username, passphrase): if error: cfg.log(error) + return error -def check_auth(*args, **kwargs): - """A tool that looks in config for 'auth.require'. If found and it - is not None, a login is required and the entry is evaluated as a - list of conditions that the user must fulfill""" - conditions = cherrypy.request.config.get('auth.require', None) - # format GET params - get_params = urllib.quote(cherrypy.request.request_line.split()[1]) - if conditions is not None: - username = cherrypy.session.get(cfg.session_key) - if username: - cherrypy.request.login = username - for condition in conditions: - # A condition is just a callable that returns true or false - if not condition(): - # Send old page as from_page parameter - raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params) - else: - # Send old page as from_page parameter - raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params) -cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) - -def require(*conditions): - """A decorator that appends conditions to the auth.require config - variable.""" - def decorate(f): - if not hasattr(f, '_cp_config'): - f._cp_config = dict() - if 'auth.require' not in f._cp_config: - f._cp_config['auth.require'] = [] - f._cp_config['auth.require'].extend(conditions) - return f - return decorate - - -# Conditions are callables that return True -# if the user fulfills the conditions they define, False otherwise -# -# They can access the current username as cherrypy.request.login -# -# Define those at will however suits the application. - -def member_of(groupname): - def check(): - # replace with actual check if is in - return cherrypy.request.login == 'joe' and groupname == 'admin' - return check - -def name_is(reqd_username): - return lambda: reqd_username == cherrypy.request.login - -# These might be handy - -def any_of(*conditions): - """Returns True if any of the conditions match""" - def check(): - for c in conditions: - if c(): - return True - return False - return check - -# By default all conditions are required, but this might still be -# needed if you want to use it inside of an any_of(...) condition -def all_of(*conditions): - """Returns True if all of the conditions match""" - def check(): - for c in conditions: - if not c(): - return False - return True - return check +# XXX: Only required until we start using Django authentication system properly +def login_required(func): + """A decorator to ensure that user is logged in before accessing a view""" + @functools.wraps(func) + def wrapper(request, *args, **kwargs): + """Check that user is logged in""" + if not request.session.get(cfg.session_key, None): + return HttpResponseRedirect( + cfg.server_dir + "/auth/login?from_page=%s" % request.path) + return func(request, *args, **kwargs) + return wrapper diff --git a/modules/lib/auth_page.py b/modules/lib/auth_page.py index 1d9b49ca3..750ec5809 100644 --- a/modules/lib/auth_page.py +++ b/modules/lib/auth_page.py @@ -2,19 +2,17 @@ Controller to provide login and logout actions """ -import cherrypy import cfg from django import forms +from django.http.response import HttpResponseRedirect +from django.template.response import TemplateResponse from gettext import gettext as _ -from plugin_mount import PagePlugin -import auth -import util + +from . import auth class LoginForm(forms.Form): # pylint: disable-msg=W0232 """Login form""" - from_page = forms.CharField(widget=forms.HiddenInput(), required=False) - username = forms.CharField(label=_('Username')) password = forms.CharField(label=_('Passphrase'), widget=forms.PasswordInput()) @@ -33,48 +31,37 @@ class LoginForm(forms.Form): # pylint: disable-msg=W0232 return self.cleaned_data -class AuthController(PagePlugin): - """Login and logout pages""" +def login(request): + """Serve the login page""" + form = None - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) + if request.method == 'POST': + form = LoginForm(request.POST, prefix='auth') + # pylint: disable-msg=E1101 + if form.is_valid(): + username = form.cleaned_data['username'] + request.session[cfg.session_key] = username + return HttpResponseRedirect(_get_from_page(request)) + else: + form = LoginForm(prefix='auth') - self.register_page('auth') + return TemplateResponse(request, 'form.html', + {'title': _('Login'), + 'form': form, + 'submit_text': _('Login')}) - def on_login(self, username): - """Called on successful login""" - def on_logout(self, username): - """Called on logout""" +def logout(request): + """Logout and redirect to origin page""" + try: + del request.session[cfg.session_key] + request.session.flush() + except KeyError: + pass - @cherrypy.expose - def login(self, from_page=cfg.server_dir+"/", **kwargs): - """Serve the login page""" - form = None + return HttpResponseRedirect(_get_from_page(request)) - if kwargs: - form = LoginForm(kwargs, prefix='auth') - # pylint: disable-msg=E1101 - if form.is_valid(): - username = form.cleaned_data['username'] - cherrypy.session[cfg.session_key] = username - cherrypy.request.login = username - self.on_login(username) - raise cherrypy.HTTPRedirect(from_page or - (cfg.server_dir + "/")) - else: - form = LoginForm(prefix='auth') - return util.render_template(template='form', title=_('Login'), - form=form, submit_text=_('Login')) - - @cherrypy.expose - def logout(self, from_page=cfg.server_dir+"/"): - sess = cherrypy.session - username = sess.get(cfg.session_key, None) - sess[cfg.session_key] = None - if username: - cherrypy.request.login = None - self.on_logout(username) - - raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/")) +def _get_from_page(request): + """Return the 'from page' of a request""" + return request.GET.get('from_page', cfg.server_dir + '/') diff --git a/modules/lib/urls.py b/modules/lib/urls.py new file mode 100644 index 000000000..7a0cac762 --- /dev/null +++ b/modules/lib/urls.py @@ -0,0 +1,29 @@ +# +# 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 . +# + +""" +URLs for the Lib module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.lib.auth_page', + url(r'^auth/login/$', 'login'), + url(r'^auth/logout/$', 'logout') + ) diff --git a/modules/lib/user_store.py b/modules/lib/user_store.py index 41d13f947..c3622ede7 100644 --- a/modules/lib/user_store.py +++ b/modules/lib/user_store.py @@ -1,11 +1,13 @@ -import cherrypy import cfg from model import User from plugin_mount import UserStoreModule from withsqlite.withsqlite import sqlite_db + class UserStore(UserStoreModule, sqlite_db): def __init__(self): + super(UserStore, self).__init__() + self.db_file = cfg.user_db sqlite_db.__init__(self, self.db_file, autocommit=True, check_same_thread=False) self.__enter__() @@ -13,25 +15,36 @@ class UserStore(UserStoreModule, sqlite_db): def close(self): self.__exit__(None,None,None) - def current(self, name=False): + def current(self, request=None, name=False): """Return current user, if there is one, else None. If name = True, return the username instead of the user.""" - try: - username = cherrypy.session.get(cfg.session_key) - if name: - return username - else: - return self.get(username) - except AttributeError: + if not request: return None - - def expert(self, username=None): + + try: + username = request.session[cfg.session_key] + except KeyError: + return None + + if name: + return username + + return self.get(username) + + def expert(self, username=None, request=None): + """Return whether the current or provided user is an expert""" if not username: - username = self.current(name=True) - groups = self.attr(username,"groups") + if not request: + return False + + username = self.current(request=request, name=True) + + groups = self.attr(username, 'groups') + if not groups: return False - return 'expert' in groups + + return 'expert' in groups def attr(self, username=None, field=None): return self.get(username)[field] diff --git a/modules/owncloud/__init__.py b/modules/owncloud/__init__.py index 544b2ebc7..e77100b70 100644 --- a/modules/owncloud/__init__.py +++ b/modules/owncloud/__init__.py @@ -20,6 +20,8 @@ Plinth module to configure ownCloud """ from . import owncloud +from .owncloud import init +__all__ = ['owncloud', 'init'] -__all__ = ['owncloud'] +DEPENDS = ['apps'] diff --git a/modules/owncloud/owncloud.py b/modules/owncloud/owncloud.py index 535ba019b..f2b1afff7 100644 --- a/modules/owncloud/owncloud.py +++ b/modules/owncloud/owncloud.py @@ -1,85 +1,81 @@ -import cherrypy from django import forms +from django.template.response import TemplateResponse from gettext import gettext as _ -from ..lib.auth import require -from plugin_mount import PagePlugin + import actions import cfg +from ..lib.auth import login_required import service -import util + + +SERVICE = None class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232 """ownCloud configuration form""" enabled = forms.BooleanField(label=_('Enable ownCloud'), required=False) - # XXX: Only present due to issue with submitting empty form - dummy = forms.CharField(label='Dummy', initial='dummy', - widget=forms.HiddenInput()) + +def init(): + """Initialize the ownCloud module""" + menu = cfg.main_menu.find('/apps') + menu.add_item('Owncloud', 'icon-picture', '/apps/owncloud', 35) + + status = get_status() + + global SERVICE # pylint: disable-msg=W0603 + SERVICE = service.Service('owncloud', _('ownCloud'), ['http', 'https'], + is_external=True, enabled=status['enabled']) -class OwnCloud(PagePlugin): - """ownCloud configuration page""" - order = 90 +@login_required +def index(request): + """Serve the ownCloud configuration page""" + status = get_status() - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('apps.owncloud') + form = None + messages = [] - cfg.html_root.apps.menu.add_item('Owncloud', 'icon-picture', - '/apps/owncloud', 35) - - status = self.get_status() - self.service = service.Service('owncloud', _('ownCloud'), - ['http', 'https'], is_external=True, - enabled=status['enabled']) - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the ownCloud configuration page""" - status = self.get_status() - - form = None - messages = [] - - if kwargs: - form = OwnCloudForm(kwargs, prefix='owncloud') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(status, form.cleaned_data, messages) - status = self.get_status() - form = OwnCloudForm(initial=status, prefix='owncloud') - else: + if request.method == 'POST': + form = OwnCloudForm(request.POST, prefix='owncloud') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(status, form.cleaned_data, messages) + status = get_status() form = OwnCloudForm(initial=status, prefix='owncloud') + else: + form = OwnCloudForm(initial=status, prefix='owncloud') - return util.render_template(template='owncloud', title=_('ownCloud'), - form=form, messages=messages) + return TemplateResponse(request, 'owncloud.html', + {'title': _('ownCloud'), + 'form': form, + 'messages_': messages}) - @staticmethod - def get_status(): - """Return the current status""" - output, error = actions.run('owncloud-setup', 'status') - if error: - raise Exception('Error getting ownCloud status: %s' % error) - return {'enabled': 'enable' in output.split()} +def get_status(): + """Return the current status""" + output, error = actions.run('owncloud-setup', 'status') + if error: + raise Exception('Error getting ownCloud status: %s' % error) - def _apply_changes(self, old_status, new_status, messages): - """Apply the changes""" - if old_status['enabled'] == new_status['enabled']: - messages.append(('info', _('Setting unchanged'))) - return + return {'enabled': 'enable' in output.split()} - if new_status['enabled']: - messages.append(('success', _('ownCloud enabled'))) - option = 'enable' - else: - messages.append(('success', _('ownCloud disabled'))) - option = 'noenable' - actions.superuser_run('owncloud-setup', [option], async=True) +def _apply_changes(old_status, new_status, messages): + """Apply the changes""" + if old_status['enabled'] == new_status['enabled']: + messages.append(('info', _('Setting unchanged'))) + return - # Send a signal to other modules that the service is - # enabled/disabled - self.service.notify_enabled(self, new_status['enabled']) + if new_status['enabled']: + messages.append(('success', _('ownCloud enabled'))) + option = 'enable' + else: + messages.append(('success', _('ownCloud disabled'))) + option = 'noenable' + + actions.superuser_run('owncloud-setup', [option], async=True) + + # Send a signal to other modules that the service is + # enabled/disabled + SERVICE.notify_enabled(None, new_status['enabled']) diff --git a/modules/owncloud/urls.py b/modules/owncloud/urls.py new file mode 100644 index 000000000..5ad666505 --- /dev/null +++ b/modules/owncloud/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the ownCloud module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.owncloud.owncloud', + url(r'^apps/owncloud/$', 'index'), + ) diff --git a/modules/packages/__init__.py b/modules/packages/__init__.py index f84ac44c1..f448daaab 100644 --- a/modules/packages/__init__.py +++ b/modules/packages/__init__.py @@ -20,6 +20,8 @@ Plinth module to manage packages """ from . import packages +from .packages import init +__all__ = ['packages', 'init'] -__all__ = ['packages'] +DEPENDS = ['system'] diff --git a/modules/packages/packages.py b/modules/packages/packages.py index 89fe69676..8db985c85 100644 --- a/modules/packages/packages.py +++ b/modules/packages/packages.py @@ -1,11 +1,10 @@ -import cherrypy from django import forms +from django.template.response import TemplateResponse from gettext import gettext as _ -from ..lib.auth import require -from plugin_mount import PagePlugin + import actions import cfg -import util +from ..lib.auth import login_required def get_modules_available(): @@ -28,10 +27,6 @@ def get_modules_enabled(): class PackagesForm(forms.Form): """Packages form""" - # XXX: Only present due to issue with submitting empty form - dummy = forms.CharField(label='Dummy', initial='dummy', - widget=forms.HiddenInput()) - def __init__(self, *args, **kwargs): # pylint: disable-msg=E1002, E1101 super(forms.Form, self).__init__(*args, **kwargs) @@ -44,90 +39,83 @@ class PackagesForm(forms.Form): label=label, required=False) -class Packages(PagePlugin): - """Package page""" - order = 20 +def init(): + """Initialize the Packages module""" + menu = cfg.main_menu.find('/sys') + menu.add_item('Package Manager', 'icon-gift', '/sys/packages', 20) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('sys.packages') - cfg.html_root.sys.menu.add_item('Package Manager', 'icon-gift', - '/sys/packages', 20) +@login_required +def index(request): + """Serve the form""" + status = get_status() - @cherrypy.expose - @require() - def index(self, *args, **kwargs): - """Serve the form""" - del args # Unused + form = None + messages = [] - status = self.get_status() - - form = None - messages = [] - - if kwargs: - form = PackagesForm(kwargs, prefix='packages') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(status, form.cleaned_data, messages) - status = self.get_status() - form = PackagesForm(initial=status, prefix='packages') - else: + if request.method == 'POST': + form = PackagesForm(request.POST, prefix='packages') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(status, form.cleaned_data, messages) + status = get_status() form = PackagesForm(initial=status, prefix='packages') + else: + form = PackagesForm(initial=status, prefix='packages') - return util.render_template(template='packages', - title=_('Add/Remove Plugins'), - form=form, messages=messages) + return TemplateResponse(request, 'packages.html', + {'title': _('Add/Remove Plugins'), + 'form': form, + 'messages_': messages}) - @staticmethod - def get_status(): - """Return the current status""" - modules_available = get_modules_available() - modules_enabled = get_modules_enabled() - return {module + '_enabled': module in modules_enabled - for module in modules_available} +def get_status(): + """Return the current status""" + modules_available = get_modules_available() + modules_enabled = get_modules_enabled() - @staticmethod - def _apply_changes(old_status, new_status, messages): - """Apply form changes""" - for field, enabled in new_status.items(): - if not field.endswith('_enabled'): - continue + return {module + '_enabled': module in modules_enabled + for module in modules_available} - if old_status[field] == new_status[field]: - continue - module = field.split('_enabled')[0] - if enabled: - output, error = actions.superuser_run( - 'module-manager', ['enable', cfg.python_root, module]) - del output # Unused +def _apply_changes(old_status, new_status, messages): + """Apply form changes""" + for field, enabled in new_status.items(): + if not field.endswith('_enabled'): + continue - # TODO: need to get plinth to load the module we just - # enabled - if error: - messages.append( - ('error', _('Error enabling module - {module}').format( - module=module))) - else: - messages.append( - ('success', _('Module enabled - {module}').format( + if old_status[field] == new_status[field]: + continue + + module = field.split('_enabled')[0] + if enabled: + output, error = actions.superuser_run( + 'module-manager', ['enable', cfg.python_root, module]) + del output # Unused + + # TODO: need to get plinth to load the module we just + # enabled + if error: + messages.append( + ('error', _('Error enabling module - {module}').format( + module=module))) + else: + messages.append( + ('success', _('Module enabled - {module}').format( + module=module))) + else: + output, error = actions.superuser_run( + 'module-manager', ['disable', cfg.python_root, module]) + del output # Unused + + # TODO: need a smoother way for plinth to unload the + # module + if error: + messages.append( + ('error', + _('Error disabling module - {module}').format( module=module))) else: - output, error = actions.superuser_run( - 'module-manager', ['disable', cfg.python_root, module]) - del output # Unused - - # TODO: need a smoother way for plinth to unload the - # module - if error: - messages.append( - ('error', - _('Error disabling module - {module}').format( - module=module))) - else: - messages.append( - ('success', _('Module disabled - {module}').format( - module=module))) + messages.append( + ('success', _('Module disabled - {module}').format( + module=module))) diff --git a/modules/packages/urls.py b/modules/packages/urls.py new file mode 100644 index 000000000..ae963fb4e --- /dev/null +++ b/modules/packages/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Packages module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.packages.packages', + url(r'^sys/packages/$', 'index'), + ) diff --git a/modules/pagekite/__init__.py b/modules/pagekite/__init__.py index fa247ab25..3a017a1cd 100644 --- a/modules/pagekite/__init__.py +++ b/modules/pagekite/__init__.py @@ -20,6 +20,8 @@ Plinth module to configure PageKite """ from . import pagekite +from .pagekite import init +__all__ = ['pagekite', 'init'] -__all__ = ['pagekite'] +DEPENDS = ['apps'] diff --git a/modules/pagekite/pagekite.py b/modules/pagekite/pagekite.py index cc7791a2d..8ad48587b 100644 --- a/modules/pagekite/pagekite.py +++ b/modules/pagekite/pagekite.py @@ -19,45 +19,38 @@ Plinth module for configuring PageKite service """ -import cherrypy from django import forms from django.core import validators +from django.template import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse from gettext import gettext as _ import actions import cfg -from ..lib.auth import require -from plugin_mount import PagePlugin -import util +from ..lib.auth import login_required -class PageKite(PagePlugin): - """PageKite menu entry and introduction page""" - order = 60 +def init(): + """Intialize the PageKite module""" + menu = cfg.main_menu.find('/apps') + menu.add_item(_('Public Visibility (PageKite)'), 'icon-flag', + '/apps/pagekite', 50) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("apps.pagekite") - cfg.html_root.apps.menu.add_item( - _("Public Visibility (PageKite)"), "icon-flag", - "/apps/pagekite", 50) +@login_required +def index(request): + """Serve introdution page""" + menu = {'title': _('PageKite'), + 'items': [{'url': '/apps/pagekite/configure', + 'text': _('Configure PageKite')}]} - @staticmethod - @cherrypy.expose - @require() - def index(**kwargs): - """Serve introdution page""" - del kwargs # Unused + sidebar_right = render_to_string('menu_block.html', {'menu': menu}, + RequestContext(request)) - menu = {'title': _('PageKite'), - 'items': [{'url': '/apps/pagekite/configure', - 'text': _('Configure PageKite')}]} - sidebar_right = util.render_template(template='menu_block', menu=menu) - - return util.render_template(template='pagekite_introduction', - title=_("Public Visibility (PageKite)"), - sidebar_right=sidebar_right) + return TemplateResponse(request, 'pagekite_introduction.html', + {'title': _('Public Visibility (PageKite)'), + 'sidebar_right': sidebar_right}) class TrimmedCharField(forms.CharField): @@ -105,121 +98,114 @@ for your account if no secret is set on the kite')) https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions')) -class Configure(PagePlugin): # pylint: disable-msg=C0103 - """Main configuration form""" - order = 65 +@login_required +def configure(request): + """Serve the configuration form""" + status = get_status() - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) + form = None + messages = [] - self.register_page("apps.pagekite.configure") - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the configuration form""" - status = self.get_status() - - form = None - messages = [] - - if kwargs: - form = ConfigureForm(kwargs, prefix='pagekite') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(status, form.cleaned_data, messages) - status = self.get_status() - form = ConfigureForm(initial=status, prefix='pagekite') - else: + if request.method == 'POST': + form = ConfigureForm(request.POST, prefix='pagekite') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(status, form.cleaned_data, messages) + status = get_status() form = ConfigureForm(initial=status, prefix='pagekite') + else: + form = ConfigureForm(initial=status, prefix='pagekite') - return util.render_template(template='pagekite_configure', - title=_('Configure PageKite'), form=form, - messages=messages) + return TemplateResponse(request, 'pagekite_configure.html', + {'title': _('Configure PageKite'), + 'form': form, + 'messages_': messages}) - def get_status(self): - """ - Return the current status of PageKite configuration by - executing various actions. - """ - status = {} - # Check if PageKite is installed - output = self._run(['get-installed']) - cfg.log('Output - %s' % output) - if output.split()[0] != 'installed': - return None +def get_status(): + """ + Return the current status of PageKite configuration by + executing various actions. + """ + status = {} - # PageKite service enabled/disabled - output = self._run(['get-status']) - status['enabled'] = (output.split()[0] == 'enabled') + # Check if PageKite is installed + output = _run(['get-installed']) + cfg.log('Output - %s' % output) + if output.split()[0] != 'installed': + return None - # PageKite kite details - output = self._run(['get-kite']) - kite_details = output.split() - status['kite_name'] = kite_details[0] - status['kite_secret'] = kite_details[1] + # PageKite service enabled/disabled + output = _run(['get-status']) + status['enabled'] = (output.split()[0] == 'enabled') - # Service status - status['service'] = {} - for service in ('http', 'ssh'): - output = self._run(['get-service-status', service]) - status[service + '_enabled'] = (output.split()[0] == 'enabled') + # PageKite kite details + output = _run(['get-kite']) + kite_details = output.split() + status['kite_name'] = kite_details[0] + status['kite_secret'] = kite_details[1] - return status + # Service status + status['service'] = {} + for service in ('http', 'ssh'): + output = _run(['get-service-status', service]) + status[service + '_enabled'] = (output.split()[0] == 'enabled') - def _apply_changes(self, old_status, new_status, messages): - """Apply the changes to PageKite configuration""" - cfg.log.info('New status is - %s' % new_status) + return status - if old_status != new_status: - self._run(['stop']) - if old_status['enabled'] != new_status['enabled']: - if new_status['enabled']: - self._run(['set-status', 'enable']) - messages.append(('success', _('PageKite enabled'))) - else: - self._run(['set-status', 'disable']) - messages.append(('success', _('PageKite disabled'))) +def _apply_changes(old_status, new_status, messages): + """Apply the changes to PageKite configuration""" + cfg.log.info('New status is - %s' % new_status) - if old_status['kite_name'] != new_status['kite_name'] or \ - old_status['kite_secret'] != new_status['kite_secret']: - self._run(['set-kite', '--kite-name', new_status['kite_name'], - '--kite-secret', new_status['kite_secret']]) - messages.append(('success', _('Kite details set'))) + if old_status != new_status: + _run(['stop']) - for service in ['http', 'ssh']: - if old_status[service + '_enabled'] != \ - new_status[service + '_enabled']: - if new_status[service + '_enabled']: - self._run(['set-service-status', service, 'enable']) - messages.append(('success', _('Service enabled: {service}') - .format(service=service))) - else: - self._run(['set-service-status', service, 'disable']) - messages.append(('success', - _('Service disabled: {service}') - .format(service=service))) - - if old_status != new_status: - self._run(['start']) - - @staticmethod - def _run(arguments, superuser=True): - """Run an given command and raise exception if there was an error""" - command = 'pagekite-configure' - - cfg.log.info('Running command - %s, %s, %s' % (command, arguments, - superuser)) - - if superuser: - output, error = actions.superuser_run(command, arguments) + if old_status['enabled'] != new_status['enabled']: + if new_status['enabled']: + _run(['set-status', 'enable']) + messages.append(('success', _('PageKite enabled'))) else: - output, error = actions.run(command, arguments) + _run(['set-status', 'disable']) + messages.append(('success', _('PageKite disabled'))) - if error: - raise Exception('Error setting/getting PageKite confguration - %s' - % error) + if old_status['kite_name'] != new_status['kite_name'] or \ + old_status['kite_secret'] != new_status['kite_secret']: + _run(['set-kite', '--kite-name', new_status['kite_name'], + '--kite-secret', new_status['kite_secret']]) + messages.append(('success', _('Kite details set'))) - return output + for service in ['http', 'ssh']: + if old_status[service + '_enabled'] != \ + new_status[service + '_enabled']: + if new_status[service + '_enabled']: + _run(['set-service-status', service, 'enable']) + messages.append(('success', _('Service enabled: {service}') + .format(service=service))) + else: + _run(['set-service-status', service, 'disable']) + messages.append(('success', + _('Service disabled: {service}') + .format(service=service))) + + if old_status != new_status: + _run(['start']) + + +def _run(arguments, superuser=True): + """Run an given command and raise exception if there was an error""" + command = 'pagekite-configure' + + cfg.log.info('Running command - %s, %s, %s' % (command, arguments, + superuser)) + + if superuser: + output, error = actions.superuser_run(command, arguments) + else: + output, error = actions.run(command, arguments) + + if error: + raise Exception('Error setting/getting PageKite confguration - %s' + % error) + + return output diff --git a/modules/pagekite/urls.py b/modules/pagekite/urls.py new file mode 100644 index 000000000..8cbad4dd5 --- /dev/null +++ b/modules/pagekite/urls.py @@ -0,0 +1,29 @@ +# +# 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 . +# + +""" +URLs for the PageKite module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.pagekite.pagekite', + url(r'^apps/pagekite/$', 'index'), + url(r'^apps/pagekite/configure/$', 'configure'), + ) diff --git a/modules/santiago/santiago.py b/modules/santiago/santiago.py index b052daab7..e9339fe55 100644 --- a/modules/santiago/santiago.py +++ b/modules/santiago/santiago.py @@ -23,14 +23,15 @@ santiago_port = 52854 # return True class Santiago(PagePlugin): - order = 90 # order of running init in PagePlugins - def __init__(self, *args, **kwargs): + order = 90 # order of running init in PagePlugins + def __init__(self): + super(Santiago, self).__init__() self.register_page("santiago") self.santiago_address = self.get_santiago_address() #TODO: multiple santiago ports #set a listener on the santiago address - def get_santiago_address(self): + def get_santiago_address(self): if 'santiago' in cfg.users['admin'] and 'address' in cfg.users['admin']['santiago']: return cfg.users['admin']['santiago']['address'] else: @@ -54,11 +55,11 @@ class Santiago(PagePlugin): print "Need to add these two lines to /etc/torrc:\n%s" % hidden_service_config return "" - def check_for_hidden_service(self): + def check_for_hidden_service(self): pass - @cherrypy.expose - def index(self, *args, **kw): + @cherrypy.expose + def index(self, *args, **kw): """ A request is a dict with some required keys: @@ -111,12 +112,13 @@ class Santiago(PagePlugin): ## Plinth page to config santiago class santiago(PagePlugin): - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10) - self.register_page("privacy.santiago") + def __init__(self): + super(Santiago, self).__init__(self) - @cherrypy.expose - @require() - def index(self): - return "Santiago's config goes here." + self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10) + self.register_page("privacy.santiago") + + @cherrypy.expose + @require() + def index(self): + return "Santiago's config goes here." diff --git a/modules/system/__init__.py b/modules/system/__init__.py index bd46fa10c..1d9349bb0 100644 --- a/modules/system/__init__.py +++ b/modules/system/__init__.py @@ -20,6 +20,7 @@ Plinth module for system section page """ from . import system +from system import init -__all__ = ['system'] +__all__ = ['system', 'init'] diff --git a/modules/system/system.py b/modules/system/system.py index cfdf92515..e94c85b85 100644 --- a/modules/system/system.py +++ b/modules/system/system.py @@ -1,21 +1,15 @@ -import cherrypy from gettext import gettext as _ -from plugin_mount import PagePlugin +from django.template.response import TemplateResponse + import cfg -import util - -sys_dir = "modules/installed/sys" -class Sys(PagePlugin): - order = 10 - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("sys") - self.menu = cfg.main_menu.add_item(_("System"), "icon-cog", "/sys", 100) - self.menu.add_item(_("Users and Groups"), "icon-user", "/sys/users", 15) +def init(): + """Initialize the system module""" + cfg.main_menu.add_item(_('System'), 'icon-cog', '/sys', 100) - @cherrypy.expose - def index(self): - return util.render_template(template='system', - title=_("System Configuration")) + +def index(request): + """Serve the index page""" + return TemplateResponse(request, 'system.html', + {'title': _('System Configuration')}) diff --git a/modules/system/urls.py b/modules/system/urls.py new file mode 100644 index 000000000..054f9afcc --- /dev/null +++ b/modules/system/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the System module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.system.system', + url(r'^sys/$', 'index'), + ) diff --git a/modules/tor/__init__.py b/modules/tor/__init__.py index 4b42293f9..0a806432a 100644 --- a/modules/tor/__init__.py +++ b/modules/tor/__init__.py @@ -20,6 +20,8 @@ Plinth module to configure Tor """ from . import tor +from .tor import init +__all__ = ['tor', 'init'] -__all__ = ['tor'] +DEPENDS = ['apps'] diff --git a/modules/tor/tor.py b/modules/tor/tor.py index 9e64240f8..1438955de 100644 --- a/modules/tor/tor.py +++ b/modules/tor/tor.py @@ -19,36 +19,35 @@ Plinth module for configuring Tor """ -import cherrypy +from django.template.response import TemplateResponse from gettext import gettext as _ -from plugin_mount import PagePlugin -from ..lib.auth import require + import actions import cfg -import util +from ..lib.auth import login_required -class tor(PagePlugin): - order = 60 # order of running init in PagePlugins - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("apps.tor") - cfg.html_root.apps.menu.add_item("Tor", "icon-eye-close", "/apps/tor", - 30) +def init(): + """Initialize the Tor module""" + menu = cfg.main_menu.find('/apps') + menu.add_item("Tor", "icon-eye-close", "/apps/tor", 30) - @cherrypy.expose - @require() - def index(self): - output, error = actions.superuser_run("tor-get-ports") - port_info = output.split("\n") - tor_ports = {} - for line in port_info: - try: - (key, val) = line.split() - tor_ports[key] = val - except ValueError: - continue - return util.render_template(template='tor', - title=_('Tor Control Panel'), - tor_ports=tor_ports) +@login_required +def index(request): + """Service the index page""" + output, error = actions.superuser_run("tor-get-ports") + del error # Unused + + port_info = output.split("\n") + tor_ports = {} + for line in port_info: + try: + (key, val) = line.split() + tor_ports[key] = val + except ValueError: + continue + + return TemplateResponse(request, 'tor.html', + {'title': _('Tor Control Panel'), + 'tor_ports': tor_ports}) diff --git a/modules/tor/urls.py b/modules/tor/urls.py new file mode 100644 index 000000000..95e0422fc --- /dev/null +++ b/modules/tor/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +URLs for the Tor module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.tor.tor', + url(r'^apps/tor/$', 'index') + ) diff --git a/modules/users/__init__.py b/modules/users/__init__.py index 7cb056bb5..3dc71aeb5 100644 --- a/modules/users/__init__.py +++ b/modules/users/__init__.py @@ -20,6 +20,8 @@ Plinth module to manage users """ from . import users +from .users import init +__all__ = ['users', 'init'] -__all__ = ['users'] +DEPENDS = ['system'] diff --git a/modules/users/urls.py b/modules/users/urls.py new file mode 100644 index 000000000..8fb5d21cd --- /dev/null +++ b/modules/users/urls.py @@ -0,0 +1,30 @@ +# +# 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 . +# + +""" +URLs for the Users module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.users.users', + url(r'^sys/users/$', 'index'), + url(r'^sys/users/add/$', 'add'), + url(r'^sys/users/edit/$', 'edit') + ) diff --git a/modules/users/users.py b/modules/users/users.py index 5722271b7..0103c7c53 100644 --- a/modules/users/users.py +++ b/modules/users/users.py @@ -1,34 +1,36 @@ -import cherrypy from django import forms from django.core import validators +from django.template import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse from gettext import gettext as _ -from ..lib.auth import require, add_user -from plugin_mount import PagePlugin + import cfg +from ..lib.auth import add_user, login_required from model import User -import util -class Users(PagePlugin): - order = 20 # order of running init in PagePlugins - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("sys.users") +def init(): + """Intialize the module""" + menu = cfg.main_menu.find('/sys') + menu.add_item(_('Users and Groups'), 'icon-user', '/sys/users', 15) - @staticmethod - @cherrypy.expose - @require() - def index(): - """Return a rendered users page""" - menu = {'title': _('Users and Groups'), - 'items': [{'url': '/sys/users/add', - 'text': _('Add User')}, - {'url': '/sys/users/edit', - 'text': _('Edit Users')}]} - sidebar_right = util.render_template(template='menu_block', - menu=menu) - return util.render_template(title="Manage Users and Groups", - sidebar_right=sidebar_right) + +@login_required +def index(request): + """Return a rendered users page""" + menu = {'title': _('Users and Groups'), + 'items': [{'url': '/sys/users/add', + 'text': _('Add User')}, + {'url': '/sys/users/edit', + 'text': _('Edit Users')}]} + + sidebar_right = render_to_string('menu_block.html', {'menu': menu}, + RequestContext(request)) + + return TemplateResponse(request, 'login_nav.html', + {'title': _('Manage Users and Groups'), + 'sidebar_right': sidebar_right}) class UserAddForm(forms.Form): # pylint: disable-msg=W0232 @@ -48,48 +50,40 @@ and alphabet'), email = forms.EmailField(label=_('Email'), required=False) -class UserAdd(PagePlugin): - """Add user page""" - order = 30 +@login_required +def add(request): + """Serve the form""" + form = None + messages = [] - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - - self.register_page('sys.users.add') - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the form""" - form = None - messages = [] - - if kwargs: - form = UserAddForm(kwargs, prefix='user') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._add_user(form.cleaned_data, messages) - form = UserAddForm(prefix='user') - else: + if request.method == 'POST': + form = UserAddForm(request.POST, prefix='user') + # pylint: disable-msg=E1101 + if form.is_valid(): + _add_user(form.cleaned_data, messages) form = UserAddForm(prefix='user') + else: + form = UserAddForm(prefix='user') - return util.render_template(template='users_add', title=_('Add User'), - form=form, messages=messages) + return TemplateResponse(request, 'users_add.html', + {'title': _('Add User'), + 'form': form, + 'messages_': messages}) - @staticmethod - def _add_user(data, messages): - """Add a user""" - if cfg.users.exists(data['username']): - messages.append( - ('error', _('User "{username}" already exists').format( - username=data['username']))) - return - add_user(data['username'], data['password'], data['full_name'], - data['email'], False) +def _add_user(data, messages): + """Add a user""" + if cfg.users.exists(data['username']): messages.append( - ('success', _('User "{username}" added').format( + ('error', _('User "{username}" already exists').format( username=data['username']))) + return + + add_user(data['username'], data['password'], data['full_name'], + data['email'], False) + messages.append( + ('success', _('User "{username}" added').format( + username=data['username']))) class UserEditForm(forms.Form): # pylint: disable-msg=W0232 @@ -108,64 +102,56 @@ class UserEditForm(forms.Form): # pylint: disable-msg=W0232 self.fields['delete_user_' + user['username']] = field -class UserEdit(PagePlugin): - """User edit page""" - order = 35 +@login_required +def edit(request): + """Serve the edit form""" + form = None + messages = [] - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - - self.register_page('sys.users.edit') - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the form""" - form = None - messages = [] - - if kwargs: - form = UserEditForm(kwargs, prefix='user') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(form.cleaned_data, messages) - form = UserEditForm(prefix='user') - else: + if request.method == 'POST': + form = UserEditForm(request.POST, prefix='user') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_edit_changes(request, form.cleaned_data, messages) form = UserEditForm(prefix='user') + else: + form = UserEditForm(prefix='user') - return util.render_template(template='users_edit', - title=_('Edit or Delete User'), - form=form, messages=messages) + return TemplateResponse(request, 'users_edit.html', + {'title': _('Edit or Delete User'), + 'form': form, + 'messages_': messages}) - @staticmethod - def _apply_changes(data, messages): - """Apply form changes""" - for field, value in data.items(): - if not value: - continue - if not field.startswith('delete_user_'): - continue +def _apply_edit_changes(request, data, messages): + """Apply form changes""" + for field, value in data.items(): + if not value: + continue - username = field.split('delete_user_')[1] + if not field.startswith('delete_user_'): + continue - cfg.log.info('%s asked to delete %s' % - (cherrypy.session.get(cfg.session_key), username)) + username = field.split('delete_user_')[1] - if username == cfg.users.current(name=True): - messages.append( - ('error', - _('Can not delete current account - "%s"') % username)) - continue + requesting_user = request.session.get(cfg.session_key, None) + cfg.log.info('%s asked to delete %s' % + (requesting_user, username)) - if not cfg.users.exists(username): - messages.append(('error', - _('User "%s" does not exist') % username)) - continue + if username == cfg.users.current(request=request, name=True): + messages.append( + ('error', + _('Can not delete current account - "%s"') % username)) + continue - try: - cfg.users.remove(username) - messages.append(('success', _('User "%s" deleted') % username)) - except IOError as exception: - messages.append(('error', _('Error deleting "%s" - %s') % - (username, exception))) + if not cfg.users.exists(username): + messages.append(('error', + _('User "%s" does not exist') % username)) + continue + + try: + cfg.users.remove(username) + messages.append(('success', _('User "%s" deleted') % username)) + except IOError as exception: + messages.append(('error', _('Error deleting "%s" - %s') % + (username, exception))) diff --git a/modules/xmpp/__init__.py b/modules/xmpp/__init__.py index a55e739c3..578564afb 100644 --- a/modules/xmpp/__init__.py +++ b/modules/xmpp/__init__.py @@ -20,6 +20,8 @@ Plinth module to configure XMPP server """ from . import xmpp +from .xmpp import init +__all__ = ['xmpp', 'init'] -__all__ = ['xmpp'] +DEPENDS = ['apps'] diff --git a/modules/xmpp/urls.py b/modules/xmpp/urls.py new file mode 100644 index 000000000..43a518c87 --- /dev/null +++ b/modules/xmpp/urls.py @@ -0,0 +1,30 @@ +# +# 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 . +# + +""" +URLs for the XMPP module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.xmpp.xmpp', + url(r'^apps/xmpp/$', 'index'), + url(r'^apps/xmpp/configure/$', 'configure'), + url(r'^apps/xmpp/register/$', 'register') + ) diff --git a/modules/xmpp/xmpp.py b/modules/xmpp/xmpp.py index ab80118c2..2974607bb 100644 --- a/modules/xmpp/xmpp.py +++ b/modules/xmpp/xmpp.py @@ -1,12 +1,13 @@ -import cherrypy from django import forms +from django.template import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse from gettext import gettext as _ -from ..lib.auth import require -from plugin_mount import PagePlugin -import cfg + import actions +import cfg +from ..lib.auth import login_required import service -import util SIDE_MENU = {'title': _('XMPP'), @@ -16,38 +17,35 @@ SIDE_MENU = {'title': _('XMPP'), 'text': 'Register XMPP Account'}]} -class XMPP(PagePlugin): - """XMPP Page""" - order = 60 +def init(): + """Initialize the XMPP module""" + menu = cfg.main_menu.find('/apps') + menu.add_item('Chat', 'icon-comment', '/../jwchat', 20) + menu.add_item('XMPP', 'icon-comment', '/apps/xmpp', 40) - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('apps.xmpp') - cfg.html_root.apps.menu.add_item('XMPP', 'icon-comment', - '/apps/xmpp', 40) + service.Service( + 'xmpp-client', _('Chat Server - client connections'), + is_external=True, enabled=True) + service.Service( + 'xmpp-server', _('Chat Server - server connections'), + is_external=True, enabled=True) + service.Service( + 'xmpp-bosh', _('Chat Server - web interface'), is_external=True, + enabled=True) - self.client_service = service.Service( - 'xmpp-client', _('Chat Server - client connections'), - is_external=True, enabled=True) - self.server_service = service.Service( - 'xmpp-server', _('Chat Server - server connections'), - is_external=True, enabled=True) - self.bosh_service = service.Service( - 'xmpp-bosh', _('Chat Server - web interface'), is_external=True, - enabled=True) - @staticmethod - @cherrypy.expose - @require() - def index(**kwargs): - """Serve XMPP page""" - del kwargs # Unused - main = "

XMPP Server Accounts and Configuration

" +@login_required +def index(request): + """Serve XMPP page""" + main = "

XMPP Server Accounts and Configuration

" - sidebar_right = util.render_template(template='menu_block', - menu=SIDE_MENU) - return util.render_template(title="XMPP Server", main=main, - sidebar_right=sidebar_right) + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) + + return TemplateResponse(request, 'login_nav.html', + {'title': _('XMPP Server'), + 'main': main, + 'sidebar_right': sidebar_right}) class ConfigureForm(forms.Form): # pylint: disable-msg=W0232 @@ -57,83 +55,65 @@ class ConfigureForm(forms.Form): # pylint: disable-msg=W0232 help_text=_('When enabled, anyone who can reach this server will be \ allowed to register an account through an XMPP client')) - # XXX: Only present due to issue with submitting empty form - dummy = forms.CharField(label='Dummy', initial='dummy', - widget=forms.HiddenInput()) +@login_required +def configure(request): + """Serve the configuration form""" + status = get_status() -class Configure(PagePlugin): - """Configuration page""" - order = 65 + form = None + messages = [] - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page("apps.xmpp.configure") - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the configuration form""" - status = self.get_status() - - form = None - messages = [] - - if kwargs: - form = ConfigureForm(kwargs, prefix='xmpp') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(status, form.cleaned_data, messages) - status = self.get_status() - form = ConfigureForm(initial=status, prefix='xmpp') - else: + if request.method == 'POST': + form = ConfigureForm(request.POST, prefix='xmpp') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(status, form.cleaned_data, messages) + status = get_status() form = ConfigureForm(initial=status, prefix='xmpp') + else: + form = ConfigureForm(initial=status, prefix='xmpp') - sidebar_right = util.render_template(template='menu_block', - menu=SIDE_MENU) - return util.render_template(template='xmpp_configure', - title=_('Configure XMPP Server'), - form=form, messages=messages, - sidebar_right=sidebar_right) + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) - @staticmethod - def get_status(): - """Return the current status""" - output, error = actions.run('xmpp-setup', 'status') - if error: - raise Exception('Error getting status: %s' % error) + return TemplateResponse(request, 'xmpp_configure.html', + {'title': _('Configure XMPP Server'), + 'form': form, + 'messages_': messages, + 'sidebar_right': sidebar_right}) - return {'inband_enabled': 'inband_enable' in output.split()} - @staticmethod - def sidebar_right(**kwargs): - """Return rendered string for sidebar on the right""" - del kwargs # Unused +def get_status(): + """Return the current status""" + output, error = actions.run('xmpp-setup', 'status') + if error: + raise Exception('Error getting status: %s' % error) - return util.render_template(template='menu_block', menu=SIDE_MENU) + return {'inband_enabled': 'inband_enable' in output.split()} - @staticmethod - def _apply_changes(old_status, new_status, messages): - """Apply the form changes""" - cfg.log.info('Status - %s, %s' % (old_status, new_status)) - if old_status['inband_enabled'] == new_status['inband_enabled']: - messages.append(('info', _('Setting unchanged'))) - return +def _apply_changes(old_status, new_status, messages): + """Apply the form changes""" + cfg.log.info('Status - %s, %s' % (old_status, new_status)) - if new_status['inband_enabled']: - messages.append(('success', _('Inband registration enabled'))) - option = 'inband_enable' - else: - messages.append(('success', _('Inband registration disabled'))) - option = 'noinband_enable' + if old_status['inband_enabled'] == new_status['inband_enabled']: + messages.append(('info', _('Setting unchanged'))) + return - cfg.log.info('Option - %s' % option) + if new_status['inband_enabled']: + messages.append(('success', _('Inband registration enabled'))) + option = 'inband_enable' + else: + messages.append(('success', _('Inband registration disabled'))) + option = 'noinband_enable' - _output, error = actions.superuser_run('xmpp-setup', [option]) - del _output - if error: - raise Exception('Error running command - %s' % error) + cfg.log.info('Option - %s' % option) + + _output, error = actions.superuser_run('xmpp-setup', [option]) + del _output # Unused + if error: + raise Exception('Error running command - %s' % error) class RegisterForm(forms.Form): # pylint: disable-msg=W0232 @@ -144,50 +124,43 @@ class RegisterForm(forms.Form): # pylint: disable-msg=W0232 label=_('Password'), widget=forms.PasswordInput()) -class Register(PagePlugin): - """User registration page""" - order = 65 +@login_required +def register(request): + """Serve the registration form""" + form = None + messages = [] - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('apps.xmpp.register') - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the registration form""" - form = None - messages = [] - - if kwargs: - form = RegisterForm(kwargs, prefix='xmpp') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._register_user(form.cleaned_data, messages) - form = RegisterForm(prefix='xmpp') - else: + if request.method == 'POST': + form = RegisterForm(request.POST, prefix='xmpp') + # pylint: disable-msg=E1101 + if form.is_valid(): + _register_user(form.cleaned_data, messages) form = RegisterForm(prefix='xmpp') + else: + form = RegisterForm(prefix='xmpp') - sidebar_right = util.render_template(template='menu_block', - menu=SIDE_MENU) - return util.render_template(template='xmpp_register', - title=_('Register XMPP Account'), - form=form, messages=messages, - sidebar_right=sidebar_right) + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) - @staticmethod - def _register_user(data, messages): - """Register a new XMPP user""" - output, error = actions.superuser_run( - 'xmpp-register', [data['username'], data['password']]) - if error: - raise Exception('Error registering user - %s' % error) + return TemplateResponse(request, 'xmpp_register.html', + {'title': _('Register XMPP Account'), + 'form': form, + 'messages_': messages, + 'sidebar_right': sidebar_right}) - if 'successfully registered' in output: - messages.append(('success', - _('Registered account for %s' % - data['username']))) - else: - messages.append(('error', - _('Failed to register account for %s: %s') % - (data['username'], output))) + +def _register_user(data, messages): + """Register a new XMPP user""" + output, error = actions.superuser_run( + 'xmpp-register', [data['username'], data['password']]) + if error: + raise Exception('Error registering user - %s' % error) + + if 'successfully registered' in output: + messages.append(('success', + _('Registered account for %s' % + data['username']))) + else: + messages.append(('error', + _('Failed to register account for %s: %s') % + (data['username'], output))) diff --git a/plinth.py b/plinth.py index e3d8666a9..ddcce62f0 100755 --- a/plinth.py +++ b/plinth.py @@ -1,28 +1,22 @@ #!/usr/bin/env python -import os, stat, sys, argparse -from gettext import gettext as _ +import os, sys, argparse import cfg import django.conf -import importlib +import django.core.wsgi if not os.path.join(cfg.file_root, "vendor") in sys.path: sys.path.append(os.path.join(cfg.file_root, "vendor")) -import re import cherrypy from cherrypy import _cpserver from cherrypy.process.plugins import Daemonizer Daemonizer(cherrypy.engine).subscribe() +import module_loader import plugin_mount import service -import util as u from logger import Logger -#from modules.auth import AuthController, require, member_of, name_is - -from withsqlite.withsqlite import sqlite_db -import socket __version__ = "0.2.14" __author__ = "James Vasile" @@ -32,79 +26,6 @@ __maintainer__ = "James Vasile" __email__ = "james@jamesvasile.com" __status__ = "Development" -import urlparse - -def error_page(status, dynamic_msg, stock_msg): - return u.render_template(template="err", title=status, main="

%s

%s" % (dynamic_msg, stock_msg)) - -def error_page_404(status, message, traceback, version): - return error_page(status, message, """

If you believe this - missing page should exist, please file a bug with either the Plinth - project (it has - an issue tracker) or the people responsible for the module you - are trying to access.

- -

Sorry for the mistake.

- """) - -def error_page_500(status, message, traceback, version): - cfg.log.error("500 Internal Server Error. Trackback is above.") - more="""

This is an internal error and not something you caused - or can fix. Please report the error on the bug tracker so - we can fix it.

""" - return error_page(status, message, "

%s

%s
" % (more, "\n".join(traceback.split("\n")))) - -class Root(plugin_mount.PagePlugin): - @cherrypy.expose - def index(self): - ## TODO: firstboot hijacking root should probably be in the firstboot module with a hook in plinth.py - with sqlite_db(cfg.store_file, table="firstboot") as db: - if not 'state' in db: - # if we created a new user db, make sure it can't be read by everyone - userdb_fname = '{}.sqlite3'.format(cfg.user_db) - os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - # cherrypy.InternalRedirect throws a 301, causing the - # browser to cache the redirect, preventing the user from - # navigating to /plinth until the browser is restarted. - raise cherrypy.HTTPRedirect('firstboot', 307) - elif db['state'] < 5: - cfg.log("First Boot state = %d" % db['state']) - raise cherrypy.InternalRedirect('firstboot/state%d' % db['state']) - if cherrypy.session.get(cfg.session_key, None): - raise cherrypy.InternalRedirect('apps') - else: - raise cherrypy.InternalRedirect('help/about') - - -def load_modules(): - """ - Read names of enabled modules in modules/enabled directory and - import them from modules directory. - """ - for name in os.listdir('modules/enabled'): - cfg.log.info('Importing modules/%s' % name) - try: - importlib.import_module('modules.{module}'.format(module=name)) - except ImportError as exception: - cfg.log.error( - 'Could not import modules/{module}: {exception}' - .format(module=name, exception=exception)) - - -def get_template_directories(): - """Return the list of template directories""" - directory = os.path.dirname(os.path.abspath(__file__)) - core_directory = os.path.join(directory, 'templates') - - directories = set((core_directory,)) - for name in os.listdir('modules/enabled'): - directories.add(os.path.join('modules', name, 'templates')) - - cfg.log.info('Template directories - %s' % directories) - - return directories - def parse_arguments(): parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') @@ -144,7 +65,13 @@ def set_config(args, element, default): # it wasn't in the config file, but set the default anyway. setattr(cfg, element, default) -def setup(): + +def setup_logging(): + """Setup logging framework""" + cfg.log = Logger() + + +def setup_configuration(): cfg = parse_arguments() try: @@ -155,55 +82,91 @@ def setup(): pass os.chdir(cfg.python_root) - cherrypy.config.update({'error_page.404': error_page_404}) - cherrypy.config.update({'error_page.500': error_page_500}) - cfg.log = Logger() - load_modules() - cfg.html_root = Root() - cfg.users = plugin_mount.UserStoreModule.get_plugins()[0] - cfg.page_plugins = plugin_mount.PagePlugin.get_plugins() - cfg.log("Loaded %d page plugins" % len(cfg.page_plugins)) - # Add an extra server - server = _cpserver.Server() - server.socket_host = '127.0.0.1' - server.socket_port = 52854 - server.subscribe() +def setup_server(): + """Setup CherryPy server""" + # Add an extra server + server = _cpserver.Server() + server.socket_host = '127.0.0.1' + server.socket_port = 52854 + server.subscribe() - # Configure default server - cherrypy.config.update( - {'server.socket_host': cfg.host, - 'server.socket_port': cfg.port, - 'server.thread_pool':10, - 'tools.staticdir.root': cfg.file_root, - 'tools.sessions.on':True, - 'tools.auth.on':True, - 'tools.sessions.storage_type':"file", - 'tools.sessions.timeout':90, - 'tools.sessions.storage_path':"%s/cherrypy_sessions" % cfg.data_dir,}) + # Configure default server + cherrypy.config.update( + {'server.socket_host': cfg.host, + 'server.socket_port': cfg.port, + 'server.thread_pool': 10}) + + application = django.core.wsgi.get_wsgi_application() + cherrypy.tree.graft(application, cfg.server_dir) + + config = { + '/': {'tools.staticdir.root': '%s/static' % cfg.file_root, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': '.'}} + cherrypy.tree.mount(None, cfg.server_dir + '/static', config) + + cherrypy.engine.signal_handler.subscribe() + + +def context_processor(request): + """Add additional context values to RequestContext for use in templates""" + path_parts = request.path.split('/') + active_menu_urls = ['/'.join(path_parts[:length]) + for length in xrange(1, len(path_parts))] + return { + 'cfg': cfg, + 'main_menu': cfg.main_menu, + 'submenu': cfg.main_menu.active_item(request), + 'request_path': request.path, + 'basehref': cfg.server_dir, + 'username': request.session.get(cfg.session_key, None), + 'active_menu_urls': active_menu_urls + } + + +def configure_django(): + """Setup Django configuration in the absense of .settings file""" + context_processors = [ + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'plinth.context_processor'] + + template_directories = module_loader.get_template_directories() + sessions_directory = os.path.join(cfg.data_dir, 'sessions') + django.conf.settings.configure( + DEBUG=False, + ALLOWED_HOSTS=['127.0.0.1', 'localhost'], + TEMPLATE_DIRS=template_directories, + INSTALLED_APPS=['bootstrapform'], + ROOT_URLCONF='urls', + SESSION_ENGINE='django.contrib.sessions.backends.file', + SESSION_FILE_PATH=sessions_directory, + STATIC_URL=cfg.server_dir + '/static/', + TEMPLATE_CONTEXT_PROCESSORS=context_processors) - config = { - '/': {'tools.staticdir.root': '%s/static' % cfg.file_root, - 'tools.proxy.on': True,}, - '/static': {'tools.staticdir.on': True, - 'tools.staticdir.dir': "."}, - '/favicon.ico':{'tools.staticfile.on': True, - 'tools.staticfile.filename': - "%s/static/theme/favicon.ico" % cfg.file_root}} - cherrypy.tree.mount(cfg.html_root, cfg.server_dir, config=config) - cherrypy.engine.signal_handler.subscribe() def main(): - # Initialize basic services + """Intialize and start the application""" + setup_logging() + service.init() - setup() + setup_configuration() - # Configure Django - template_directories = get_template_directories() - django.conf.settings.configure(TEMPLATE_DIRS=template_directories, - INSTALLED_APPS=['bootstrapform']) + configure_django() + + module_loader.load_modules() + + cfg.users = plugin_mount.UserStoreModule.get_plugins()[0] + + setup_server() cherrypy.engine.start() cherrypy.engine.block() diff --git a/plugin_mount.py b/plugin_mount.py index 38ac1e313..09d95d09d 100644 --- a/plugin_mount.py +++ b/plugin_mount.py @@ -18,10 +18,14 @@ class PluginMount(type): def get_plugins(cls, *args, **kwargs): return cls.init_plugins(*args, **kwargs) -class MultiplePluginViolation: + +class MultiplePluginViolation(Exception): + """Multiple plugins found for a type where only one is expected""" pass + class PluginMountSingular(PluginMount): + """Plugin mounter that allows only one plugin of this meta type""" def __init__(cls, name, bases, attrs): if not hasattr(cls, 'plugins'): cls.plugins = [] @@ -29,42 +33,9 @@ class PluginMountSingular(PluginMount): if len(cls.plugins) > 0: raise MultiplePluginViolation cls.plugins.append(cls) - - -def _setattr_deep(obj, path, value): - """If path is 'x.y.z' or ['x', 'y', 'z'] then perform obj.x.y.z = value""" - if isinstance(path, basestring): - path = path.split('.') - - for part in path[:-1]: - obj = getattr(obj, part) - - setattr(obj, path[-1], value) -class PagePlugin: - """ - Mount point for page plugins. Page plugins provide display pages - in the interface (one menu item, for example). - - order - How early should this plugin be loaded? Lower order is earlier. - """ - - order = 50 - - __metaclass__ = PluginMount - def __init__(self, *args, **kwargs): - """If cfg.html_root is none, then this is the html_root.""" - if not cfg.html_root: - cfg.log('Setting html root to %s' % self.__class__.__name__) - cfg.html_root = self - - def register_page(self, url): - cfg.log.info("Registering page: %s" % url) - _setattr_deep(cfg.html_root, url, self) - - -class UserStoreModule: +class UserStoreModule(object): """ Mount Point for plugins that will manage the user backend storage, where we keep a hash for each user. @@ -79,5 +50,5 @@ class UserStoreModule: compatibility with third party software. A future version of Plinth is likely to require LDAP. """ - __metaclass__ = PluginMountSingular # singular because we can only use one user store at a time - + # Singular because we can only use one user store at a time + __metaclass__ = PluginMountSingular diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 000000000..b4e9a30d1 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,36 @@ +{% extends 'login_nav.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 %} + +{% block title_block %} +404 +{% endblock %} + +{% block main_block %} + +

Requested page {{ request_path }} was not found.

+ +

If you believe this missing page should exist, please file a bug with either + the Plinth project (it + has an issue tracker) or the people responsible for the module you are + trying to access.

+ +

Sorry for the mistake.

+ +{% endblock %} diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 000000000..485f3df3c --- /dev/null +++ b/templates/500.html @@ -0,0 +1,32 @@ +{% extends 'login_nav.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 %} + +{% block title_block %} +500 +{% endblock %} + +{% block main_block %} + +

This is an internal error and not something you caused or can fix. Please + report the error on + the bug tracker so we + can fix it.

+ +{% endblock %} diff --git a/templates/base.html b/templates/base.html index ebaea525c..e26f099b3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,3 +1,4 @@ +{% load static %} @@ -24,25 +25,25 @@ - + - {% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %} + {% if title %} {{ title }} {% else %} FreedomBox {% endif %} - + - - - + + + - + - - + + {{ css|safe }} @@ -57,7 +58,10 @@ - FreedomBoxFreedomBox Dashboard + + FreedomBox + + FreedomBox {% block add_nav_and_login %} {% endblock %} @@ -111,10 +115,6 @@ free software offered to you under the terms of the GNU Affero General Public License, Version 3 or later. This Plinth theme was built by Sean "Diggity" O'Brien. -

-

Current page: {{ current_url }}

-

-

{% endblock %}

@@ -123,11 +123,11 @@ - + - + - + {% block js_block %} {{ js|safe }} diff --git a/templates/login_nav.html b/templates/login_nav.html index dbb32f66a..3e9f7f34e 100644 --- a/templates/login_nav.html +++ b/templates/login_nav.html @@ -5,8 +5,13 @@ diff --git a/templates/messages.html b/templates/messages.html index fa5404495..4aac25d88 100644 --- a/templates/messages.html +++ b/templates/messages.html @@ -17,7 +17,7 @@ # {% endcomment %} -{% for severity, message in messages %} +{% for severity, message in messages_ %}
× {{ message }} diff --git a/tests/actions_test.py b/tests/actions_test.py index c78fdce72..09fbae144 100644 --- a/tests/actions_test.py +++ b/tests/actions_test.py @@ -5,7 +5,6 @@ from actions import superuser_run, run import os import shlex import subprocess -import sys import unittest class TestPrivileged(unittest.TestCase): diff --git a/tests/auth_test.py b/tests/auth_test.py index 6ea5d0252..08806748b 100644 --- a/tests/auth_test.py +++ b/tests/auth_test.py @@ -1,18 +1,18 @@ #! /usr/bin/env python # -*- mode: python; mode: auto-fill; fill-column: 80 -*- -import user_store, auth +import auth from logger import Logger import cfg import unittest import cherrypy import plugin_mount import os -from model import User cfg.log = Logger() cherrypy.log.access_file = None + class Auth(unittest.TestCase): """Test check_credentials function of auth to confirm it works as expected""" diff --git a/tests/user_store_test.py b/tests/user_store_test.py index e6a3e8433..def0c0c93 100644 --- a/tests/user_store_test.py +++ b/tests/user_store_test.py @@ -1,7 +1,6 @@ #! /usr/bin/env python # -*- mode: python; mode: auto-fill; fill-column: 80 -*- -import user_store from logger import Logger import cfg import unittest @@ -13,6 +12,7 @@ cfg.log = Logger() cherrypy.log.access_file = None + class UserStore(unittest.TestCase): """Test each function of user_store to confirm they work as expected""" @@ -83,4 +83,4 @@ class UserStore(unittest.TestCase): return user if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/urls.py b/urls.py new file mode 100644 index 000000000..a2611e009 --- /dev/null +++ b/urls.py @@ -0,0 +1,28 @@ +# +# 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 . +# + +""" +Django URLconf file containing all urls +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'views', + url(r'^$', 'index') + ) diff --git a/util.py b/util.py index 39e044e77..b5d66d932 100644 --- a/util.py +++ b/util.py @@ -1,10 +1,8 @@ import os import sys -import cherrypy import cfg import sqlite3 -from django.template.loader import render_to_string from filedict import FileDict @@ -55,31 +53,6 @@ def find_keys(dic, val): """return the key of dictionary dic given the value""" return [k for k, v in dic.iteritems() if v == val] -class Message(): - def __init__(self, msg=''): - self.text = msg - def add(self, text): - self.text += "
%s" % text - - -def render_template(template='login_nav', **kwargs): - for key in ['sidebar_left', 'sidebar_right', 'main', 'js', 'nav', 'css', - 'title', 'basehref']: - if not key in kwargs: - kwargs[key] = '' - - if kwargs['basehref'] == '': - kwargs['basehref'] = cfg.server_dir - - kwargs['template'] = template - kwargs['main_menu'] = cfg.main_menu - kwargs['submenu'] = cfg.main_menu.active_item() - kwargs['current_url'] = cherrypy.url() - kwargs['username'] = cherrypy.session.get(cfg.session_key) - kwargs['cfg'] = cfg - - return render_to_string(template + '.html', kwargs) - def filedict_con(filespec=None, table='dict'): """TODO: better error handling in filedict_con""" diff --git a/views.py b/views.py new file mode 100644 index 000000000..140962f5c --- /dev/null +++ b/views.py @@ -0,0 +1,52 @@ +# +# 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 . +# + +""" +Main Plinth views +""" + +from django.http.response import HttpResponseRedirect +import os +import stat + +import cfg +from withsqlite.withsqlite import sqlite_db + + +def index(request): + """Serve the main index page""" + # TODO: Move firstboot handling to firstboot module somehow + with sqlite_db(cfg.store_file, table='firstboot') as database: + if not 'state' in database: + # If we created a new user db, make sure it can't be read by + # everyone + userdb_fname = '{}.sqlite3'.format(cfg.user_db) + os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) + # Permanent redirect causes the browser to cache the redirect, + # preventing the user from navigating to /plinth until the + # browser is restarted. + return HttpResponseRedirect(cfg.server_dir + '/firstboot') + + if database['state'] < 5: + cfg.log('First boot state = %d' % database['state']) + return HttpResponseRedirect( + cfg.server_dir + '/firstboot/state%d' % database['state']) + + if request.session.get(cfg.session_key, None): + return HttpResponseRedirect(cfg.server_dir + '/apps') + + return HttpResponseRedirect(cfg.server_dir + '/help/about')