Merge pull request #82 from SunilMohanAdapa/django-routing

Use Django dispatcher instead of CherryPy dispatcher
This commit is contained in:
Nick Daly 2014-07-07 00:37:02 +00:00
commit e656e3ee47
72 changed files with 1874 additions and 1387 deletions

View File

@ -52,7 +52,7 @@ specified and linked otherwise.
- modules/first_boot/first_boot.py :: -
- modules/help/help.py :: -
- modules/lib/auth_page.py :: -
- modules/lib/auth.py :: Derived from [[http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions][Arnar Birisson's CherryPy wiki code]].
- modules/lib/auth.py :: -
- modules/lib/user_store.py :: -
- modules/owncloud/owncloud.py :: -
- modules/packages/packages.py :: -

View File

@ -31,7 +31,7 @@ install: default apache-install freedombox-setup-install
cp share/init.d/plinth $(DESTDIR)/etc/init.d
cp -a lib/* $(DESTDIR)/usr/lib
install plinth $(DESTDIR)/usr/bin/
mkdir -p $(DESTDIR)/var/lib/plinth/cherrypy_sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
mkdir -p $(DESTDIR)/var/lib/plinth/sessions $(DESTDIR)/var/log/plinth $(DESTDIR)/var/run
mkdir -p $(DESTDIR)/var/lib/plinth/data
rm -f $(DESTDIR)/var/lib/plinth/users/sqlite3.distrib
@ -46,7 +46,7 @@ uninstall:
$(DESTDIR)/usr/share/man/man1/plinth.1.gz $(DESTDIR)/var/run/plinth.pid
dirs:
@mkdir -p data/cherrypy_sessions
@mkdir -p data/sessions
config: Makefile
@test -f plinth.config || cp plinth.sample.config plinth.config
@ -59,7 +59,7 @@ html:
@$(MAKE) -s -C doc html
clean:
@rm -f cherrypy.config data/cherrypy_sessions/*
@rm -f cherrypy.config data/sessions/*
@find . -name "*~" -exec rm {} \;
@find . -name ".#*" -exec rm {} \;
@find . -name "#*" -exec rm {} \;

1
cfg.py
View File

@ -32,7 +32,6 @@ pidfile = get_item(parser, 'Path', 'pidfile')
host = get_item(parser, 'Network', 'host')
port = int(get_item(parser, 'Network', 'port'))
html_root = None
main_menu = Menu()
if store_file.endswith(".sqlite3"):

View File

@ -10,12 +10,15 @@ import json
import UserDict
import sqlite3
class DefaultArg:
class DefaultArg(object):
pass
class Solutions:
class Solutions(object):
Sqlite3 = 0
class FileDict(UserDict.DictMixin):
"A dictionary that stores its data persistantly in a file"
@ -134,7 +137,7 @@ class FileDict(UserDict.DictMixin):
def batch(self):
return self._Batch(self)
class _Batch:
class _Batch(object):
def __init__(self, d):
self.__d = d

View File

@ -6,7 +6,8 @@ cherrypy.log.error_file = cfg.status_log_file
cherrypy.log.access_file = cfg.access_log_file
cherrypy.log.screen = False
class Logger():
class Logger(object):
"""By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL."""
def log(self, msg, level="DEBUG"):
try:

29
menu.py
View File

@ -1,9 +1,8 @@
from urlparse import urlparse
import cherrypy
import cfg
class Menu():
class Menu(object):
"""One menu item."""
def __init__(self, label="", icon="", url="#", order=50):
"""label is the text that is displayed on the menu.
@ -29,6 +28,17 @@ class Menu():
self.order = order
self.items = []
def find(self, url, basehref=True):
"""Return a menu item with given URL"""
if basehref and url.startswith('/'):
url = cfg.server_dir + url
for item in self.items:
if item.url == url:
return item
raise KeyError('Menu item not found')
def sort_items(self):
"""Sort the items in self.items by order."""
self.items = sorted(self.items, key=lambda x: x.order, reverse=False)
@ -48,16 +58,17 @@ class Menu():
self.sort_items()
return item
def active_p(self):
"""Returns True if this menu item is active, otherwise False.
def is_active(self, request_path):
"""
Returns True if this menu item is active, otherwise False.
We can tell if a menu is active if the menu item points
anywhere above url we are visiting in the url tree."""
return urlparse(cherrypy.url()).path.startswith(self.url)
anywhere above url we are visiting in the url tree.
"""
return request_path.startswith(self.url)
def active_item(self):
def active_item(self, request):
"""Return item list (e.g. submenu) of active menu item."""
path = urlparse(cherrypy.url()).path
for item in self.items:
if path.startswith(item.url):
if request.path.startswith(item.url):
return item

View File

@ -3,6 +3,8 @@ class User(dict):
is a bcrypt hash of the password), salt, groups, and an email address.
They can be blank or None, but the keys must exist. """
def __init__(self, dict=None):
super(User, self).__init__()
for key in ['username', 'name', 'passphrase', 'salt', 'email']:
self[key] = ''
for key in ['groups']:

137
module_loader.py Normal file
View File

@ -0,0 +1,137 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Discover, load and manage Plinth modules
"""
import django
import importlib
import os
import cfg
import urls
def load_modules():
"""
Read names of enabled modules in modules/enabled directory and
import them from modules directory.
"""
module_names = []
modules = {}
for name in os.listdir('modules/enabled'):
full_name = 'modules.{module}'.format(module=name)
cfg.log.info('Importing {full_name}'.format(full_name=full_name))
try:
module = importlib.import_module(full_name)
modules[name] = module
module_names.append(name)
except ImportError as exception:
cfg.log.error(
'Could not import modules/{module}: {exception}'
.format(module=name, exception=exception))
_include_module_urls(full_name)
ordered_modules = []
remaining_modules = dict(modules)
for module_name in modules:
if module_name not in remaining_modules:
continue
module = remaining_modules.pop(module_name)
try:
_insert_modules(module_name, module, remaining_modules,
ordered_modules)
except KeyError:
cfg.log.error('Unsatified dependency for module - %s' %
(module_name,))
cfg.log.debug('Module load order - %s' % ordered_modules)
for module_name in ordered_modules:
_initialize_module(modules[module_name])
def _insert_modules(module_name, module, remaining_modules, ordered_modules):
"""Insert modules into a list based on dependency order"""
if module_name in ordered_modules:
return
dependencies = []
try:
dependencies = module.DEPENDS
except AttributeError:
pass
for dependency in dependencies:
if dependency in ordered_modules:
continue
try:
module = remaining_modules.pop(dependency)
except KeyError:
cfg.log.error('Not found or circular dependency - %s, %s' %
(module_name, dependency))
raise
_insert_modules(dependency, module, remaining_modules, ordered_modules)
ordered_modules.append(module_name)
def _include_module_urls(module_name):
"""Include the module's URLs in global project URLs list"""
url_module = module_name + '.urls'
try:
urls.urlpatterns += django.conf.urls.patterns(
'', django.conf.urls.url(
r'', django.conf.urls.include(url_module)))
except ImportError:
cfg.log.debug('No URLs for {module}'.format(module=module_name))
def _initialize_module(module):
"""Call initialization method in the module if it exists"""
try:
init = module.init
except AttributeError:
cfg.log.debug('No init() for module - {module}'
.format(module=module.__name__))
return
try:
init()
except Exception as exception:
cfg.log.error('Exception while running init for {module}: {exception}'
.format(module=module, exception=exception))
def get_template_directories():
"""Return the list of template directories"""
directory = os.path.dirname(os.path.abspath(__file__))
core_directory = os.path.join(directory, 'templates')
directories = set((core_directory,))
for name in os.listdir('modules/enabled'):
directories.add(os.path.join('modules', name, 'templates'))
cfg.log.info('Template directories - %s' % directories)
return directories

View File

@ -16,10 +16,10 @@
#
"""
Plinth module for apps section page
Plinth module for Apps section page
"""
from . import apps
from .apps import init
__all__ = ['apps']
__all__ = ['apps', 'init']

View File

@ -1,18 +1,14 @@
import cherrypy
from django.template.response import TemplateResponse
from gettext import gettext as _
from plugin_mount import PagePlugin
import cfg
import util
class Apps(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps")
self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
self.menu.add_item("Chat", "icon-comment", "/../jwchat", 30)
def init():
"""Initailize the apps module"""
cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
@cherrypy.expose
def index(self):
return util.render_template(template='apps',
title=_('User Applications'))
def index(request):
"""Serve the apps index page"""
return TemplateResponse(request, 'apps.html', {'title': _('Applications')})

28
modules/apps/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Apps module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.apps.apps',
url(r'^apps/$', 'index')
)

View File

@ -20,6 +20,8 @@ Plinth module for basic system configuration
"""
from . import config
from .config import init
__all__ = ['config', 'init']
__all__ = ['config']
DEPENDS = ['system']

View File

@ -19,17 +19,16 @@
Plinth module for configuring timezone, hostname etc.
"""
import cherrypy
from django import forms
from django.core import validators
from django.template.response import TemplateResponse
from gettext import gettext as _
import re
import socket
import actions
import cfg
from ..lib.auth import require
from plugin_mount import PagePlugin
from ..lib.auth import login_required
import util
@ -89,67 +88,66 @@ and must not be greater than 63 characters in length.'),
return time_zones
class Configuration(PagePlugin):
"""System configuration page"""
def __init__(self, *args, **kwargs):
del args # Unused
del kwargs # Unused
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Configure'), 'icon-cog', '/sys/config', 10)
self.register_page('sys.config')
self.menu = cfg.html_root.sys.menu.add_item(_('Configure'), 'icon-cog',
'/sys/config', 10)
@login_required
def index(request):
"""Serve the configuration form"""
status = get_status()
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
form = None
messages = []
is_expert = cfg.users.expert(request=request)
if request.method == 'POST' and is_expert:
form = ConfigurationForm(request.POST, prefix='configuration')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(status, form.cleaned_data, messages)
status = get_status()
form = ConfigurationForm(initial=status,
prefix='configuration')
else:
form = ConfigurationForm(initial=status, prefix='configuration')
if kwargs and cfg.users.expert():
form = ConfigurationForm(kwargs, prefix='configuration')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigurationForm(initial=status,
prefix='configuration')
return TemplateResponse(request, 'config.html',
{'title': _('General Configuration'),
'form': form,
'messages_': messages,
'is_expert': is_expert})
def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
'time_zone': util.slurp('/etc/timezone').rstrip()}
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
if old_status['hostname'] != new_status['hostname']:
if not set_hostname(new_status['hostname']):
messages.append(('error', _('Setting hostname failed')))
else:
form = ConfigurationForm(initial=status, prefix='configuration')
return util.render_template(template='config',
title=_('General Configuration'),
form=form, messages=messages)
@staticmethod
def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
'time_zone': util.slurp('/etc/timezone').rstrip()}
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
if old_status['hostname'] != new_status['hostname']:
set_hostname(new_status['hostname'])
messages.append(('success', _('Hostname set')))
else:
messages.append(('info', _('Hostname is unchanged')))
else:
messages.append(('info', _('Hostname is unchanged')))
if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change',
[new_status['time_zone']])
del output # Unused
if error:
messages.append(('error',
_('Error setting time zone - %s') % error))
else:
messages.append(('success', _('Time zone set')))
if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change',
[new_status['time_zone']])
del output # Unused
if error:
messages.append(('error',
_('Error setting time zone - %s') % error))
else:
messages.append(('info', _('Time zone is unchanged')))
messages.append(('success', _('Time zone set')))
else:
messages.append(('info', _('Time zone is unchanged')))
def set_hostname(hostname):
@ -166,6 +164,7 @@ def set_hostname(hostname):
# don't persist/cache change unless it was saved successfuly
sys_store = util.filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
except OSError as exception:
raise cherrypy.HTTPError(500,
'Updating hostname failed: %s' % exception)
except OSError:
return False
return True

View File

@ -22,7 +22,7 @@
{% block main_block %}
{% if cfg.users.expert %}
{% if is_expert %}
{% include 'messages.html' %}

28
modules/config/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Configuration module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.config.config',
url(r'^sys/config/$', 'index'),
)

View File

@ -16,10 +16,12 @@
#
"""
Plinth module to system diagnostics
Plinth module for system diagnostics
"""
from . import diagnostics
from .diagnostics import init
__all__ = ['diagnostics', 'init']
__all__ = ['diagnostics']
DEPENDS = ['system']

View File

@ -19,39 +19,32 @@
Plinth module for running diagnostics
"""
import cherrypy
from gettext import gettext as _
from ..lib.auth import require
from plugin_mount import PagePlugin
from django.template.response import TemplateResponse
import actions
import cfg
import util
from ..lib.auth import login_required
class diagnostics(PagePlugin):
order = 30
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.diagnostics")
cfg.html_root.sys.menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30)
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30)
@cherrypy.expose
@require()
def index(self):
return util.render_template(template='diagnostics',
title=_('System Diagnostics'))
class test(PagePlugin):
order = 31
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.diagnostics.test")
@login_required
def index(request):
"""Serve the index page"""
return TemplateResponse(request, 'diagnostics.html',
{'title': _('System Diagnostics')})
@cherrypy.expose
@require()
def index(self):
output, error = actions.superuser_run("diagnostic-test")
return util.render_template(template='diagnostics_test',
title=_('Diagnostic Test'),
diagnostics_output=output,
diagnostics_error=error)
@login_required
def test(request):
"""Run diagnostics and the output page"""
output, error = actions.superuser_run("diagnostic-test")
return TemplateResponse(request, 'diagnostics_test.html',
{'title': _('Diagnostic Test'),
'diagnostics_output': output,
'diagnostics_error': error})

View File

@ -0,0 +1,29 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Diagnostics module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.diagnostics.diagnostics',
url(r'^sys/diagnostics/$', 'index'),
url(r'^sys/diagnostics/test/$', 'test'),
)

View File

@ -20,6 +20,8 @@ Plinth module for expert mode configuration
"""
from . import expert_mode
from .expert_mode import init
__all__ = ['expert_mode', 'init']
__all__ = ['expert_mode']
DEPENDS = ['system']

View File

@ -1,79 +1,66 @@
import cherrypy
from django import forms
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import require
from plugin_mount import PagePlugin
import cfg
import util
from ..lib.auth import login_required
class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
"""Form to configure expert mode"""
expert_mode = forms.BooleanField(
label=_('Expert Mode'), required=False)
# XXX: Only present due to issue with submitting empty form
dummy = forms.CharField(label='Dummy', initial='dummy',
widget=forms.HiddenInput())
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Expert Mode'), 'icon-cog', '/sys/expert', 10)
class Experts(PagePlugin):
"""Expert forms page"""
order = 60
@login_required
def index(request):
"""Serve the configuration form"""
status = get_status(request)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.expert')
form = None
messages = []
cfg.html_root.sys.menu.add_item(_('Expert Mode'), 'icon-cog',
'/sys/expert', 10)
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
cfg.log.info('Args - %s' % kwargs)
form = None
messages = []
if kwargs:
form = ExpertsForm(kwargs, prefix='experts')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(form.cleaned_data, messages)
status = self.get_status()
form = ExpertsForm(initial=status, prefix='experts')
else:
if request.method == 'POST':
form = ExpertsForm(request.POST, prefix='experts')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(request, form.cleaned_data, messages)
status = get_status(request)
form = ExpertsForm(initial=status, prefix='experts')
else:
form = ExpertsForm(initial=status, prefix='experts')
return util.render_template(template='expert_mode',
title=_('Expert Mode'), form=form,
messages=messages)
return TemplateResponse(request, 'expert_mode.html',
{'title': _('Expert Mode'),
'form': form,
'messages_': messages})
@staticmethod
def get_status():
"""Return the current status"""
return {'expert_mode': cfg.users.expert()}
@staticmethod
def _apply_changes(new_status, messages):
"""Apply expert mode configuration"""
message = ('info', _('Settings unchanged'))
def get_status(request):
"""Return the current status"""
return {'expert_mode': cfg.users.expert(request=request)}
user = cfg.users.current()
if new_status['expert_mode']:
if not 'expert' in user['groups']:
user['groups'].append('expert')
message = ('success', _('Expert mode enabled'))
else:
if 'expert' in user['groups']:
user['groups'].remove('expert')
message = ('success', _('Expert mode disabled'))
def _apply_changes(request, new_status, messages):
"""Apply expert mode configuration"""
message = ('info', _('Settings unchanged'))
cfg.users.set(user['username'], user)
messages.append(message)
user = cfg.users.current(request=request)
if new_status['expert_mode']:
if not 'expert' in user['groups']:
user['groups'].append('expert')
message = ('success', _('Expert mode enabled'))
else:
if 'expert' in user['groups']:
user['groups'].remove('expert')
message = ('success', _('Expert mode disabled'))
cfg.users.set(user['username'], user)
messages.append(message)

View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Expert Mode module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.expert_mode.expert_mode',
url(r'^sys/expert/$', 'index'),
)

View File

@ -20,6 +20,8 @@ Plinth module to configure a firewall
"""
from . import firewall
from .firewall import init
__all__ = ['firewall', 'init']
__all__ = ['firewall']
DEPENDS = ['system']

View File

@ -19,138 +19,134 @@
Plinth module to configure a firewall
"""
import cherrypy
from django.template.response import TemplateResponse
from gettext import gettext as _
import actions
import cfg
from ..lib.auth import require
from plugin_mount import PagePlugin
from ..lib.auth import login_required
import service as service_module
import util
class Firewall(PagePlugin):
"""Firewall menu entry and introduction page"""
order = 40
def init():
"""Initailze firewall module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Firewall'), 'icon-flag', '/sys/firewall', 50)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
service_module.ENABLED.connect(on_service_enabled)
self.register_page('sys.firewall')
cfg.html_root.sys.menu.add_item(_('Firewall'), 'icon-flag',
'/sys/firewall', 50)
service_module.ENABLED.connect(self.on_service_enabled)
@login_required
def index(request):
"""Serve introcution page"""
if not get_installed_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_installed'})
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve introcution page"""
del kwargs # Unused
if not get_enabled_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_running'})
if not self.get_installed_status():
return util.render_template(template='firewall',
title=_("Firewall"),
firewall_status='not_installed')
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
if not self.get_enabled_status():
return util.render_template(template='firewall',
title=_("Firewall"),
firewall_status='not_running')
return TemplateResponse(
request, 'firewall.html',
{'title': _('Firewall'),
'services': service_module.SERVICES.values(),
'internal_enabled_services': internal_enabled_services,
'external_enabled_services': external_enabled_services})
internal_enabled_services = self.get_enabled_services(zone='internal')
external_enabled_services = self.get_enabled_services(zone='external')
return util.render_template(
template='firewall', title=_('Firewall'),
services=service_module.SERVICES.values(),
internal_enabled_services=internal_enabled_services,
external_enabled_services=external_enabled_services)
def get_installed_status():
"""Return whether firewall is installed"""
output = _run(['get-installed'], superuser=True)
return output.split()[0] == 'installed'
def get_installed_status(self):
"""Return whether firewall is installed"""
output = self._run(['get-installed'], superuser=True)
return output.split()[0] == 'installed'
def get_enabled_status(self):
"""Return whether firewall is installed"""
output = self._run(['get-status'], superuser=True)
return output.split()[0] == 'running'
def get_enabled_status():
"""Return whether firewall is installed"""
output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running'
def get_enabled_services(self, zone):
"""Return the status of various services currently enabled"""
output = self._run(['get-enabled-services', '--zone', zone],
superuser=True)
return output.split()
def add_service(self, port, zone):
"""Enable a service in firewall"""
self._run(['add-service', port, '--zone', zone], superuser=True)
def get_enabled_services(zone):
"""Return the status of various services currently enabled"""
output = _run(['get-enabled-services', '--zone', zone], superuser=True)
return output.split()
def remove_service(self, port, zone):
"""Remove a service in firewall"""
self._run(['remove-service', port, '--zone', zone], superuser=True)
def on_service_enabled(self, sender, service_id, enabled, **kwargs):
"""
Enable/disable firewall ports when a service is
enabled/disabled.
"""
del sender # Unused
del kwargs # Unused
def add_service(port, zone):
"""Enable a service in firewall"""
_run(['add-service', port, '--zone', zone], superuser=True)
internal_enabled_services = self.get_enabled_services(zone='internal')
external_enabled_services = self.get_enabled_services(zone='external')
cfg.log.info('Service enabled - %s, %s' % (service_id, enabled))
service = service_module.SERVICES[service_id]
for port in service.ports:
if enabled:
if port not in internal_enabled_services:
self.add_service(port, zone='internal')
def remove_service(port, zone):
"""Remove a service in firewall"""
_run(['remove-service', port, '--zone', zone], superuser=True)
if (service.is_external and
port not in external_enabled_services):
self.add_service(port, zone='external')
else:
# service already configured.
pass
def on_service_enabled(sender, service_id, enabled, **kwargs):
"""
Enable/disable firewall ports when a service is
enabled/disabled.
"""
del sender # Unused
del kwargs # Unused
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
cfg.log.info('Service enabled - %s, %s' % (service_id, enabled))
service = service_module.SERVICES[service_id]
for port in service.ports:
if enabled:
if port not in internal_enabled_services:
add_service(port, zone='internal')
if (service.is_external and
port not in external_enabled_services):
add_service(port, zone='external')
else:
if port in internal_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id]
if not any(enabled_services_on_port):
self.remove_service(port, zone='internal')
if port in external_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id and
service_.is_external]
if not any(enabled_services_on_port):
self.remove_service(port, zone='external')
@staticmethod
def _run(arguments, superuser=False):
"""Run an given command and raise exception if there was an error"""
command = 'firewall'
cfg.log.info('Running command - %s, %s, %s' % (command, arguments,
superuser))
if superuser:
output, error = actions.superuser_run(command, arguments)
# service already configured.
pass
else:
output, error = actions.run(command, arguments)
if port in internal_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id]
if not any(enabled_services_on_port):
remove_service(port, zone='internal')
if error:
raise Exception('Error setting/getting firewalld confguration - %s'
% error)
if port in external_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id and
service_.is_external]
if not any(enabled_services_on_port):
remove_service(port, zone='external')
return output
def _run(arguments, superuser=False):
"""Run an given command and raise exception if there was an error"""
command = 'firewall'
cfg.log.info('Running command - %s, %s, %s' % (command, arguments,
superuser))
if superuser:
output, error = actions.superuser_run(command, arguments)
else:
output, error = actions.run(command, arguments)
if error:
raise Exception('Error setting/getting firewalld confguration - %s'
% error)
return output

View File

@ -25,7 +25,7 @@ and outgoing network traffic on your {{ cfg.box_name }}. Keeping a
firewall enabled and properly configured reduces risk of security
threat from the Internet.</p>
<p>The following the current status:</p>
<p>The following is the current status:</p>
{% if firewall_status = 'not_installed' %}
<p>Firewall is not installed. Please install it. Firewall comes

28
modules/firewall/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Firewall module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.firewall.firewall',
url(r'^sys/firewall/$', 'index')
)

View File

@ -21,5 +21,4 @@ Plinth module for first boot wizard
from . import first_boot
__all__ = ['first_boot']

View File

@ -18,16 +18,16 @@ The Plinth first-connection process has several stages:
4. The user interacts with the box normally.
"""
import cherrypy
from django import forms
from django.core import validators
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from gettext import gettext as _
from plugin_mount import PagePlugin
from ..lib.auth import add_user
from ..config import config
from withsqlite.withsqlite import sqlite_db
import cfg
import util
## TODO: flesh out these tests values
@ -66,132 +66,124 @@ below. This key should not be the same as your key because you are not your \
FreedomBox!'))
class FirstBoot(PagePlugin):
"""First boot wizard"""
def index(request):
"""Serve the index first boot page"""
return state0(request)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
# this is the url this page will hang off of (/firstboot)
self.register_page('firstboot')
self.register_page('firstboot/state0')
self.register_page('firstboot/state1')
def generate_box_key():
"""Generate a box key"""
return "fake key"
@cherrypy.expose
def index(self, *args, **kwargs):
return self.state0(*args, **kwargs)
def generate_box_key(self):
return "fake key"
def state0(request):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
@cherrypy.expose
def state0(self, **kwargs):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
All the parameters are form inputs. They get passed in when
the form is submitted. This method checks the inputs and if
they validate, uses them to take action. If they do not
validate, it displays the form to give the user a chance to
input correct values. It might display an error message (in
the message parameter).
All the parameters are form inputs. They get passed in when
the form is submitted. This method checks the inputs and if
they validate, uses them to take action. If they do not
validate, it displays the form to give the user a chance to
input correct values. It might display an error message (in
the message parameter).
message is an optional string that we can display to the
user. It's a good place to put error messages.
"""
try:
if _read_state() >= 5:
return HttpResponseRedirect(cfg.server_dir)
except KeyError:
pass
message is an optional string that we can display to the
user. It's a good place to put error messages.
"""
try:
if FirstBoot._read_state() >= 5:
raise cherrypy.HTTPRedirect(cfg.server_dir, 302)
except KeyError:
pass
## Until LDAP is in place, we'll put the box key in the cfg.store_file
status = get_state0()
## Until LDAP is in place, we'll put the box key in the cfg.store_file
status = self.get_state0()
form = None
messages = []
form = None
messages = []
if request.method == 'POST':
form = State0Form(request.POST, prefix='firstboot')
# pylint: disable-msg=E1101
if form.is_valid():
success = _apply_state0(status, form.cleaned_data, messages)
if kwargs:
form = State0Form(kwargs, prefix='firstboot')
# pylint: disable-msg=E1101
if form.is_valid():
success = self._apply_state0(status, form.cleaned_data,
messages)
if success:
# Everything is good, permanently mark and move to page 2
_write_state(1)
return HttpResponseRedirect(
cfg.server_dir + '/firstboot/state1')
else:
form = State0Form(initial=status, prefix='firstboot')
if success:
# Everything is good, permanently mark and move to page 2
FirstBoot._write_state(1)
raise cherrypy.HTTPRedirect('state1', 302)
return TemplateResponse(request, 'firstboot_state0.html',
{'title': _('First Boot!'),
'form': form,
'messages_': messages})
def get_state0():
"""Return the state for form state 0"""
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
database:
return {'hostname': config.get_hostname(),
'box_key': database.get('box_key', None)}
def _apply_state0(old_state, new_state, messages):
"""Apply changes in state 0 form"""
success = True
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
database:
database['about'] = 'Information about this FreedomBox'
if new_state['box_key']:
database['box_key'] = new_state['box_key']
elif not old_state['box_key']:
database['box_key'] = generate_box_key()
if old_state['hostname'] != new_state['hostname']:
config.set_hostname(new_state['hostname'])
error = add_user(new_state['username'], new_state['password'],
'First user, please change', '', True)
if error:
messages.append(
('error', _('User account creation failed: %s') % error))
success = False
else:
form = State0Form(initial=status, prefix='firstboot')
messages.append(('success', _('User account created')))
return util.render_template(template='firstboot_state0',
title=_('First Boot!'), form=form,
messages=messages)
return success
@staticmethod
def get_state0():
"""Return the state for form state 0"""
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
database:
return {'hostname': config.get_hostname(),
'box_key': database.get('box_key', None)}
def _apply_state0(self, old_state, new_state, messages):
"""Apply changes in state 0 form"""
success = True
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
database:
database['about'] = 'Information about this FreedomBox'
def state1(request):
"""
State 1 is when we have a box name and key. In this state,
our task is to provide a certificate and maybe to guide the
user through installing it. We automatically move to State 2,
which is an HTTPS connection.
if new_state['box_key']:
database['box_key'] = new_state['box_key']
elif not old_state['box_key']:
database['box_key'] = self.generate_box_key()
TODO: HTTPS failure in State 2 should returns to state 1.
"""
# TODO complete first_boot handling
# Make sure the user is not stuck on a dead end for now.
_write_state(5)
if old_state['hostname'] != new_state['hostname']:
config.set_hostname(new_state['hostname'])
return TemplateResponse(request, 'firstboot_state1.html',
{'title': _('Installing the Certificate')})
error = add_user(new_state['username'], new_state['password'],
'First user, please change', '', True)
if error:
messages.append(
('error', _('User account creation failed: %s') % error))
success = False
else:
messages.append(('success', _('User account created')))
return success
def _read_state():
"""Read the current state from database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
return database['state']
@staticmethod
@cherrypy.expose
def state1():
"""
State 1 is when we have a box name and key. In this state,
our task is to provide a certificate and maybe to guide the
user through installing it. We automatically move to State 2,
which is an HTTPS connection.
TODO: HTTPS failure in State 2 should returns to state 1.
"""
# TODO complete first_boot handling
# Make sure the user is not stuck on a dead end for now.
FirstBoot._write_state(5)
return util.render_template(template='firstboot_state1',
title=_('Installing the Certificate'))
@staticmethod
def _read_state():
"""Read the current state from database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
return database['state']
@staticmethod
def _write_state(state):
"""Write state to database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
database['state'] = state
def _write_state(state):
"""Write state to database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
database['state'] = state

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/>.
#
"""
URLs for the First Boot module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.first_boot.first_boot',
url(r'^firstboot/$', 'index'),
url(r'^firstboot/state0/$', 'state0'),
url(r'^firstboot/state1/$', 'state1')
)

View File

@ -20,6 +20,6 @@ Plinth module for help pages
"""
from . import help # pylint: disable-msg=W0622
from .help import init
__all__ = ['help']
__all__ = ['help', 'init']

View File

@ -1,46 +1,40 @@
import os
import cherrypy
from gettext import gettext as _
from plugin_mount import PagePlugin
from django.template.response import TemplateResponse
import cfg
import util
class Help(PagePlugin):
order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("help")
self.menu = cfg.main_menu.add_item(_("Documentation"), "icon-book", "/help", 101)
self.menu.add_item(_("Where to Get Help"), "icon-search", "/help/index", 5)
self.menu.add_item(_("Developer's Manual"), "icon-info-sign", "/help/view/plinth", 10)
self.menu.add_item(_("FAQ"), "icon-question-sign", "/help/view/faq", 20)
self.menu.add_item(_("%s Wiki" % cfg.box_name), "icon-pencil", "http://wiki.debian.org/FreedomBox", 30)
self.menu.add_item(_("About"), "icon-star", "/help/about", 100)
@cherrypy.expose
def index(self):
return util.render_template(template='help',
title=_('Documentation and FAQ'))
@cherrypy.expose
def about(self):
return util.render_template(
template='about',
title=_('About the {box_name}').format(box_name=cfg.box_name))
def init():
"""Initialize the Help module"""
menu = cfg.main_menu.add_item(_('Documentation'), 'icon-book', '/help',
101)
menu.add_item(_("Where to Get Help"), "icon-search", "/help/index", 5)
menu.add_item(_('Developer\'s Manual'), 'icon-info-sign',
'/help/view/plinth', 10)
menu.add_item(_('FAQ'), 'icon-question-sign', '/help/view/faq', 20)
menu.add_item(_('%s Wiki' % cfg.box_name), 'icon-pencil',
'http://wiki.debian.org/FreedomBox', 30)
menu.add_item(_('About'), 'icon-star', '/help/about', 100)
class View(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("help.view")
def index(request):
"""Serve the index page"""
return TemplateResponse(request, 'help.html',
{'title': _('Documentation and FAQ')})
@cherrypy.expose
def default(self, page=''):
if page not in ['design', 'plinth', 'hacking', 'faq']:
raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page)
with open(os.path.join("doc", "%s.part.html" % page), 'r') as IF:
main = IF.read()
return util.render_template(title=_("%s Documentation" %
cfg.product_name), main=main)
def about(request):
"""Serve the about page"""
title = _('About the {box_name}').format(box_name=cfg.box_name)
return TemplateResponse(request, 'about.html', {'title': title})
def default(request, page=''):
"""Serve the documentation pages"""
with open(os.path.join('doc', '%s.part.html' % page), 'r') as input_file:
main = input_file.read()
title = _('%s Documentation') % cfg.product_name
return TemplateResponse(request, 'login_nav.html',
{'title': title, 'main': main})

34
modules/help/urls.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/>.
#
"""
URLs for the Help module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.help.help',
url(r'^help/$', 'index'),
url(r'^help/index/$', 'index'),
url(r'^help/about/$', 'about'),
url(r'^help/view/(?P<page>design)/$', 'default'),
url(r'^help/view/(?P<page>plinth)/$', 'default'),
url(r'^help/view/(?P<page>hacking)/$', 'default'),
url(r'^help/view/(?P<page>faq)/$', 'default'),
)

View File

@ -23,7 +23,6 @@ from . import auth
from . import auth_page
from . import user_store
__all__ = ['auth',
'auth_page',
'user_store']

View File

@ -1,20 +1,13 @@
# Form based authentication for CherryPy. Requires the
# Session tool to be loaded.
#
# Thanks for this code is owed to Arnar Birgisson -at - gmail.com. It
# is based on code he wrote that was retrieved from
# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
# on 1 February 2011.
import cherrypy
import urllib
from django.http.response import HttpResponseRedirect
import functools
from passlib.hash import bcrypt
from passlib.exc import PasswordSizeError
import cfg
import random
from model import User
cfg.session_key = '_cp_username'
cfg.session_key = '_username'
def add_user(username, passphrase, name='', email='', expert=False):
"""Add a new user with specified username and passphrase.
@ -49,6 +42,7 @@ def add_user(username, passphrase, name='', email='', expert=False):
cfg.log(error)
return error
def check_credentials(username, passphrase):
"""Verifies credentials for username and passphrase.
@ -87,79 +81,20 @@ def check_credentials(username, passphrase):
if error:
cfg.log(error)
return error
def check_auth(*args, **kwargs):
"""A tool that looks in config for 'auth.require'. If found and it
is not None, a login is required and the entry is evaluated as a
list of conditions that the user must fulfill"""
conditions = cherrypy.request.config.get('auth.require', None)
# format GET params
get_params = urllib.quote(cherrypy.request.request_line.split()[1])
if conditions is not None:
username = cherrypy.session.get(cfg.session_key)
if username:
cherrypy.request.login = username
for condition in conditions:
# A condition is just a callable that returns true or false
if not condition():
# Send old page as from_page parameter
raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params)
else:
# Send old page as from_page parameter
raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params)
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
def require(*conditions):
"""A decorator that appends conditions to the auth.require config
variable."""
def decorate(f):
if not hasattr(f, '_cp_config'):
f._cp_config = dict()
if 'auth.require' not in f._cp_config:
f._cp_config['auth.require'] = []
f._cp_config['auth.require'].extend(conditions)
return f
return decorate
# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current username as cherrypy.request.login
#
# Define those at will however suits the application.
def member_of(groupname):
def check():
# replace with actual check if <username> is in <groupname>
return cherrypy.request.login == 'joe' and groupname == 'admin'
return check
def name_is(reqd_username):
return lambda: reqd_username == cherrypy.request.login
# These might be handy
def any_of(*conditions):
"""Returns True if any of the conditions match"""
def check():
for c in conditions:
if c():
return True
return False
return check
# By default all conditions are required, but this might still be
# needed if you want to use it inside of an any_of(...) condition
def all_of(*conditions):
"""Returns True if all of the conditions match"""
def check():
for c in conditions:
if not c():
return False
return True
return check
# XXX: Only required until we start using Django authentication system properly
def login_required(func):
"""A decorator to ensure that user is logged in before accessing a view"""
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
"""Check that user is logged in"""
if not request.session.get(cfg.session_key, None):
return HttpResponseRedirect(
cfg.server_dir + "/auth/login?from_page=%s" % request.path)
return func(request, *args, **kwargs)
return wrapper

View File

@ -2,19 +2,17 @@
Controller to provide login and logout actions
"""
import cherrypy
import cfg
from django import forms
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from gettext import gettext as _
from plugin_mount import PagePlugin
import auth
import util
from . import auth
class LoginForm(forms.Form): # pylint: disable-msg=W0232
"""Login form"""
from_page = forms.CharField(widget=forms.HiddenInput(), required=False)
username = forms.CharField(label=_('Username'))
password = forms.CharField(label=_('Passphrase'),
widget=forms.PasswordInput())
@ -33,48 +31,37 @@ class LoginForm(forms.Form): # pylint: disable-msg=W0232
return self.cleaned_data
class AuthController(PagePlugin):
"""Login and logout pages"""
def login(request):
"""Serve the login page"""
form = None
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
if request.method == 'POST':
form = LoginForm(request.POST, prefix='auth')
# pylint: disable-msg=E1101
if form.is_valid():
username = form.cleaned_data['username']
request.session[cfg.session_key] = username
return HttpResponseRedirect(_get_from_page(request))
else:
form = LoginForm(prefix='auth')
self.register_page('auth')
return TemplateResponse(request, 'form.html',
{'title': _('Login'),
'form': form,
'submit_text': _('Login')})
def on_login(self, username):
"""Called on successful login"""
def on_logout(self, username):
"""Called on logout"""
def logout(request):
"""Logout and redirect to origin page"""
try:
del request.session[cfg.session_key]
request.session.flush()
except KeyError:
pass
@cherrypy.expose
def login(self, from_page=cfg.server_dir+"/", **kwargs):
"""Serve the login page"""
form = None
return HttpResponseRedirect(_get_from_page(request))
if kwargs:
form = LoginForm(kwargs, prefix='auth')
# pylint: disable-msg=E1101
if form.is_valid():
username = form.cleaned_data['username']
cherrypy.session[cfg.session_key] = username
cherrypy.request.login = username
self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or
(cfg.server_dir + "/"))
else:
form = LoginForm(prefix='auth')
return util.render_template(template='form', title=_('Login'),
form=form, submit_text=_('Login'))
@cherrypy.expose
def logout(self, from_page=cfg.server_dir+"/"):
sess = cherrypy.session
username = sess.get(cfg.session_key, None)
sess[cfg.session_key] = None
if username:
cherrypy.request.login = None
self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/"))
def _get_from_page(request):
"""Return the 'from page' of a request"""
return request.GET.get('from_page', cfg.server_dir + '/')

29
modules/lib/urls.py Normal file
View File

@ -0,0 +1,29 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Lib module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.lib.auth_page',
url(r'^auth/login/$', 'login'),
url(r'^auth/logout/$', 'logout')
)

View File

@ -1,11 +1,13 @@
import cherrypy
import cfg
from model import User
from plugin_mount import UserStoreModule
from withsqlite.withsqlite import sqlite_db
class UserStore(UserStoreModule, sqlite_db):
def __init__(self):
super(UserStore, self).__init__()
self.db_file = cfg.user_db
sqlite_db.__init__(self, self.db_file, autocommit=True, check_same_thread=False)
self.__enter__()
@ -13,25 +15,36 @@ class UserStore(UserStoreModule, sqlite_db):
def close(self):
self.__exit__(None,None,None)
def current(self, name=False):
def current(self, request=None, name=False):
"""Return current user, if there is one, else None.
If name = True, return the username instead of the user."""
try:
username = cherrypy.session.get(cfg.session_key)
if name:
return username
else:
return self.get(username)
except AttributeError:
if not request:
return None
def expert(self, username=None):
try:
username = request.session[cfg.session_key]
except KeyError:
return None
if name:
return username
return self.get(username)
def expert(self, username=None, request=None):
"""Return whether the current or provided user is an expert"""
if not username:
username = self.current(name=True)
groups = self.attr(username,"groups")
if not request:
return False
username = self.current(request=request, name=True)
groups = self.attr(username, 'groups')
if not groups:
return False
return 'expert' in groups
return 'expert' in groups
def attr(self, username=None, field=None):
return self.get(username)[field]

View File

@ -20,6 +20,8 @@ Plinth module to configure ownCloud
"""
from . import owncloud
from .owncloud import init
__all__ = ['owncloud', 'init']
__all__ = ['owncloud']
DEPENDS = ['apps']

View File

@ -1,85 +1,81 @@
import cherrypy
from django import forms
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import require
from plugin_mount import PagePlugin
import actions
import cfg
from ..lib.auth import login_required
import service
import util
SERVICE = None
class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232
"""ownCloud configuration form"""
enabled = forms.BooleanField(label=_('Enable ownCloud'), required=False)
# XXX: Only present due to issue with submitting empty form
dummy = forms.CharField(label='Dummy', initial='dummy',
widget=forms.HiddenInput())
def init():
"""Initialize the ownCloud module"""
menu = cfg.main_menu.find('/apps')
menu.add_item('Owncloud', 'icon-picture', '/apps/owncloud', 35)
status = get_status()
global SERVICE # pylint: disable-msg=W0603
SERVICE = service.Service('owncloud', _('ownCloud'), ['http', 'https'],
is_external=True, enabled=status['enabled'])
class OwnCloud(PagePlugin):
"""ownCloud configuration page"""
order = 90
@login_required
def index(request):
"""Serve the ownCloud configuration page"""
status = get_status()
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('apps.owncloud')
form = None
messages = []
cfg.html_root.apps.menu.add_item('Owncloud', 'icon-picture',
'/apps/owncloud', 35)
status = self.get_status()
self.service = service.Service('owncloud', _('ownCloud'),
['http', 'https'], is_external=True,
enabled=status['enabled'])
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the ownCloud configuration page"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = OwnCloudForm(kwargs, prefix='owncloud')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = OwnCloudForm(initial=status, prefix='owncloud')
else:
if request.method == 'POST':
form = OwnCloudForm(request.POST, prefix='owncloud')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(status, form.cleaned_data, messages)
status = get_status()
form = OwnCloudForm(initial=status, prefix='owncloud')
else:
form = OwnCloudForm(initial=status, prefix='owncloud')
return util.render_template(template='owncloud', title=_('ownCloud'),
form=form, messages=messages)
return TemplateResponse(request, 'owncloud.html',
{'title': _('ownCloud'),
'form': form,
'messages_': messages})
@staticmethod
def get_status():
"""Return the current status"""
output, error = actions.run('owncloud-setup', 'status')
if error:
raise Exception('Error getting ownCloud status: %s' % error)
return {'enabled': 'enable' in output.split()}
def get_status():
"""Return the current status"""
output, error = actions.run('owncloud-setup', 'status')
if error:
raise Exception('Error getting ownCloud status: %s' % error)
def _apply_changes(self, old_status, new_status, messages):
"""Apply the changes"""
if old_status['enabled'] == new_status['enabled']:
messages.append(('info', _('Setting unchanged')))
return
return {'enabled': 'enable' in output.split()}
if new_status['enabled']:
messages.append(('success', _('ownCloud enabled')))
option = 'enable'
else:
messages.append(('success', _('ownCloud disabled')))
option = 'noenable'
actions.superuser_run('owncloud-setup', [option], async=True)
def _apply_changes(old_status, new_status, messages):
"""Apply the changes"""
if old_status['enabled'] == new_status['enabled']:
messages.append(('info', _('Setting unchanged')))
return
# Send a signal to other modules that the service is
# enabled/disabled
self.service.notify_enabled(self, new_status['enabled'])
if new_status['enabled']:
messages.append(('success', _('ownCloud enabled')))
option = 'enable'
else:
messages.append(('success', _('ownCloud disabled')))
option = 'noenable'
actions.superuser_run('owncloud-setup', [option], async=True)
# Send a signal to other modules that the service is
# enabled/disabled
SERVICE.notify_enabled(None, new_status['enabled'])

28
modules/owncloud/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the ownCloud module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.owncloud.owncloud',
url(r'^apps/owncloud/$', 'index'),
)

View File

@ -20,6 +20,8 @@ Plinth module to manage packages
"""
from . import packages
from .packages import init
__all__ = ['packages', 'init']
__all__ = ['packages']
DEPENDS = ['system']

View File

@ -1,11 +1,10 @@
import cherrypy
from django import forms
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import require
from plugin_mount import PagePlugin
import actions
import cfg
import util
from ..lib.auth import login_required
def get_modules_available():
@ -28,10 +27,6 @@ def get_modules_enabled():
class PackagesForm(forms.Form):
"""Packages form"""
# XXX: Only present due to issue with submitting empty form
dummy = forms.CharField(label='Dummy', initial='dummy',
widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1002, E1101
super(forms.Form, self).__init__(*args, **kwargs)
@ -44,90 +39,83 @@ class PackagesForm(forms.Form):
label=label, required=False)
class Packages(PagePlugin):
"""Package page"""
order = 20
def init():
"""Initialize the Packages module"""
menu = cfg.main_menu.find('/sys')
menu.add_item('Package Manager', 'icon-gift', '/sys/packages', 20)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.packages')
cfg.html_root.sys.menu.add_item('Package Manager', 'icon-gift',
'/sys/packages', 20)
@login_required
def index(request):
"""Serve the form"""
status = get_status()
@cherrypy.expose
@require()
def index(self, *args, **kwargs):
"""Serve the form"""
del args # Unused
form = None
messages = []
status = self.get_status()
form = None
messages = []
if kwargs:
form = PackagesForm(kwargs, prefix='packages')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = PackagesForm(initial=status, prefix='packages')
else:
if request.method == 'POST':
form = PackagesForm(request.POST, prefix='packages')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(status, form.cleaned_data, messages)
status = get_status()
form = PackagesForm(initial=status, prefix='packages')
else:
form = PackagesForm(initial=status, prefix='packages')
return util.render_template(template='packages',
title=_('Add/Remove Plugins'),
form=form, messages=messages)
return TemplateResponse(request, 'packages.html',
{'title': _('Add/Remove Plugins'),
'form': form,
'messages_': messages})
@staticmethod
def get_status():
"""Return the current status"""
modules_available = get_modules_available()
modules_enabled = get_modules_enabled()
return {module + '_enabled': module in modules_enabled
for module in modules_available}
def get_status():
"""Return the current status"""
modules_available = get_modules_available()
modules_enabled = get_modules_enabled()
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply form changes"""
for field, enabled in new_status.items():
if not field.endswith('_enabled'):
continue
return {module + '_enabled': module in modules_enabled
for module in modules_available}
if old_status[field] == new_status[field]:
continue
module = field.split('_enabled')[0]
if enabled:
output, error = actions.superuser_run(
'module-manager', ['enable', cfg.python_root, module])
del output # Unused
def _apply_changes(old_status, new_status, messages):
"""Apply form changes"""
for field, enabled in new_status.items():
if not field.endswith('_enabled'):
continue
# TODO: need to get plinth to load the module we just
# enabled
if error:
messages.append(
('error', _('Error enabling module - {module}').format(
module=module)))
else:
messages.append(
('success', _('Module enabled - {module}').format(
if old_status[field] == new_status[field]:
continue
module = field.split('_enabled')[0]
if enabled:
output, error = actions.superuser_run(
'module-manager', ['enable', cfg.python_root, module])
del output # Unused
# TODO: need to get plinth to load the module we just
# enabled
if error:
messages.append(
('error', _('Error enabling module - {module}').format(
module=module)))
else:
messages.append(
('success', _('Module enabled - {module}').format(
module=module)))
else:
output, error = actions.superuser_run(
'module-manager', ['disable', cfg.python_root, module])
del output # Unused
# TODO: need a smoother way for plinth to unload the
# module
if error:
messages.append(
('error',
_('Error disabling module - {module}').format(
module=module)))
else:
output, error = actions.superuser_run(
'module-manager', ['disable', cfg.python_root, module])
del output # Unused
# TODO: need a smoother way for plinth to unload the
# module
if error:
messages.append(
('error',
_('Error disabling module - {module}').format(
module=module)))
else:
messages.append(
('success', _('Module disabled - {module}').format(
module=module)))
messages.append(
('success', _('Module disabled - {module}').format(
module=module)))

28
modules/packages/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Packages module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.packages.packages',
url(r'^sys/packages/$', 'index'),
)

View File

@ -20,6 +20,8 @@ Plinth module to configure PageKite
"""
from . import pagekite
from .pagekite import init
__all__ = ['pagekite', 'init']
__all__ = ['pagekite']
DEPENDS = ['apps']

View File

@ -19,45 +19,38 @@
Plinth module for configuring PageKite service
"""
import cherrypy
from django import forms
from django.core import validators
from django.template import RequestContext
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from gettext import gettext as _
import actions
import cfg
from ..lib.auth import require
from plugin_mount import PagePlugin
import util
from ..lib.auth import login_required
class PageKite(PagePlugin):
"""PageKite menu entry and introduction page"""
order = 60
def init():
"""Intialize the PageKite module"""
menu = cfg.main_menu.find('/apps')
menu.add_item(_('Public Visibility (PageKite)'), 'icon-flag',
'/apps/pagekite', 50)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps.pagekite")
cfg.html_root.apps.menu.add_item(
_("Public Visibility (PageKite)"), "icon-flag",
"/apps/pagekite", 50)
@login_required
def index(request):
"""Serve introdution page"""
menu = {'title': _('PageKite'),
'items': [{'url': '/apps/pagekite/configure',
'text': _('Configure PageKite')}]}
@staticmethod
@cherrypy.expose
@require()
def index(**kwargs):
"""Serve introdution page"""
del kwargs # Unused
sidebar_right = render_to_string('menu_block.html', {'menu': menu},
RequestContext(request))
menu = {'title': _('PageKite'),
'items': [{'url': '/apps/pagekite/configure',
'text': _('Configure PageKite')}]}
sidebar_right = util.render_template(template='menu_block', menu=menu)
return util.render_template(template='pagekite_introduction',
title=_("Public Visibility (PageKite)"),
sidebar_right=sidebar_right)
return TemplateResponse(request, 'pagekite_introduction.html',
{'title': _('Public Visibility (PageKite)'),
'sidebar_right': sidebar_right})
class TrimmedCharField(forms.CharField):
@ -105,121 +98,114 @@ for your account if no secret is set on the kite'))
https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions</a>'))
class Configure(PagePlugin): # pylint: disable-msg=C0103
"""Main configuration form"""
order = 65
@login_required
def configure(request):
"""Serve the configuration form"""
status = get_status()
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
form = None
messages = []
self.register_page("apps.pagekite.configure")
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = ConfigureForm(kwargs, prefix='pagekite')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigureForm(initial=status, prefix='pagekite')
else:
if request.method == 'POST':
form = ConfigureForm(request.POST, prefix='pagekite')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(status, form.cleaned_data, messages)
status = get_status()
form = ConfigureForm(initial=status, prefix='pagekite')
else:
form = ConfigureForm(initial=status, prefix='pagekite')
return util.render_template(template='pagekite_configure',
title=_('Configure PageKite'), form=form,
messages=messages)
return TemplateResponse(request, 'pagekite_configure.html',
{'title': _('Configure PageKite'),
'form': form,
'messages_': messages})
def get_status(self):
"""
Return the current status of PageKite configuration by
executing various actions.
"""
status = {}
# Check if PageKite is installed
output = self._run(['get-installed'])
cfg.log('Output - %s' % output)
if output.split()[0] != 'installed':
return None
def get_status():
"""
Return the current status of PageKite configuration by
executing various actions.
"""
status = {}
# PageKite service enabled/disabled
output = self._run(['get-status'])
status['enabled'] = (output.split()[0] == 'enabled')
# Check if PageKite is installed
output = _run(['get-installed'])
cfg.log('Output - %s' % output)
if output.split()[0] != 'installed':
return None
# PageKite kite details
output = self._run(['get-kite'])
kite_details = output.split()
status['kite_name'] = kite_details[0]
status['kite_secret'] = kite_details[1]
# PageKite service enabled/disabled
output = _run(['get-status'])
status['enabled'] = (output.split()[0] == 'enabled')
# Service status
status['service'] = {}
for service in ('http', 'ssh'):
output = self._run(['get-service-status', service])
status[service + '_enabled'] = (output.split()[0] == 'enabled')
# PageKite kite details
output = _run(['get-kite'])
kite_details = output.split()
status['kite_name'] = kite_details[0]
status['kite_secret'] = kite_details[1]
return status
# Service status
status['service'] = {}
for service in ('http', 'ssh'):
output = _run(['get-service-status', service])
status[service + '_enabled'] = (output.split()[0] == 'enabled')
def _apply_changes(self, old_status, new_status, messages):
"""Apply the changes to PageKite configuration"""
cfg.log.info('New status is - %s' % new_status)
return status
if old_status != new_status:
self._run(['stop'])
if old_status['enabled'] != new_status['enabled']:
if new_status['enabled']:
self._run(['set-status', 'enable'])
messages.append(('success', _('PageKite enabled')))
else:
self._run(['set-status', 'disable'])
messages.append(('success', _('PageKite disabled')))
def _apply_changes(old_status, new_status, messages):
"""Apply the changes to PageKite configuration"""
cfg.log.info('New status is - %s' % new_status)
if old_status['kite_name'] != new_status['kite_name'] or \
old_status['kite_secret'] != new_status['kite_secret']:
self._run(['set-kite', '--kite-name', new_status['kite_name'],
'--kite-secret', new_status['kite_secret']])
messages.append(('success', _('Kite details set')))
if old_status != new_status:
_run(['stop'])
for service in ['http', 'ssh']:
if old_status[service + '_enabled'] != \
new_status[service + '_enabled']:
if new_status[service + '_enabled']:
self._run(['set-service-status', service, 'enable'])
messages.append(('success', _('Service enabled: {service}')
.format(service=service)))
else:
self._run(['set-service-status', service, 'disable'])
messages.append(('success',
_('Service disabled: {service}')
.format(service=service)))
if old_status != new_status:
self._run(['start'])
@staticmethod
def _run(arguments, superuser=True):
"""Run an given command and raise exception if there was an error"""
command = 'pagekite-configure'
cfg.log.info('Running command - %s, %s, %s' % (command, arguments,
superuser))
if superuser:
output, error = actions.superuser_run(command, arguments)
if old_status['enabled'] != new_status['enabled']:
if new_status['enabled']:
_run(['set-status', 'enable'])
messages.append(('success', _('PageKite enabled')))
else:
output, error = actions.run(command, arguments)
_run(['set-status', 'disable'])
messages.append(('success', _('PageKite disabled')))
if error:
raise Exception('Error setting/getting PageKite confguration - %s'
% error)
if old_status['kite_name'] != new_status['kite_name'] or \
old_status['kite_secret'] != new_status['kite_secret']:
_run(['set-kite', '--kite-name', new_status['kite_name'],
'--kite-secret', new_status['kite_secret']])
messages.append(('success', _('Kite details set')))
return output
for service in ['http', 'ssh']:
if old_status[service + '_enabled'] != \
new_status[service + '_enabled']:
if new_status[service + '_enabled']:
_run(['set-service-status', service, 'enable'])
messages.append(('success', _('Service enabled: {service}')
.format(service=service)))
else:
_run(['set-service-status', service, 'disable'])
messages.append(('success',
_('Service disabled: {service}')
.format(service=service)))
if old_status != new_status:
_run(['start'])
def _run(arguments, superuser=True):
"""Run an given command and raise exception if there was an error"""
command = 'pagekite-configure'
cfg.log.info('Running command - %s, %s, %s' % (command, arguments,
superuser))
if superuser:
output, error = actions.superuser_run(command, arguments)
else:
output, error = actions.run(command, arguments)
if error:
raise Exception('Error setting/getting PageKite confguration - %s'
% error)
return output

29
modules/pagekite/urls.py Normal file
View File

@ -0,0 +1,29 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the PageKite module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.pagekite.pagekite',
url(r'^apps/pagekite/$', 'index'),
url(r'^apps/pagekite/configure/$', 'configure'),
)

View File

@ -23,14 +23,15 @@ santiago_port = 52854
# return True
class Santiago(PagePlugin):
order = 90 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
order = 90 # order of running init in PagePlugins
def __init__(self):
super(Santiago, self).__init__()
self.register_page("santiago")
self.santiago_address = self.get_santiago_address() #TODO: multiple santiago ports
#set a listener on the santiago address
def get_santiago_address(self):
def get_santiago_address(self):
if 'santiago' in cfg.users['admin'] and 'address' in cfg.users['admin']['santiago']:
return cfg.users['admin']['santiago']['address']
else:
@ -54,11 +55,11 @@ class Santiago(PagePlugin):
print "Need to add these two lines to /etc/torrc:\n%s" % hidden_service_config
return ""
def check_for_hidden_service(self):
def check_for_hidden_service(self):
pass
@cherrypy.expose
def index(self, *args, **kw):
@cherrypy.expose
def index(self, *args, **kw):
"""
A request is a dict with some required keys:
@ -111,12 +112,13 @@ class Santiago(PagePlugin):
## Plinth page to config santiago
class santiago(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10)
self.register_page("privacy.santiago")
def __init__(self):
super(Santiago, self).__init__(self)
@cherrypy.expose
@require()
def index(self):
return "Santiago's config goes here."
self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10)
self.register_page("privacy.santiago")
@cherrypy.expose
@require()
def index(self):
return "Santiago's config goes here."

View File

@ -20,6 +20,7 @@ Plinth module for system section page
"""
from . import system
from system import init
__all__ = ['system']
__all__ = ['system', 'init']

View File

@ -1,21 +1,15 @@
import cherrypy
from gettext import gettext as _
from plugin_mount import PagePlugin
from django.template.response import TemplateResponse
import cfg
import util
sys_dir = "modules/installed/sys"
class Sys(PagePlugin):
order = 10
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys")
self.menu = cfg.main_menu.add_item(_("System"), "icon-cog", "/sys", 100)
self.menu.add_item(_("Users and Groups"), "icon-user", "/sys/users", 15)
def init():
"""Initialize the system module"""
cfg.main_menu.add_item(_('System'), 'icon-cog', '/sys', 100)
@cherrypy.expose
def index(self):
return util.render_template(template='system',
title=_("System Configuration"))
def index(request):
"""Serve the index page"""
return TemplateResponse(request, 'system.html',
{'title': _('System Configuration')})

28
modules/system/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the System module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.system.system',
url(r'^sys/$', 'index'),
)

View File

@ -20,6 +20,8 @@ Plinth module to configure Tor
"""
from . import tor
from .tor import init
__all__ = ['tor', 'init']
__all__ = ['tor']
DEPENDS = ['apps']

View File

@ -19,36 +19,35 @@
Plinth module for configuring Tor
"""
import cherrypy
from django.template.response import TemplateResponse
from gettext import gettext as _
from plugin_mount import PagePlugin
from ..lib.auth import require
import actions
import cfg
import util
from ..lib.auth import login_required
class tor(PagePlugin):
order = 60 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps.tor")
cfg.html_root.apps.menu.add_item("Tor", "icon-eye-close", "/apps/tor",
30)
def init():
"""Initialize the Tor module"""
menu = cfg.main_menu.find('/apps')
menu.add_item("Tor", "icon-eye-close", "/apps/tor", 30)
@cherrypy.expose
@require()
def index(self):
output, error = actions.superuser_run("tor-get-ports")
port_info = output.split("\n")
tor_ports = {}
for line in port_info:
try:
(key, val) = line.split()
tor_ports[key] = val
except ValueError:
continue
return util.render_template(template='tor',
title=_('Tor Control Panel'),
tor_ports=tor_ports)
@login_required
def index(request):
"""Service the index page"""
output, error = actions.superuser_run("tor-get-ports")
del error # Unused
port_info = output.split("\n")
tor_ports = {}
for line in port_info:
try:
(key, val) = line.split()
tor_ports[key] = val
except ValueError:
continue
return TemplateResponse(request, 'tor.html',
{'title': _('Tor Control Panel'),
'tor_ports': tor_ports})

28
modules/tor/urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
URLs for the Tor module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.tor.tor',
url(r'^apps/tor/$', 'index')
)

View File

@ -20,6 +20,8 @@ Plinth module to manage users
"""
from . import users
from .users import init
__all__ = ['users', 'init']
__all__ = ['users']
DEPENDS = ['system']

30
modules/users/urls.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/>.
#
"""
URLs for the Users module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.users.users',
url(r'^sys/users/$', 'index'),
url(r'^sys/users/add/$', 'add'),
url(r'^sys/users/edit/$', 'edit')
)

View File

@ -1,34 +1,36 @@
import cherrypy
from django import forms
from django.core import validators
from django.template import RequestContext
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import require, add_user
from plugin_mount import PagePlugin
import cfg
from ..lib.auth import add_user, login_required
from model import User
import util
class Users(PagePlugin):
order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.users")
def init():
"""Intialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Users and Groups'), 'icon-user', '/sys/users', 15)
@staticmethod
@cherrypy.expose
@require()
def index():
"""Return a rendered users page"""
menu = {'title': _('Users and Groups'),
'items': [{'url': '/sys/users/add',
'text': _('Add User')},
{'url': '/sys/users/edit',
'text': _('Edit Users')}]}
sidebar_right = util.render_template(template='menu_block',
menu=menu)
return util.render_template(title="Manage Users and Groups",
sidebar_right=sidebar_right)
@login_required
def index(request):
"""Return a rendered users page"""
menu = {'title': _('Users and Groups'),
'items': [{'url': '/sys/users/add',
'text': _('Add User')},
{'url': '/sys/users/edit',
'text': _('Edit Users')}]}
sidebar_right = render_to_string('menu_block.html', {'menu': menu},
RequestContext(request))
return TemplateResponse(request, 'login_nav.html',
{'title': _('Manage Users and Groups'),
'sidebar_right': sidebar_right})
class UserAddForm(forms.Form): # pylint: disable-msg=W0232
@ -48,48 +50,40 @@ and alphabet'),
email = forms.EmailField(label=_('Email'), required=False)
class UserAdd(PagePlugin):
"""Add user page"""
order = 30
@login_required
def add(request):
"""Serve the form"""
form = None
messages = []
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.users.add')
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the form"""
form = None
messages = []
if kwargs:
form = UserAddForm(kwargs, prefix='user')
# pylint: disable-msg=E1101
if form.is_valid():
self._add_user(form.cleaned_data, messages)
form = UserAddForm(prefix='user')
else:
if request.method == 'POST':
form = UserAddForm(request.POST, prefix='user')
# pylint: disable-msg=E1101
if form.is_valid():
_add_user(form.cleaned_data, messages)
form = UserAddForm(prefix='user')
else:
form = UserAddForm(prefix='user')
return util.render_template(template='users_add', title=_('Add User'),
form=form, messages=messages)
return TemplateResponse(request, 'users_add.html',
{'title': _('Add User'),
'form': form,
'messages_': messages})
@staticmethod
def _add_user(data, messages):
"""Add a user"""
if cfg.users.exists(data['username']):
messages.append(
('error', _('User "{username}" already exists').format(
username=data['username'])))
return
add_user(data['username'], data['password'], data['full_name'],
data['email'], False)
def _add_user(data, messages):
"""Add a user"""
if cfg.users.exists(data['username']):
messages.append(
('success', _('User "{username}" added').format(
('error', _('User "{username}" already exists').format(
username=data['username'])))
return
add_user(data['username'], data['password'], data['full_name'],
data['email'], False)
messages.append(
('success', _('User "{username}" added').format(
username=data['username'])))
class UserEditForm(forms.Form): # pylint: disable-msg=W0232
@ -108,64 +102,56 @@ class UserEditForm(forms.Form): # pylint: disable-msg=W0232
self.fields['delete_user_' + user['username']] = field
class UserEdit(PagePlugin):
"""User edit page"""
order = 35
@login_required
def edit(request):
"""Serve the edit form"""
form = None
messages = []
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.users.edit')
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the form"""
form = None
messages = []
if kwargs:
form = UserEditForm(kwargs, prefix='user')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(form.cleaned_data, messages)
form = UserEditForm(prefix='user')
else:
if request.method == 'POST':
form = UserEditForm(request.POST, prefix='user')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_edit_changes(request, form.cleaned_data, messages)
form = UserEditForm(prefix='user')
else:
form = UserEditForm(prefix='user')
return util.render_template(template='users_edit',
title=_('Edit or Delete User'),
form=form, messages=messages)
return TemplateResponse(request, 'users_edit.html',
{'title': _('Edit or Delete User'),
'form': form,
'messages_': messages})
@staticmethod
def _apply_changes(data, messages):
"""Apply form changes"""
for field, value in data.items():
if not value:
continue
if not field.startswith('delete_user_'):
continue
def _apply_edit_changes(request, data, messages):
"""Apply form changes"""
for field, value in data.items():
if not value:
continue
username = field.split('delete_user_')[1]
if not field.startswith('delete_user_'):
continue
cfg.log.info('%s asked to delete %s' %
(cherrypy.session.get(cfg.session_key), username))
username = field.split('delete_user_')[1]
if username == cfg.users.current(name=True):
messages.append(
('error',
_('Can not delete current account - "%s"') % username))
continue
requesting_user = request.session.get(cfg.session_key, None)
cfg.log.info('%s asked to delete %s' %
(requesting_user, username))
if not cfg.users.exists(username):
messages.append(('error',
_('User "%s" does not exist') % username))
continue
if username == cfg.users.current(request=request, name=True):
messages.append(
('error',
_('Can not delete current account - "%s"') % username))
continue
try:
cfg.users.remove(username)
messages.append(('success', _('User "%s" deleted') % username))
except IOError as exception:
messages.append(('error', _('Error deleting "%s" - %s') %
(username, exception)))
if not cfg.users.exists(username):
messages.append(('error',
_('User "%s" does not exist') % username))
continue
try:
cfg.users.remove(username)
messages.append(('success', _('User "%s" deleted') % username))
except IOError as exception:
messages.append(('error', _('Error deleting "%s" - %s') %
(username, exception)))

View File

@ -20,6 +20,8 @@ Plinth module to configure XMPP server
"""
from . import xmpp
from .xmpp import init
__all__ = ['xmpp', 'init']
__all__ = ['xmpp']
DEPENDS = ['apps']

30
modules/xmpp/urls.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/>.
#
"""
URLs for the XMPP module
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.xmpp.xmpp',
url(r'^apps/xmpp/$', 'index'),
url(r'^apps/xmpp/configure/$', 'configure'),
url(r'^apps/xmpp/register/$', 'register')
)

View File

@ -1,12 +1,13 @@
import cherrypy
from django import forms
from django.template import RequestContext
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import require
from plugin_mount import PagePlugin
import cfg
import actions
import cfg
from ..lib.auth import login_required
import service
import util
SIDE_MENU = {'title': _('XMPP'),
@ -16,38 +17,35 @@ SIDE_MENU = {'title': _('XMPP'),
'text': 'Register XMPP Account'}]}
class XMPP(PagePlugin):
"""XMPP Page"""
order = 60
def init():
"""Initialize the XMPP module"""
menu = cfg.main_menu.find('/apps')
menu.add_item('Chat', 'icon-comment', '/../jwchat', 20)
menu.add_item('XMPP', 'icon-comment', '/apps/xmpp', 40)
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('apps.xmpp')
cfg.html_root.apps.menu.add_item('XMPP', 'icon-comment',
'/apps/xmpp', 40)
service.Service(
'xmpp-client', _('Chat Server - client connections'),
is_external=True, enabled=True)
service.Service(
'xmpp-server', _('Chat Server - server connections'),
is_external=True, enabled=True)
service.Service(
'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=True)
self.client_service = service.Service(
'xmpp-client', _('Chat Server - client connections'),
is_external=True, enabled=True)
self.server_service = service.Service(
'xmpp-server', _('Chat Server - server connections'),
is_external=True, enabled=True)
self.bosh_service = service.Service(
'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=True)
@staticmethod
@cherrypy.expose
@require()
def index(**kwargs):
"""Serve XMPP page"""
del kwargs # Unused
main = "<p>XMPP Server Accounts and Configuration</p>"
@login_required
def index(request):
"""Serve XMPP page"""
main = "<p>XMPP Server Accounts and Configuration</p>"
sidebar_right = util.render_template(template='menu_block',
menu=SIDE_MENU)
return util.render_template(title="XMPP Server", main=main,
sidebar_right=sidebar_right)
sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU},
RequestContext(request))
return TemplateResponse(request, 'login_nav.html',
{'title': _('XMPP Server'),
'main': main,
'sidebar_right': sidebar_right})
class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
@ -57,83 +55,65 @@ class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
help_text=_('When enabled, anyone who can reach this server will be \
allowed to register an account through an XMPP client'))
# XXX: Only present due to issue with submitting empty form
dummy = forms.CharField(label='Dummy', initial='dummy',
widget=forms.HiddenInput())
@login_required
def configure(request):
"""Serve the configuration form"""
status = get_status()
class Configure(PagePlugin):
"""Configuration page"""
order = 65
form = None
messages = []
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps.xmpp.configure")
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = ConfigureForm(kwargs, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigureForm(initial=status, prefix='xmpp')
else:
if request.method == 'POST':
form = ConfigureForm(request.POST, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(status, form.cleaned_data, messages)
status = get_status()
form = ConfigureForm(initial=status, prefix='xmpp')
else:
form = ConfigureForm(initial=status, prefix='xmpp')
sidebar_right = util.render_template(template='menu_block',
menu=SIDE_MENU)
return util.render_template(template='xmpp_configure',
title=_('Configure XMPP Server'),
form=form, messages=messages,
sidebar_right=sidebar_right)
sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU},
RequestContext(request))
@staticmethod
def get_status():
"""Return the current status"""
output, error = actions.run('xmpp-setup', 'status')
if error:
raise Exception('Error getting status: %s' % error)
return TemplateResponse(request, 'xmpp_configure.html',
{'title': _('Configure XMPP Server'),
'form': form,
'messages_': messages,
'sidebar_right': sidebar_right})
return {'inband_enabled': 'inband_enable' in output.split()}
@staticmethod
def sidebar_right(**kwargs):
"""Return rendered string for sidebar on the right"""
del kwargs # Unused
def get_status():
"""Return the current status"""
output, error = actions.run('xmpp-setup', 'status')
if error:
raise Exception('Error getting status: %s' % error)
return util.render_template(template='menu_block', menu=SIDE_MENU)
return {'inband_enabled': 'inband_enable' in output.split()}
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
cfg.log.info('Status - %s, %s' % (old_status, new_status))
if old_status['inband_enabled'] == new_status['inband_enabled']:
messages.append(('info', _('Setting unchanged')))
return
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
cfg.log.info('Status - %s, %s' % (old_status, new_status))
if new_status['inband_enabled']:
messages.append(('success', _('Inband registration enabled')))
option = 'inband_enable'
else:
messages.append(('success', _('Inband registration disabled')))
option = 'noinband_enable'
if old_status['inband_enabled'] == new_status['inband_enabled']:
messages.append(('info', _('Setting unchanged')))
return
cfg.log.info('Option - %s' % option)
if new_status['inband_enabled']:
messages.append(('success', _('Inband registration enabled')))
option = 'inband_enable'
else:
messages.append(('success', _('Inband registration disabled')))
option = 'noinband_enable'
_output, error = actions.superuser_run('xmpp-setup', [option])
del _output
if error:
raise Exception('Error running command - %s' % error)
cfg.log.info('Option - %s' % option)
_output, error = actions.superuser_run('xmpp-setup', [option])
del _output # Unused
if error:
raise Exception('Error running command - %s' % error)
class RegisterForm(forms.Form): # pylint: disable-msg=W0232
@ -144,50 +124,43 @@ class RegisterForm(forms.Form): # pylint: disable-msg=W0232
label=_('Password'), widget=forms.PasswordInput())
class Register(PagePlugin):
"""User registration page"""
order = 65
@login_required
def register(request):
"""Serve the registration form"""
form = None
messages = []
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('apps.xmpp.register')
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the registration form"""
form = None
messages = []
if kwargs:
form = RegisterForm(kwargs, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
self._register_user(form.cleaned_data, messages)
form = RegisterForm(prefix='xmpp')
else:
if request.method == 'POST':
form = RegisterForm(request.POST, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
_register_user(form.cleaned_data, messages)
form = RegisterForm(prefix='xmpp')
else:
form = RegisterForm(prefix='xmpp')
sidebar_right = util.render_template(template='menu_block',
menu=SIDE_MENU)
return util.render_template(template='xmpp_register',
title=_('Register XMPP Account'),
form=form, messages=messages,
sidebar_right=sidebar_right)
sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU},
RequestContext(request))
@staticmethod
def _register_user(data, messages):
"""Register a new XMPP user"""
output, error = actions.superuser_run(
'xmpp-register', [data['username'], data['password']])
if error:
raise Exception('Error registering user - %s' % error)
return TemplateResponse(request, 'xmpp_register.html',
{'title': _('Register XMPP Account'),
'form': form,
'messages_': messages,
'sidebar_right': sidebar_right})
if 'successfully registered' in output:
messages.append(('success',
_('Registered account for %s' %
data['username'])))
else:
messages.append(('error',
_('Failed to register account for %s: %s') %
(data['username'], output)))
def _register_user(data, messages):
"""Register a new XMPP user"""
output, error = actions.superuser_run(
'xmpp-register', [data['username'], data['password']])
if error:
raise Exception('Error registering user - %s' % error)
if 'successfully registered' in output:
messages.append(('success',
_('Registered account for %s' %
data['username'])))
else:
messages.append(('error',
_('Failed to register account for %s: %s') %
(data['username'], output)))

209
plinth.py
View File

@ -1,28 +1,22 @@
#!/usr/bin/env python
import os, stat, sys, argparse
from gettext import gettext as _
import os, sys, argparse
import cfg
import django.conf
import importlib
import django.core.wsgi
if not os.path.join(cfg.file_root, "vendor") in sys.path:
sys.path.append(os.path.join(cfg.file_root, "vendor"))
import re
import cherrypy
from cherrypy import _cpserver
from cherrypy.process.plugins import Daemonizer
Daemonizer(cherrypy.engine).subscribe()
import module_loader
import plugin_mount
import service
import util as u
from logger import Logger
#from modules.auth import AuthController, require, member_of, name_is
from withsqlite.withsqlite import sqlite_db
import socket
__version__ = "0.2.14"
__author__ = "James Vasile"
@ -32,79 +26,6 @@ __maintainer__ = "James Vasile"
__email__ = "james@jamesvasile.com"
__status__ = "Development"
import urlparse
def error_page(status, dynamic_msg, stock_msg):
return u.render_template(template="err", title=status, main="<p>%s</p>%s" % (dynamic_msg, stock_msg))
def error_page_404(status, message, traceback, version):
return error_page(status, message, """<p>If you believe this
missing page should exist, please file a bug with either the Plinth
project (<a href="https://github.com/NickDaly/Plinth/issues">it has
an issue tracker</a>) or the people responsible for the module you
are trying to access.</p>
<p>Sorry for the mistake.</p>
""")
def error_page_500(status, message, traceback, version):
cfg.log.error("500 Internal Server Error. Trackback is above.")
more="""<p>This is an internal error and not something you caused
or can fix. Please report the error on the <a
href="https://github.com/jvasile/Plinth/issues">bug tracker</a> so
we can fix it.</p>"""
return error_page(status, message, "<p>%s</p><pre>%s</pre>" % (more, "\n".join(traceback.split("\n"))))
class Root(plugin_mount.PagePlugin):
@cherrypy.expose
def index(self):
## TODO: firstboot hijacking root should probably be in the firstboot module with a hook in plinth.py
with sqlite_db(cfg.store_file, table="firstboot") as db:
if not 'state' in db:
# if we created a new user db, make sure it can't be read by everyone
userdb_fname = '{}.sqlite3'.format(cfg.user_db)
os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
# cherrypy.InternalRedirect throws a 301, causing the
# browser to cache the redirect, preventing the user from
# navigating to /plinth until the browser is restarted.
raise cherrypy.HTTPRedirect('firstboot', 307)
elif db['state'] < 5:
cfg.log("First Boot state = %d" % db['state'])
raise cherrypy.InternalRedirect('firstboot/state%d' % db['state'])
if cherrypy.session.get(cfg.session_key, None):
raise cherrypy.InternalRedirect('apps')
else:
raise cherrypy.InternalRedirect('help/about')
def load_modules():
"""
Read names of enabled modules in modules/enabled directory and
import them from modules directory.
"""
for name in os.listdir('modules/enabled'):
cfg.log.info('Importing modules/%s' % name)
try:
importlib.import_module('modules.{module}'.format(module=name))
except ImportError as exception:
cfg.log.error(
'Could not import modules/{module}: {exception}'
.format(module=name, exception=exception))
def get_template_directories():
"""Return the list of template directories"""
directory = os.path.dirname(os.path.abspath(__file__))
core_directory = os.path.join(directory, 'templates')
directories = set((core_directory,))
for name in os.listdir('modules/enabled'):
directories.add(os.path.join('modules', name, 'templates'))
cfg.log.info('Template directories - %s' % directories)
return directories
def parse_arguments():
parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.')
@ -144,7 +65,13 @@ def set_config(args, element, default):
# it wasn't in the config file, but set the default anyway.
setattr(cfg, element, default)
def setup():
def setup_logging():
"""Setup logging framework"""
cfg.log = Logger()
def setup_configuration():
cfg = parse_arguments()
try:
@ -155,55 +82,91 @@ def setup():
pass
os.chdir(cfg.python_root)
cherrypy.config.update({'error_page.404': error_page_404})
cherrypy.config.update({'error_page.500': error_page_500})
cfg.log = Logger()
load_modules()
cfg.html_root = Root()
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
cfg.page_plugins = plugin_mount.PagePlugin.get_plugins()
cfg.log("Loaded %d page plugins" % len(cfg.page_plugins))
# Add an extra server
server = _cpserver.Server()
server.socket_host = '127.0.0.1'
server.socket_port = 52854
server.subscribe()
def setup_server():
"""Setup CherryPy server"""
# Add an extra server
server = _cpserver.Server()
server.socket_host = '127.0.0.1'
server.socket_port = 52854
server.subscribe()
# Configure default server
cherrypy.config.update(
{'server.socket_host': cfg.host,
'server.socket_port': cfg.port,
'server.thread_pool':10,
'tools.staticdir.root': cfg.file_root,
'tools.sessions.on':True,
'tools.auth.on':True,
'tools.sessions.storage_type':"file",
'tools.sessions.timeout':90,
'tools.sessions.storage_path':"%s/cherrypy_sessions" % cfg.data_dir,})
# Configure default server
cherrypy.config.update(
{'server.socket_host': cfg.host,
'server.socket_port': cfg.port,
'server.thread_pool': 10})
application = django.core.wsgi.get_wsgi_application()
cherrypy.tree.graft(application, cfg.server_dir)
config = {
'/': {'tools.staticdir.root': '%s/static' % cfg.file_root,
'tools.staticdir.on': True,
'tools.staticdir.dir': '.'}}
cherrypy.tree.mount(None, cfg.server_dir + '/static', config)
cherrypy.engine.signal_handler.subscribe()
def context_processor(request):
"""Add additional context values to RequestContext for use in templates"""
path_parts = request.path.split('/')
active_menu_urls = ['/'.join(path_parts[:length])
for length in xrange(1, len(path_parts))]
return {
'cfg': cfg,
'main_menu': cfg.main_menu,
'submenu': cfg.main_menu.active_item(request),
'request_path': request.path,
'basehref': cfg.server_dir,
'username': request.session.get(cfg.session_key, None),
'active_menu_urls': active_menu_urls
}
def configure_django():
"""Setup Django configuration in the absense of .settings file"""
context_processors = [
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'plinth.context_processor']
template_directories = module_loader.get_template_directories()
sessions_directory = os.path.join(cfg.data_dir, 'sessions')
django.conf.settings.configure(
DEBUG=False,
ALLOWED_HOSTS=['127.0.0.1', 'localhost'],
TEMPLATE_DIRS=template_directories,
INSTALLED_APPS=['bootstrapform'],
ROOT_URLCONF='urls',
SESSION_ENGINE='django.contrib.sessions.backends.file',
SESSION_FILE_PATH=sessions_directory,
STATIC_URL=cfg.server_dir + '/static/',
TEMPLATE_CONTEXT_PROCESSORS=context_processors)
config = {
'/': {'tools.staticdir.root': '%s/static' % cfg.file_root,
'tools.proxy.on': True,},
'/static': {'tools.staticdir.on': True,
'tools.staticdir.dir': "."},
'/favicon.ico':{'tools.staticfile.on': True,
'tools.staticfile.filename':
"%s/static/theme/favicon.ico" % cfg.file_root}}
cherrypy.tree.mount(cfg.html_root, cfg.server_dir, config=config)
cherrypy.engine.signal_handler.subscribe()
def main():
# Initialize basic services
"""Intialize and start the application"""
setup_logging()
service.init()
setup()
setup_configuration()
# Configure Django
template_directories = get_template_directories()
django.conf.settings.configure(TEMPLATE_DIRS=template_directories,
INSTALLED_APPS=['bootstrapform'])
configure_django()
module_loader.load_modules()
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
setup_server()
cherrypy.engine.start()
cherrypy.engine.block()

View File

@ -18,10 +18,14 @@ class PluginMount(type):
def get_plugins(cls, *args, **kwargs):
return cls.init_plugins(*args, **kwargs)
class MultiplePluginViolation:
class MultiplePluginViolation(Exception):
"""Multiple plugins found for a type where only one is expected"""
pass
class PluginMountSingular(PluginMount):
"""Plugin mounter that allows only one plugin of this meta type"""
def __init__(cls, name, bases, attrs):
if not hasattr(cls, 'plugins'):
cls.plugins = []
@ -29,42 +33,9 @@ class PluginMountSingular(PluginMount):
if len(cls.plugins) > 0:
raise MultiplePluginViolation
cls.plugins.append(cls)
def _setattr_deep(obj, path, value):
"""If path is 'x.y.z' or ['x', 'y', 'z'] then perform obj.x.y.z = value"""
if isinstance(path, basestring):
path = path.split('.')
for part in path[:-1]:
obj = getattr(obj, part)
setattr(obj, path[-1], value)
class PagePlugin:
"""
Mount point for page plugins. Page plugins provide display pages
in the interface (one menu item, for example).
order - How early should this plugin be loaded? Lower order is earlier.
"""
order = 50
__metaclass__ = PluginMount
def __init__(self, *args, **kwargs):
"""If cfg.html_root is none, then this is the html_root."""
if not cfg.html_root:
cfg.log('Setting html root to %s' % self.__class__.__name__)
cfg.html_root = self
def register_page(self, url):
cfg.log.info("Registering page: %s" % url)
_setattr_deep(cfg.html_root, url, self)
class UserStoreModule:
class UserStoreModule(object):
"""
Mount Point for plugins that will manage the user backend storage,
where we keep a hash for each user.
@ -79,5 +50,5 @@ class UserStoreModule:
compatibility with third party software. A future version of
Plinth is likely to require LDAP.
"""
__metaclass__ = PluginMountSingular # singular because we can only use one user store at a time
# Singular because we can only use one user store at a time
__metaclass__ = PluginMountSingular

36
templates/404.html Normal file
View File

@ -0,0 +1,36 @@
{% extends 'login_nav.html' %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% block title_block %}
404
{% endblock %}
{% block main_block %}
<p>Requested page {{ request_path }} was not found.</p>
<p>If you believe this missing page should exist, please file a bug with either
the Plinth project (<a href="https://github.com/NickDaly/Plinth/issues">it
has an issue tracker</a>) or the people responsible for the module you are
trying to access.</p>
<p>Sorry for the mistake.</p>
{% endblock %}

32
templates/500.html Normal file
View File

@ -0,0 +1,32 @@
{% extends 'login_nav.html' %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% block title_block %}
500
{% endblock %}
{% block main_block %}
<p>This is an internal error and not something you caused or can fix. Please
report the error on
the <a href="https://github.com/NickDaly/Plinth/issues">bug tracker</a> so we
can fix it.</p>
{% endblock %}

View File

@ -1,3 +1,4 @@
{% load static %}
<!doctype html>
<!--[if lt IE 7 ]> <html class="ie ie6 no-js" lang="en"> <![endif]-->
<!--[if IE 7 ]> <html class="ie ie7 no-js" lang="en"> <![endif]-->
@ -24,25 +25,25 @@
<meta name="msnbot" content="noindex, nofollow, noarchive, noodp" />
<meta name="slurp" content="noindex, nofollow, noarchive, noodp, noydir" />
<meta name="title" content="{% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %}" />
<meta name="title" content="{% if title %} {{ title }} {% else %} FreedomBox {% endif %}" />
<meta name="description" content="Plinth administrative interface for the FreedomBox" />
<title>{% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %}</title>
<title>{% if title %} {{ title }} {% else %} FreedomBox {% endif %}</title>
<!-- This is the traditional favicon. Size: 16x16 or 32x32, transparency is OK -->
<link rel="shortcut icon" href="{{ basehref }}/static/theme/img/favicon.ico" />
<link rel="shortcut icon" href="{% static 'theme/img/favicon.ico' %}"/>
<!-- The is the icon for iOS's Web Clip. Size: 57x57 for older iPhones, 72x72 for iPads, 114x114 for iPhone4
- To prevent iOS from applying its styles to the icon name it thusly: apple-touch-icon-precomposed.png
- Transparency is not recommended (iOS will put a black BG behind the icon) -->
<link rel="apple-touch-icon" sizes="57x57" href="{{ basehref }}/static/theme/img/apple-touch-icon-57px-precomposed.png" />
<link rel="apple-touch-icon" sizes="72x72" href="{{ basehref }}/static/theme/img/apple-touch-icon-72px-precomposed.png" />
<link rel="apple-touch-icon" sizes="114x114" href="{{ basehref }}/static/theme/img/apple-touch-icon-114px-precomposed.png" />
<link rel="apple-touch-icon" sizes="57x57" href="{% static 'theme/img/apple-touch-icon-57px-precomposed.png' %}"/>
<link rel="apple-touch-icon" sizes="72x72" href="{% static 'theme/img/apple-touch-icon-72px-precomposed.png' %}"/>
<link rel="apple-touch-icon" sizes="114x114" href="{% static 'theme/img/apple-touch-icon-114px-precomposed.png' %}"/>
<!-- Bootstrap base CSS -->
<link rel="stylesheet" href="{{ basehref }}/static/theme/css/bootstrap.min.css" />
<link rel="stylesheet" href="{% static 'theme/css/bootstrap.min.css' %}"/>
<!-- Bootstrap responsive CSS -->
<link rel="stylesheet" href="{{ basehref }}/static/theme/css/bootstrap-responsive.min.css" />
<link rel="stylesheet" href="{{ basehref }}/static/theme/css/plinth.css" />
<link rel="stylesheet" href="{% static 'theme/css/bootstrap-responsive.min.css' %}"/>
<link rel="stylesheet" href="{% static 'theme/css/plinth.css' %}"/>
<!-- CSS from previous Plinth template, not sure what to keep yet -->
{{ css|safe }}
<!-- End Plinth CSS -->
@ -57,7 +58,10 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a href="{{ basehref }}/" class="logo-top"><img src="{{ basehref }}/static/theme/img/freedombox-logo-32px.png" alt="FreedomBox" /></a><a class="brand" href="{{ basehref }}/">FreedomBox Dashboard</a>
<a href="{{ basehref }}/" class="logo-top">
<img src="{% static 'theme/img/freedombox-logo-32px.png' %}" alt="FreedomBox" />
</a>
<a class="brand" href="{{ basehref }}/">FreedomBox</a>
{% block add_nav_and_login %}
{% endblock %}
</div><!--/.nav-collapse -->
@ -111,10 +115,6 @@
free software offered to you under the terms of
the <a href="http://www.gnu.org/licenses/agpl.html" target="_blank">GNU Affero General Public
License</a>, Version 3 or later. This Plinth theme was built by <a href="http://seandiggity.com" target="_blank">Sean &quot;Diggity&quot; O&apos;Brien</a>.
</p>
<p>Current page: {{ current_url }}</p>
<p>
</p>
{% endblock %}</p>
</footer>
@ -123,11 +123,11 @@
<!-- JavaScript <script> tags are placed at the end of the document to speed up initial page loads-->
<!-- Local link to system Modernizr (includes HTML5 Shiv) -->
<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/modernizr.min.js"></script>
<script type="text/javascript" src="{% static 'theme/js/libs/modernizr.min.js' %}"></script>
<!-- Local link to system jQuery -->
<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/jquery.min.js"></script>
<script type="text/javascript" src="{% static 'theme/js/libs/jquery.min.js' %}"></script>
<!-- Local link to system Bootstrap JS -->
<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap.min.js"></script>
<script type="text/javascript" src="{% static 'theme/js/libs/bootstrap.min.js' %}"></script>
{% block js_block %}
{{ js|safe }}

View File

@ -5,8 +5,13 @@
<ul class="nav">
{% for item in main_menu.items %}
<li class="{{ item.active_p|yesno:"active," }}">
<a href="{{ item.url }}" class="{{ item.active_p|yesno:"active," }}">
{% if item.url in active_menu_urls %}
<li class="active">
<a href="{{ item.url }}" class="active">
{% else %}
<li>
<a href="{{ item.url }}">
{% endif %}
<span class="{{ item.icon }} icon-white nav-icon"></span>
{{ item.label }}
</a>

View File

@ -21,16 +21,21 @@
<li class="nav-header">Menu</li>
{% for item in menu.items %}
<li class="{{ item.active_p|yesno:"active," }}">
<a href="{{ item.url }}" class="{{ item.active_p|yesno:"active," }}">
<span class="{{ item.icon }} {{ item.active_p|yesno:"icon-white," }}
sidenav-icon"></span>
{{ item.label }}
</a>
{% if item.items %}
{% include "menu.html" with menu=item %}
{% endif %}
</li>
{% if item.url in active_menu_urls %}
<li class="active">
<a href="{{ item.url }}" class="active">
<span class="{{ item.icon }} icon-white sidenav-icon"></span>
{% else %}
<li>
<a href="{{ item.url }}">
<span class="{{ item.icon }} sidenav-icon"></span>
{% endif %}
{{ item.label }}
</a>
{% if item.items %}
{% include "menu.html" with menu=item %}
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -17,7 +17,7 @@
#
{% endcomment %}
{% for severity, message in messages %}
{% for severity, message in messages_ %}
<div class='alert alert-{{ severity }} alert-dismissable'>
<a class="close" data-dismiss="alert">&times;</a>
{{ message }}

View File

@ -5,7 +5,6 @@ from actions import superuser_run, run
import os
import shlex
import subprocess
import sys
import unittest
class TestPrivileged(unittest.TestCase):

View File

@ -1,18 +1,18 @@
#! /usr/bin/env python
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
import user_store, auth
import auth
from logger import Logger
import cfg
import unittest
import cherrypy
import plugin_mount
import os
from model import User
cfg.log = Logger()
cherrypy.log.access_file = None
class Auth(unittest.TestCase):
"""Test check_credentials function of auth to confirm it works as expected"""

View File

@ -1,7 +1,6 @@
#! /usr/bin/env python
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
import user_store
from logger import Logger
import cfg
import unittest
@ -13,6 +12,7 @@ cfg.log = Logger()
cherrypy.log.access_file = None
class UserStore(unittest.TestCase):
"""Test each function of user_store to confirm they work as expected"""
@ -83,4 +83,4 @@ class UserStore(unittest.TestCase):
return user
if __name__ == "__main__":
unittest.main()
unittest.main()

28
urls.py Normal file
View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Django URLconf file containing all urls
"""
from django.conf.urls import patterns, url
urlpatterns = patterns( # pylint: disable-msg=C0103
'views',
url(r'^$', 'index')
)

27
util.py
View File

@ -1,10 +1,8 @@
import os
import sys
import cherrypy
import cfg
import sqlite3
from django.template.loader import render_to_string
from filedict import FileDict
@ -55,31 +53,6 @@ def find_keys(dic, val):
"""return the key of dictionary dic given the value"""
return [k for k, v in dic.iteritems() if v == val]
class Message():
def __init__(self, msg=''):
self.text = msg
def add(self, text):
self.text += "<br />%s" % text
def render_template(template='login_nav', **kwargs):
for key in ['sidebar_left', 'sidebar_right', 'main', 'js', 'nav', 'css',
'title', 'basehref']:
if not key in kwargs:
kwargs[key] = ''
if kwargs['basehref'] == '':
kwargs['basehref'] = cfg.server_dir
kwargs['template'] = template
kwargs['main_menu'] = cfg.main_menu
kwargs['submenu'] = cfg.main_menu.active_item()
kwargs['current_url'] = cherrypy.url()
kwargs['username'] = cherrypy.session.get(cfg.session_key)
kwargs['cfg'] = cfg
return render_to_string(template + '.html', kwargs)
def filedict_con(filespec=None, table='dict'):
"""TODO: better error handling in filedict_con"""

52
views.py Normal file
View File

@ -0,0 +1,52 @@
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Main Plinth views
"""
from django.http.response import HttpResponseRedirect
import os
import stat
import cfg
from withsqlite.withsqlite import sqlite_db
def index(request):
"""Serve the main index page"""
# TODO: Move firstboot handling to firstboot module somehow
with sqlite_db(cfg.store_file, table='firstboot') as database:
if not 'state' in database:
# If we created a new user db, make sure it can't be read by
# everyone
userdb_fname = '{}.sqlite3'.format(cfg.user_db)
os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
# Permanent redirect causes the browser to cache the redirect,
# preventing the user from navigating to /plinth until the
# browser is restarted.
return HttpResponseRedirect(cfg.server_dir + '/firstboot')
if database['state'] < 5:
cfg.log('First boot state = %d' % database['state'])
return HttpResponseRedirect(
cfg.server_dir + '/firstboot/state%d' % database['state'])
if request.session.get(cfg.session_key, None):
return HttpResponseRedirect(cfg.server_dir + '/apps')
return HttpResponseRedirect(cfg.server_dir + '/help/about')