diff --git a/.gitignore b/.gitignore index ba9c727d6..3b9739d04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ current-*.tar.gz *.pyc *.py.bak +*.swp *.tiny.css data/*.log data/cherrypy_sessions @@ -28,4 +29,4 @@ data/plinth.sqlite3 predepend build/ *.pid -.emacs.desktop* \ No newline at end of file +.emacs.desktop* diff --git a/actions.py b/actions.py index 38ab78d25..083dd520c 100644 --- a/actions.py +++ b/actions.py @@ -32,7 +32,8 @@ Actions run commands with this contract (version 1.1): C. Only one action can be called at a time. - This prevents us from appending multiple (unexpected) actions to the call. + This prevents us from appending multiple (unexpected) actions to the + call. $ action="echo '$options'; echo 'oops'" $ options="hi" @@ -51,8 +52,8 @@ Actions run commands with this contract (version 1.1): easier than detecting if it occurs. The options list is coerced into a space-separated string before being - shell-escaped. Option lists including shell escape characters may need to - be unescaped on the receiving end. + shell-escaped. Option lists including shell escape characters may need + to be unescaped on the receiving end. E. Actions must exist in the actions directory. @@ -72,11 +73,19 @@ Actions run commands with this contract (version 1.1): """ +import logging import os -import pipes, shlex, subprocess +import pipes +import subprocess + +import cfg +from errors import ActionError -def run(action, options = None, async = False): +LOGGER = logging.getLogger(__name__) + + +def run(action, options=None, async=False): """Safely run a specific action as the current user. See actions._run for more information. @@ -84,7 +93,8 @@ def run(action, options = None, async = False): """ return _run(action, options, async, False) -def superuser_run(action, options = None, async = False): + +def superuser_run(action, options=None, async=False): """Safely run a specific action as root. See actions._run for more information. @@ -92,27 +102,26 @@ def superuser_run(action, options = None, async = False): """ return _run(action, options, async, True) -def _run(action, options = None, async = False, run_as_root = False): + +def _run(action, options=None, async=False, run_as_root=False): """Safely run a specific action as a normal user or root. - actions are pulled from the actions directory. + Actions are pulled from the actions directory. + - options are added to the action command. + - async: run asynchronously or wait for the command to complete. + - run_as_root: execute the command through sudo. - options are added to the action command. - - async: run asynchronously or wait for the command to complete. - - run_as_root: execute the command through sudo. """ - DIRECTORY = "actions" - - if options == None: + if options is None: options = [] # contract 3A and 3B: don't call anything outside of the actions directory. if os.sep in action: raise ValueError("Action can't contain:" + os.sep) - cmd = DIRECTORY + os.sep + action + cmd = os.path.join(cfg.actions_dir, action) + if not os.path.realpath(cmd).startswith(cfg.actions_dir): + raise ValueError("Action has to be in directory %s" % cfg.actions_dir) # contract 3C: interpret shell escape sequences as literal file names. # contract 3E: fail if the action doesn't exist or exists elsewhere. @@ -121,25 +130,32 @@ def _run(action, options = None, async = False, run_as_root = False): cmd = [cmd] - # contract: 3C, 3D: don't allow users to insert escape characters in options + # contract: 3C, 3D: don't allow users to insert escape characters in + # options if options: if not hasattr(options, "__iter__"): options = [options] - cmd += [pipes.quote(option) for option in options] # contract 1: commands can run via sudo. if run_as_root: cmd = ["sudo", "-n"] + cmd + LOGGER.info('Executing command - %s', cmd) + # contract 3C: don't interpret shell escape sequences. # contract 5 (and 6-ish). proc = subprocess.Popen( cmd, - stdout = subprocess.PIPE, - stderr= subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=False) if not async: output, error = proc.communicate() - return output, error + if proc.returncode != 0: + LOGGER.error('Error executing command - %s, %s, %s', cmd, output, + error) + raise ActionError(action, output, error) + + return output diff --git a/cfg.py b/cfg.py index c322b404e..4844393c4 100644 --- a/cfg.py +++ b/cfg.py @@ -12,6 +12,7 @@ python_root = None data_dir = None store_file = None user_db = None +actions_dir = None status_log_file = None access_log_file = None pidfile = None @@ -19,7 +20,7 @@ host = None port = None debug = False no_daemon = False -server_dir = '' +server_dir = '/' main_menu = Menu() @@ -41,6 +42,7 @@ def read(): ('Path', 'data_dir'), ('Path', 'store_file'), ('Path', 'user_db'), + ('Path', 'actions_dir'), ('Path', 'status_log_file'), ('Path', 'access_log_file'), ('Path', 'pidfile'), diff --git a/context_processors.py b/context_processors.py new file mode 100644 index 000000000..087158953 --- /dev/null +++ b/context_processors.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 . +# + +""" +Django context processors to provide common data to templates. +""" + +import re +import cfg + + +def common(request): + """Add additional context values to RequestContext for use in templates.""" + slash_indices = [match.start() for match in re.finditer('/', request.path)] + active_menu_urls = [request.path[:index + 1] for index in slash_indices] + return { + 'cfg': cfg, + 'submenu': cfg.main_menu.active_item(request), + 'active_menu_urls': active_menu_urls + } diff --git a/doc/Makefile b/doc/Makefile index c45bdbc99..aa6ae989a 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -82,7 +82,7 @@ latex: $(LATEX) # This gets us the html sections complete with TOC, but without the -# HTML and head section boilerplate. /help/view uses the parts. +# HTML and head section boilerplate. /help/page uses the parts. %.part.html: %.html csplit -s -f $@ $< '%.*%' sed '1d' $@00 > $@01 diff --git a/errors.py b/errors.py new file mode 100644 index 000000000..325413dfa --- /dev/null +++ b/errors.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 . +# + +""" +Project specific errors +""" + + +class PlinthError(Exception): + """Base class for all Plinth specific errors.""" + pass + + +class ActionError(PlinthError): + """Use this error for exceptions when executing an action.""" + pass diff --git a/menu.py b/menu.py index 8688e7a9d..147f8fef1 100644 --- a/menu.py +++ b/menu.py @@ -1,5 +1,4 @@ -from urlparse import urlparse -import cfg +from django.core.urlresolvers import reverse class Menu(object): @@ -20,19 +19,20 @@ class Menu(object): orders, but feel free to disregard that. If you need more granularity, don't bother renumbering things. Feel free to use fractional orders. - """ + """ self.label = label self.icon = icon self.url = url self.order = order + # TODO: With an ordered dictionary for self.items we could access the + # items by their URL directly instead of searching for them each time, + # which we do currently with the 'get' method 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 - + def get(self, urlname, url_args=None, url_kwargs=None): + """Return a menu item with given URL name.""" + url = reverse(urlname, args=url_args, kwargs=url_kwargs) for item in self.items: if item.url == url: return item @@ -43,32 +43,28 @@ class Menu(object): """Sort the items in self.items by order.""" self.items = sorted(self.items, key=lambda x: x.order, reverse=False) - def add_item(self, label, icon, url, order=50, basehref=True): - """This method creates a menu item with the parameters, adds - that menu item to this menu, and returns the item. + def add_urlname(self, label, icon, urlname, order=50, url_args=None, + url_kwargs=None): + """Add a named URL to the menu (via add_item). - If BASEHREF is true and url start with a slash, prepend the - cfg.server_dir to it""" + url_args and url_kwargs will be passed on to Django reverse(). - if basehref and url.startswith("/"): - url = cfg.server_dir + url + """ + url = reverse(urlname, args=url_args, kwargs=url_kwargs) + return self.add_item(label, icon, url, order) + def add_item(self, label, icon, url, order=50): + """Create a new menu item with given parameters, add it to this menu and + return it. + + """ item = Menu(label=label, icon=icon, url=url, order=order) self.items.append(item) self.sort_items() return item - 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 request_path.startswith(self.url) - def active_item(self, request): - """Return item list (e.g. submenu) of active menu item.""" + """Return the first active item (e.g. submenu) that is found.""" for item in self.items: if request.path.startswith(item.url): return item diff --git a/module_loader.py b/module_loader.py index cf990394b..70f7763b3 100644 --- a/module_loader.py +++ b/module_loader.py @@ -25,9 +25,12 @@ import logging import os import urls +import cfg LOGGER = logging.getLogger(__name__) +LOADED_MODULES = [] + def load_modules(): """ @@ -47,8 +50,10 @@ def load_modules(): except Exception as exception: LOGGER.exception('Could not import modules/%s: %s', name, exception) + if cfg.debug: + raise - _include_module_urls(full_name) + _include_module_urls(full_name, name) ordered_modules = [] remaining_modules = dict(modules) @@ -68,6 +73,7 @@ def load_modules(): for module_name in ordered_modules: _initialize_module(modules[module_name]) + LOADED_MODULES.append(module_name) def _insert_modules(module_name, module, remaining_modules, ordered_modules): @@ -97,15 +103,17 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules): ordered_modules.append(module_name) -def _include_module_urls(module_name): +def _include_module_urls(module_name, namespace): """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))) + r'', django.conf.urls.include(url_module, namespace))) except ImportError: LOGGER.debug('No URLs for %s', module_name) + if cfg.debug: + raise def _initialize_module(module): @@ -121,6 +129,8 @@ def _initialize_module(module): except Exception as exception: LOGGER.exception('Exception while running init for %s: %s', module, exception) + if cfg.debug: + raise def get_template_directories(): diff --git a/modules/apps/apps.py b/modules/apps/apps.py index a5ae5596f..206ae5da0 100644 --- a/modules/apps/apps.py +++ b/modules/apps/apps.py @@ -6,7 +6,7 @@ import cfg def init(): """Initailize the apps module""" - cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80) + cfg.main_menu.add_urlname("Apps", "icon-download-alt", "apps:index", 80) def index(request): diff --git a/modules/apps/urls.py b/modules/apps/urls.py index a65383f51..b47f44cde 100644 --- a/modules/apps/urls.py +++ b/modules/apps/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.apps.apps', - url(r'^apps/$', 'index') + url(r'^apps/$', 'index', name='index') ) diff --git a/modules/config/config.py b/modules/config/config.py index 4aa6f299e..1d11a9008 100644 --- a/modules/config/config.py +++ b/modules/config/config.py @@ -95,8 +95,8 @@ and must not be greater than 63 characters in length.'), def init(): """Initialize the module""" - menu = cfg.main_menu.find('/sys') - menu.add_item(_('Configure'), 'icon-cog', '/sys/config', 10) + menu = cfg.main_menu.get('system:index') + menu.add_urlname(_('Configure'), 'icon-cog', 'config:index', 10) @login_required @@ -133,20 +133,22 @@ def get_status(): def _apply_changes(request, old_status, new_status): """Apply the form changes""" if old_status['hostname'] != new_status['hostname']: - if not set_hostname(new_status['hostname']): - messages.error(request, _('Setting hostname failed')) + try: + set_hostname(new_status['hostname']) + except Exception as exception: + messages.error(request, _('Error setting hostname: %s') % + str(exception)) else: messages.success(request, _('Hostname set')) else: messages.info(request, _('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.error(request, - _('Error setting time zone - %s') % error) + try: + actions.superuser_run('timezone-change', [new_status['time_zone']]) + except Exception as exception: + messages.error(request, _('Error setting time zone: %s') % + str(exception)) else: messages.success(request, _('Time zone set')) else: @@ -160,11 +162,6 @@ def set_hostname(hostname): hostname = str(hostname) LOGGER.info('Changing hostname to - %s', hostname) - try: - actions.superuser_run('xmpp-pre-hostname-change') - actions.superuser_run('hostname-change', hostname) - actions.superuser_run('xmpp-hostname-change', hostname, async=True) - except OSError: - return False - - return True + actions.superuser_run('xmpp-pre-hostname-change') + actions.superuser_run('hostname-change', hostname) + actions.superuser_run('xmpp-hostname-change', hostname, async=True) diff --git a/modules/config/urls.py b/modules/config/urls.py index e40bdf15a..e80f518dc 100644 --- a/modules/config/urls.py +++ b/modules/config/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.config.config', - url(r'^sys/config/$', 'index'), + url(r'^sys/config/$', 'index', name='index'), ) diff --git a/modules/diagnostics/diagnostics.py b/modules/diagnostics/diagnostics.py index 854bdf2ed..32b66310c 100644 --- a/modules/diagnostics/diagnostics.py +++ b/modules/diagnostics/diagnostics.py @@ -25,12 +25,13 @@ from gettext import gettext as _ import actions import cfg +from errors import ActionError def init(): """Initialize the module""" - menu = cfg.main_menu.find('/sys') - menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30) + menu = cfg.main_menu.get('system:index') + menu.add_urlname("Diagnostics", "icon-screenshot", "diagnostics:index", 30) @login_required @@ -43,7 +44,15 @@ def index(request): @login_required def test(request): """Run diagnostics and the output page""" - output, error = actions.superuser_run("diagnostic-test") + output = '' + error = '' + try: + output = actions.superuser_run("diagnostic-test") + except ActionError as exception: + output, error = exception.args[1:] + except Exception as exception: + error = str(exception) + return TemplateResponse(request, 'diagnostics_test.html', {'title': _('Diagnostic Test'), 'diagnostics_output': output, diff --git a/modules/diagnostics/templates/diagnostics.html b/modules/diagnostics/templates/diagnostics.html index f104ef2f8..c85203f08 100644 --- a/modules/diagnostics/templates/diagnostics.html +++ b/modules/diagnostics/templates/diagnostics.html @@ -24,8 +24,8 @@ system to confirm that network services are running and configured properly. It may take a minute to complete.

-

Run diagnostic test -»

+

+ Run diagnostic test » +

{% endblock %} diff --git a/modules/diagnostics/urls.py b/modules/diagnostics/urls.py index 5b3d94653..4ed974f9b 100644 --- a/modules/diagnostics/urls.py +++ b/modules/diagnostics/urls.py @@ -24,6 +24,6 @@ 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'), + url(r'^sys/diagnostics/$', 'index', name='index'), + url(r'^sys/diagnostics/test/$', 'test', name='test'), ) diff --git a/modules/expert_mode/expert_mode.py b/modules/expert_mode/expert_mode.py index 0c213625f..3ec98bb76 100644 --- a/modules/expert_mode/expert_mode.py +++ b/modules/expert_mode/expert_mode.py @@ -16,8 +16,8 @@ class ExpertsForm(forms.Form): # pylint: disable-msg=W0232 def init(): """Initialize the module""" - menu = cfg.main_menu.find('/sys') - menu.add_item(_('Expert Mode'), 'icon-cog', '/sys/expert', 10) + menu = cfg.main_menu.get('system:index') + menu.add_urlname(_('Expert Mode'), 'icon-cog', 'expert_mode:index', 10) @login_required diff --git a/modules/expert_mode/urls.py b/modules/expert_mode/urls.py index 83ed46428..ffb97da53 100644 --- a/modules/expert_mode/urls.py +++ b/modules/expert_mode/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.expert_mode.expert_mode', - url(r'^sys/expert/$', 'index'), + url(r'^sys/expert/$', 'index', name='index'), ) diff --git a/modules/firewall/firewall.py b/modules/firewall/firewall.py index e75b9fdb2..760e980e6 100644 --- a/modules/firewall/firewall.py +++ b/modules/firewall/firewall.py @@ -34,8 +34,8 @@ LOGGER = logging.getLogger(__name__) def init(): """Initailze firewall module""" - menu = cfg.main_menu.find('/sys') - menu.add_item(_('Firewall'), 'icon-flag', '/sys/firewall', 50) + menu = cfg.main_menu.get('system:index') + menu.add_urlname(_('Firewall'), 'icon-flag', 'firewall:index', 50) service_module.ENABLED.connect(on_service_enabled) @@ -141,15 +141,7 @@ def _run(arguments, superuser=False): """Run an given command and raise exception if there was an error""" command = 'firewall' - LOGGER.info('Running command - %s, %s, %s', command, arguments, superuser) - if superuser: - output, error = actions.superuser_run(command, arguments) + return 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 + return actions.run(command, arguments) diff --git a/modules/firewall/urls.py b/modules/firewall/urls.py index 62ae413e7..be2f1494a 100644 --- a/modules/firewall/urls.py +++ b/modules/firewall/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.firewall.firewall', - url(r'^sys/firewall/$', 'index') + url(r'^sys/firewall/$', 'index', name='index') ) diff --git a/modules/first_boot/first_boot.py b/modules/first_boot/first_boot.py index c9ecf5ff9..f784c507b 100644 --- a/modules/first_boot/first_boot.py +++ b/modules/first_boot/first_boot.py @@ -21,6 +21,7 @@ The Plinth first-connection process has several stages: from django import forms from django.contrib import messages from django.core import validators +from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect from django.template.response import TemplateResponse from gettext import gettext as _ @@ -94,7 +95,7 @@ def state0(request): """ try: if _read_state() >= 5: - return HttpResponseRedirect(cfg.server_dir) + return HttpResponseRedirect(reverse('index')) except KeyError: pass @@ -112,8 +113,7 @@ def state0(request): if success: # Everything is good, permanently mark and move to page 2 _write_state(1) - return HttpResponseRedirect( - cfg.server_dir + '/firstboot/state1') + return HttpResponseRedirect(reverse('first_boot:state1')) else: form = State0Form(initial=status, prefix='firstboot') diff --git a/modules/first_boot/middleware.py b/modules/first_boot/middleware.py new file mode 100644 index 000000000..17ae7c734 --- /dev/null +++ b/modules/first_boot/middleware.py @@ -0,0 +1,55 @@ +# +# 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 middleware to redirect to firstboot wizard if it has not be run +yet. +""" + +from django.core.urlresolvers import reverse +from django.http.response import HttpResponseRedirect +import logging + +import cfg +from withsqlite.withsqlite import sqlite_db + + +LOGGER = logging.getLogger(__name__) + + +class FirstBootMiddleware(object): + """Forward to firstboot page if firstboot isn't finished yet.""" + + @staticmethod + def process_request(request): + """Handle a request as Django middleware request handler.""" + # Prevent redirecting to first boot wizard in a loop by + # checking if we are already in first boot wizard. + if request.path.startswith(reverse('first_boot:index')): + return + + with sqlite_db(cfg.store_file, table='firstboot') as database: + if 'state' not in database: + # Permanent redirect causes the browser to cache the redirect, + # preventing the user from navigating to /plinth until the + # browser is restarted. + return HttpResponseRedirect(reverse('first_boot:index')) + + if database['state'] < 5: + LOGGER.info('First boot state - %d', database['state']) + return HttpResponseRedirect(reverse('first_boot:state%d' % + database['state'])) diff --git a/modules/first_boot/templates/firstboot_state1.html b/modules/first_boot/templates/firstboot_state1.html index 7a40fcf1e..fdc675f39 100644 --- a/modules/first_boot/templates/firstboot_state1.html +++ b/modules/first_boot/templates/firstboot_state1.html @@ -23,7 +23,7 @@ {% block main_block %}

Welcome screen not completely implemented yet. Press continue to see the rest of the + href="{% url 'apps:index' %}">continue to see the rest of the web interface.

    diff --git a/modules/first_boot/urls.py b/modules/first_boot/urls.py index 6fa4aadc4..68669e6e9 100644 --- a/modules/first_boot/urls.py +++ b/modules/first_boot/urls.py @@ -24,7 +24,7 @@ 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') + url(r'^firstboot/$', 'index', name='index'), + url(r'^firstboot/state0/$', 'state0', name='state0'), + url(r'^firstboot/state1/$', 'state1', name='state1') ) diff --git a/modules/help/help.py b/modules/help/help.py index ce72c6d84..0d73e6df2 100644 --- a/modules/help/help.py +++ b/modules/help/help.py @@ -1,5 +1,6 @@ import os from gettext import gettext as _ +from django.http import Http404 from django.template.response import TemplateResponse import cfg @@ -7,15 +8,17 @@ import cfg 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 = cfg.main_menu.add_urlname(_('Documentation'), 'icon-book', + 'help:index', 101) + menu.add_urlname(_('Where to Get Help'), 'icon-search', + 'help:index_explicit', 5) + menu.add_urlname(_('Developer\'s Manual'), 'icon-info-sign', + 'help:helppage', 10, url_args=('plinth',)) + menu.add_urlname(_('FAQ'), 'icon-question-sign', 'help:helppage', 20, + url_args=('faq',)) 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) + menu.add_urlname(_('About'), 'icon-star', 'help:about', 100) def index(request): @@ -30,10 +33,14 @@ def about(request): 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() +def helppage(request, page): + """Serve a help page from the 'doc' directory""" + try: + with open(os.path.join('doc', '%s.part.html' % page), 'r') \ + as input_file: + main = input_file.read() + except IOError: + raise Http404 title = _('%s Documentation') % cfg.product_name return TemplateResponse(request, 'base.html', diff --git a/modules/help/templates/about.html b/modules/help/templates/about.html index 740afa925..973751fcf 100644 --- a/modules/help/templates/about.html +++ b/modules/help/templates/about.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load static %} {% comment %} # # This file is part of Plinth. @@ -20,7 +21,7 @@ {% block main_block %} -

    We live in a world where our use of the network is mediated by diff --git a/modules/help/templates/help.html b/modules/help/templates/help.html index 6fd191d75..6269ecfa7 100644 --- a/modules/help/templates/help.html +++ b/modules/help/templates/help.html @@ -23,7 +23,7 @@

    There are a variety of places to go for help with {{ cfg.product_name }} and the box it runs on.

    -

    This front end has a +

    This front end has a developer's manual. It isn't complete, but it is the first place to look. Feel free to offer suggestions, edits, and screenshots for completing it!

    @@ -39,7 +39,7 @@ about the {{ cfg.box_name }} and almost surely know nothing of this front end, but they are an incredible resource for general Debian issues.

    -

    There is no FAQ because +

    There is no FAQ because the question frequency is currently zero for all questions.

    {% endblock %} diff --git a/modules/help/urls.py b/modules/help/urls.py index 3c623ebcc..38adfc807 100644 --- a/modules/help/urls.py +++ b/modules/help/urls.py @@ -24,11 +24,11 @@ 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'), - ) + # having two urls for one page is a hack to help the current url/menu + # system highlight the correct menu item. Every submenu-item with the same + # url prefix as the main-menu is highlighted automatically. + url(r'^help/$', 'index', name='index'), + url(r'^help/index/$', 'index', name='index_explicit'), + url(r'^help/about/$', 'about', name='about'), + url(r'^help/page/(plinth|hacking|faq)/$', 'helppage', name='helppage'), +) diff --git a/modules/lib/urls.py b/modules/lib/urls.py index 20f497f35..2ff9a80b9 100644 --- a/modules/lib/urls.py +++ b/modules/lib/urls.py @@ -20,14 +20,13 @@ URLs for the Lib module """ from django.conf.urls import patterns, url - -import cfg +from django.core.urlresolvers import reverse_lazy urlpatterns = patterns( # pylint: disable-msg=C0103 '', url(r'^accounts/login/$', 'django.contrib.auth.views.login', - {'template_name': 'login.html'}), + {'template_name': 'login.html'}, name='login'), url(r'^accounts/logout/$', 'django.contrib.auth.views.logout', - {'next_page': cfg.server_dir}) + {'next_page': reverse_lazy('index')}, name='logout') ) diff --git a/modules/owncloud/owncloud.py b/modules/owncloud/owncloud.py index 1507569b6..2949d5f89 100644 --- a/modules/owncloud/owncloud.py +++ b/modules/owncloud/owncloud.py @@ -19,8 +19,8 @@ class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232 def init(): """Initialize the ownCloud module""" - menu = cfg.main_menu.find('/apps') - menu.add_item('Owncloud', 'icon-picture', '/apps/owncloud', 35) + menu = cfg.main_menu.get('apps:index') + menu.add_urlname('Owncloud', 'icon-picture', 'owncloud:index', 35) status = get_status() @@ -53,10 +53,7 @@ def index(request): def get_status(): """Return the current status""" - output, error = actions.run('owncloud-setup', 'status') - if error: - raise Exception('Error getting ownCloud status: %s' % error) - + output = actions.run('owncloud-setup', 'status') return {'enabled': 'enable' in output.split()} diff --git a/modules/owncloud/templates/owncloud.html b/modules/owncloud/templates/owncloud.html index 5dd7c618c..a06103df6 100644 --- a/modules/owncloud/templates/owncloud.html +++ b/modules/owncloud/templates/owncloud.html @@ -28,7 +28,7 @@ {{ form|bootstrap }}

    When enabled, the owncloud installation will be available - from owncloud on the web server. Visit + from owncloud on the web server. Visit this URL to set up the initial administration account for owncloud.

    diff --git a/modules/owncloud/urls.py b/modules/owncloud/urls.py index 5ad666505..faa23d43b 100644 --- a/modules/owncloud/urls.py +++ b/modules/owncloud/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.owncloud.owncloud', - url(r'^apps/owncloud/$', 'index'), + url(r'^apps/owncloud/$', 'index', name='index'), ) diff --git a/modules/packages/packages.py b/modules/packages/packages.py index cbae9198a..ee4c52717 100644 --- a/modules/packages/packages.py +++ b/modules/packages/packages.py @@ -10,19 +10,13 @@ import cfg def get_modules_available(): """Return list of all modules""" - output, error = actions.run('module-manager', ['list-available']) - if error: - raise Exception('Error getting modules: %s' % error) - + output = actions.run('module-manager', ['list-available']) return output.split() def get_modules_enabled(): """Return list of all modules""" - output, error = actions.run('module-manager', ['list-enabled']) - if error: - raise Exception('Error getting enabled modules - %s' % error) - + output = actions.run('module-manager', ['list-enabled']) return output.split() @@ -42,8 +36,8 @@ class PackagesForm(forms.Form): def init(): """Initialize the Packages module""" - menu = cfg.main_menu.find('/sys') - menu.add_item('Package Manager', 'icon-gift', '/sys/packages', 20) + menu = cfg.main_menu.get('system:index') + menu.add_urlname('Package Manager', 'icon-gift', 'packages:index', 20) @login_required @@ -88,13 +82,12 @@ def _apply_changes(request, old_status, new_status): 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: + try: + actions.superuser_run( + 'module-manager', ['enable', cfg.python_root, module]) + except Exception: + # TODO: need to get plinth to load the module we just + # enabled messages.error( request, _('Error enabling module - {module}').format( module=module)) @@ -103,13 +96,12 @@ def _apply_changes(request, old_status, new_status): request, _('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: + try: + actions.superuser_run( + 'module-manager', ['disable', cfg.python_root, module]) + except Exception: + # TODO: need a smoother way for plinth to unload the + # module messages.error( request, _('Error disabling module - {module}').format( module=module)) diff --git a/modules/packages/urls.py b/modules/packages/urls.py index ae963fb4e..60faf35bf 100644 --- a/modules/packages/urls.py +++ b/modules/packages/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.packages.packages', - url(r'^sys/packages/$', 'index'), + url(r'^sys/packages/$', 'index', name='index'), ) diff --git a/modules/pagekite/pagekite.py b/modules/pagekite/pagekite.py index 9856f9da1..f6e90ced0 100644 --- a/modules/pagekite/pagekite.py +++ b/modules/pagekite/pagekite.py @@ -23,6 +23,7 @@ from django import forms from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core import validators +from django.core.urlresolvers import reverse_lazy from django.template import RequestContext from django.template.loader import render_to_string from django.template.response import TemplateResponse @@ -38,16 +39,16 @@ LOGGER = logging.getLogger(__name__) def init(): """Intialize the PageKite module""" - menu = cfg.main_menu.find('/apps') - menu.add_item(_('Public Visibility (PageKite)'), 'icon-flag', - '/apps/pagekite', 50) + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(_('Public Visibility (PageKite)'), 'icon-flag', + 'pagekite:index', 50) @login_required def index(request): """Serve introdution page""" menu = {'title': _('PageKite'), - 'items': [{'url': '/apps/pagekite/configure', + 'items': [{'url': reverse_lazy('pagekite:configure'), 'text': _('Configure PageKite')}]} sidebar_right = render_to_string('menu_block.html', {'menu': menu}, @@ -197,15 +198,7 @@ def _run(arguments, superuser=True): """Run an given command and raise exception if there was an error""" command = 'pagekite-configure' - LOGGER.info('Running command - %s, %s, %s', command, arguments, superuser) - if superuser: - output, error = actions.superuser_run(command, arguments) + return 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 + return actions.run(command, arguments) diff --git a/modules/pagekite/templates/pagekite_introduction.html b/modules/pagekite/templates/pagekite_introduction.html index 21668bdea..d1b675aea 100644 --- a/modules/pagekite/templates/pagekite_introduction.html +++ b/modules/pagekite/templates/pagekite_introduction.html @@ -49,7 +49,7 @@ there. In future, it might be possible to use your buddy's

    Configure + href="{% url 'pagekite:configure' %}">Configure PageKite

    diff --git a/modules/pagekite/urls.py b/modules/pagekite/urls.py index 8cbad4dd5..3db1d2f98 100644 --- a/modules/pagekite/urls.py +++ b/modules/pagekite/urls.py @@ -24,6 +24,6 @@ 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'), + url(r'^apps/pagekite/$', 'index', name='index'), + url(r'^apps/pagekite/configure/$', 'configure', name='configure'), ) diff --git a/modules/system/system.py b/modules/system/system.py index e94c85b85..961461983 100644 --- a/modules/system/system.py +++ b/modules/system/system.py @@ -6,7 +6,7 @@ import cfg def init(): """Initialize the system module""" - cfg.main_menu.add_item(_('System'), 'icon-cog', '/sys', 100) + cfg.main_menu.add_urlname(_('System'), 'icon-cog', 'system:index', 100) def index(request): diff --git a/modules/system/urls.py b/modules/system/urls.py index 054f9afcc..5982ce850 100644 --- a/modules/system/urls.py +++ b/modules/system/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.system.system', - url(r'^sys/$', 'index'), + url(r'^sys/$', 'index', name='index'), ) diff --git a/modules/tor/tor.py b/modules/tor/tor.py index ee3cdfdb9..335250983 100644 --- a/modules/tor/tor.py +++ b/modules/tor/tor.py @@ -29,15 +29,14 @@ import cfg def init(): """Initialize the Tor module""" - menu = cfg.main_menu.find('/apps') - menu.add_item("Tor", "icon-eye-close", "/apps/tor", 30) + menu = cfg.main_menu.get('apps:index') + menu.add_urlname("Tor", "icon-eye-close", "tor:index", 30) @login_required def index(request): """Service the index page""" - output, error = actions.superuser_run("tor-get-ports") - del error # Unused + output = actions.superuser_run("tor-get-ports") port_info = output.split("\n") tor_ports = {} diff --git a/modules/tor/urls.py b/modules/tor/urls.py index 95e0422fc..4c13cb138 100644 --- a/modules/tor/urls.py +++ b/modules/tor/urls.py @@ -24,5 +24,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( # pylint: disable-msg=C0103 'modules.tor.tor', - url(r'^apps/tor/$', 'index') + url(r'^apps/tor/$', 'index', name='index') ) diff --git a/modules/users/urls.py b/modules/users/urls.py index 8fb5d21cd..0c46aa72e 100644 --- a/modules/users/urls.py +++ b/modules/users/urls.py @@ -24,7 +24,7 @@ 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') + url(r'^sys/users/$', 'index', name='index'), + url(r'^sys/users/add/$', 'add', name='add'), + url(r'^sys/users/edit/$', 'edit', name='edit') ) diff --git a/modules/users/users.py b/modules/users/users.py index 4ce4bc316..5197b28ba 100644 --- a/modules/users/users.py +++ b/modules/users/users.py @@ -3,6 +3,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core import validators +from django.core.urlresolvers import reverse_lazy from django.template import RequestContext from django.template.loader import render_to_string from django.template.response import TemplateResponse @@ -18,17 +19,17 @@ LOGGER = logging.getLogger(__name__) def init(): """Intialize the module""" - menu = cfg.main_menu.find('/sys') - menu.add_item(_('Users and Groups'), 'icon-user', '/sys/users', 15) + menu = cfg.main_menu.get('system:index') + menu.add_urlname(_('Users and Groups'), 'icon-user', 'users:index', 15) @login_required def index(request): """Return a rendered users page""" menu = {'title': _('Users and Groups'), - 'items': [{'url': '/sys/users/add', + 'items': [{'url': reverse_lazy('users:add'), 'text': _('Add User')}, - {'url': '/sys/users/edit', + {'url': reverse_lazy('users:edit'), 'text': _('Edit Users')}]} sidebar_right = render_to_string('menu_block.html', {'menu': menu}, diff --git a/modules/xmpp/urls.py b/modules/xmpp/urls.py index 43a518c87..050026d81 100644 --- a/modules/xmpp/urls.py +++ b/modules/xmpp/urls.py @@ -24,7 +24,7 @@ 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') + url(r'^apps/xmpp/$', 'index', name='index'), + url(r'^apps/xmpp/configure/$', 'configure', name='configure'), + url(r'^apps/xmpp/register/$', 'register', name='register') ) diff --git a/modules/xmpp/xmpp.py b/modules/xmpp/xmpp.py index 592dd3b01..fb09eef94 100644 --- a/modules/xmpp/xmpp.py +++ b/modules/xmpp/xmpp.py @@ -1,6 +1,7 @@ from django import forms from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse_lazy from django.template import RequestContext from django.template.loader import render_to_string from django.template.response import TemplateResponse @@ -14,18 +15,26 @@ import service LOGGER = logging.getLogger(__name__) -SIDE_MENU = {'title': _('XMPP'), - 'items': [{'url': '/apps/xmpp/configure', - 'text': 'Configure XMPP Server'}, - {'url': '/apps/xmpp/register', - 'text': 'Register XMPP Account'}]} +SIDE_MENU = { + 'title': _('XMPP'), + 'items': [ + { + 'url': reverse_lazy('xmpp:configure'), + 'text': _('Configure XMPP Server'), + }, + { + 'url': reverse_lazy('xmpp:register'), + 'text': _('Register XMPP Account'), + } + ] +} def init(): """Initialize the XMPP module""" - menu = cfg.main_menu.find('/apps') + menu = cfg.main_menu.get('apps:index') menu.add_item('Chat', 'icon-comment', '/../jwchat', 20) - menu.add_item('XMPP', 'icon-comment', '/apps/xmpp', 40) + menu.add_urlname('XMPP', 'icon-comment', 'xmpp:index', 40) service.Service( 'xmpp-client', _('Chat Server - client connections'), @@ -88,10 +97,7 @@ def configure(request): def get_status(): """Return the current status""" - output, error = actions.run('xmpp-setup', 'status') - if error: - raise Exception('Error getting status: %s' % error) - + output = actions.run('xmpp-setup', 'status') return {'inband_enabled': 'inband_enable' in output.split()} @@ -111,11 +117,7 @@ def _apply_changes(request, old_status, new_status): option = 'noinband_enable' LOGGER.info('Option - %s', option) - - _output, error = actions.superuser_run('xmpp-setup', [option]) - del _output # Unused - if error: - raise Exception('Error running command - %s' % error) + actions.superuser_run('xmpp-setup', [option]) class RegisterForm(forms.Form): # pylint: disable-msg=W0232 @@ -151,10 +153,8 @@ def register(request): def _register_user(request, data): """Register a new XMPP user""" - output, error = actions.superuser_run( + output = actions.superuser_run( 'xmpp-register', [data['username'], data['password']]) - if error: - raise Exception('Error registering user - %s' % error) if 'successfully registered' in output: messages.success(request, _('Registered account for %s') % diff --git a/plinth.py b/plinth.py index 674835196..969e0b9b4 100755 --- a/plinth.py +++ b/plinth.py @@ -35,6 +35,7 @@ def parse_arguments(): parser.add_argument( '--pidfile', default='plinth.pid', help='specify a file in which the server may write its pid') + # TODO: server_dir is actually a url prefix; use a better variable name parser.add_argument( '--server_dir', default='/', help='web server path under which to serve') @@ -101,7 +102,22 @@ def setup_server(): '/': {'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.tree.mount(None, django.conf.settings.STATIC_URL, config) + + # TODO: our modules are mimicking django apps. It'd be better to convert + # our modules to Django apps instead of reinventing the wheel. + # (we'll still have to serve the static files with cherrypy though) + for module in module_loader.LOADED_MODULES: + static_dir = os.path.join(cfg.file_root, 'modules', module, 'static') + if not os.path.isdir(static_dir): + continue + + config = { + '/': {'tools.staticdir.root': static_dir, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': '.'}} + urlprefix = "%s%s" % (django.conf.settings.STATIC_URL, module) + cherrypy.tree.mount(None, urlprefix, config) if not cfg.no_daemon: Daemonizer(cherrypy.engine).subscribe() @@ -109,23 +125,14 @@ def setup_server(): 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, - 'active_menu_urls': active_menu_urls - } - - def configure_django(): """Setup Django configuration in the absense of .settings file""" + + # In module_loader.py we reverse URLs for the menu before having a proper + # request. In this case, get_script_prefix (used by reverse) returns the + # wrong prefix. Set it here manually to have the correct prefix right away. + django.core.urlresolvers.set_script_prefix(cfg.server_dir) + context_processors = [ 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', @@ -134,7 +141,7 @@ def configure_django(): 'django.core.context_processors.static', 'django.core.context_processors.tz', 'django.contrib.messages.context_processors.messages', - 'plinth.context_processor'] + 'context_processors.common'] logging_configuration = { 'version': 1, @@ -179,9 +186,18 @@ def configure_django(): 'django.contrib.contenttypes', 'django.contrib.messages'], LOGGING=logging_configuration, - LOGIN_URL=cfg.server_dir + '/accounts/login/', - LOGIN_REDIRECT_URL=cfg.server_dir + '/', - LOGOUT_URL=cfg.server_dir + '/accounts/logout/', + LOGIN_URL='lib:login', + LOGIN_REDIRECT_URL='apps:index', + LOGOUT_URL='lib:logout', + MIDDLEWARE_CLASSES=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modules.first_boot.middleware.FirstBootMiddleware', + ), ROOT_URLCONF='urls', SESSION_ENGINE='django.contrib.sessions.backends.file', SESSION_FILE_PATH=sessions_directory, diff --git a/plinth.sample.config b/plinth.sample.config index bbd023a23..4a900b7c9 100644 --- a/plinth.sample.config +++ b/plinth.sample.config @@ -10,6 +10,7 @@ log_dir = %(data_dir)s pid_dir = %(data_dir)s python_root = %(file_root)s server_dir = plinth/ +actions_dir = %(file_root)s/actions # file locations store_file = %(data_dir)s/store.sqlite3 diff --git a/templates/base.html b/templates/base.html index 9dfab0814..c5c142875 100644 --- a/templates/base.html +++ b/templates/base.html @@ -47,6 +47,8 @@ {{ css|safe }} + {% block app_head %}{% endblock %} + {% block page_head %}{% endblock %} @@ -59,15 +61,15 @@ - + FreedomBox - FreedomBox + FreedomBox {% block add_nav_and_login %}