Merge pull request #91 from SunilMohanAdapa/fonfon-dev

Additional fixes over merge request #89
This commit is contained in:
Nick Daly 2014-08-19 21:34:20 -05:00
commit b78df30c8b
54 changed files with 428 additions and 456 deletions

3
.gitignore vendored
View File

@ -1,6 +1,7 @@
current-*.tar.gz current-*.tar.gz
*.pyc *.pyc
*.py.bak *.py.bak
*.swp
*.tiny.css *.tiny.css
data/*.log data/*.log
data/cherrypy_sessions data/cherrypy_sessions
@ -28,4 +29,4 @@ data/plinth.sqlite3
predepend predepend
build/ build/
*.pid *.pid
.emacs.desktop* .emacs.desktop*

View File

@ -32,7 +32,8 @@ Actions run commands with this contract (version 1.1):
C. Only one action can be called at a time. 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'" $ action="echo '$options'; echo 'oops'"
$ options="hi" $ options="hi"
@ -51,8 +52,8 @@ Actions run commands with this contract (version 1.1):
easier than detecting if it occurs. easier than detecting if it occurs.
The options list is coerced into a space-separated string before being The options list is coerced into a space-separated string before being
shell-escaped. Option lists including shell escape characters may need to shell-escaped. Option lists including shell escape characters may need
be unescaped on the receiving end. to be unescaped on the receiving end.
E. Actions must exist in the actions directory. 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 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. """Safely run a specific action as the current user.
See actions._run for more information. See actions._run for more information.
@ -84,7 +93,8 @@ def run(action, options = None, async = False):
""" """
return _run(action, options, 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. """Safely run a specific action as root.
See actions._run for more information. See actions._run for more information.
@ -92,27 +102,26 @@ def superuser_run(action, options = None, async = False):
""" """
return _run(action, options, async, True) 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. """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 is None:
if options == None:
options = [] options = []
# contract 3A and 3B: don't call anything outside of the actions directory. # contract 3A and 3B: don't call anything outside of the actions directory.
if os.sep in action: if os.sep in action:
raise ValueError("Action can't contain:" + os.sep) 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 3C: interpret shell escape sequences as literal file names.
# contract 3E: fail if the action doesn't exist or exists elsewhere. # 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] 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 options:
if not hasattr(options, "__iter__"): if not hasattr(options, "__iter__"):
options = [options] options = [options]
cmd += [pipes.quote(option) for option in options] cmd += [pipes.quote(option) for option in options]
# contract 1: commands can run via sudo. # contract 1: commands can run via sudo.
if run_as_root: if run_as_root:
cmd = ["sudo", "-n"] + cmd cmd = ["sudo", "-n"] + cmd
LOGGER.info('Executing command - %s', cmd)
# contract 3C: don't interpret shell escape sequences. # contract 3C: don't interpret shell escape sequences.
# contract 5 (and 6-ish). # contract 5 (and 6-ish).
proc = subprocess.Popen( proc = subprocess.Popen(
cmd, cmd,
stdout = subprocess.PIPE, stdout=subprocess.PIPE,
stderr= subprocess.PIPE, stderr=subprocess.PIPE,
shell=False) shell=False)
if not async: if not async:
output, error = proc.communicate() 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

4
cfg.py
View File

@ -12,6 +12,7 @@ python_root = None
data_dir = None data_dir = None
store_file = None store_file = None
user_db = None user_db = None
actions_dir = None
status_log_file = None status_log_file = None
access_log_file = None access_log_file = None
pidfile = None pidfile = None
@ -19,7 +20,7 @@ host = None
port = None port = None
debug = False debug = False
no_daemon = False no_daemon = False
server_dir = '' server_dir = '/'
main_menu = Menu() main_menu = Menu()
@ -41,6 +42,7 @@ def read():
('Path', 'data_dir'), ('Path', 'data_dir'),
('Path', 'store_file'), ('Path', 'store_file'),
('Path', 'user_db'), ('Path', 'user_db'),
('Path', 'actions_dir'),
('Path', 'status_log_file'), ('Path', 'status_log_file'),
('Path', 'access_log_file'), ('Path', 'access_log_file'),
('Path', 'pidfile'), ('Path', 'pidfile'),

34
context_processors.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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
}

View File

@ -82,7 +82,7 @@ latex: $(LATEX)
# This gets us the html sections complete with TOC, but without the # 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 %.part.html: %.html
csplit -s -f $@ $< '%.*<body>%' csplit -s -f $@ $< '%.*<body>%'
sed '1d' $@00 > $@01 sed '1d' $@00 > $@01

30
errors.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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

46
menu.py
View File

@ -1,5 +1,4 @@
from urlparse import urlparse from django.core.urlresolvers import reverse
import cfg
class Menu(object): class Menu(object):
@ -20,19 +19,20 @@ class Menu(object):
orders, but feel free to disregard that. If you need more orders, but feel free to disregard that. If you need more
granularity, don't bother renumbering things. Feel free to granularity, don't bother renumbering things. Feel free to
use fractional orders. use fractional orders.
"""
"""
self.label = label self.label = label
self.icon = icon self.icon = icon
self.url = url self.url = url
self.order = order 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 = [] self.items = []
def find(self, url, basehref=True): def get(self, urlname, url_args=None, url_kwargs=None):
"""Return a menu item with given URL""" """Return a menu item with given URL name."""
if basehref and url.startswith('/'): url = reverse(urlname, args=url_args, kwargs=url_kwargs)
url = cfg.server_dir + url
for item in self.items: for item in self.items:
if item.url == url: if item.url == url:
return item return item
@ -43,32 +43,28 @@ class Menu(object):
"""Sort the items in self.items by order.""" """Sort the items in self.items by order."""
self.items = sorted(self.items, key=lambda x: x.order, reverse=False) self.items = sorted(self.items, key=lambda x: x.order, reverse=False)
def add_item(self, label, icon, url, order=50, basehref=True): def add_urlname(self, label, icon, urlname, order=50, url_args=None,
"""This method creates a menu item with the parameters, adds url_kwargs=None):
that menu item to this menu, and returns the item. """Add a named URL to the menu (via add_item).
If BASEHREF is true and url start with a slash, prepend the url_args and url_kwargs will be passed on to Django reverse().
cfg.server_dir to it"""
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) item = Menu(label=label, icon=icon, url=url, order=order)
self.items.append(item) self.items.append(item)
self.sort_items() self.sort_items()
return item 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): 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: for item in self.items:
if request.path.startswith(item.url): if request.path.startswith(item.url):
return item return item

View File

@ -25,9 +25,12 @@ import logging
import os import os
import urls import urls
import cfg
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
LOADED_MODULES = []
def load_modules(): def load_modules():
""" """
@ -47,8 +50,10 @@ def load_modules():
except Exception as exception: except Exception as exception:
LOGGER.exception('Could not import modules/%s: %s', LOGGER.exception('Could not import modules/%s: %s',
name, exception) name, exception)
if cfg.debug:
raise
_include_module_urls(full_name) _include_module_urls(full_name, name)
ordered_modules = [] ordered_modules = []
remaining_modules = dict(modules) remaining_modules = dict(modules)
@ -68,6 +73,7 @@ def load_modules():
for module_name in ordered_modules: for module_name in ordered_modules:
_initialize_module(modules[module_name]) _initialize_module(modules[module_name])
LOADED_MODULES.append(module_name)
def _insert_modules(module_name, module, remaining_modules, ordered_modules): 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) 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""" """Include the module's URLs in global project URLs list"""
url_module = module_name + '.urls' url_module = module_name + '.urls'
try: try:
urls.urlpatterns += django.conf.urls.patterns( urls.urlpatterns += django.conf.urls.patterns(
'', django.conf.urls.url( '', django.conf.urls.url(
r'', django.conf.urls.include(url_module))) r'', django.conf.urls.include(url_module, namespace)))
except ImportError: except ImportError:
LOGGER.debug('No URLs for %s', module_name) LOGGER.debug('No URLs for %s', module_name)
if cfg.debug:
raise
def _initialize_module(module): def _initialize_module(module):
@ -121,6 +129,8 @@ def _initialize_module(module):
except Exception as exception: except Exception as exception:
LOGGER.exception('Exception while running init for %s: %s', LOGGER.exception('Exception while running init for %s: %s',
module, exception) module, exception)
if cfg.debug:
raise
def get_template_directories(): def get_template_directories():

View File

@ -6,7 +6,7 @@ import cfg
def init(): def init():
"""Initailize the apps module""" """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): def index(request):

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.apps.apps', 'modules.apps.apps',
url(r'^apps/$', 'index') url(r'^apps/$', 'index', name='index')
) )

View File

@ -95,8 +95,8 @@ and must not be greater than 63 characters in length.'),
def init(): def init():
"""Initialize the module""" """Initialize the module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item(_('Configure'), 'icon-cog', '/sys/config', 10) menu.add_urlname(_('Configure'), 'icon-cog', 'config:index', 10)
@login_required @login_required
@ -133,20 +133,22 @@ def get_status():
def _apply_changes(request, old_status, new_status): def _apply_changes(request, old_status, new_status):
"""Apply the form changes""" """Apply the form changes"""
if old_status['hostname'] != new_status['hostname']: if old_status['hostname'] != new_status['hostname']:
if not set_hostname(new_status['hostname']): try:
messages.error(request, _('Setting hostname failed')) set_hostname(new_status['hostname'])
except Exception as exception:
messages.error(request, _('Error setting hostname: %s') %
str(exception))
else: else:
messages.success(request, _('Hostname set')) messages.success(request, _('Hostname set'))
else: else:
messages.info(request, _('Hostname is unchanged')) messages.info(request, _('Hostname is unchanged'))
if old_status['time_zone'] != new_status['time_zone']: if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change', try:
[new_status['time_zone']]) actions.superuser_run('timezone-change', [new_status['time_zone']])
del output # Unused except Exception as exception:
if error: messages.error(request, _('Error setting time zone: %s') %
messages.error(request, str(exception))
_('Error setting time zone - %s') % error)
else: else:
messages.success(request, _('Time zone set')) messages.success(request, _('Time zone set'))
else: else:
@ -160,11 +162,6 @@ def set_hostname(hostname):
hostname = str(hostname) hostname = str(hostname)
LOGGER.info('Changing hostname to - %s', hostname) LOGGER.info('Changing hostname to - %s', hostname)
try: actions.superuser_run('xmpp-pre-hostname-change')
actions.superuser_run('xmpp-pre-hostname-change') actions.superuser_run('hostname-change', hostname)
actions.superuser_run('hostname-change', hostname) actions.superuser_run('xmpp-hostname-change', hostname, async=True)
actions.superuser_run('xmpp-hostname-change', hostname, async=True)
except OSError:
return False
return True

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.config.config', 'modules.config.config',
url(r'^sys/config/$', 'index'), url(r'^sys/config/$', 'index', name='index'),
) )

View File

@ -25,12 +25,13 @@ from gettext import gettext as _
import actions import actions
import cfg import cfg
from errors import ActionError
def init(): def init():
"""Initialize the module""" """Initialize the module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30) menu.add_urlname("Diagnostics", "icon-screenshot", "diagnostics:index", 30)
@login_required @login_required
@ -43,7 +44,15 @@ def index(request):
@login_required @login_required
def test(request): def test(request):
"""Run diagnostics and the output page""" """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', return TemplateResponse(request, 'diagnostics_test.html',
{'title': _('Diagnostic Test'), {'title': _('Diagnostic Test'),
'diagnostics_output': output, 'diagnostics_output': output,

View File

@ -24,8 +24,8 @@
system to confirm that network services are running and configured system to confirm that network services are running and configured
properly. It may take a minute to complete.</p> properly. It may take a minute to complete.</p>
<p><a class="btn btn-primary btn-large" <p><a class="btn btn-primary btn-large" href="{% url 'diagnostics:test' %}">
href="{{ basehref }}/sys/diagnostics/test">Run diagnostic test Run diagnostic test &raquo;
&raquo;</a></p> </a></p>
{% endblock %} {% endblock %}

View File

@ -24,6 +24,6 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.diagnostics.diagnostics', 'modules.diagnostics.diagnostics',
url(r'^sys/diagnostics/$', 'index'), url(r'^sys/diagnostics/$', 'index', name='index'),
url(r'^sys/diagnostics/test/$', 'test'), url(r'^sys/diagnostics/test/$', 'test', name='test'),
) )

View File

@ -16,8 +16,8 @@ class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
def init(): def init():
"""Initialize the module""" """Initialize the module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item(_('Expert Mode'), 'icon-cog', '/sys/expert', 10) menu.add_urlname(_('Expert Mode'), 'icon-cog', 'expert_mode:index', 10)
@login_required @login_required

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.expert_mode.expert_mode', 'modules.expert_mode.expert_mode',
url(r'^sys/expert/$', 'index'), url(r'^sys/expert/$', 'index', name='index'),
) )

View File

@ -34,8 +34,8 @@ LOGGER = logging.getLogger(__name__)
def init(): def init():
"""Initailze firewall module""" """Initailze firewall module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item(_('Firewall'), 'icon-flag', '/sys/firewall', 50) menu.add_urlname(_('Firewall'), 'icon-flag', 'firewall:index', 50)
service_module.ENABLED.connect(on_service_enabled) 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""" """Run an given command and raise exception if there was an error"""
command = 'firewall' command = 'firewall'
LOGGER.info('Running command - %s, %s, %s', command, arguments, superuser)
if superuser: if superuser:
output, error = actions.superuser_run(command, arguments) return actions.superuser_run(command, arguments)
else: else:
output, error = actions.run(command, arguments) return actions.run(command, arguments)
if error:
raise Exception('Error setting/getting firewalld confguration - %s'
% error)
return output

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.firewall.firewall', 'modules.firewall.firewall',
url(r'^sys/firewall/$', 'index') url(r'^sys/firewall/$', 'index', name='index')
) )

View File

@ -21,6 +21,7 @@ The Plinth first-connection process has several stages:
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core import validators from django.core import validators
from django.core.urlresolvers import reverse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
@ -94,7 +95,7 @@ def state0(request):
""" """
try: try:
if _read_state() >= 5: if _read_state() >= 5:
return HttpResponseRedirect(cfg.server_dir) return HttpResponseRedirect(reverse('index'))
except KeyError: except KeyError:
pass pass
@ -112,8 +113,7 @@ def state0(request):
if success: if success:
# Everything is good, permanently mark and move to page 2 # Everything is good, permanently mark and move to page 2
_write_state(1) _write_state(1)
return HttpResponseRedirect( return HttpResponseRedirect(reverse('first_boot:state1'))
cfg.server_dir + '/firstboot/state1')
else: else:
form = State0Form(initial=status, prefix='firstboot') form = State0Form(initial=status, prefix='firstboot')

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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']))

View File

@ -23,7 +23,7 @@
{% block main_block %} {% block main_block %}
<p>Welcome screen not completely implemented yet. Press <a <p>Welcome screen not completely implemented yet. Press <a
href="{{basehref }}/apps">continue</a> to see the rest of the href="{% url 'apps:index' %}">continue</a> to see the rest of the
web interface.</p> web interface.</p>
<ul> <ul>

View File

@ -24,7 +24,7 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.first_boot.first_boot', 'modules.first_boot.first_boot',
url(r'^firstboot/$', 'index'), url(r'^firstboot/$', 'index', name='index'),
url(r'^firstboot/state0/$', 'state0'), url(r'^firstboot/state0/$', 'state0', name='state0'),
url(r'^firstboot/state1/$', 'state1') url(r'^firstboot/state1/$', 'state1', name='state1')
) )

View File

@ -1,5 +1,6 @@
import os import os
from gettext import gettext as _ from gettext import gettext as _
from django.http import Http404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
import cfg import cfg
@ -7,15 +8,17 @@ import cfg
def init(): def init():
"""Initialize the Help module""" """Initialize the Help module"""
menu = cfg.main_menu.add_item(_('Documentation'), 'icon-book', '/help', menu = cfg.main_menu.add_urlname(_('Documentation'), 'icon-book',
101) 'help:index', 101)
menu.add_item(_("Where to Get Help"), "icon-search", "/help/index", 5) menu.add_urlname(_('Where to Get Help'), 'icon-search',
menu.add_item(_('Developer\'s Manual'), 'icon-info-sign', 'help:index_explicit', 5)
'/help/view/plinth', 10) menu.add_urlname(_('Developer\'s Manual'), 'icon-info-sign',
menu.add_item(_('FAQ'), 'icon-question-sign', '/help/view/faq', 20) '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', menu.add_item(_('%s Wiki' % cfg.box_name), 'icon-pencil',
'http://wiki.debian.org/FreedomBox', 30) '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): def index(request):
@ -30,10 +33,14 @@ def about(request):
return TemplateResponse(request, 'about.html', {'title': title}) return TemplateResponse(request, 'about.html', {'title': title})
def default(request, page=''): def helppage(request, page):
"""Serve the documentation pages""" """Serve a help page from the 'doc' directory"""
with open(os.path.join('doc', '%s.part.html' % page), 'r') as input_file: try:
main = input_file.read() 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 title = _('%s Documentation') % cfg.product_name
return TemplateResponse(request, 'base.html', return TemplateResponse(request, 'base.html',

View File

@ -1,4 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of Plinth.
@ -20,7 +21,7 @@
{% block main_block %} {% block main_block %}
<img src="{{ basehref }}/static/theme/img/freedombox-logo-250px.png" <img src="{% static 'theme/img/freedombox-logo-250px.png' %}"
class="main-graphic" /> class="main-graphic" />
<p>We live in a world where our use of the network is mediated by <p>We live in a world where our use of the network is mediated by

View File

@ -23,7 +23,7 @@
<p>There are a variety of places to go for help with <p>There are a variety of places to go for help with
{{ cfg.product_name }} and the box it runs on.</p> {{ cfg.product_name }} and the box it runs on.</p>
<p>This front end has a <a href="{{ basehref }}/help/view/plinth"> <p>This front end has a <a href="{% url 'help:helppage' 'plinth' %}">
developer's manual</a>. It isn't complete, but it is the first place developer's manual</a>. It isn't complete, but it is the first place
to look. Feel free to offer suggestions, edits, and screenshots for to look. Feel free to offer suggestions, edits, and screenshots for
completing it!</p> completing it!</p>
@ -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 front end, but they are an incredible resource for general Debian
issues.</p> issues.</p>
<p>There is no <a href="{{ basehref }}/help/view/faq">FAQ</a> because <p>There is no <a href="{% url 'help:helppage' 'faq' %}">FAQ</a> because
the question frequency is currently zero for all questions.</p> the question frequency is currently zero for all questions.</p>
{% endblock %} {% endblock %}

View File

@ -24,11 +24,11 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.help.help', 'modules.help.help',
url(r'^help/$', 'index'), # having two urls for one page is a hack to help the current url/menu
url(r'^help/index/$', 'index'), # system highlight the correct menu item. Every submenu-item with the same
url(r'^help/about/$', 'about'), # url prefix as the main-menu is highlighted automatically.
url(r'^help/view/(?P<page>design)/$', 'default'), url(r'^help/$', 'index', name='index'),
url(r'^help/view/(?P<page>plinth)/$', 'default'), url(r'^help/index/$', 'index', name='index_explicit'),
url(r'^help/view/(?P<page>hacking)/$', 'default'), url(r'^help/about/$', 'about', name='about'),
url(r'^help/view/(?P<page>faq)/$', 'default'), url(r'^help/page/(plinth|hacking|faq)/$', 'helppage', name='helppage'),
) )

View File

@ -20,14 +20,13 @@ URLs for the Lib module
""" """
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse_lazy
import cfg
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'', '',
url(r'^accounts/login/$', 'django.contrib.auth.views.login', 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', url(r'^accounts/logout/$', 'django.contrib.auth.views.logout',
{'next_page': cfg.server_dir}) {'next_page': reverse_lazy('index')}, name='logout')
) )

View File

@ -19,8 +19,8 @@ class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232
def init(): def init():
"""Initialize the ownCloud module""" """Initialize the ownCloud module"""
menu = cfg.main_menu.find('/apps') menu = cfg.main_menu.get('apps:index')
menu.add_item('Owncloud', 'icon-picture', '/apps/owncloud', 35) menu.add_urlname('Owncloud', 'icon-picture', 'owncloud:index', 35)
status = get_status() status = get_status()
@ -53,10 +53,7 @@ def index(request):
def get_status(): def get_status():
"""Return the current status""" """Return the current status"""
output, error = actions.run('owncloud-setup', 'status') output = actions.run('owncloud-setup', 'status')
if error:
raise Exception('Error getting ownCloud status: %s' % error)
return {'enabled': 'enable' in output.split()} return {'enabled': 'enable' in output.split()}

View File

@ -28,7 +28,7 @@
{{ form|bootstrap }} {{ form|bootstrap }}
<p>When enabled, the owncloud installation will be available <p>When enabled, the owncloud installation will be available
from <a href="/owncloud">owncloud</a> on the web server. Visit from <a href="/owncloud">owncloud</a> on the web server. Visit
this URL to set up the initial administration account for this URL to set up the initial administration account for
owncloud.</p> owncloud.</p>

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.owncloud.owncloud', 'modules.owncloud.owncloud',
url(r'^apps/owncloud/$', 'index'), url(r'^apps/owncloud/$', 'index', name='index'),
) )

View File

@ -10,19 +10,13 @@ import cfg
def get_modules_available(): def get_modules_available():
"""Return list of all modules""" """Return list of all modules"""
output, error = actions.run('module-manager', ['list-available']) output = actions.run('module-manager', ['list-available'])
if error:
raise Exception('Error getting modules: %s' % error)
return output.split() return output.split()
def get_modules_enabled(): def get_modules_enabled():
"""Return list of all modules""" """Return list of all modules"""
output, error = actions.run('module-manager', ['list-enabled']) output = actions.run('module-manager', ['list-enabled'])
if error:
raise Exception('Error getting enabled modules - %s' % error)
return output.split() return output.split()
@ -42,8 +36,8 @@ class PackagesForm(forms.Form):
def init(): def init():
"""Initialize the Packages module""" """Initialize the Packages module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item('Package Manager', 'icon-gift', '/sys/packages', 20) menu.add_urlname('Package Manager', 'icon-gift', 'packages:index', 20)
@login_required @login_required
@ -88,13 +82,12 @@ def _apply_changes(request, old_status, new_status):
module = field.split('_enabled')[0] module = field.split('_enabled')[0]
if enabled: if enabled:
output, error = actions.superuser_run( try:
'module-manager', ['enable', cfg.python_root, module]) actions.superuser_run(
del output # Unused 'module-manager', ['enable', cfg.python_root, module])
except Exception:
# TODO: need to get plinth to load the module we just # TODO: need to get plinth to load the module we just
# enabled # enabled
if error:
messages.error( messages.error(
request, _('Error enabling module - {module}').format( request, _('Error enabling module - {module}').format(
module=module)) module=module))
@ -103,13 +96,12 @@ def _apply_changes(request, old_status, new_status):
request, _('Module enabled - {module}').format( request, _('Module enabled - {module}').format(
module=module)) module=module))
else: else:
output, error = actions.superuser_run( try:
'module-manager', ['disable', cfg.python_root, module]) actions.superuser_run(
del output # Unused 'module-manager', ['disable', cfg.python_root, module])
except Exception:
# TODO: need a smoother way for plinth to unload the # TODO: need a smoother way for plinth to unload the
# module # module
if error:
messages.error( messages.error(
request, _('Error disabling module - {module}').format( request, _('Error disabling module - {module}').format(
module=module)) module=module))

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.packages.packages', 'modules.packages.packages',
url(r'^sys/packages/$', 'index'), url(r'^sys/packages/$', 'index', name='index'),
) )

View File

@ -23,6 +23,7 @@ from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core import validators from django.core import validators
from django.core.urlresolvers import reverse_lazy
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -38,16 +39,16 @@ LOGGER = logging.getLogger(__name__)
def init(): def init():
"""Intialize the PageKite module""" """Intialize the PageKite module"""
menu = cfg.main_menu.find('/apps') menu = cfg.main_menu.get('apps:index')
menu.add_item(_('Public Visibility (PageKite)'), 'icon-flag', menu.add_urlname(_('Public Visibility (PageKite)'), 'icon-flag',
'/apps/pagekite', 50) 'pagekite:index', 50)
@login_required @login_required
def index(request): def index(request):
"""Serve introdution page""" """Serve introdution page"""
menu = {'title': _('PageKite'), menu = {'title': _('PageKite'),
'items': [{'url': '/apps/pagekite/configure', 'items': [{'url': reverse_lazy('pagekite:configure'),
'text': _('Configure PageKite')}]} 'text': _('Configure PageKite')}]}
sidebar_right = render_to_string('menu_block.html', {'menu': menu}, 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""" """Run an given command and raise exception if there was an error"""
command = 'pagekite-configure' command = 'pagekite-configure'
LOGGER.info('Running command - %s, %s, %s', command, arguments, superuser)
if superuser: if superuser:
output, error = actions.superuser_run(command, arguments) return actions.superuser_run(command, arguments)
else: else:
output, error = actions.run(command, arguments) return actions.run(command, arguments)
if error:
raise Exception('Error setting/getting PageKite confguration - %s'
% error)
return output

View File

@ -49,7 +49,7 @@ there. In future, it might be possible to use your buddy's
<p> <p>
<a class='btn btn-primary btn-lg' <a class='btn btn-primary btn-lg'
href="{{ basehref }}/apps/pagekite/configure">Configure href="{% url 'pagekite:configure' %}">Configure
PageKite</a> PageKite</a>
</p> </p>

View File

@ -24,6 +24,6 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.pagekite.pagekite', 'modules.pagekite.pagekite',
url(r'^apps/pagekite/$', 'index'), url(r'^apps/pagekite/$', 'index', name='index'),
url(r'^apps/pagekite/configure/$', 'configure'), url(r'^apps/pagekite/configure/$', 'configure', name='configure'),
) )

View File

@ -6,7 +6,7 @@ import cfg
def init(): def init():
"""Initialize the system module""" """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): def index(request):

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.system.system', 'modules.system.system',
url(r'^sys/$', 'index'), url(r'^sys/$', 'index', name='index'),
) )

View File

@ -29,15 +29,14 @@ import cfg
def init(): def init():
"""Initialize the Tor module""" """Initialize the Tor module"""
menu = cfg.main_menu.find('/apps') menu = cfg.main_menu.get('apps:index')
menu.add_item("Tor", "icon-eye-close", "/apps/tor", 30) menu.add_urlname("Tor", "icon-eye-close", "tor:index", 30)
@login_required @login_required
def index(request): def index(request):
"""Service the index page""" """Service the index page"""
output, error = actions.superuser_run("tor-get-ports") output = actions.superuser_run("tor-get-ports")
del error # Unused
port_info = output.split("\n") port_info = output.split("\n")
tor_ports = {} tor_ports = {}

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.tor.tor', 'modules.tor.tor',
url(r'^apps/tor/$', 'index') url(r'^apps/tor/$', 'index', name='index')
) )

View File

@ -24,7 +24,7 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.users.users', 'modules.users.users',
url(r'^sys/users/$', 'index'), url(r'^sys/users/$', 'index', name='index'),
url(r'^sys/users/add/$', 'add'), url(r'^sys/users/add/$', 'add', name='add'),
url(r'^sys/users/edit/$', 'edit') url(r'^sys/users/edit/$', 'edit', name='edit')
) )

View File

@ -3,6 +3,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import validators from django.core import validators
from django.core.urlresolvers import reverse_lazy
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -18,17 +19,17 @@ LOGGER = logging.getLogger(__name__)
def init(): def init():
"""Intialize the module""" """Intialize the module"""
menu = cfg.main_menu.find('/sys') menu = cfg.main_menu.get('system:index')
menu.add_item(_('Users and Groups'), 'icon-user', '/sys/users', 15) menu.add_urlname(_('Users and Groups'), 'icon-user', 'users:index', 15)
@login_required @login_required
def index(request): def index(request):
"""Return a rendered users page""" """Return a rendered users page"""
menu = {'title': _('Users and Groups'), menu = {'title': _('Users and Groups'),
'items': [{'url': '/sys/users/add', 'items': [{'url': reverse_lazy('users:add'),
'text': _('Add User')}, 'text': _('Add User')},
{'url': '/sys/users/edit', {'url': reverse_lazy('users:edit'),
'text': _('Edit Users')}]} 'text': _('Edit Users')}]}
sidebar_right = render_to_string('menu_block.html', {'menu': menu}, sidebar_right = render_to_string('menu_block.html', {'menu': menu},

View File

@ -24,7 +24,7 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.xmpp.xmpp', 'modules.xmpp.xmpp',
url(r'^apps/xmpp/$', 'index'), url(r'^apps/xmpp/$', 'index', name='index'),
url(r'^apps/xmpp/configure/$', 'configure'), url(r'^apps/xmpp/configure/$', 'configure', name='configure'),
url(r'^apps/xmpp/register/$', 'register') url(r'^apps/xmpp/register/$', 'register', name='register')
) )

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse_lazy
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -14,18 +15,26 @@ import service
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
SIDE_MENU = {'title': _('XMPP'), SIDE_MENU = {
'items': [{'url': '/apps/xmpp/configure', 'title': _('XMPP'),
'text': 'Configure XMPP Server'}, 'items': [
{'url': '/apps/xmpp/register', {
'text': 'Register XMPP Account'}]} 'url': reverse_lazy('xmpp:configure'),
'text': _('Configure XMPP Server'),
},
{
'url': reverse_lazy('xmpp:register'),
'text': _('Register XMPP Account'),
}
]
}
def init(): def init():
"""Initialize the XMPP module""" """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('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( service.Service(
'xmpp-client', _('Chat Server - client connections'), 'xmpp-client', _('Chat Server - client connections'),
@ -88,10 +97,7 @@ def configure(request):
def get_status(): def get_status():
"""Return the current status""" """Return the current status"""
output, error = actions.run('xmpp-setup', 'status') output = actions.run('xmpp-setup', 'status')
if error:
raise Exception('Error getting status: %s' % error)
return {'inband_enabled': 'inband_enable' in output.split()} return {'inband_enabled': 'inband_enable' in output.split()}
@ -111,11 +117,7 @@ def _apply_changes(request, old_status, new_status):
option = 'noinband_enable' option = 'noinband_enable'
LOGGER.info('Option - %s', option) LOGGER.info('Option - %s', option)
actions.superuser_run('xmpp-setup', [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 class RegisterForm(forms.Form): # pylint: disable-msg=W0232
@ -151,10 +153,8 @@ def register(request):
def _register_user(request, data): def _register_user(request, data):
"""Register a new XMPP user""" """Register a new XMPP user"""
output, error = actions.superuser_run( output = actions.superuser_run(
'xmpp-register', [data['username'], data['password']]) 'xmpp-register', [data['username'], data['password']])
if error:
raise Exception('Error registering user - %s' % error)
if 'successfully registered' in output: if 'successfully registered' in output:
messages.success(request, _('Registered account for %s') % messages.success(request, _('Registered account for %s') %

View File

@ -35,6 +35,7 @@ def parse_arguments():
parser.add_argument( parser.add_argument(
'--pidfile', default='plinth.pid', '--pidfile', default='plinth.pid',
help='specify a file in which the server may write its 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( parser.add_argument(
'--server_dir', default='/', '--server_dir', default='/',
help='web server path under which to serve') 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.root': '%s/static' % cfg.file_root,
'tools.staticdir.on': True, 'tools.staticdir.on': True,
'tools.staticdir.dir': '.'}} '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: if not cfg.no_daemon:
Daemonizer(cherrypy.engine).subscribe() Daemonizer(cherrypy.engine).subscribe()
@ -109,23 +125,14 @@ def setup_server():
cherrypy.engine.signal_handler.subscribe() 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(): def configure_django():
"""Setup Django configuration in the absense of .settings file""" """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 = [ context_processors = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug', 'django.core.context_processors.debug',
@ -134,7 +141,7 @@ def configure_django():
'django.core.context_processors.static', 'django.core.context_processors.static',
'django.core.context_processors.tz', 'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'plinth.context_processor'] 'context_processors.common']
logging_configuration = { logging_configuration = {
'version': 1, 'version': 1,
@ -179,9 +186,18 @@ def configure_django():
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.messages'], 'django.contrib.messages'],
LOGGING=logging_configuration, LOGGING=logging_configuration,
LOGIN_URL=cfg.server_dir + '/accounts/login/', LOGIN_URL='lib:login',
LOGIN_REDIRECT_URL=cfg.server_dir + '/', LOGIN_REDIRECT_URL='apps:index',
LOGOUT_URL=cfg.server_dir + '/accounts/logout/', 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', ROOT_URLCONF='urls',
SESSION_ENGINE='django.contrib.sessions.backends.file', SESSION_ENGINE='django.contrib.sessions.backends.file',
SESSION_FILE_PATH=sessions_directory, SESSION_FILE_PATH=sessions_directory,

View File

@ -10,6 +10,7 @@ log_dir = %(data_dir)s
pid_dir = %(data_dir)s pid_dir = %(data_dir)s
python_root = %(file_root)s python_root = %(file_root)s
server_dir = plinth/ server_dir = plinth/
actions_dir = %(file_root)s/actions
# file locations # file locations
store_file = %(data_dir)s/store.sqlite3 store_file = %(data_dir)s/store.sqlite3

View File

@ -47,6 +47,8 @@
<!-- CSS from previous Plinth template, not sure what to keep yet --> <!-- CSS from previous Plinth template, not sure what to keep yet -->
{{ css|safe }} {{ css|safe }}
<!-- End Plinth CSS --> <!-- End Plinth CSS -->
{% block app_head %}<!-- placeholder for app/module-specific head files -->{% endblock %}
{% block page_head %}<!-- placeholder for page-specific head files -->{% endblock %}
</head> </head>
<body> <body>
<!--[if lt IE 7]><p class=chromeframe>Your browser is <em>ancient!</em> <a href="http://mozilla.org/firefox">Upgrade to a modern version of Firefox</a> to experience this site.</p><![endif]--> <!--[if lt IE 7]><p class=chromeframe>Your browser is <em>ancient!</em> <a href="http://mozilla.org/firefox">Upgrade to a modern version of Firefox</a> to experience this site.</p><![endif]-->
@ -59,15 +61,15 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</a> </a>
<a href="{{ basehref }}/" class="logo-top"> <a href="{% url 'index' %}" class="logo-top">
<img src="{% static 'theme/img/freedombox-logo-32px.png' %}" <img src="{% static 'theme/img/freedombox-logo-32px.png' %}"
alt="FreedomBox" /> alt="FreedomBox" />
</a> </a>
<a class="brand" href="{{ basehref }}/">FreedomBox</a> <a class="brand" href="{% url 'index' %}">FreedomBox</a>
{% block add_nav_and_login %} {% block add_nav_and_login %}
<div class="nav-collapse"> <div class="nav-collapse">
<ul class="nav"> <ul class="nav">
{% for item in main_menu.items %} {% for item in cfg.main_menu.items %}
{% if item.url in active_menu_urls %} {% if item.url in active_menu_urls %}
<li class="active"> <li class="active">
<a href="{{ item.url }}" class="active"> <a href="{{ item.url }}" class="active">
@ -85,15 +87,15 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p class="navbar-text pull-right"> <p class="navbar-text pull-right">
<i class="icon-user icon-white nav-icon"></i> <i class="icon-user icon-white nav-icon"></i>
Logged in as <a href="{{ user.username }}">{{ user.username }}</a>. Logged in as <a href="#">{{ user.username }}</a>.
<a href="{{ basehref }}/accounts/logout" title="Log out"> <a href="{% url 'lib:logout' %}" title="Log out">
Log out</a>. Log out</a>.
</p> </p>
{% else %} {% else %}
<p class="navbar-text pull-right"> <p class="navbar-text pull-right">
Not logged in. Not logged in.
<i class="icon-user icon-white nav-icon"></i> <i class="icon-user icon-white nav-icon"></i>
<a href="{{ basehref }}/accounts/login" title="Log in"> <a href="{% url 'lib:login' %}" title="Log in">
Log in</a>. Log in</a>.
</p> </p>
{% endif %} {% endif %}
@ -169,5 +171,7 @@
{% block js_block %} {% block js_block %}
{{ js|safe }} {{ js|safe }}
{% endblock %} {% endblock %}
{% block app_js %}<!-- placeholder for app-specific js files -->{% endblock %}
{% block page_js %}<!-- placeholder for page-specific js files -->{% endblock %}
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
<li class='nav-header'>{{ menu.title }}</li> <li class='nav-header'>{{ menu.title }}</li>
{% for item in menu.items %} {% for item in menu.items %}
<li> <li>
<a href="{{ basehref }}{{ item.url }}">{{ item.text }}</a> <a href="{{ item.url }}">{{ item.text }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -7,6 +7,11 @@ import shlex
import subprocess import subprocess
import unittest import unittest
import cfg
ROOT_DIR = os.path.split(os.path.abspath(os.path.split(__file__)[0]))[0]
cfg.actions_dir = os.path.join(ROOT_DIR, 'actions')
class TestPrivileged(unittest.TestCase): class TestPrivileged(unittest.TestCase):
"""Verify that privileged actions perform as expected. """Verify that privileged actions perform as expected.
@ -25,10 +30,9 @@ class TestPrivileged(unittest.TestCase):
os.remove("actions/echo") os.remove("actions/echo")
os.remove("actions/id") os.remove("actions/id")
def test_run_as_root(self): def notest_run_as_root(self):
"""1. Privileged actions run as root. """1. Privileged actions run as root. """
# TODO: it's not allowed to call a symlink in the actions dir anymore
"""
self.assertEqual( self.assertEqual(
"0", # user 0 is root "0", # user 0 is root
superuser_run("id", "-ur")[0].strip()) superuser_run("id", "-ur")[0].strip())
@ -75,45 +79,33 @@ class TestPrivileged(unittest.TestCase):
for option in options: for option in options:
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
output = run(action, option) output = run(action, option)
# if it somewhow doesn't error, we'd better not evaluate
# if it somewhow doesn't error, we'd better not evaluate the # the data.
# data.
self.assertFalse("2" in output[0]) self.assertFalse("2" in output[0])
def test_breakout_option_string(self): def test_breakout_option_string(self):
"""3D. Option strings can't be used to run other actions. """3D. Option strings can't be used to run other actions.
Verify that shell control characters aren't interpreted. Verify that shell control characters aren't interpreted.
""" """
action = "echo" action = "echo"
# counting is safer than actual badness. # counting is safer than actual badness.
options = "good; echo $((1+1))" options = "good; echo $((1+1))"
self.assertRaises(ValueError, run, action, options)
output, error = run(action, options)
self.assertFalse("2" in output)
def test_breakout_option_list(self): def test_breakout_option_list(self):
"""3D. Option lists can't be used to run other actions. """3D. Option lists can't be used to run other actions.
Verify that only a string of options is accepted and that we can't just Verify that only a string of options is accepted and that we can't just
tack additional shell control characters onto the list. tack additional shell control characters onto the list.
""" """
action = "echo" action = "echo"
# counting is safer than actual badness. # counting is safer than actual badness.
options = ["good", ";", "echo $((1+1))"] options = ["good", ";", "echo $((1+1))"]
output, error = run(action, options)
# we'd better not evaluate the data. # we'd better not evaluate the data.
self.assertFalse("2" in output) self.assertRaises(ValueError, run, action, options)
def test_multiple_options(self): def notest_multiple_options(self):
"""4. Multiple options can be provided as a list. """ 4. Multiple options can be provided as a list. """
# TODO: it's not allowed to call a symlink in the actions dir anymore
"""
self.assertEqual( self.assertEqual(
subprocess.check_output(shlex.split("id -ur")).strip(), subprocess.check_output(shlex.split("id -ur")).strip(),
run("id", ["-u" ,"-r"])[0].strip()) run("id", ["-u" ,"-r"])[0].strip())

View File

@ -1,67 +0,0 @@
#! /usr/bin/env python
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
import auth
from logger import Logger
import cfg
import unittest
import cherrypy
import plugin_mount
import os
cfg.log = Logger()
cherrypy.log.access_file = None
class Auth(unittest.TestCase):
"""Test check_credentials function of auth to confirm it works as expected"""
def setUp(self):
cfg.user_db = os.path.join(cfg.file_root, "tests/testdata/users");
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
def tearDown(self):
for user in cfg.users.get_all():
cfg.users.remove(user[0])
cfg.users.close()
def test_add_user(self):
self.assertIsNone(auth.add_user("test_user", "password"))
self.assertIsNotNone(auth.add_user(None, "password"))
self.assertIsNotNone(auth.add_user("test_user", None))
self.assertIsNotNone(auth.add_user("test_user", "password"))
def test_password_check(self):
auth.add_user("test_user", "password")
# check_credentials returns None if there is no error,
# or returns error string
self.assertIsNone(auth.check_credentials("test_user", "password"))
self.assertIsNotNone(auth.check_credentials("test_user", "wrong"))
def test_nonexistent_user(self):
self.assertIsNotNone(auth.check_credentials("test_user", "password"))
def test_password_too_long(self):
password = "x" * 4097
self.assertIsNotNone(auth.add_user("test_user", password))
self.assertIsNotNone(auth.check_credentials("test_user", password))
def test_salt_is_random(self):
auth.add_user("test_user1", "password")
auth.add_user("test_user2", "password")
self.assertNotEqual(
cfg.users["test_user1"]["salt"],
cfg.users["test_user2"]["salt"]
)
def test_hash_is_random(self):
auth.add_user("test_user1", "password")
auth.add_user("test_user2", "password")
self.assertNotEqual(
cfg.users["test_user1"]["passphrase"],
cfg.users["test_user2"]["passphrase"]
)
if __name__ == "__main__":
unittest.main()

View File

@ -1,86 +0,0 @@
#! /usr/bin/env python
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
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 UserStore(unittest.TestCase):
"""Test each function of user_store to confirm they work as expected"""
def setUp(self):
cfg.user_db = os.path.join(cfg.file_root, "tests/testdata/users");
self.userstore = plugin_mount.UserStoreModule.get_plugins()[0]
def tearDown(self):
for user in self.userstore.get_all():
self.userstore.remove(user[0])
self.userstore.close()
def test_user_does_not_exist(self):
self.assertEqual(self.userstore.exists("notausername"),False)
def test_user_does_exist(self):
self.add_user("isausername", False)
self.assertEqual(self.userstore.exists("isausername"),True)
def test_add_user(self):
self.assertEqual(len(self.userstore.items()),0)
self.add_user("test_user", False)
self.assertEqual(len(self.userstore.items()),1)
def test_user_is_in_expert_group(self):
self.add_user("test_user", True)
self.assertEqual(self.userstore.expert("test_user"),True)
def test_user_is_not_in_expert_group(self):
self.add_user("test_user", False)
self.assertEqual(self.userstore.expert("test_user"),False)
def test_user_removal(self):
self.assertEqual(len(self.userstore.items()),0)
self.add_user("test_user", False)
self.assertEqual(len(self.userstore.items()),1)
self.userstore.remove("test_user")
self.assertEqual(len(self.userstore.items()),0)
def test_get_user_email_attribute(self):
self.add_user("test_user", False,"test@home")
self.assertEqual(self.userstore.attr("test_user","email"),"test@home")
def test_get_user(self):
test_user = self.add_user("test_user", False)
self.assertEqual(self.userstore.get("test_user"),test_user)
def test_get_all_users(self):
self.add_user("test_user1", False)
self.add_user("test_user2", False)
self.assertEqual(len(self.userstore.get_all()),2)
def add_user(self, test_username, add_to_expert_group, email=''):
test_user = self.create_user(test_username, email)
if add_to_expert_group:
test_user = self.add_user_to_expert_group(test_user)
self.userstore.set(test_username,test_user)
return test_user
def create_user(self, username, email=''):
test_user = User()
test_user["username"] = username
test_user["email"] = email
return test_user
def add_user_to_expert_group(self, user):
user["groups"] = ["expert"]
return user
if __name__ == "__main__":
unittest.main()

View File

@ -24,5 +24,5 @@ from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'views', 'views',
url(r'^$', 'index') url(r'^$', 'index', name='index')
) )

26
util.py
View File

@ -2,23 +2,23 @@ import os
def mkdir(newdir): def mkdir(newdir):
"""works the way a good mkdir should :) """works the way a good mkdir should :)
- already exists, silently complete - already exists, silently complete
- regular file in the way, raise an exception - regular file in the way, raise an exception
- parent directory(ies) does not exist, make them as well - parent directory(ies) does not exist, make them as well
""" """
if os.path.isdir(newdir): if os.path.isdir(newdir):
pass pass
elif os.path.isfile(newdir): elif os.path.isfile(newdir):
raise OSError("a file with the same name as the desired " \ raise OSError("a file with the same name as the desired " \
"dir, '%s', already exists." % newdir) "dir, '%s', already exists." % newdir)
else: else:
head, tail = os.path.split(newdir) head, tail = os.path.split(newdir)
if head and not os.path.isdir(head): if head and not os.path.isdir(head):
mkdir(head) mkdir(head)
#print "mkdir %s" % repr(newdir) #print "mkdir %s" % repr(newdir)
if tail: if tail:
os.mkdir(newdir) os.mkdir(newdir)
def slurp(filespec): def slurp(filespec):

View File

@ -19,32 +19,13 @@
Main Plinth views Main Plinth views
""" """
from django.core.urlresolvers import reverse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
import logging
import cfg
from withsqlite.withsqlite import sqlite_db
LOGGER = logging.getLogger(__name__)
def index(request): def index(request):
"""Serve the main index page""" """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:
# 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:
LOGGER.info('First boot state - %d', database['state'])
return HttpResponseRedirect(
cfg.server_dir + '/firstboot/state%d' % database['state'])
if request.user.is_authenticated(): if request.user.is_authenticated():
return HttpResponseRedirect(cfg.server_dir + '/apps') return HttpResponseRedirect(reverse('apps:index'))
return HttpResponseRedirect(cfg.server_dir + '/help/about') return HttpResponseRedirect(reverse('help:about'))