Merge pull request #77 from SunilMohanAdapa/template-cleanup

Template cleanup
This commit is contained in:
Nick Daly 2014-05-27 00:54:59 +00:00
commit 4669e10d6c
76 changed files with 2820 additions and 1691 deletions

1
.gitignore vendored
View File

@ -19,7 +19,6 @@ doc/TODO.mdwn
doc/oneline.txt doc/oneline.txt
doc/plinth.1 doc/plinth.1
plinth.config plinth.config
templates/*.py
TODO TODO
\#* \#*
.#* .#*

View File

@ -2,10 +2,9 @@
## Installing Plinth ## Installing Plinth
Install the python-cheetah package, pandoc, python-augeas, and Install the pandoc, python-augeas, and bjsonrpc:
bjsonrpc:
apt-get install python-cheetah pandoc python-augeas python-bjsonrpc python-cherrypy3 python-simplejson apt-get install augeas-tools libpython2.7 pandoc psmisc python2.7 python-augeas python-bcrypt python-bjsonrpc python-cherrypy3 python-django python-passlib python-simplejson python-bootstrapform sudo
Unzip and untar the source into a directory. Change to the directory Unzip and untar the source into a directory. Change to the directory
containing the program. Run: containing the program. Run:
@ -23,8 +22,6 @@ and the default password is "secret".
* cherrypy - python web engine v3+ * cherrypy - python web engine v3+
* cheetah - Debian has python-cheetah package
* python - tested with version 2.6.6 * python - tested with version 2.6.6
* *GNU Make* is used to build the templates and such. * *GNU Make* is used to build the templates and such.
@ -43,6 +40,8 @@ The documentation has some dependencies too.
* *GNU Make* processes /doc/Makefile. * *GNU Make* processes /doc/Makefile.
* *python-bootstrapform* - Render django forms for Twitter Bootstrap
## Building the Documentation ## Building the Documentation
Documentation has been collected into a pdf that can be built using Documentation has been collected into a pdf that can be built using

View File

@ -73,12 +73,10 @@ specified and linked otherwise.
- share/apache2/plinth-ssl.conf :: - - share/apache2/plinth-ssl.conf :: -
- share/init.d/plinth :: - - share/init.d/plinth :: -
- sudoers/plinth :: - - sudoers/plinth :: -
- templates/base.tmpl :: [[file:templates/base.tmpl::the%20<a%20href%3D"http:/www.gnu.org/licenses/agpl.html"%20target%3D"_blank">GNU%20Affero%20General%20Public][GNU Affero General Public License, Version 3 or later]] - templates/base.html:: [[file:templates/base.tmpl::the%20<a%20href%3D"http:/www.gnu.org/licenses/agpl.html"%20target%3D"_blank">GNU%20Affero%20General%20Public][GNU Affero General Public License, Version 3 or later]]
- templates/err.tmpl :: - - templates/err.html:: -
- templates/__init__.py :: - - templates/login_nav.html:: -
- templates/login_nav.tmpl :: - - templates/two_col.html:: -
- templates/Makefile :: -
- templates/two_col.tmpl :: -
- tests/actions_test.py :: - - tests/actions_test.py :: -
- tests/auth_test.py :: - - tests/auth_test.py :: -
- tests/testdata/users.sqlite3 :: - - tests/testdata/users.sqlite3 :: -

View File

@ -13,11 +13,11 @@ DATADIR=/usr/share/plinth
PYDIR=$(DATADIR)/python/plinth PYDIR=$(DATADIR)/python/plinth
## Catch-all targets ## Catch-all targets
default: config dirs template css docs default: config dirs css docs
all: default all: default
predepend: predepend:
sudo sh -c "apt-get install augeas-tools libpython2.7 pandoc psmisc python2.7 python-augeas python-bcrypt python-bjsonrpc python-cheetah python-cherrypy3 python-django python-passlib python-simplejson sudo" sudo sh -c "apt-get install augeas-tools libpython2.7 pandoc psmisc python2.7 python-augeas python-bcrypt python-bjsonrpc python-cherrypy3 python-django python-passlib python-simplejson python-bootstrapform sudo"
git submodule init git submodule init
git submodule update git submodule update
touch predepend touch predepend
@ -58,10 +58,6 @@ config: Makefile
@cat $< | python -c 'import re,sys;print re.sub("\s*([{};,:])\s*", "\\1", re.sub("/\*.*?\*/", "", re.sub("\s+", " ", sys.stdin.read())))' > $@ @cat $< | python -c 'import re,sys;print re.sub("\s*([{};,:])\s*", "\\1", re.sub("/\*.*?\*/", "", re.sub("\s+", " ", sys.stdin.read())))' > $@
css: $(COMPRESSED_CSS) css: $(COMPRESSED_CSS)
template:
@$(MAKE) -s -C templates
templates: template
docs: docs:
@$(MAKE) -s -C doc @$(MAKE) -s -C doc
doc: docs doc: docs
@ -78,7 +74,6 @@ clean:
@find . -name "*.pyc" -exec rm {} \; @find . -name "*.pyc" -exec rm {} \;
@find . -name "*.bak" -exec rm {} \; @find . -name "*.bak" -exec rm {} \;
@$(MAKE) -s -C doc clean @$(MAKE) -s -C doc clean
@$(MAKE) -s -C templates clean
rm -f plinth.config rm -f plinth.config
rm -f predepend rm -f predepend

View File

@ -41,7 +41,7 @@ $(SOURCES):
@rm -f $@ @rm -f $@
@ln -s ../$(patsubst %.mdwn,%,$@) $@ @ln -s ../$(patsubst %.mdwn,%,$@) $@
../TODO : $(TODO_SOURCES) ../*.py ../modules/*.py ../Makefile Makefile ../templates/Makefile ../TODO : $(TODO_SOURCES) ../*.py ../modules/*.py ../Makefile Makefile
grep -ro --exclude=.git* --exclude=plinth.1 --exclude=*.tex --exclude=*.html \ grep -ro --exclude=.git* --exclude=plinth.1 --exclude=*.tex --exclude=*.html \
--exclude=README.mdwn --exclude=INSTALL.mdwn \ --exclude=README.mdwn --exclude=INSTALL.mdwn \
--exclude=TODO.mdwn --exclude=COPYING.mdwn \ --exclude=TODO.mdwn --exclude=COPYING.mdwn \

View File

@ -51,15 +51,6 @@ metaclasses can be created to make new classes of plugins.
Any place that might be affected by arbitrary addition of modules Any place that might be affected by arbitrary addition of modules
should have its own metaclass. should have its own metaclass.
### FromPlugin
Each form displayed in the interface should inherit from FormPlugin.
This will allow the system to display multiple forms on the page in
the correct order. Forms will likely also want to inherit from
PagePlugin because after the user clicks submit, the plugin usually
displays a responsive message or an error (along with the form to fill
out again).
## Coding Practices for Modules ## Coding Practices for Modules
All the All the

View File

@ -1,8 +1,9 @@
# Themes and Templates # Themes and Templates
The visual look and feel of the front end is described in theme files The visual look and feel of the front end is described in theme files
while <a href="http://cheetahtemplate.org">Cheetah templates</a> while <a
handle layout. href="https://docs.djangoproject.com/en/1.7/topics/templates/">Django
templates</a> handle layout.
## Themes ## Themes
@ -17,43 +18,35 @@ runtime, but it is theoretically possible.
## Templates ## Templates
Plinth uses the Cheetah templating system. Templates are stored in Plinth uses the Django templating system. Templates are stored in
`/templates`. Template requirements are not specified. `/templates`. Template requirements are not specified.
TODO: formalize the template spec so template writers know what they need to implement and where they can deviate. TODO: formalize the template spec so template writers know what they
need to implement and where they can deviate.
In this section, I'll attempt to document some of the assumptions the In this section, I'll attempt to document some of the assumptions the
program has about templates. The goal is that if you write a tempate program has about templates. The goal is that if you write a tempate
that implements the spec, it should work just fine. that implements the spec, it should work just fine.
### The Template Stack ### The Template Stack
The template is a hierarchical stack, where some templates extend on The template is a hierarchical stack, where some templates extend on
others. At the base of this stack is `base.tmpl`. It should specify others. At the base of this stack is `base.tmpl`. It should specify
variables as blocks (rather than using the $ notation). This allows sections as blocks (rather than using the variables). This allows
other templates to easily override the base template. other templates to easily override the base template.
Next up is the `page.tmpl`. It extends `base.tmpl` and simply
replaces all the blocks with interpolable variables. Most of the
time, `page.tmpl` is what you will actually use to create pages.
`err.tmpl` builds on top of `page.tmpl` by adding some decoration to `err.tmpl` builds on top of `page.tmpl` by adding some decoration to
the title field. the title field.
### Layout ### Layout
Plinth expects a `main` block. This is where the Plinth expects a `main` block. This is where the meat of the content
meat of the content goes. It is the center pain in the default goes. It is the center pain in the default layout. There is a
layout. There is a `title` block that the program will fill with text `title` block that the program will fill with text describing the
describing the current page. `sidebar_left` contains the submenu current page. `sidebar_left` contains the submenu navigation menu,
navigation menu, and `sidebar_right` is where we put all short text and `sidebar_right` is where we put all short text that helps the
that helps the admin fill out forms. They don't have to be sidebars, admin fill out forms. They don't have to be sidebars, and they don't
and they don't have to go on the left and right. have to go on the left and right.
It is possible to override the `footer`, but I haven't yet found a It is possible to override the `footer`, but I haven't yet found a
reason to do so. reason to do so.
## Cheetah
This section is for Cheetah hints that are especially useful in this context.
TODO: add Cheetah hints

2
fabfile.py vendored
View File

@ -150,7 +150,7 @@ def apache():
@task @task
def deps(): def deps():
"Basic plinth dependencies" "Basic plinth dependencies"
sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme python-augeas python-bjsonrpc') sudo('apt-get install --no-install-recommends -y python make pandoc python-simplejson python-pyme python-augeas python-bjsonrpc python-django python-bootstrapform')
@task @task
def update(): def update():

View File

@ -2,37 +2,25 @@ import cherrypy
from gettext import gettext as _ from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
from forms import Form
import cfg import cfg
import util
class Apps(PagePlugin): class Apps(PagePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps") self.register_page("apps")
self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80) self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
self.menu.add_item("Chat", "icon-comment", "/../jwchat", 30) self.menu.add_item("Chat", "icon-comment", "/../jwchat", 30)
self.menu.add_item("Photo Gallery", "icon-picture", "/apps/photos", 35) self.menu.add_item("Photo Gallery", "icon-picture", "/apps/photos", 35)
@cherrypy.expose @cherrypy.expose
def index(self): def index(self):
main = """ return util.render_template(template='apps',
<p>User Applications are web apps hosted on your %s.</p> title=_('User Applications'))
<p>Eventually this box could be your photo sharing site, your
instant messaging site, your social networking site, your news
site. Remember web portals? We can be one of those too.
Many of the services you use on the web could soon be on site
and under your control!</p>
""" % (cfg.product_name)
return self.fill_template(title="User Applications", main=main, sidebar_right='')
@cherrypy.expose @cherrypy.expose
@require() @require()
def photos(self): def photos(self):
return self.fill_template(title="Photo Gallery", main='', sidebar_right=""" return util.render_template(template='photos',
<strong>Photo Gallery</strong><p>Your photos might well be the most valuable title=_('Photo Gallery'))
digital property you have, so why trust it to companies that have no
investment in the sentimental value of your family snaps? Keep those
photos local, backed up, easily accessed and free from the whims of
some other websites business model.</p>
""")

View File

@ -1,82 +1,85 @@
import cherrypy import cherrypy
from django import forms
from gettext import gettext as _ from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
from forms import Form
import actions import actions
import cfg import cfg
import service import service
from util import Message import util
class Owncloud(PagePlugin, FormPlugin):
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())
class OwnCloud(PagePlugin):
"""ownCloud configuration page"""
order = 90 order = 90
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps.owncloud") self.register_page('apps.owncloud')
cfg.html_root.apps.menu.add_item("Owncloud", "icon-picture", "/apps/owncloud", 35)
cfg.html_root.apps.menu.add_item('Owncloud', 'icon-picture',
'/apps/owncloud', 35)
status = self.get_status()
self.service = service.Service('owncloud', _('ownCloud'), self.service = service.Service('owncloud', _('ownCloud'),
['http', 'https'], is_external=True, ['http', 'https'], is_external=True,
enabled=self.is_enabled) enabled=status['enabled'])
def is_enabled(self):
"""Return whether ownCloud is enabled"""
output, error = actions.run('owncloud-setup', 'status')
if error:
raise Exception('Error getting ownCloud status: %s' % error)
return 'enable' in output.split()
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self, **kwargs): def index(self, **kwargs):
owncloud_enable = self.is_enabled() """Serve the ownCloud configuration page"""
status = self.get_status()
if 'submitted' in kwargs: form = None
owncloud_enable = self.process_form(kwargs) messages = []
main = self.form(owncloud_enable)
sidebar_right="""
<strong>ownCloud</strong></br>
<p>ownCloud gives you universal access to your files through a web interface or WebDAV. It also provides a platform to easily view & sync your contacts, calendars and bookmarks across all your devices and enables basic editing right on the web. Installation has minimal server requirements, doesn't need special permissions and is quick. ownCloud is extendable via a simple but powerful API for applications and plugins.
</p>
"""
return self.fill_template(title="Owncloud", main=main, sidebar_right=sidebar_right)
def form(self, owncloud_enable, message=None): if kwargs:
form = Form(title="Configuration", form = OwnCloudForm(kwargs, prefix='owncloud')
action=cfg.server_dir + "/apps/owncloud/index", # pylint: disable-msg=E1101
name="configure_owncloud", if form.is_valid():
message=message) self._apply_changes(status, form.cleaned_data, messages)
form.checkbox(_("Enable Owncloud"), name="owncloud_enable", id="owncloud_enable", checked=owncloud_enable) status = self.get_status()
# hidden field is needed because checkbox doesn't post if not checked form = OwnCloudForm(initial=status, prefix='owncloud')
form.hidden(name="submitted", value="True") else:
form.html(_("""<p>When enabled, the owncloud installation will be available from <a href="/owncloud">owncloud</a> on the web server. Visit this URL to set up the initial administration account for owncloud.</p>""")) form = OwnCloudForm(initial=status, prefix='owncloud')
form.html(_("""<p><strong>Note: Setting up owncloud for the first time might take 5 minutes or more, depending on download bandwidth from the Debian APT sources.</p>"""))
form.submit(_("Update setup"))
return form.render()
def process_form(self, kwargs): return util.render_template(template='owncloud', title=_('ownCloud'),
checkedinfo = { form=form, messages=messages)
'enable' : False,
}
opts = [] @staticmethod
for k in kwargs.keys(): def get_status():
if 'on' == kwargs[k]: """Return the current status"""
shortk = k.split("owncloud_").pop() output, error = actions.run('owncloud-setup', 'status')
checkedinfo[shortk] = True if error:
raise Exception('Error getting ownCloud status: %s' % error)
for key in checkedinfo.keys(): return {'enabled': 'enable' in output.split()}
if checkedinfo[key]:
opts.append(key) def _apply_changes(self, old_status, new_status, messages):
else: """Apply the changes"""
opts.append('no'+key) if old_status['enabled'] == new_status['enabled']:
actions.superuser_run("owncloud-setup", opts, async=True) messages.append(('info', _('Setting unchanged')))
return
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 # Send a signal to other modules that the service is
# enabled/disabled # enabled/disabled
self.service.notify_enabled(self, checkedinfo['enable']) self.service.notify_enabled(self, new_status['enabled'])
return checkedinfo['enable']

View File

@ -0,0 +1,31 @@
{% 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 main_block %}
<p>User Applications are web apps hosted on your
{{ cfg.product_name }}.</p>
<p>Eventually this box could be your photo sharing site, your instant
messaging site, your social networking site, your news site. Remember
web portals? We can be one of those too. Many of the services you
use on the web could soon be on site and under your control!</p>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<p>When enabled, the owncloud installation will be available
from <a href="/owncloud">owncloud</a> on the web server. Visit
this URL to set up the initial administration account for
owncloud.</p>
<p><strong>Note: Setting up owncloud for the first time might take
5 minutes or more, depending on download bandwidth from the
Debian APT sources.</strong></p>
<input type="submit" class="btn-primary" value="Update setup"/>
</form>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>ownCloud</h3>
<p>ownCloud gives you universal access to your files through a web
interface or WebDAV. It also provides a platform to easily view
& sync your contacts, calendars and bookmarks across all your
devices and enables basic editing right on the web. Installation
has minimal server requirements, doesn't need special
permissions and is quick. ownCloud is extendable via a simple
but powerful API for applications and plugins.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% 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 main_block %}
<p>Your photos might well be the most valuable digital property you
have, so why trust it to companies that have no investment in the
sentimental value of your family snaps? Keep those photos local,
backed up, easily accessed and free from the whims of some other
websites business model.</p>
{% endblock %}

View File

@ -1,16 +1,5 @@
from urlparse import urlparse """
import os, cherrypy, re First Boot: Initial Plinth Configuration.
from gettext import gettext as _
from plugin_mount import PagePlugin, PluginMount, FormPlugin
from modules.auth import require, add_user
from forms import Form
import util as u
from withsqlite.withsqlite import sqlite_db
import cfg
import config
from model import User
"""First Boot: Initial Plinth Configuration.
See docs/design/first-connection.mdwn for details. See docs/design/first-connection.mdwn for details.
@ -27,27 +16,76 @@ The Plinth first-connection process has several stages:
3. The box detects the network's configuration and restarts networking. 3. The box detects the network's configuration and restarts networking.
4. The user interacts with the box normally. 4. The user interacts with the box normally.
""" """
import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _
from plugin_mount import PagePlugin
from modules.auth import add_user
from withsqlite.withsqlite import sqlite_db
import cfg
import config
import util
## TODO: flesh out these tests values
def valid_box_key(value):
"""Check whether box key is valid"""
del value # Unused
return True
class State0Form(forms.Form): # pylint: disable-msg=W0232
"""First boot state 0 form"""
hostname = forms.CharField(
label=_('Name your FreedomBox'),
help_text=_('For convenience, your FreedomBox needs a name. It \
should be something short that does not contain spaces or punctuation. \
"Willard" would be a good name while "Freestyle McFreedomBox!!!" would \
not. It must be alphanumeric, start with an alphabet and must not be greater \
than 63 characters in length.'),
validators=[
validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$',
_('Invalid hostname'))])
username = forms.CharField(label=_('Username'))
password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput())
box_key = forms.CharField(
label=_('Box\'s key (optional)'), required=False,
widget=forms.Textarea(), validators=[valid_box_key],
help_text=_('Cryptographic keys are used so that Box\'s identity can \
proved when talking to you. This key can be auto-generated, but if one \
already exists (from a prior FreedomBox, for example), you can paste it \
below. This key should not be the same as your key because you are not your \
FreedomBox!'))
class FirstBoot(PagePlugin): class FirstBoot(PagePlugin):
"""First boot wizard"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("firstboot") # this is the url this page will hang off of (/firstboot)
# 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')
@cherrypy.expose @cherrypy.expose
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
return self.state0(*args, **kwargs) return self.state0(*args, **kwargs)
## TODO: flesh out these tests values
def valid_box_key_p(self, key):
return True
def generate_box_key(self): def generate_box_key(self):
return "fake key" return "fake key"
@cherrypy.expose @cherrypy.expose
def state0(self, message="", hostname="", box_key="", submitted=False, username="", password="", **kwargs): def state0(self, **kwargs):
""" """
In this state, we do time config over HTTP, name the box and In this state, we do time config over HTTP, name the box and
server key selection. server key selection.
@ -64,68 +102,70 @@ class FirstBoot(PagePlugin):
""" """
# FIXME: reject connection attempt if db["state"] >= 5. # FIXME: reject connection attempt if db["state"] >= 5.
## Until LDAP is in place, we'll put the box key in the cfg.store_file
status = self.get_state0()
## Until LDAP is in place, we'll put the box name and key in the cfg.store_file form = None
## Must resist the sick temptation to write an LDAP interface to the sqlite file messages = []
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as db:
db['about'] = "This table is for information about this FreedomBox" if kwargs:
if hostname: form = State0Form(kwargs, prefix='firstboot')
if '' == config.valid_hostname(hostname): # pylint: disable-msg=E1101
config.set_hostname(hostname) if form.is_valid():
else: success = self._apply_state0(status, form.cleaned_data,
message += _("Invalid box name: %s") % config.valid_hostname(hostname) messages)
if success:
# Everything is good, permanently mark and move to page 2
with sqlite_db(cfg.store_file, table="firstboot",
autocommit=True) as database:
database['state'] = 1
raise cherrypy.InternalRedirect('state1')
else:
form = State0Form(initial=status, prefix='firstboot')
return util.render_template(template='firstboot_state0',
title=_('First Boot!'), form=form,
messages=messages)
@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'
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()
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: else:
hostname = config.get_hostname() messages.append(('success', _('User account created')))
if box_key:
if self.valid_box_key_p(box_key):
db['box_key'] = box_key
else:
message += _("Invalid key!")
elif 'box_key' in db and db['box_key']:
box_key = _("We already have a key for this box on file.") # TODO: Think this through and handle more gracefully. Seriously.
elif submitted and not box_key:
box_key = self.generate_box_key()
db['box_key'] = box_key
if username and password:
error = add_user(username, password, 'First user - please change', '', True)
if error:
message += _("User account creation failed: %s") % error
validuser = False
else:
validuser = True
else:
validuser = False
if hostname and box_key and '' == config.valid_hostname(hostname) and self.valid_box_key_p(box_key) and validuser: return success
## Update state to 1 and head there
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
db['state']=1
raise cherrypy.InternalRedirect('state1')
main = "<p>Welcome. It looks like this FreedomBox isn't set up yet. We'll need to ask you a just few questions to get started.</p>"
form = Form(title="Welcome to Your FreedomBox!",
action="", # stay at firstboot
name="whats_my_name",
message=message)
form.html("<p>For convenience, your FreedomBox needs a name. It should be something short that doesn't contain spaces or punctuation. 'Willard' would be a good name. 'Freestyle McFreedomBox!!!' would not.</p>")
form.text_input('Name your FreedomBox', id="hostname", value=hostname)
form.html("<p><strong>Initial user and password.</strong> Access to this web interface is protected by knowing a username and password. Provide one here to register the initial privileged user. The password can be changed and other users added later.</p>")
form.text_input('Username:', id="username", value=username)
form.text_input('Password:', id="password", type='password')
form.html("<p>%(box_name)s uses cryptographic keys so it can prove its identity when talking to you. %(box_name)s can make a key for itself, but if one already exists (from a prior FreedomBox, for example), you can paste it below. This key should not be the same as your key because you are not your FreedomBox!</p>" % {'box_name':cfg.box_name})
form.text_box("If you want, paste your box's key here.", id="box_key", value=box_key)
form.hidden(name="submitted", value="True")
form.submit("Box it up!")
main += form.render()
return self.fill_template(
template="base",
title=_("First Boot!"),
main=main,
sidebar_right=sidebar_right)
@staticmethod
@cherrypy.expose @cherrypy.expose
def state1(self, message=None): def state1():
""" """
State 1 is when we have a box name and key. In this state, 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 our task is to provide a certificate and maybe to guide the
@ -134,31 +174,11 @@ class FirstBoot(PagePlugin):
TODO: HTTPS failure in State 2 should returns to state 1. TODO: HTTPS failure in State 2 should returns to state 1.
""" """
main = """
<p>Welcome screen not completely implemented yet. Press <a
href="../router">continue</a> to see the rest of the web interface.</p>
<ul>
<li>TODO: explain all this cert stuff to the user.</li>
<li>TODO: add instrux for installing certificate.</li>
<li>TODO: add steps for after you have installed certificate.</li>
<ul>
"""
# TODO complete first_boot handling # TODO complete first_boot handling
# Make sure the user is not stuck on a dead end for now. # Make sure the user is not stuck on a dead end for now.
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db: with sqlite_db(cfg.store_file, table='firstboot', autocommit=True) as \
db['state']=5 database:
database['state'] = 5
# TODO: Update state to 2 and head there return util.render_template(template='firstboot_state1',
# with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db: title=_('Installing the Certificate'))
# db['state']=1
# # TODO: switch to HTTPS
# raise cherrypy.InternalRedirect('state1')
return self.fill_template(
template="base",
title=_("Installing the Certificate"),
main=main,
sidebar_right=sidebar_right)
sidebar_right=_("""<strong>Getting Help</strong><p>We've done our best to make your FreedomBox easy to use. If you have questions during setup, there are a few places to turn for help. TODO: add links to such help.</p>""")

View File

@ -3,6 +3,9 @@ import cherrypy
from gettext import gettext as _ from gettext import gettext as _
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import cfg import cfg
import util
class Help(PagePlugin): class Help(PagePlugin):
order = 20 # order of running init in PagePlugins order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -17,61 +20,15 @@ class Help(PagePlugin):
@cherrypy.expose @cherrypy.expose
def index(self): def index(self):
main=""" return util.render_template(template='help',
<p>There are a variety of places to go for help with Plinth title=_('Documentation and FAQ'))
and the box it runs on.</p>
<p>This front end has a <a
href="/help/view/plinth">developer's manual</a>. It isn't
complete, but it is the first place to look. Feel free to
offer suggestions, edits, and screenshots for completing
it!</p>
<p><a href="http://wiki.debian.org/FreedomBox" target="_blank">A section of
the Debian wiki</a> is devoted to the %(box)s. At some
point the documentation in the wiki and the documentation in
the manual should dovetail.</p>
<p>There
are Debian gurus in the \#debian channels of both
irc.freenode.net and irc.oftc.net. They probably don't know
much about the %(box)s and almost surely know nothing of
this front end, but they are an incredible resource for
general Debian issues.</p>
<p>There is no <a href="/help/view/faq">FAQ</a> because
the question frequency is currently zero for all
questions.</p>
""" % {'box':cfg.box_name}
return self.fill_template(title="Documentation and FAQ", main=main)
@cherrypy.expose @cherrypy.expose
def about(self): def about(self):
return self.fill_template(title=_("About the %s" % cfg.box_name), main=""" return util.render_template(
<img src="/static/theme/img/freedombox-logo-250px.png" class="main-graphic" /> template='about',
<p>We live in a world where our use of the network is title=_('About the {box_name}').format(box_name=cfg.box_name))
mediated by those who often do not have our best
interests at heart. By building software that does not rely on
a central service, we can regain control and privacy. By
keeping our data in our homes, we gain useful legal
protections over it. By giving back power to the users over
their networks and machines, we are returning the Internet to
its intended peer-to-peer architecture.</p>
<p>In order to bring about the new network order, it is
paramount that it is easy to convert to it. The hardware it
runs on must be cheap. The software it runs on must be easy to
install and administrate by anybody. It must be easy to
transition from existing services.</p>
<p><a class="btn btn-primary btn-large" href="http://wiki.debian.org/FreedomBox" target="_blank">Learn more &raquo;</a></p>""",
sidebar_right=_("""<strong>Our Goal</strong><p>There are a number of projects working to realize a future
of distributed services; we aim to bring them all together in
a convenient package.</p>
<p>For more information about the FreedomBox project, see the
<a href="http://wiki.debian.org/FreedomBox">Debian
Wiki</a>.</p>
"""))
class View(PagePlugin): class View(PagePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -82,7 +39,8 @@ class View(PagePlugin):
def default(self, page=''): def default(self, page=''):
if page not in ['design', 'plinth', 'hacking', 'faq']: if page not in ['design', 'plinth', 'hacking', 'faq']:
raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page) raise cherrypy.HTTPError(404, "The path '/help/view/%s' was not found." % page)
return self.fill_template(template="err", main="<p>Sorry, as much as I would like to show you that page, I don't seem to have a page named %s!</p>" % page)
with open(os.path.join("doc", "%s.part.html" % page), 'r') as IF: with open(os.path.join("doc", "%s.part.html" % page), 'r') as IF:
main = IF.read() main = IF.read()
return self.fill_template(title=_("%s Documentation" % cfg.product_name), main=main) return util.render_template(title=_("%s Documentation" %
cfg.product_name), main=main)

View File

@ -0,0 +1,61 @@
{% 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 main_block %}
<img src="{{ basehref }}/static/theme/img/freedombox-logo-250px.png"
class="main-graphic" />
<p>We live in a world where our use of the network is mediated by
those who often do not have our best interests at heart. By building
software that does not rely on a central service, we can regain
control and privacy. By keeping our data in our homes, we gain useful
legal protections over it. By giving back power to the users over
their networks and machines, we are returning the Internet to its
intended peer-to-peer architecture.</p>
<p>In order to bring about the new network order, it is
paramount that it is easy to convert to it. The hardware it
runs on must be cheap. The software it runs on must be easy to
install and administrate by anybody. It must be easy to
transition from existing services.</p>
<p><a class="btn btn-primary btn-large"
href="http://wiki.debian.org/FreedomBox" target="_blank">Learn more
&raquo;</a></p>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Our Goal</h3>
<p>There are a number of projects working to realize a future of
distributed services; we aim to bring them all together in a
convenient package.</p>
<p>For more information about the FreedomBox project, see the
<a href="http://wiki.debian.org/FreedomBox">Debian Wiki</a>.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% 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 main_block %}
<p>There are a variety of places to go for help with
{{ cfg.product_name }} and the box it runs on.</p>
<p>This front end has a <a href="{{ basehref }}/help/view/plinth">
developer's manual</a>. It isn't complete, but it is the first place
to look. Feel free to offer suggestions, edits, and screenshots for
completing it!</p>
<p><a href="http://wiki.debian.org/FreedomBox" target="_blank">A
section of the Debian wiki</a> is devoted to the {{ cfg.box_name }}.
At some point the documentation in the wiki and the documentation in
the manual should dovetail.</p>
<p>There are Debian gurus in the #debian channels of both
irc.freenode.net and irc.oftc.net. They probably don't know much
about the {{ cfg.box_name }} and almost surely know nothing of this
front end, but they are an incredible resource for general Debian
issues.</p>
<p>There is no <a href="{{ basehref }}/help/view/faq">FAQ</a> because
the question frequency is currently zero for all questions.</p>
{% endblock %}

View File

@ -67,12 +67,14 @@ def check_credentials(username, passphrase):
cfg.log(error) cfg.log(error)
return error return error
bad_authentication = "Bad user-name or password." bad_authentication = "Bad username or password."
hashed_password = None hashed_password = None
if username in cfg.users: if username not in cfg.users or 'passphrase' not in cfg.users[username]:
if "passphrase" in cfg.users[username]: cfg.log(bad_authentication)
hashed_password = cfg.users[username]['passphrase'] return bad_authentication
hashed_password = cfg.users[username]['passphrase']
try: try:
# time-dependent comparison when non-ASCII characters are used. # time-dependent comparison when non-ASCII characters are used.

View File

@ -1,43 +1,73 @@
"""
Controller to provide login and logout actions
"""
import cherrypy import cherrypy
import cfg import cfg
from django import forms
from gettext import gettext as _
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
from modules.forms import Form import auth
from auth import * import util
# Controller to provide login and logout actions
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())
def clean(self):
"""Check for valid credentials"""
# pylint: disable-msg=E1101
if 'username' in self._errors or 'password' in self._errors:
return self.cleaned_data
error_msg = auth.check_credentials(self.cleaned_data['username'],
self.cleaned_data['password'])
if error_msg:
raise forms.ValidationError(error_msg, code='invalid_credentials')
return self.cleaned_data
class AuthController(PagePlugin): class AuthController(PagePlugin):
"""Login and logout pages"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("auth")
self.register_page('auth')
def on_login(self, username): def on_login(self, username):
"""Called on successful login""" """Called on successful login"""
def on_logout(self, username): def on_logout(self, username):
"""Called on logout""" """Called on logout"""
def get_loginform(self, username, msg='', from_page=cfg.server_dir+"/"):
form = Form(title="Login", action=cfg.server_dir + "/auth/login", message=msg)
form.text_input(name="from_page", value=from_page, type="hidden")
form.text_input("Username", name="username", value=username)
form.text_input("Passphrase", name="passphrase", type="password")
form.submit(label="Login")
return self.fill_template(main=form.render(), sidebar_right=" ")
@cherrypy.expose @cherrypy.expose
def login(self, username=None, passphrase=None, from_page=cfg.server_dir+"/", **kwargs): def login(self, from_page=cfg.server_dir+"/", **kwargs):
if username is None or passphrase is None: """Serve the login page"""
return self.get_loginform("", from_page=from_page) form = None
error_msg = check_credentials(username, passphrase) if kwargs:
if error_msg: form = LoginForm(kwargs, prefix='auth')
return self.get_loginform(username, error_msg, from_page) # 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: else:
cherrypy.session[cfg.session_key] = cherrypy.request.login = username form = LoginForm(prefix='auth')
self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/")) return util.render_template(template='form', title=_('Login'),
form=form, submit_text=_('Login'))
@cherrypy.expose @cherrypy.expose
def logout(self, from_page=cfg.server_dir+"/"): def logout(self, from_page=cfg.server_dir+"/"):
sess = cherrypy.session sess = cherrypy.session
@ -46,4 +76,5 @@ class AuthController(PagePlugin):
if username: if username:
cherrypy.request.login = None cherrypy.request.login = None
self.on_logout(username) self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/")) raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/"))

View File

@ -1,151 +0,0 @@
"""
The Form class is a helper class that takes parameters and method
calls and can return html for a form with appropriate hooks for css
styling. It should allow you to display a form but have the
formatting and styling added by the class. You can worry less about
how it looks while still getting consistent, decent-looking forms.
Take a look at the FirstBoot class for an example of forms in action.
Copyright 2011-2013 James Vasile
This software is released to you (yes, you) under the terms of the GNU
Affero General Public License, version 3 or later, available at
<http://www.gnu.org/licenses/agpl.html>.
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.
"""
class Form():
def __init__(self, action=None, cls='form', title=None, onsubmit=None, name=None, message='', method="post"):
action = self.get_form_attrib_text('action', action)
onsubmit = self.get_form_attrib_text('onsubmit', onsubmit)
name = self.get_form_attrib_text('name', name)
self.pretext = ' <form class="%s" method="%s" %s%s%s>\n' % (cls, method, action, onsubmit, name)
if title:
self.pretext += ' <h2>%s</h2>\n' % title
if message:
self.message = "<h3>%s</h3>" % message
else:
self.message = ''
self.text = ''
self.end_text = "</form>\n"
def get_form_attrib_text(self, field, val):
if val:
return ' %s="%s"' % (field, val)
else:
return ''
def html(self, html):
self.text += html
def dropdown(self, label='', name=None, id=None, vals=None, select=None, onchange=''):
"""vals is a list of values.
select is the index in vals of the selected item. None means no item is selected yet."""
name, id = self.name_or_id(name, id)
self.text += (""" <label>
<span>%(label)s</span>
<select name="%(name)s" id="%(id)s" onchange="%(onchange)s">\n"""
% {'label':label, 'name':name, 'id':id, 'onchange':onchange})
for i in range(len(vals)):
v = vals[i]
if i == select:
selected = "SELECTED"
else:
selected = ''
self.text +=" <option value=\"%s\" %s>%s</option>\n" % (v, selected, v)
self.text += """ </select>
</label>\n"""
def dotted_quad(self, label='', name=None, id=None, quad=None):
name, id = self.name_or_id(name, id)
if not quad:
quad = [0,0,0,0]
self.text += """ <label>
<span>%(label)s</span>
<input type="text" class="inputtextnowidth" name="%(name)s0" id="%(id)s0" value="%(q0)s" maxlength="3" size="1"/><strong>.</strong>
<input type="text" class="inputtextnowidth" name="%(name)s1" id="%(id)s1" value="%(q1)s" maxlength="3" size="1"/><strong>.</strong>
<input type="text" class="inputtextnowidth" name="%(name)s2" id="%(id)s2" value="%(q2)s" maxlength="3" size="1"/><strong>.</strong>
<input type="text" class="inputtextnowidth" name="%(name)s3" id="%(id)s3" value="%(q3)s" maxlength="3" size="1"/>
</label>""" % {'label':label, 'name':name, 'id':id, 'q0':quad[0], 'q1':quad[1], 'q2':quad[2], 'q3':quad[3]}
def text_input(self, label='', name=None, id=None, type='text', value='', size=20):
name, id = self.name_or_id(name, id)
if type=="hidden":
self.text += '<input type="%s" class="inputtext" name="%s" id="%s" value="%s"/>' % (type, name, id, value)
else:
self.text += """ <label>
<span>%s</span>
<input type="%s" class="inputtext" name="%s" id="%s" value="%s" size="%s"/>
</label>""" % (label, type, name, id, value, size)
def hidden(self, name=None, id=None, value=''):
self.text_input(type="hidden", name=name, id=id, value=value)
def text_box(self, label='', name=None, id=None, value=""):
name, id = self.name_or_id(name, id)
self.text += """
<label>
<span>%s</span>
<textarea class="textbox" name="%s" id="%s">%s</textarea>
</label>""" % (label, name, id, value)
def submit(self, label='', name=None, id=None):
name, id = self.name_or_id(name, id)
self.text += """
<div class="submit">
<label><span></span>
<input type="submit" class="btn-primary" value="%s" name="%s" id="%s" />
</label></div>\n""" % (label, name, id)
def submit_row(self, buttons):
"""buttons is a list of tuples, each containing label, name, id. Name and id are optional."""
self.text += '<div class="submit"><label>'
button_text = ''
for button in buttons:
label = button[0]
try:
name = button[1]
except:
name = None
try:
id = button[2]
except:
id = None
name, id = self.name_or_id(name, id)
if button_text != '':
button_text += "&nbsp;"
button_text += '<input type="submit" class="btn-primary" value="%s" name="%s" id="%s" />\n' % (label, name, id)
self.text += '%s</div></label>' % button_text
def name_or_id(self, name, id):
if not name: name = id
if not id: id = name
if not name: name = ''
if not id: id = ''
return name, id
def checkbox(self, label='', name='', id='', checked=''):
name, id = self.name_or_id(name, id)
if checked:
checked = 'checked="on"'
self.text += """
<div class="checkbox">
<label>
<span>%s</span>
<input type=checkbox name="%s" id="%s" %s/>
</label></div>\n""" % (label, name, id, checked)
def radiobutton(self, label='', name='', id='', value='', checked=''):
name, id = self.name_or_id(name, id)
if checked:
checked = 'checked="on"'
self.text += """
<div class="radio">
<label>
<span>%s</span>
<input type="radio" name="%s" id="%s" value="%s" %s/>
</label></div>\n""" % (label, name, id, value, checked)
def get_checkbox(self, name='', id=''):
return '<input type=checkbox name="%s" id="%s" />\n' % self.name_or_id(name, id)
def render(self):
return self.pretext+self.message+self.text+self.end_text

View File

@ -3,6 +3,8 @@ from gettext import gettext as _
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
from modules.auth import require from modules.auth import require
import cfg import cfg
import util
class Privacy(PagePlugin): class Privacy(PagePlugin):
order = 20 # order of running init in PagePlugins order = 20 # order of running init in PagePlugins
@ -22,20 +24,5 @@ class Privacy(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def config(self): def config(self):
main=""" return util.render_template(template='privacy_config',
<p>Privacy controls are not yet implemented. This page is a title=_('Privacy Control Panel'))
placeholder and a promise: privacy is important enough that it
is a founding consideration, not an afterthought.</p>
"""
return self.fill_template(title=_("Privacy Control Panel"), main=main,
sidebar_right=_("""<strong>Statement of Principles</strong><p>When we say your
privacy is important, it's not just an empty pleasantry. We really
mean it. Your privacy control panel should give you fine-grained
control over exactly who can access your %s and the
information on it.</p>
<p>Your personal information should not leave this box without your
knowledge and direction. And if companies or government wants this
information, they have to ask <strong>you</strong> for it. This gives you a
chance to refuse and also tells you who wants your data.</p>
""") % cfg.product_name)

View File

@ -0,0 +1,46 @@
{% 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 main_block %}
<p>Privacy controls are not yet implemented. This page is a
placeholder and a promise: privacy is important enough that it is a
founding consideration, not an afterthought.</p>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Statement of Principles</h3>
<p>When we say your privacy is important, it's not just an empty
pleasantry. We really mean it. Your privacy control panel should
give you fine-grained control over exactly who can access your {{
cfg.product_name }} and the information on it.</p>
<p>Your personal information should not leave this box without your
knowledge and direction. And if companies or government wants this
information, they have to ask <strong>you</strong> for it. This
gives you a chance to refuse and also tells you who wants your
data.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% 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 main_block %}
<p>Tor is an anonymous communication system. You can learn more about
it from the <a href="https://www.torproject.org/">Tor Project</a>
website. For best protection when web surfing, the Tor Project
recommends that you use
the <a href="https://www.torproject.org/download/download-easy.html.en">Tor
Browser Bundle</a>.</p>
<p>A Tor SOCKS port is available on your FreedomBox on TCP port
9050.</p>
<p>Your FreedomBox is configured as a Tor bridge with obfsproxy, so it
can help circumvent censorship. If your FreedomBox is behind a router
or firewall, you should make sure the following ports are open, and
port-forwarded, if necessary:</p>
<table class="table table-bordered table-condensed span2">
{% for name, port in tor_ports.items %}
<tr>
<td>{{ name }}</td>
<td>{{ port }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -25,6 +25,8 @@ from plugin_mount import PagePlugin
from modules.auth import require from modules.auth import require
import actions import actions
import cfg import cfg
import util
class tor(PagePlugin): class tor(PagePlugin):
order = 30 # order of running init in PagePlugins order = 30 # order of running init in PagePlugins
@ -38,24 +40,14 @@ class tor(PagePlugin):
def index(self): def index(self):
output, error = actions.superuser_run("tor-get-ports") output, error = actions.superuser_run("tor-get-ports")
port_info = output.split("\n") port_info = output.split("\n")
ports = {} tor_ports = {}
for line in port_info: for line in port_info:
try: try:
(key, val) = line.split() (key, val) = line.split()
ports[key] = val tor_ports[key] = val
except ValueError: except ValueError:
continue continue
main = _(""" return util.render_template(template='tor',
<p>Tor is an anonymous communication system. You can learn more about it from the <a href="https://www.torproject.org/">Tor Project</a> website. For best protection when web surfing, the Tor Project recommends that you use the <a href="https://www.torproject.org/download/download-easy.html.en">Tor Browser Bundle</a>.</p> title=_('Tor Control Panel'),
<p>A Tor SOCKS port is available on your FreedomBox on TCP port 9050.</p> tor_ports=tor_ports)
<p>Your FreedomBox is configured as a Tor bridge with obfsproxy, so it can help circumvent censorship. If your FreedomBox is behind a router or firewall, you should make sure the following ports are open, and port-forwarded, if necessary:</p>
""")
main += '<table class="table table-bordered table-condensed span2">'
for key in ports:
main += "<tr><td>" + str(key) + "</td>"
main += "<td>" + str(ports[key]) + "</td></tr>"
main += "</table>"
return self.fill_template(title=_("Tor Control Panel"), main=main)

View File

@ -1,6 +1,8 @@
import cherrypy import cherrypy
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
from modules.auth import require from modules.auth import require
import util
class Info(PagePlugin): class Info(PagePlugin):
title = 'Info' title = 'Info'
@ -13,6 +15,7 @@ class Info(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
return self.fill_template(title="Router Information", main=""" return util.render_template(title="Router Information", main="""
<p>Eventually we will display a bunch of info, graphs and logs about the routing functions here.</p> <p>Eventually we will display a bunch of info, graphs and logs about
the routing functions here.</p>
""") """)

View File

@ -20,14 +20,14 @@ Plinth module for configuring PageKite service
""" """
import cherrypy import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _ from gettext import gettext as _
import actions import actions
import cfg import cfg
from forms import Form
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
import re
import util import util
@ -39,83 +39,103 @@ class PageKite(PagePlugin):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("router.setup.pagekite") self.register_page("router.setup.pagekite")
self.register_page("router.setup.pagekite.configure")
cfg.html_root.router.setup.menu.add_item( cfg.html_root.router.setup.menu.add_item(
_("Public Visibility (PageKite)"), "icon-flag", _("Public Visibility (PageKite)"), "icon-flag",
"/router/setup/pagekite", 50) "/router/setup/pagekite", 50)
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self, **kwargs): def index(**kwargs):
"""Serve introcution page""" """Serve introdution page"""
del kwargs # Unused del kwargs # Unused
main = _(""" menu = {'title': _('PageKite'),
<p>PageKite is a system for exposing {box_name} services when you 'items': [{'url': '/router/setup/pagekite/configure',
don't have a direct connection to the Internet. You only need this 'text': _('Configure PageKite')}]}
service if your {box_name} services are unreachable from the rest of sidebar_right = util.render_template(template='menu_block', menu=menu)
the Internet. This includes the following situations: </p>
<ul> return util.render_template(template='pagekite_introduction',
<li>{box_name} is behind a restricted firewall.</li> title=_("Public Visibility (PageKite)"),
sidebar_right=sidebar_right)
<li>{box_name} is connected to a (wireless) router which you don't
control.</li>
<li>Your ISP does not provide you an external IP address and instead
provides Internet connection through NAT.</li>
<li>Your ISP does not provide you a static IP address and your IP
address changes evertime you connect to Internet.</li>
<li>Your ISP limits incoming connections.</li>
</ul>
<p>PageKite works around NAT, firewalls and IP-address limitations by
using a combination of tunnels and reverse proxies. Currently,
exposing web server and SSH server are supported. An intermediary
server with direct Internet access is required. Currently, only
pagekite.net server is supported and you will need an account
there. In future, it might be possible to use your buddy's {box_name}
for this.</p>
<p><a class='btn btn-primary btn-lg'
href="{server_dir}/router/setup/pagekite/configure">Configure
PageKite</a></p>
""").format(box_name=cfg.box_name, server_dir=cfg.server_dir)
sidebar_right = _('''
<strong>PageKite</strong>
<p><a href="{server_dir}/router/setup/pagekite/configure">Configure
PageKite</a> </p>''').format(server_dir=cfg.server_dir)
return self.fill_template(title=_("Public Visibility (PageKite)"),
main=main, sidebar_right=sidebar_right)
class configure(FormPlugin, PagePlugin): # pylint: disable-msg=C0103 class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField"""
def clean(self, value):
"""Clean and validate the field value"""
if value:
value = value.strip()
return super(TrimmedCharField, self).clean(value)
class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
"""Form to configure PageKite"""
enabled = forms.BooleanField(label=_('Enable PageKite'),
required=False)
server = forms.CharField(
label=_('Server'), required=False,
help_text=_('Currently only pagekite.net server is supported'),
widget=forms.TextInput(attrs={'placeholder': 'pagekite.net',
'disabled': 'disabled'}))
kite_name = TrimmedCharField(
label=_('Kite name'),
help_text=_('Example: mybox1-myacc.pagekite.me'),
validators=[
validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$',
_('Invalid kite name'))])
kite_secret = TrimmedCharField(
label=_('Kite secret'),
help_text=_('A secret associated with the kite or the default secret \
for your account if no secret is set on the kite'))
http_enabled = forms.BooleanField(
label=_('Web Server (HTTP)'), required=False,
help_text=_('Site will be available at \
<a href="http://mybox1-myacc.pagekite.me">http://mybox1-myacc.pagekite.me \
</a>'))
ssh_enabled = forms.BooleanField(
label=_('Secure Shell (SSH)'), required=False,
help_text=_('See SSH client setup <a href="\
https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions</a>'))
class Configure(PagePlugin): # pylint: disable-msg=C0103
"""Main configuration form""" """Main configuration form"""
order = 65 order = 65
url = ["/router/setup/pagekite/configure"] def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
js = """ self.register_page("router.setup.pagekite.configure")
<script type="text/javascript">
(function($) {
$('#pagekite-server').attr("disabled", "disabled"); @cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
$('#pagekite-enable').change(function() { form = None
if ($('#pagekite-enable').prop('checked')) { messages = []
$('#pagekite-post-enable-form').show('slow');
} else {
$('#pagekite-post-enable-form').hide('slow');
}
});
})(jQuery); if kwargs:
</script> 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:
form = ConfigureForm(initial=status, prefix='pagekite')
return util.render_template(template='pagekite_configure',
title=_('Configure PageKite'), form=form,
messages=messages)
def get_status(self): def get_status(self):
""" """
@ -144,99 +164,11 @@ $('#pagekite-enable').change(function() {
status['service'] = {} status['service'] = {}
for service in ('http', 'ssh'): for service in ('http', 'ssh'):
output = self._run(['get-service-status', service]) output = self._run(['get-service-status', service])
status['service'][service] = (output.split()[0] == 'enabled') status[service + '_enabled'] = (output.split()[0] == 'enabled')
return status return status
def main(self, *args, **kwargs): def _apply_changes(self, old_status, new_status, messages):
"""Build and return the main content area which is the form"""
del args # unused
status = self.get_status()
if not status:
return _('''
<p>PageKite is not installed, please install it. PageKite comes
pre-installed with {box_name}. On any Debian based system (such as
{box_name}) you may install it using the command 'aptitude install
pagekite'</p>''').format(box_name=cfg.box_name)
try:
message = kwargs['message'].text
except KeyError:
message = None
form = Form(
title="Configure PageKite",
action=cfg.server_dir + "/router/setup/pagekite/configure/",
name="configure_pagekite_form", message=message)
form.checkbox(_("Enable PageKite"), name="pagekite_enable",
id="pagekite-enable", checked=status['enabled'])
show_form = "block" if status['enabled'] else "none"
form.html('''
<div id='pagekite-post-enable-form'
style='display: {show_form}'>'''.format(show_form=show_form))
form.html(_("<h3>PageKite Account</h3>"))
form.text_input(_("Server"), name="pagekite_server",
id="pagekite-server", value="pagekite.net")
form.text_input(_("Kite name"), name="pagekite_kite_name",
id="pagekite-kite-name", value=status['kite_name'])
form.text_input(_("Kite secret"), name="pagekite_kite_secret",
id="pagekite-kite-secret", value=status['kite_secret'])
form.html(_("<h3>Services</h3>"))
form.checkbox(_("Web Server (HTTP)"), name="pagekite_http_enable",
id="pagekite-http-enable",
checked=status['service']['http'])
form.checkbox(_("Secure Shell (SSH)"), name="pagekite_ssh_enable",
id="pagekite-ssh-enable",
checked=status['service']['ssh'])
form.html("</div>") # pagekite-post-enable-form
form.submit(_("Update setup"))
return form.render()
def process_form(self, **kwargs):
"""Handle form submission"""
status = self.get_status()
message, new_status = self.validate_form(**kwargs)
if not message.text:
self.apply_changes(status, new_status, message)
return message
@staticmethod
def validate_form(**kwargs):
"""Check whether all the input form values are correct"""
new_status = {}
message = util.Message()
domain_name_re = r'^[\w-]{1,63}(\.[\w-]{1,63})*$'
pagekite_kite_name = kwargs.get('pagekite_kite_name', '').strip()
if not re.match(domain_name_re, pagekite_kite_name):
message.add(_('Invalid kite name'))
else:
new_status['kite_name'] = pagekite_kite_name
pagekite_kite_secret = kwargs.get('pagekite_kite_secret', '').strip()
if not pagekite_kite_secret:
message.add(_('Invalid kite secret'))
else:
new_status['kite_secret'] = pagekite_kite_secret
new_status['enabled'] = (kwargs.get('pagekite_enable') == 'on')
new_status['service'] = {
'http': (kwargs.get('pagekite_http_enable') == 'on'),
'ssh': (kwargs.get('pagekite_ssh_enable') == 'on')
}
return message, new_status
def apply_changes(self, old_status, new_status, message):
"""Apply the changes to PageKite configuration""" """Apply the changes to PageKite configuration"""
cfg.log.info('New status is - %s' % new_status) cfg.log.info('New status is - %s' % new_status)
@ -246,27 +178,29 @@ pagekite'</p>''').format(box_name=cfg.box_name)
if old_status['enabled'] != new_status['enabled']: if old_status['enabled'] != new_status['enabled']:
if new_status['enabled']: if new_status['enabled']:
self._run(['set-status', 'enable']) self._run(['set-status', 'enable'])
message.add(_('PageKite enabled')) messages.append(('success', _('PageKite enabled')))
else: else:
self._run(['set-status', 'disable']) self._run(['set-status', 'disable'])
message.add(_('PageKite disabled')) messages.append(('success', _('PageKite disabled')))
if old_status['kite_name'] != new_status['kite_name'] or \ if old_status['kite_name'] != new_status['kite_name'] or \
old_status['kite_secret'] != new_status['kite_secret']: old_status['kite_secret'] != new_status['kite_secret']:
self._run(['set-kite', '--kite-name', new_status['kite_name'], self._run(['set-kite', '--kite-name', new_status['kite_name'],
'--kite-secret', new_status['kite_secret']]) '--kite-secret', new_status['kite_secret']])
message.add(_('Kite details set')) messages.append(('success', _('Kite details set')))
for service, old_value in old_status['service'].items(): for service in ['http', 'ssh']:
if old_value != new_status['service'][service]: if old_status[service + '_enabled'] != \
if new_status['service'][service]: new_status[service + '_enabled']:
if new_status[service + '_enabled']:
self._run(['set-service-status', service, 'enable']) self._run(['set-service-status', service, 'enable'])
message.add(_('Service enabled: {service}') messages.append(('success', _('Service enabled: {service}')
.format(service=service)) .format(service=service)))
else: else:
self._run(['set-service-status', service, 'disable']) self._run(['set-service-status', service, 'disable'])
message.add(_('Service disabled: {service}') messages.append(('success',
.format(service=service)) _('Service disabled: {service}')
.format(service=service)))
if old_status != new_status: if old_status != new_status:
self._run(['start']) self._run(['start'])

View File

@ -1,154 +1,150 @@
from urlparse import urlparse import cherrypy
import os, cherrypy from django import forms
from gettext import gettext as _ from gettext import gettext as _
from plugin_mount import PagePlugin, PluginMount, FormPlugin from plugin_mount import PagePlugin
from modules.auth import require from modules.auth import require
from forms import Form
from util import *
import cfg import cfg
import util
class Router(PagePlugin):
"""Router page"""
order = 9 # order of running init in PagePlugins
class router(PagePlugin):
order = 9 # order of running init in PagePlugins
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.register_page("router") PagePlugin.__init__(self, args, kwargs)
self.menu = cfg.main_menu.add_item("Router", "icon-retweet", "/router", 10)
self.menu.add_item("Wireless", "icon-signal", "/router/wireless", 12)
self.menu.add_item("Firewall", "icon-fire", "/router/firewall", 18)
self.menu.add_item("Hotspot and Mesh", "icon-map-marker", "/router/hotspot")
self.menu.add_item("Info", "icon-list-alt", "/router/info", 100)
self.register_page('router')
self.menu = cfg.main_menu.add_item('Router', 'icon-retweet', '/router',
10)
self.menu.add_item('Wireless', 'icon-signal', '/router/wireless', 12)
self.menu.add_item('Firewall', 'icon-fire', '/router/firewall', 18)
self.menu.add_item('Hotspot and Mesh', 'icon-map-marker',
'/router/hotspot')
self.menu.add_item('Info', 'icon-list-alt', '/router/info', 100)
@staticmethod
@cherrypy.expose @cherrypy.expose
def index(self): def index():
"""This isn't an internal redirect, because we need the url to """This isn't an internal redirect, because we need the url to
reflect that we've moved down into the submenu hierarchy. reflect that we've moved down into the submenu hierarchy.
Otherwise, it's hard to know which menu portion to make active Otherwise, it's hard to know which menu portion to make active
or expand or contract.""" or expand or contract."""
raise cherrypy.HTTPRedirect(cfg.server_dir + '/router/setup') raise cherrypy.HTTPRedirect(cfg.server_dir + '/router/setup')
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def wireless(self): def wireless():
return self.fill_template(title="Wireless", main="<p>wireless setup: essid, etc.</p>") """Serve the wireless page"""
return util.render_template(title="Wireless",
main="<p>wireless setup: essid, etc.</p>")
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def firewall(self): def firewall():
return self.fill_template(title="Firewall", main="<p>Iptables twiddling.</p>") """Serve the firewall page"""
return util.render_template(title="Firewall",
main="<p>Iptables twiddling.</p>")
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def hotspot(self): def hotspot():
return self.fill_template(title="Hotspot and Mesh", main="<p>connection sharing setup.</p>") """Serve the hotspot page"""
return util.render_template(title="Hotspot and Mesh",
main="<p>connection sharing setup.</p>")
class WANForm(forms.Form): # pylint: disable-msg=W0232
"""WAN setup form"""
connection_type = forms.ChoiceField(
label=_('Connection Type'),
choices=[('dhcp', _('DHCP')), ('static_ip', _('Static IP'))])
class setup(PagePlugin): wan_ip = forms.IPAddressField(label=_('WAN IP Address'), required=False)
subnet_mask = forms.IPAddressField(label=_('Subnet Mask'), required=False)
dns_1 = forms.IPAddressField(label=_('Static DNS 1'), required=False)
dns_2 = forms.IPAddressField(label=_('Static DNS 2'), required=False)
dns_3 = forms.IPAddressField(label=_('Static DNS 3'), required=False)
class Setup(PagePlugin):
"""Router setup page"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.register_page("router.setup") PagePlugin.__init__(self, args, kwargs)
self.menu = cfg.html_root.router.menu.add_item("General Setup", "icon-cog", "/router/setup", 10)
self.menu.add_item("Dynamic DNS", "icon-flag", "/router/setup/ddns", 20) self.register_page('router.setup')
self.menu.add_item("MAC Address Clone", "icon-barcode", "/router/setup/mac_address", 30)
self.menu = cfg.html_root.router.menu.add_item(
'General Setup', 'icon-cog', '/router/setup', 10)
self.menu.add_item('Dynamic DNS', 'icon-flag', '/router/setup/ddns',
20)
self.menu.add_item('MAC Address Clone', 'icon-barcode',
'/router/setup/mac_address', 30)
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self, **kwargs):
parts = self.forms('/router/setup') """Return the setup page"""
parts['title'] = "General Router Setup" status = self.get_status()
parts['sidebar_right']="""<strong>Introduction</strong><p>Your %s is a replacement for your
wireless router. By default, it should do everything your usual
router does. With the addition of some extra modules, its abilities
can rival those of high-end routers costing hundreds of dollars.</p>
""" % cfg.box_name + parts['sidebar_right']
if not cfg.users.expert():
parts['main'] += """<p>In basic mode, you don't need to do any
router setup before you can go online. Just plug your
%(product)s in to your cable or DSL modem and the router
will try to get you on the internet using DHCP.</p>
<p>If that fails, you might need to resort to the expert form = None
options. Enable expert mode in the "%(product)s System / messages = []
Configure" menu.</p>""" % {'product':cfg.box_name}
if kwargs:
form = WANForm(kwargs, prefix='router')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = WANForm(initial=status, prefix='router')
else: else:
parts['main'] += "<p>router name, domain name, router IP, dhcp</p>" form = WANForm(initial=status, prefix='router')
return self.fill_template(**parts)
return util.render_template(template='router_setup',
title=_('General Router Setup'),
form=form, messages=messages)
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def ddns(self): def ddns():
return self.fill_template(title="Dynamic DNS", main="<p>Masquerade setup</p>") """Return the DDNS page"""
return util.render_template(title="Dynamic DNS",
main="<p>Masquerade setup</p>")
@staticmethod
@cherrypy.expose @cherrypy.expose
@require() @require()
def mac_address(self): def mac_address():
return self.fill_template(title="MAC Address Cloning", """Return the MAC address page"""
main="<p>Your router can pretend to have a different MAC address on any interface.</p>") return util.render_template(
title="MAC Address Cloning",
main="<p>Your router can pretend to have a different MAC address \
on any interface.</p>")
@staticmethod
def get_status():
"""Return the current status"""
store = util.filedict_con(cfg.store_file, 'router')
return {'connection_type': store.get('connection_type', 'dhcp')}
class wan(FormPlugin, PagePlugin): @staticmethod
url = ["/router/setup"] def _apply_changes(old_status, new_status, messages):
order = 10 """Apply the changes"""
print 'Apply changes - %s, %s', old_status, new_status
if old_status['connection_type'] == new_status['connection_type']:
return
js = """ store = util.filedict_con(cfg.store_file, 'router')
<script type="text/javascript"> store['connection_type'] = new_status['connection_type']
(function($) {
function hideshow_static() {
var show_or_hide = ($('#connect_type').val() == 'Static IP')
$('#static_ip_form').toggle(show_or_hide);
}
$(document).ready(function() {
$('#connect_type').change(hideshow_static);
hideshow_static();
});
})(jQuery);
</script>"""
def sidebar_right(self, *args, **kwargs):
side=''
if cfg.users.expert():
side += """<strong>WAN Connection Type</strong>
<h3>DHCP</h3><p>DHCP allows your router to automatically
connect with the upstream network. If you are unsure what
option to choose, stick with DHCP. It is usually
correct.
<h3>Static IP</h3><p>If you want to setup your connection
manually, you can enter static IP information. This option is
for those who know what they're doing. As such, it is only
available in expert mode.</p>"""
return side
def main(self, wan_ip0=0, wan_ip1=0, wan_ip2=0, wan_ip3=0,
subnet0=0, subnet1=0, subnet2=0, subnet3=0,
gateway0=0, gateway1=0, gateway2=0, gateway3=0,
dns10=0, dns11=0, dns12=0, dns13=0,
dns20=0, dns21=0, dns22=0, dns23=0,
dns30=0, dns31=0, dns32=0, dns33=0,
message=None, **kwargs):
if not cfg.users.expert():
return ''
store = filedict_con(cfg.store_file, 'router')
defaults = {'connect_type': 'DHCP'}
for key, value in defaults.items():
if not key in kwargs:
try:
kwargs[key] = store[key]
except KeyError:
store[key] = kwargs[key] = value
form = Form(title="WAN Connection",
action=cfg.server_dir + "/router/setup/wan/index",
name="wan_connection_form",
message=message)
form.dropdown('Connection Type', vals=["DHCP", "Static IP"], id="connect_type")
form.html('<div id="static_ip_form">')
form.dotted_quad("WAN IP Address", name="wan_ip", quad=[wan_ip0, wan_ip1, wan_ip2, wan_ip3])
form.dotted_quad("Subnet Mask", name="subnet", quad=[subnet0, subnet1, subnet2, subnet3])
form.dotted_quad("Gateway", name="gateway", quad=[gateway0, gateway1, gateway2, gateway3])
form.dotted_quad("Static DNS 1", name="dns1", quad=[dns10, dns11, dns12, dns13])
form.dotted_quad("Static DNS 2", name="dns2", quad=[dns20, dns21, dns22, dns23])
form.dotted_quad("Static DNS 3", name="dns3", quad=[dns30, dns31, dns32, dns33])
form.html('</div>')
form.submit("Set Wan")
return form.render()
messages.append(('success', _('Connection type set')))
messages.append(('info', _('IP address settings unimplemented')))

View File

@ -0,0 +1,77 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% if pagekite_status = 'not_installed' %}
<p>PageKite is not installed, please install it. PageKite comes
pre-installed with {{ cfg.box_name }}. On any Debian based system
(such as {{ cfg.box_name }}) you may install it using the command
'aptitude install pagekite'</p>
{% else %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{% include 'bootstrapform/field.html' with field=form.enabled %}
<div id='pagekite-post-enabled-form'
style='display: {{ form.enabled.value|yesno:'block,none' }};'>
<h3>PageKite Account</h3>
{% include 'bootstrapform/field.html' with field=form.server %}
{% include 'bootstrapform/field.html' with field=form.kite_name %}
{% include 'bootstrapform/field.html' with field=form.kite_secret %}
<h3>Services</h3>
{% include 'bootstrapform/field.html' with field=form.http_enabled %}
{% include 'bootstrapform/field.html' with field=form.ssh_enabled %}
</div>
<input type="submit" class="btn-primary" value="Update setup"/>
</form>
{% endif %}
{% endblock %}
{% block js_block %}
{{ js|safe }}
<script type="text/javascript">
(function($) {
$('#id_pagekite-enabled').change(function() {
if ($('#id_pagekite-enabled').prop('checked')) {
$('#pagekite-post-enabled-form').show('slow');
} else {
$('#pagekite-post-enabled-form').hide('slow');
}
});
})(jQuery);
</script>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% 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 main_block %}
<p>PageKite is a system for exposing {{ cfg.box_name }} services when
you don't have a direct connection to the Internet. You only need this
service if your {{ cfg.box_name }} services are unreachable from the
rest of the Internet. This includes the following situations: </p>
<ul>
<li>{{ cfg.box_name }} is behind a restricted firewall.</li>
<li>{{ cfg.box_name }} is connected to a (wireless) router which you
don't control.</li>
<li>Your ISP does not provide you an external IP address and instead
provides Internet connection through NAT.</li>
<li>Your ISP does not provide you a static IP address and your IP
address changes evertime you connect to Internet.</li>
<li>Your ISP limits incoming connections.</li>
</ul>
<p>PageKite works around NAT, firewalls and IP-address limitations by
using a combination of tunnels and reverse proxies. Currently,
exposing web server and SSH server are supported. An intermediary
server with direct Internet access is required. Currently, only
pagekite.net server is supported and you will need an account
there. In future, it might be possible to use your buddy's
{{ cfg.box_name }} for this.</p>
<p>
<a class='btn btn-primary btn-lg'
href="{{ basehref }}/router/setup/pagekite/configure">Configure
PageKite</a>
</p>
{% endblock %}

View File

@ -0,0 +1,112 @@
{% 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 main_block %}
{% if cfg.users.expert %}
<h3>WAN Connection</h3>
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{% include 'bootstrapform/field.html' with field=form.connection_type %}
<div id="static_ip_form"
style='display:
{% if form.connection_type.value = 'static_ip' %} block
{% else %} none {% endif %};'>
{% include 'bootstrapform/field.html' with field=form.wan_ip %}
{% include 'bootstrapform/field.html' with field=form.subnet_mask %}
{% include 'bootstrapform/field.html' with field=form.dns_1 %}
{% include 'bootstrapform/field.html' with field=form.dns_2 %}
{% include 'bootstrapform/field.html' with field=form.dns_3 %}
</div>
<input type="submit" class="btn-primary" value="Set WAN"/>
</form>
{% else %}
<p>In basic mode, you don't need to do any router setup before you
can go online. Just plug your {{ cfg.product_name }} in to your
cable or DSL modem and the router will try to get you on the
internet using DHCP.</p>
<p>If that fails, you might need to resort to the expert options.
Enable expert mode in the "{{ cfg.product_name }} / System /
Configure" menu.</p>
{% endif %}
{% endblock %}
{% block sidebar_right_block %}
<h3>Introduction</h3>
<p>Your {{ cfg.box_name }} is a replacement for your wireless
router. By default, it should do everything your usual router
does. With the addition of some extra modules, its abilities can
rival those of high-end routers costing hundreds of dollars.</p>
{% if cfg.users.expert %}
<h3>WAN Connection Type</h3>
<h3>DHCP</h3>
<p>DHCP allows your router to automatically connect with the
upstream network. If you are unsure what option to choose,
stick with DHCP. It is usually correct.</p>
<h3>Static IP</h3>
<p>If you want to setup your connection manually, you can enter
static IP information. This option is for those who know what
they're doing. As such, it is only available in expert
mode.</p>
{% endif %}
{% endblock %}
{% block js_block %}
{{ js|safe }}
<script type="text/javascript">
(function($) {
function hideshow_static() {
var value = $('#id_router-connection_type').val();
var show_or_hide = (value == 'static_ip');
$('#static_ip_form').toggle(show_or_hide);
}
$(document).ready(function() {
$('#id_router-connection_type').change(hideshow_static);
hideshow_static();
});
})(jQuery);
</script>
{% endblock %}

View File

@ -2,6 +2,8 @@ import cherrypy
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import cfg import cfg
import util
class Services(PagePlugin): class Services(PagePlugin):
order = 9 # order of running init in PagePlugins order = 9 # order of running init in PagePlugins
@ -18,9 +20,4 @@ class Services(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def openid(self): def openid(self):
return self.fill_template(title="Open ID", main='', sidebar_right=""" return util.render_template(template='openid', title="Open ID")
<strong>One Login for Every Site</strong><p>Your %s is also an OpenID
machine. It can generate credentials that allow you to log in to many
websites without the need to remember or enter a separate username and
password at each one.</p>
""" % cfg.box_name)

View File

@ -0,0 +1,29 @@
{% 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 main_block %}
<p class='lead'>One Login for Every Site</p>
<p>Your {{ cfg.box_name }} is also an OpenID machine. It can generate
credentials that allow you to log in to many websites without the need
to remember or enter a separate username and password at each one.</p>
{% endblock %}

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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Update setup"/>
</form>
{% endblock %}

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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Register XMPP Account"/>
</form>
{% endblock %}

View File

@ -1,20 +1,28 @@
import cherrypy import cherrypy
from django import forms
from gettext import gettext as _ from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
import cfg import cfg
from forms import Form
import actions import actions
import service import service
from util import Message import util
class xmpp(PagePlugin):
SIDE_MENU = {'title': _('XMPP'),
'items': [{'url': '/services/xmpp/configure',
'text': 'Configure XMPP Server'},
{'url': '/services/xmpp/register',
'text': 'Register XMPP Account'}]}
class XMPP(PagePlugin):
"""XMPP Page"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("services.xmpp") self.register_page('services.xmpp')
self.register_page("services.xmpp.configure") cfg.html_root.services.menu.add_item('XMPP', 'icon-comment',
self.register_page("services.xmpp.register") '/services/xmpp', 40)
cfg.html_root.services.menu.add_item("XMPP", "icon-comment", "/services/xmpp", 40)
self.client_service = service.Service( self.client_service = service.Service(
'xmpp-client', _('Chat Server - client connections'), 'xmpp-client', _('Chat Server - client connections'),
@ -26,97 +34,156 @@ class xmpp(PagePlugin):
'xmpp-bosh', _('Chat Server - web interface'), is_external=True, 'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=True) enabled=True)
@staticmethod
@cherrypy.expose
@require()
def index(**kwargs):
"""Serve XMPP page"""
del kwargs # Unused
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)
class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
"""Configuration form"""
inband_enabled = forms.BooleanField(
label=_('Allow In-Band Registration'), required=False,
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())
class Configure(PagePlugin):
"""Configuration page"""
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("services.xmpp.configure")
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self, **kwargs): def index(self, **kwargs):
main = "<p>XMPP Server Accounts and Configuration</p>" """Serve the configuration form"""
sidebar_right = '<strong><a href="'+cfg.server_dir+'/services/xmpp/configure">Configure XMPP Server</a></strong><br />' status = self.get_status()
sidebar_right = sidebar_right + '<strong><a href="'+cfg.server_dir+'/services/xmpp/register">Register XMPP Account</a></strong>'
return self.fill_template(title="XMPP Server", main=main, sidebar_right=sidebar_right)
class configure(FormPlugin, PagePlugin): form = None
url = ["/services/xmpp/configure"] messages = []
sidebar_left = '' if kwargs:
sidebar_right = _("<strong>Configure XMPP Server</strong>") 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:
form = ConfigureForm(initial=status, prefix='xmpp')
def main(self, xmpp_inband_enable=False, message=None, *args, **kwargs): sidebar_right = util.render_template(template='menu_block',
output, error = actions.superuser_run("xmpp-setup", 'status') menu=SIDE_MENU)
return util.render_template(template='xmpp_configure',
title=_('Configure XMPP Server'),
form=form, messages=messages,
sidebar_right=sidebar_right)
@staticmethod
def get_status():
"""Return the current status"""
output, error = actions.run('xmpp-setup', 'status')
if error: if error:
raise Exception("something is wrong: " + error) raise Exception('Error getting status: %s' % error)
if "inband_enable" in output.split():
xmpp_inband_enable = True
form = Form(title="Configure XMPP Server", return {'inband_enabled': 'inband_enable' in output.split()}
action=cfg.server_dir + "/services/xmpp/configure/index",
name="configure_xmpp_form",
message=message)
form.checkbox(_("Allow In-Band Registration"), name="xmpp_inband_enable",
id="xmpp_inband_enable", checked=xmpp_inband_enable)
# hidden field is needed because checkbox doesn't post if not checked
form.hidden(name="submitted", value="True")
form.html(_("<p>When enabled, anyone who can reach this server will be allowed to register an account through an XMPP client.</p>"))
form.submit(_("Update setup"))
return form.render()
def process_form(self, **kwargs): @staticmethod
checkedinfo = { def sidebar_right(**kwargs):
'inband_enable' : False, """Return rendered string for sidebar on the right"""
} del kwargs # Unused
opts = [] return util.render_template(template='menu_block', menu=SIDE_MENU)
for k in kwargs.keys():
if 'on' == kwargs[k]:
shortk = k.split("xmpp_").pop()
checkedinfo[shortk] = True
for key in checkedinfo.keys(): @staticmethod
if checkedinfo[key]: def _apply_changes(old_status, new_status, messages):
opts.append(key) """Apply the form changes"""
else: cfg.log.info('Status - %s, %s' % (old_status, new_status))
opts.append('no'+key)
actions.run("xmpp-setup", opts)
main = self.main(checkedinfo['inband_enable']) if old_status['inband_enabled'] == new_status['inband_enabled']:
return self.fill_template(title="XMPP Server Configuration", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) messages.append(('info', _('Setting unchanged')))
return
class register(FormPlugin, PagePlugin): if new_status['inband_enabled']:
url = ["/services/xmpp/register"] messages.append(('success', _('Inband registration enabled')))
option = 'inband_enable'
else:
messages.append(('success', _('Inband registration disabled')))
option = 'noinband_enable'
sidebar_left = '' cfg.log.info('Option - %s' % option)
sidebar_right = _("<strong>Register XMPP Account</strong>")
def main(self, username='', message=None, *args, **kwargs): _output, error = actions.superuser_run('xmpp-setup', [option])
form = Form(title="Register XMPP Account", del _output
action=cfg.server_dir + "/services/xmpp/register/index", if error:
name="register_xmpp_form", raise Exception('Error running command - %s' % error)
message=message)
form.text_input(_("Username"), name="username", value=username)
form.text_input(_("Password"), name="password", type="password")
form.submit(label=_("Register XMPP Account"), name="register")
return form.render()
def process_form(self, username=None, password=None, **kwargs):
msg = Message()
if not username: msg.add = _("Must specify a username!") class RegisterForm(forms.Form): # pylint: disable-msg=W0232
if not password: msg.add = _("Must specify a password!") """Configuration form"""
username = forms.CharField(label=_('Username'))
if username and password: password = forms.CharField(
output, error = actions.superuser_run( label=_('Password'), widget=forms.PasswordInput())
"xmpp-register", [username, password])
if error:
raise Exception("something is wrong: " + error)
if "successfully registered" in output:
msg.add = _("Registered account for %s." % username)
else:
msg.add = _("Failed to register account for %s: %s" % (username, output))
cfg.log(msg.text) class Register(PagePlugin):
main = self.main(username, msg=msg.text) """User registration page"""
return self.fill_template(
title="XMPP Server Configuration", def __init__(self, *args, **kwargs):
main=main, PagePlugin.__init__(self, *args, **kwargs)
sidebar_left=self.sidebar_left, self.register_page('services.xmpp.register')
sidebar_right=self.sidebar_right)
@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:
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)
@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)
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)))

View File

@ -1,7 +1,10 @@
import cherrypy import cherrypy
from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import cfg import cfg
import util
class FileExplorer(PagePlugin): class FileExplorer(PagePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -12,24 +15,5 @@ class FileExplorer(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
main = """ return util.render_template(template='file_explorer',
<p>File explorer for users that also have shell accounts.</p> <p>Until title=_('File Explorer'))
that is written (and it will be a while), we should install <a
href="http://www.mollify.org/demo.php">mollify</a> or <a
href="http://www.ajaxplorer.info/wordpress/demo/">ajaxplorer</a>, but
of which seem to have some support for playing media files in the
browser (as opposed to forcing users to download and play them
locally). The downsides to third-party explorers are: they're don't
fit within our theme system, they require separate login, and they're
written in php, which will make integrating them hard.</p>
<p>There are, of course, many other options for php-based file
explorers. These were the ones I saw that might do built-in media
players.</p>
<p>For python-friendly options, check out <a
href="http://blogfreakz.com/jquery/web-based-filemanager/">FileManager</a>.
It appears to be mostly javascript with some bindings to make it
python-friendly.</p>
"""
return self.fill_template(title="File Explorer", main=main, sidebar_right='')

View File

@ -3,6 +3,8 @@ from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import cfg import cfg
import util
class Sharing(PagePlugin): class Sharing(PagePlugin):
order = 9 # order of running init in PagePlugins order = 9 # order of running init in PagePlugins
@ -24,13 +26,9 @@ class Sharing(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def files(self): def files(self):
return self.fill_template(title="File Server", main='', sidebar_right=_(""" return util.render_template(template='sharing',
<strong>Freedom NAS</strong><p> The %s can make your spare hard drives accessible to your title=_('File Server'))
local network, thus acting as a NAS server. We currently support
sharing files via NFS and SMB.
TODO: this is not true. We currently support no sharing.</p>
""" % cfg.box_name))
#TODO: move PrinterSharing to another file, as it should be an optional module (most people don't care about printer sharing) #TODO: move PrinterSharing to another file, as it should be an optional module (most people don't care about printer sharing)
class PrinterSharing(PagePlugin): class PrinterSharing(PagePlugin):
@ -42,12 +40,5 @@ class PrinterSharing(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
main = """ return util.render_template(template='sharing_printer',
<p>TODO: Setup and install SAMBA</p> title=_('Printer Sharing'))
<p>TODO: Setup and install CUPS</p>
"""
return self.fill_template(title="Printer Sharing", main=main, sidebar_right="""
<strong>Share Your Printer</strong><p> The %s can share your printer via Samba and CUPS.</p>
""" % cfg.box_name)

View File

@ -0,0 +1,42 @@
{% 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 main_block %}
<p>File explorer for users that also have shell accounts.</p> <p>Until
that is written (and it will be a while), we should
install <a href="http://www.mollify.org/demo.php">mollify</a>
or <a href="http://www.ajaxplorer.info/wordpress/demo/">ajaxplorer</a>,
but of which seem to have some support for playing media files in the
browser (as opposed to forcing users to download and play them
locally). The downsides to third-party explorers are: they're don't
fit within our theme system, they require separate login, and they're
written in php, which will make integrating them hard.</p>
<p>There are, of course, many other options for php-based file
explorers. These were the ones I saw that might do built-in media
players.</p>
<p>For python-friendly options, check out <a
href="http://blogfreakz.com/jquery/web-based-filemanager/">FileManager</a>.
It appears to be mostly javascript with some bindings to make it
python-friendly.</p>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% 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 sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Freedom NAS</h3>
<p> The {{ cfg.box_name }} can make your spare hard drives
accessible to your local network, thus acting as a NAS server. We
currently support sharing files via NFS and SMB.</p>
<p>TODO: this is not true. We currently support no sharing.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% 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 main_block %}
<p>TODO: Setup and install SAMBA</p>
<p>TODO: Setup and install CUPS</p>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Share Your Printer</h3>
<p> The {{ cfg.box_name }} can share your printer via Samba and
CUPS.</p>
</div>
{% endblock %}

View File

@ -20,6 +20,8 @@ Plinth module for configuring timezone, hostname etc.
""" """
import cherrypy import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _ from gettext import gettext as _
try: try:
import simplejson as json import simplejson as json
@ -30,58 +32,124 @@ import socket
import actions import actions
import cfg import cfg
from forms import Form
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
import util import util
class Config(PagePlugin): def get_hostname():
"""Return the hostname"""
return socket.gethostname()
class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField"""
def clean(self, value):
"""Clean and validate the field value"""
if value:
value = value.strip()
return super(TrimmedCharField, self).clean(value)
class ConfigurationForm(forms.Form):
"""Main system configuration form"""
time_zone = forms.ChoiceField(
label=_('Time Zone'),
help_text=_('Set your timezone to get accurate timestamps. \
This information will be used to set your systemwide timezone'))
# We're more conservative than RFC 952 and RFC 1123
hostname = TrimmedCharField(
label=_('Hostname'),
help_text=_('Your hostname is the local name by which other machines \
on your LAN can reach you. It must be alphanumeric, start with an alphabet \
and must not be greater than 63 characters in length.'),
validators=[
validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$',
_('Invalid hostname'))])
def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1101, W0233
forms.Form.__init__(self, *args, **kwargs)
self.fields['time_zone'].choices = [(zone, zone)
for zone in self.get_time_zones()]
@staticmethod
def get_time_zones():
"""Return list of available time zones"""
# XXX: Get rid of a custom file and read the values from /usr
module_file = __file__
if module_file.endswith(".pyc"):
module_file = module_file[:-1]
module_dir = os.path.dirname(os.path.realpath(module_file))
time_zones_file = os.path.join(module_dir, 'time_zones')
return json.loads(util.slurp(time_zones_file))
class Configuration(PagePlugin):
"""System configuration page""" """System configuration page"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
del args # Unused del args # Unused
del kwargs # Unused del kwargs # Unused
self.register_page("sys.config") self.register_page('sys.config')
self.menu = cfg.html_root.sys.menu.add_item(_('Configure'), 'icon-cog',
'/sys/config', 10)
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self, **kwargs):
"""Serve configuration page""" """Serve the configuration form"""
parts = self.forms('/sys/config') status = self.get_status()
parts['title'] = _("Configure this {box_name}") \
.format(box_name=cfg.box_name)
return self.fill_template(**parts) # pylint: disable-msg=W0142 form = None
messages = []
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')
else:
form = ConfigurationForm(initial=status, prefix='configuration')
def valid_hostname(name): return util.render_template(template='config',
""" title=_('General Configuration'),
Return '' if name is a valid hostname by our standards (not just form=form, messages=messages)
by RFC 952 and RFC 1123. We're more conservative than the
standard. If hostname isn't valid, return message explaining why.
"""
message = ''
if len(name) > 63:
message += "<br />Hostname too long (max is 63 characters)"
if not util.is_alphanumeric(name): @staticmethod
message += "<br />Hostname must be alphanumeric" def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
'time_zone': util.slurp('/etc/timezone').rstrip()}
if not name[0] in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": @staticmethod
message += "<br />Hostname must start with a letter" 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')))
return message if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change',
[new_status['time_zone']])
def get_hostname(): del output # Unused
"""Return the current hostname of the system""" if error:
return socket.gethostname() messages.append(('error',
_('Error setting time zone - %s') % error))
else:
def get_time_zone(): messages.append(('success', _('Time zone set')))
"""Return currently set system's timezone""" else:
return util.slurp('/etc/timezone').rstrip() messages.append(('info', _('Time zone is unchanged')))
def set_hostname(hostname): def set_hostname(hostname):
@ -101,82 +169,3 @@ def set_hostname(hostname):
except OSError as exception: except OSError as exception:
raise cherrypy.HTTPError(500, raise cherrypy.HTTPError(500,
'Updating hostname failed: %s' % exception) 'Updating hostname failed: %s' % exception)
class general(FormPlugin, PagePlugin):
"""Form to update hostname and time zone"""
url = ["/sys/config"]
order = 30
@staticmethod
def help(*args, **kwargs):
"""Build and return the help content area"""
del args # Unused
del kwargs # Unused
return _('''
<p>Set your timezone to get accurate timestamps. {product} will use
this information to set your {box}'s systemwide timezone.</p>''').format(
product=cfg.product_name, box=cfg.box_name)
def main(self, message='', time_zone=None, **kwargs):
"""Build and return the main content area which is the form"""
del kwargs # Unused
if not cfg.users.expert():
return _('''
<p>Only members of the expert group are allowed to see and modify the system
setup.</p>''')
if not time_zone:
time_zone = get_time_zone()
# Get the list of supported timezones and the index in that
# list of the current one
module_file = __file__
if module_file.endswith(".pyc"):
module_file = module_file[:-1]
module_dir = os.path.dirname(os.path.realpath(module_file))
time_zones_file = os.path.join(module_dir, 'time_zones')
time_zones = json.loads(util.slurp(time_zones_file))
try:
time_zone_id = time_zones.index(time_zone)
except ValueError:
cfg.log.critical("Unknown Time Zone: %s" % time_zone)
raise cherrypy.HTTPError(500, "Unknown Time Zone: %s" % time_zone)
# And now, the form.
form = Form(title=_("General Config"),
action=cfg.server_dir + "/sys/config/general/index",
name="config_general_form",
message=message)
form.html(self.help())
form.dropdown(_("Time Zone"), name="time_zone", vals=time_zones,
select=time_zone_id)
form.html('''
<p>Your hostname is the local name by which other machines on your LAN
can reach you.</p>''')
form.text_input('Hostname', name='hostname', value=get_hostname())
form.submit(_("Submit"))
return form.render()
@staticmethod
def process_form(time_zone='', hostname='', *args, **kwargs):
"""Handle form submission"""
del args # Unused
del kwargs # Unused
message = ''
if hostname != get_hostname():
msg = valid_hostname(hostname)
if msg == '':
set_hostname(hostname)
else:
message += msg
time_zone = time_zone.strip()
if time_zone != get_time_zone():
cfg.log.info("Setting timezone to %s" % time_zone)
actions.superuser_run("timezone-change", [time_zone])
return message or "Settings updated."

View File

@ -19,12 +19,14 @@
Plinth module for running diagnostics Plinth module for running diagnostics
""" """
import os, cherrypy import cherrypy
from gettext import gettext as _ from gettext import gettext as _
from auth import require from auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import actions import actions
import cfg import cfg
import util
class diagnostics(PagePlugin): class diagnostics(PagePlugin):
order = 30 order = 30
@ -36,13 +38,8 @@ class diagnostics(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
main = _(""" return util.render_template(template='diagnostics',
<p>The system diagnostic test will run a number of checks on your title=_('System Diagnostics'))
system to confirm that network services are running and configured
properly. It may take a minute to complete.</p>
""")
main += '<p><a class="btn btn-primary btn-large" href="'+cfg.server_dir+'/sys/diagnostics/test">Run diagnostic test &raquo;</a></p>'
return self.fill_template(title=_("System Diagnostics"), main=main)
class test(PagePlugin): class test(PagePlugin):
order = 31 order = 31
@ -53,17 +50,8 @@ class test(PagePlugin):
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self):
main = ''
output, error = actions.superuser_run("diagnostic-test") output, error = actions.superuser_run("diagnostic-test")
return util.render_template(template='diagnostics_test',
if error: title=_('Diagnostic Test'),
main += _("The diagnostic test encountered an error:</br>") diagnostics_output=output,
for line in error.split('\n'): diagnostics_error=error)
main += line + "</br>"
if output:
main += _("Output of diagnostic test:</br>")
for line in output.split('\n'):
main += line + "</br>"
return self.fill_template(title=_("Diagnostic Test"), main=main)

View File

@ -1,73 +1,79 @@
import os
import cherrypy import cherrypy
try: from django import forms
import simplejson as json
except ImportError:
import json
from gettext import gettext as _ from gettext import gettext as _
from filedict import FileDict
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
import cfg import cfg
from forms import Form import util
from model import User
from util import *
class experts(FormPlugin, PagePlugin):
url = ["/sys/config"]
order = 10
def help(self, *args, **kwargs): class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
side = _(#"""<strong>Expert Mode</strong> """Form to configure expert mode"""
"""
<p>The %(box)s can be administered in two modes, 'basic'
and 'expert'. Basic mode hides a lot of features and
configuration options that most users will never need to think
about. Expert mode allows you to get into the details.</p>
<p>Most users can operate the %(box)s by configuring the expert_mode = forms.BooleanField(
limited number of options visible in Basic mode. For the sake label=_('Expert Mode'), required=False)
of simplicity and ease of use, we hid most of %(product)s's
less frequently used options. But if you want more
sophisticated features, you can enable Expert mode, and
%(product)s will present more advanced menu options.</p>
<p>You should be aware that it might be possible to render # XXX: Only present due to issue with submitting empty form
your %(box)s inaccessible via Expert mode options.</p> dummy = forms.CharField(label='Dummy', initial='dummy',
""" % {'box':cfg.box_name, 'product':cfg.product_name}) widget=forms.HiddenInput())
return side
def main(self, expert=None, message='', **kwargs): class Experts(PagePlugin):
"""Note that kwargs contains '':"submit" if this is coming """Expert forms page"""
from a submitted form. If kwargs is empty, it's a fresh form order = 60
with no user input, which means it should just reflect the
state of the stored data."""
if not kwargs and expert == None:
expert = cfg.users.expert()
cfg.log("Expert mode is %s" % expert)
form = Form(title=_("Expert Mode"),
action=cfg.server_dir + "/sys/config/experts",
name="expert_mode_form",
message=message )
form.html(self.help())
form.checkbox(_("Expert Mode"), name="expert", checked=expert)
form.submit(_("Submit"))
return form.render()
def process_form(self, expert='', *args, **kwargs): def __init__(self, *args, **kwargs):
user = cfg.users.get() PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.config.expert')
message = 'settings unchanged' cfg.html_root.sys.config.menu.add_item(_('Expert mode'), 'icon-cog',
'/sys/config/expert', 10)
if expert: @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:
form = ExpertsForm(initial=status, prefix='experts')
return util.render_template(template='expert_mode',
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'))
user = cfg.users.current()
if new_status['expert_mode']:
if not 'expert' in user['groups']: if not 'expert' in user['groups']:
user['groups'].append('expert') user['groups'].append('expert')
message = "enabled" message = ('success', _('Expert mode enabled'))
else: else:
if 'expert' in user['groups']: if 'expert' in user['groups']:
user['groups'].remove('expert') user['groups'].remove('expert')
message = "disabled" message = ('success', _('Expert mode disabled'))
cfg.users.set(user) cfg.users.set(user['username'], user)
return "Expert mode %s." % message messages.append(message)

View File

@ -27,6 +27,7 @@ import cfg
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin from plugin_mount import PagePlugin
import service as service_module import service as service_module
import util
class Firewall(PagePlugin): class Firewall(PagePlugin):
@ -48,89 +49,24 @@ class Firewall(PagePlugin):
"""Serve introcution page""" """Serve introcution page"""
del kwargs # Unused del kwargs # Unused
# XXX: Use templates here instead of generating HTML
main = _('''
<p>Firewall is a network security system that controls the incoming
and outgoing network traffic on your {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>
''').format(box_name=cfg.box_name)
if not self.get_installed_status(): if not self.get_installed_status():
status = _(''' return util.render_template(template='firewall',
<p>Firewall is not installed. Please install it. Firewall comes title=_("Firewall"),
pre-installed with {box_name}. On any Debian based system (such as firewall_status='not_installed')
{box_name}) you may install it using the command 'aptitude install
firewalld'</p>''').format(box_name=cfg.box_name)
return self.fill_template(title=_("Firewall"), main=main + status)
if not self.get_enabled_status(): if not self.get_enabled_status():
status = _(''' return util.render_template(template='firewall',
<p>Firewall daemon is not running. Please run it. Firewall comes title=_("Firewall"),
enabled by default on {box_name}. On any Debian based system (such as firewall_status='not_running')
{box_name}) you may run it using the command 'service firewalld start'
or in case of system with systemd 'systemctl start firewalld'</p>
''').format(box_name=cfg.box_name)
return self.fill_template(title=_("Firewall"), main=main + status) internal_enabled_services = self.get_enabled_services(zone='internal')
external_enabled_services = self.get_enabled_services(zone='external')
internal_enabled_sevices = self.get_enabled_services(zone='internal') return util.render_template(
external_enabled_sevices = self.get_enabled_services(zone='external') template='firewall', title=_('Firewall'),
services=service_module.SERVICES.values(),
services_info = '<ul>' internal_enabled_services=internal_enabled_services,
for service in service_module.SERVICES.values(): external_enabled_services=external_enabled_services)
if service.is_enabled():
service_text = _('Enabled')
service_class = 'firewall-permitted'
else:
service_text = _('Disabled')
service_class = 'firewall-blocked'
port_info = []
for port in service.ports:
if port in internal_enabled_sevices and \
port in external_enabled_sevices:
text = _('Permitted')
css_class = 'firewall-permitted'
elif port in internal_enabled_sevices:
text = _('Permitted (internal only)')
css_class = 'firewall-permitted'
elif port in external_enabled_sevices:
text = _('Permitted (external only)')
css_class = 'firewall-permitted'
else:
text = _('Blocked')
css_class = 'firewall-blocked'
info = _('''
<li>{port}: <span class={css_class}>{text}</span></li>
''').format(port=port, css_class=css_class, text=text)
port_info.append(info)
port_info = '<ul>{port_info}</ul>'.format(
port_info=''.join(port_info))
services_info += _('''
<li>
{name}: <span class={service_class}>{service_text}</span>
{port_info}
</li>
''').format(
name=service.name, service_class=service_class,
service_text=service_text, port_info=port_info)
services_info += '</ul>'
footnote = '''
<p><em>The operation of the firewall is automatic. When you enable a
service it is automatically permitted in the firewall and you disable
a service is automatically disabled in the firewall.</em></p>'''
return self.fill_template(title=_("Firewall"), main=main +
services_info + footnote)
def get_installed_status(self): def get_installed_status(self):
"""Return whether firewall is installed""" """Return whether firewall is installed"""

View File

@ -1,83 +1,133 @@
import cherrypy import cherrypy
from django import forms
from gettext import gettext as _ from gettext import gettext as _
from auth import require from auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
from forms import Form
import actions import actions
import cfg import cfg
from util import Message import util
class Packages(PagePlugin, FormPlugin):
def get_modules_available():
"""Return list of all modules"""
output, error = actions.run('module-manager', ['list-available'])
if error:
raise Exception('Error getting modules: %s' % error)
return output.split()
def get_modules_enabled():
"""Return list of all modules"""
output, error = actions.run('module-manager', ['list-enabled'])
if error:
raise Exception('Error getting enabled modules - %s' % error)
return output.split()
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)
modules_available = get_modules_available()
for module in modules_available:
label = _('Enable {module}').format(module=module)
self.fields[module + '_enabled'] = forms.BooleanField(
label=label, required=False)
class Packages(PagePlugin):
"""Package page"""
order = 20 order = 20
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.packages") self.register_page('sys.packages')
cfg.html_root.sys.menu.add_item("Package Manager", "icon-gift", "/sys/packages", 20)
cfg.html_root.sys.menu.add_item('Package Manager', 'icon-gift',
'/sys/packages', 20)
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
output, error = actions.run("module-manager", ["list-available"]) """Serve the form"""
if error: del args # Unused
raise Exception("something is wrong: " + error)
modules_available = output.split()
output, error = actions.run("module-manager", \
["list-enabled", cfg.python_root])
if error:
raise Exception("something is wrong: " + error)
modules_enabled = output.split()
if 'submitted' in kwargs: status = self.get_status()
del kwargs['submitted']
modules_selected = map(lambda x: x.split("_")[0], kwargs.keys()) form = None
for module in modules_available: messages = []
if module in modules_enabled:
if module not in modules_selected: if kwargs:
output, error = actions.superuser_run(\ form = PackagesForm(kwargs, prefix='packages')
"module-manager",\ # pylint: disable-msg=E1101
["disable", cfg.python_root, module]) if form.is_valid():
# TODO: need a smoother way for plinth self._apply_changes(status, form.cleaned_data, messages)
# to unload the module status = self.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)
@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}
@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
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: else:
if module in modules_selected: messages.append(
output, error = actions.superuser_run(\ ('success', _('Module enabled - {module}').format(
"module-manager",\ module=module)))
["enable", cfg.python_root, module]) else:
# TODO: need to get plinth to load output, error = actions.superuser_run(
# the module we just enabled 'module-manager', ['disable', cfg.python_root, module])
del output # Unused
main = _(""" # TODO: need a smoother way for plinth to unload the
<p>aptitude purge modules</p> # module
<p>aptitude install modules</p> if error:
<p>The modules should depend on the appropriate Debian packages.</p>""") messages.append(
main += self.form(self, Message(output), args, kwargs) ('error',
sidebar_right = _(""" _('Error disabling module - {module}').format(
<strong>Help</strong> module=module)))
<p>On this page, you can add or remove %s plugins to your %s.</p> else:
<p>Plugins are just Debian packages, so Debian's usual package management features should make this job fairly easy.</p>""" % (cfg.product_name, cfg.box_name)) messages.append(
return self.fill_template(title=_("Add/Remove Plugins"), main=main, sidebar_right=sidebar_right) ('success', _('Module disabled - {module}').format(
module=module)))
def form(self, message=None, *args, **kwargs):
output, error = actions.run("module-manager", ["list-available"])
if error:
raise Exception("something is wrong: " + error)
modules_available = output.split()
output, error = actions.run("module-manager",\
["list-enabled", cfg.python_root])
if error:
raise Exception("something is wrong: " + error)
modules_enabled = output.split()
form = Form(title="Manage Plugins",
action=cfg.server_dir + "/sys/packages/index",
name="manage_modules",
message=message)
for module in modules_available:
form.checkbox(_("Enable %s" % module), name="%s_enable" % module, id="%s_enable" % module, checked = True if module in modules_enabled else False)
form.hidden(name="submitted", value="True")
form.html(_("""<p>Enabling a plugin will cause a corresponding page to appear in Plinth.</p>"""))
form.submit(_("Update setup"))
return form.render()

View File

@ -1,17 +1,8 @@
import os
import cherrypy import cherrypy
try:
import simplejson as json
except ImportError:
import json
from gettext import gettext as _ from gettext import gettext as _
from filedict import FileDict from plugin_mount import PagePlugin
from auth import require
from plugin_mount import PagePlugin, FormPlugin
import cfg import cfg
from forms import Form import util
from model import User
from util import *
sys_dir = "modules/installed/sys" sys_dir = "modules/installed/sys"
@ -22,15 +13,9 @@ class Sys(PagePlugin):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys") self.register_page("sys")
self.menu = cfg.main_menu.add_item(_("System"), "icon-cog", "/sys", 100) self.menu = cfg.main_menu.add_item(_("System"), "icon-cog", "/sys", 100)
self.menu.add_item(_("Configure"), "icon-cog", "/sys/config", 10)
self.menu.add_item(_("Users and Groups"), "icon-user", "/sys/users", 15) self.menu.add_item(_("Users and Groups"), "icon-user", "/sys/users", 15)
@cherrypy.expose @cherrypy.expose
def index(self): def index(self):
return self.fill_template(title=_("System Configuration"), main=_(""" return util.render_template(template='system',
<p>In this section, you can control the %(product)s's title=_("System Configuration"))
underlying system, as opposed to its various applications and
services. These options affect the %(product)s at its most
general level. This is where you add/remove users, install
applications, reboot, etc.</p>
""" % {'product':cfg.product_name}))

View File

@ -0,0 +1,45 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% if cfg.users.expert %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Submit"/>
</form>
{% else %}
<p>Only members of the expert group are allowed to see and modify
the system setup.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,31 @@
{% 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 main_block %}
<p>The system diagnostic test will run a number of checks on your
system to confirm that network services are running and configured
properly. It may take a minute to complete.</p>
<p><a class="btn btn-primary btn-large"
href="{{ basehref }}/sys/diagnostics/test">Run diagnostic test
&raquo;</a></p>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% 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 main_block %}
{% if diagnostics_error %}
<p>The diagnostic test encountered an error:<p>
<pre>{{ diagnostics_error }}</pre>
{% endif %}
{% if diagnostics_output %}
<p>Output of diagnostic test:</p>
<pre>{{ diagnostics_output }}</pre>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,51 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<p>The {{ cfg.box_name }} can be administered in two modes, 'basic'
and 'expert'. Basic mode hides a lot of features and configuration
options that most users will never need to think about. Expert mode
allows you to get into the details.</p>
<p>Most users can operate the {{ cfg.box_name }} by configuring the
limited number of options visible in Basic mode. For the sake of
simplicity and ease of use, we hid most of {{ cfg.product_name }}'s
less frequently used options. But if you want more sophisticated
features, you can enable Expert mode, and {{ cfg.product_name }}
will present more advanced menu options.</p>
<p>You should be aware that it might be possible to render your
{{ cfg.box_name }} inaccessible via Expert mode options.</p>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Submit"/>
</form>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% 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 main_block %}
<p>Firewall is a network security system that controls the incoming
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>
{% if firewall_status = 'not_installed' %}
<p>Firewall is not installed. Please install it. Firewall comes
pre-installed with {{ cfg.box_name }}. On any Debian based system
(such as {{ cfg.box_name }}) you may install it using the command 'aptitude
install firewalld'</p>
{% elif firewall_status = 'not_running' %}
<p>Firewall daemon is not running. Please run it. Firewall comes
enabled by default on {{ cfg.box_name }}. On any Debian based system
(such as {{ cfg.box_name }}) you may run it using the command 'service
firewalld start' or in case of a system with systemd 'systemctl start
firewalld'</p>
{% else %}
<ul>
{% for service in services %}
<li>{{ service.name }}:
{% if service.is_enabled %}
<span class='firewall-permitted'>Enabled</span>
{% else %}
<span class='firewall-blocked'>Disabled</span>
{% endif %}
<ul>
{% for port in service.ports %}
<li> {{ port }}:
{% if port in internal_enabled_services and port in external_enabled_services %}
<span class='firewall-permitted'>Permitted</span>
{% elif port in internal_enabled_services %}
<span class='firewall-permitted'>Permitted (internal only)</span>
{% elif port in external_enabled_services %}
<span class='firewall-permitted'>Permitted (external only)</span>
{% else %}
<span class='firewall-blocked'>Blocked</span>
{% endif %}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
<p><em>The operation of the firewall is automatic. When you enable a
service it is automatically permitted in the firewall and you disable
a service is automatically disabled in the firewall.</em></p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,63 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<p>aptitude purge modules</p>
<p>aptitude install modules</p>
<p>The modules should depend on the appropriate Debian packages.</p>
<h2>Manage Plugins</h2>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<p>Enabling a plugin will cause a corresponding page to appear in
Plinth.</p>
<input type="submit" class="btn-primary" value="Update setup"/>
</form>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Help</h3>
<p>On this page, you can add or remove {{ cfg.product_name }}
plugins to your {{ cfg.box_name }}.</p>
<p>Plugins are just Debian packages, so Debian's usual package
management features should make this job fairly easy.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% 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 main_block %}
<p>In this section, you can control the {{ cfg.product_name }}'s
underlying system, as opposed to its various applications and
services. These options affect the {{ cfg.product_name }} at its most
general level. This is where you add/remove users, install
applications, reboot, etc.</p>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Add User"/>
</form>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Add User</h3>
<p>Adding a user via this administrative
interface <strong>might</strong> create a system user. For
example, if you provide a user with ssh access, she will need a
system account. If you don't know what that means, don't worry
about it.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Delete User"/>
</form>
{% endblock %}
{% block sidebar_right_block %}
<div class="well sidebar-nav">
<h3>Edit Users</h3>
<p>Click on a user's name to go to a screen for editing that
user's account.</p>
<h3>Delete Users</h3>
<p>Check the box next to a users' names and then click "Delete
User" to remove users from {{ cfg.product_name }} and the
{{ cfg.box_name }} system.</p>
<p>Deleting users is permanent!</p>
</div>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% if cfg.users.expert %}
{% include 'messages.html' %}
<p>For security reasons, neither WAN Administration nor WAN SSH is
available to the `admin` user account.</p>
<p>TODO: in expert mode, tell user they can ssh in to enable admin
from WAN, do their business, then disable it. It would be good to
enable the option and autodisable it when the ssh connection
dies.</p>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Submit"/>
</form>
{% else %}
<p>This page is available only in expert mode.</p>
{% endif %}
{% endblock %}

View File

@ -1,121 +1,172 @@
import os, cherrypy import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _ from gettext import gettext as _
from auth import require, add_user import auth
from plugin_mount import PagePlugin, FormPlugin from auth import require
from plugin_mount import PagePlugin
import cfg import cfg
from forms import Form
from util import *
from model import User from model import User
import util
class users(PagePlugin):
class Users(PagePlugin):
order = 20 # order of running init in PagePlugins order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs) PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.users") self.register_page("sys.users")
self.register_page("sys.users.add")
self.register_page("sys.users.edit") @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)
class UserAddForm(forms.Form): # pylint: disable-msg=W0232
"""Form to add a new user"""
username = forms.CharField(
label=_('Username'),
help_text=_('Must be lower case alphanumeric and start with \
and alphabet'),
validators=[
validators.RegexValidator(r'^[a-z][a-z0-9]*$',
_('Invalid username'))])
password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput())
full_name = forms.CharField(label=_('Full name'), required=False)
email = forms.EmailField(label=_('Email'), required=False)
class UserAdd(PagePlugin):
"""Add user page"""
order = 30
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.users.add')
@cherrypy.expose @cherrypy.expose
@require() @require()
def index(self): def index(self, **kwargs):
sidebar_right = '<strong><a href="'+cfg.server_dir+'/sys/users/add">Add User</a></strong><br/><strong><a href="'+cfg.server_dir+'/sys/users/edit">Edit Users</a></strong>' """Serve the form"""
return self.fill_template(title="Manage Users and Groups", sidebar_right=sidebar_right) form = None
messages = []
class add(FormPlugin, PagePlugin): if kwargs:
url = ["/sys/users/add"] form = UserAddForm(kwargs, prefix='user')
order = 30 # pylint: disable-msg=E1101
if form.is_valid():
sidebar_left = '' self._add_user(form.cleaned_data, messages)
sidebar_right = _("""<strong>Add User</strong><p>Adding a user via this form = UserAddForm(prefix='user')
administrative interface <strong>might</strong> create a system user.
For example, if you provide a user with ssh access, she will
need a system account. If you don't know what that means,
don't worry about it.</p>""")
def main(self, username='', name='', email='', message=None, *args, **kwargs):
form = Form(title="Add User",
action=cfg.server_dir + "/sys/users/add/index",
name="add_user_form",
message=message)
form.text_input(_("Username"), name="username", value=username)
form.text_input(_("Full name"), name="name", value=name)
form.text_input(_("Email"), name="email", value=email)
form.text_input(_("Password"), name="password", type="password")
form.submit(label=_("Create User"), name="create")
return form.render()
def process_form(self, username=None, name=None, email=None, password=None, **kwargs):
msg = Message()
error = add_user(username, password, name, email, False)
if error:
msg.text = error
else: else:
msg.add = _("%s saved." % username) form = UserAddForm(prefix='user')
cfg.log(msg.text) return util.render_template(template='users_add', title=_('Add User'),
main = self.main(username, name, email, msg=msg.text) form=form, messages=messages)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
class edit(FormPlugin, PagePlugin): @staticmethod
url = ["/sys/users/edit"] def _add_user(data, messages):
order = 35 """Add a user"""
if cfg.users.exists(data['username']):
messages.append(
('error', _('User "{username}" already exists').format(
username=data['username'])))
return
sidebar_left = '' auth.add_user(data['username'], data['password'], data['full_name'],
sidebar_right = _("""<strong>Edit Users</strong><p>Click on a user's name to data['email'], False)
go to a screen for editing that user's account.</p><strong>Delete messages.append(
Users</strong><p>Check the box next to a users' names and then click ('success', _('User "{username}" added').format(
"Delete User" to remove users from %s and the %s username=data['username'])))
system.</p><p>Deleting users is permanent!</p>""" % (cfg.product_name, cfg.box_name))
class UserEditForm(forms.Form): # pylint: disable-msg=W0232
"""Form to edit/delete a user"""
def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1002
super(forms.Form, self).__init__(*args, **kwargs)
def main(self, msg=''):
users = cfg.users.get_all() users = cfg.users.get_all()
add_form = Form(title=_("Edit or Delete User"), action=cfg.server_dir + "/sys/users/edit", message=msg)
add_form.html('<span class="indent"><strong>Delete</strong><br /></span>')
for uname in users: for uname in users:
user = User(uname[1]) user = User(uname[1])
add_form.html('<span class="indent">&nbsp;&nbsp;%s&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' %
add_form.get_checkbox(name=user['username']) +
'<a href="'+cfg.server_dir+'/sys/users/edit?username=%s">%s (%s)</a><br /></span>' %
(user['username'], user['name'], user['username']))
add_form.submit(label=_("Delete User"), name="delete")
return add_form.render()
def process_form(self, **kwargs): label = '%s (%s)' % (user['name'], user['username'])
if 'delete' in kwargs: field = forms.BooleanField(label=label, required=False)
msg = Message() # pylint: disable-msg=E1101
usernames = find_keys(kwargs, 'on') self.fields['delete_user_' + user['username']] = field
cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames))
if usernames:
for username in usernames:
if cfg.users.exists(username):
try:
cfg.users.remove(username)
msg.add(_("Deleted user %s." % username))
except IOError, e:
if cfg.users.exists(username):
m = _("Error on deletion, user %s not fully deleted: %s" % (username, e))
cfg.log.error(m)
msg.add(m)
else:
m = _('Deletion failed on %s: %s' % (username, e))
cfg.log.error(m)
msg.add(m)
else:
cfg.log.warning(_("Can't delete %s. User does not exist." % username))
msg.add(_("User %s does not exist." % username))
else:
msg.add = _("Must specify at least one valid, existing user.")
main = self.main(msg=msg.text)
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
sidebar_right = ''
u = cfg.users[kwargs['username']]
if not u:
main = _("<p>Could not find a user with username of %s!</p>" % kwargs['username'])
return self.fill_template(template="err", title=_("Unknown User"), main=main,
sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
main = _("""<strong>Edit User '%s'</strong>""" % u['username']) class UserEdit(PagePlugin):
sidebar_right = '' """User edit page"""
return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right) order = 35
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:
form = UserEditForm(prefix='user')
return util.render_template(template='users_edit',
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
username = field.split('delete_user_')[1]
cfg.log.info('%s asked to delete %s' %
(cherrypy.session.get(cfg.session_key), username))
if username == cfg.users.current(name=True):
messages.append(
('error',
_('Can not delete current account - "%s"') % username))
continue
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

@ -1,78 +1,85 @@
import os
import cherrypy import cherrypy
try: from django import forms
import simplejson as json
except ImportError:
import json
from gettext import gettext as _ from gettext import gettext as _
from modules.auth import require from modules.auth import require
from plugin_mount import PagePlugin, FormPlugin from plugin_mount import PagePlugin
import cfg import cfg
from forms import Form import util
from model import User
from util import *
class wan(FormPlugin, PagePlugin):
url = ["/sys/config"]
order = 20
def help(self, *args, **kwargs): class WanForm(forms.Form): # pylint: disable-msg=W0232
if not cfg.users.expert(): """Form to configure wan settings"""
return ''
return _(#"""<h4>Admin from WAN</h4>
"""<p>If you check this box, this front
end will be reachable from the WAN. If your %(box)s
connects you to the internet, that means you'll be able to log
in to the front end from the internet. This might be
convenient, but it is also <strong>dangerous</strong>, since it can
enable attackers to gain access to your %(box)s from the
outside world. All they'll need is your username and
passphrase, which they might guess or they might simply try
every posible combination of letters and numbers until they
get in. If you enable the WAN administration option, you
<strong>must</strong> use long and complex passphrases.</p>
<p>For security reasons, neither WAN Administration nor WAN wan_admin = forms.BooleanField(
SSH is available to the `admin` user account.</p> label=_('Allow access to Plinth from WAN'),
required=False,
help_text=_('If you check this box, this front end will be reachable \
from the WAN. If your {{ box_name }} connects you to the internet, that \
means you\'ll be able to log in to the front end from the internet. This \
might be convenient, but it is also <strong>dangerous</strong>, since it can \
enable attackers to gain access to your {{ box_name }} from the outside \
world. All they\'ll need is your username and passphrase, which they might \
guess or they might simply try every posible combination of letters and \
numbers until they get in. If you enable the WAN administration option, you \
<strong>must</strong> use long and complex passphrases.').format(
box_name=cfg.box_name))
<p>TODO: in expert mode, tell user they can ssh in to enable lan_ssh = forms.BooleanField(
admin from WAN, do their business, then disable it. It would label=_('Allow SSH access from LAN'),
be good to enable the option and autodisable it when the ssh required=False)
connection dies.</p>
""" % {'product':cfg.product_name, 'box':cfg.box_name})
def main(self, message='', **kwargs): wan_ssh = forms.BooleanField(
store = filedict_con(cfg.store_file, 'sys') label=_('Allow SSH access from WAN'),
required=False)
defaults = {'wan_admin': '', # XXX: Only present due to issue with submitting empty form
'wan_ssh': '', dummy = forms.CharField(label='Dummy', initial='dummy',
'lan_ssh': '', widget=forms.HiddenInput())
}
for key, value in defaults.items():
if not key in kwargs:
try:
kwargs[key] = store[key]
except KeyError:
store[key] = kwargs[key] = value
form = Form(title=_("Accessing the %s" % cfg.box_name),
action=cfg.server_dir + "/sys/config/wan/",
name="admin_wan_form",
message=message)
form.html(self.help())
if cfg.users.expert():
form.checkbox(_("Allow access to Plinth from WAN"), name="wan_admin", checked=kwargs['wan_admin'])
form.checkbox(_("Allow SSH access from LAN"), name="lan_ssh", checked=kwargs['lan_ssh'])
form.checkbox(_("Allow SSH access from WAN"), name="wan_ssh", checked=kwargs['wan_ssh'])
# Hidden field is needed because checkbox doesn't post if not checked class Wan(PagePlugin):
form.hidden(name="submitted", value="True") order = 60
form.submit(_("Submit")) def __init__(self, *args, **kwargs):
return form.render() PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.config.wan')
def process_form(self, wan_admin='', wan_ssh='', lan_ssh='', *args, **kwargs): cfg.html_root.sys.config.menu.add_item(_('WAN'), 'icon-cog',
store = filedict_con(cfg.store_file, 'sys') '/sys/config/wan', 20)
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
if kwargs and cfg.users.expert():
form = WanForm(kwargs, prefix='wan')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(form.cleaned_data, messages)
status = self.get_status()
form = WanForm(initial=status, prefix='wan')
else:
form = WanForm(initial=status, prefix='wan')
title = _('Accessing the {box_name}').format(box_name=cfg.box_name)
return util.render_template(template='wan', title=title, form=form,
messages=messages)
@staticmethod
def get_status():
"""Return the current status"""
return util.filedict_con(cfg.store_file, 'sys')
@staticmethod
def _apply_changes(new_status, messages):
"""Apply the changes after form submission"""
store = util.filedict_con(cfg.store_file, 'sys')
for field in ['wan_admin', 'wan_ssh', 'lan_ssh']: for field in ['wan_admin', 'wan_ssh', 'lan_ssh']:
store[field] = locals()[field] store[field] = new_status[field]
return "Settings updated."
messages.append(('success', _('Setting updated')))

View File

@ -0,0 +1,28 @@
{% 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 %}
<div class="well sidebar-nav">
<h3>Getting Help</h3>
<p>We've done our best to make your FreedomBox easy to use. If
you have questions during setup, there are a few places to turn
for help. TODO: add links to such help.</p>
</div>

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block main_block %}
<h2>Welcome to Your FreedomBox!</h2>
{% include 'messages.html' %}
<p>Welcome. It looks like this FreedomBox isn't set up yet. We'll
need to ask you a just few questions to get started.</p>
<form class="form" method="post">
{% csrf_token %}
{% include 'bootstrapform/field.html' with field=form.hostname %}
<p><strong>Initial user and password.</strong> Access to this web
interface is protected by knowing a username and password.
Provide one here to register the initial privileged user. The
password can be changed and other users added later.</p>
{% include 'bootstrapform/field.html' with field=form.username %}
{% include 'bootstrapform/field.html' with field=form.password %}
{% include 'bootstrapform/field.html' with field=form.box_key %}
<input type="submit" class="btn-primary" value="Box it up!"/>
</form>
{% endblock %}
{% block sidebar_right_block %}
{% include "firstboot_sidebar.html" %}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block main_block %}
<p>Welcome screen not completely implemented yet. Press <a
href="{{basehref }}/router">continue</a> to see the rest of the
web interface.</p>
<ul>
<li>TODO: explain all this cert stuff to the user.</li>
<li>TODO: add instrux for installing certificate.</li>
<li>TODO: add steps for after you have installed certificate.</li>
</ul>
{% endblock %}
{% block sidebar_right_block %}
{% include "firstboot_sidebar.html" %}
{% endblock %}

View File

@ -3,6 +3,7 @@
import os, stat, sys, argparse import os, stat, sys, argparse
from gettext import gettext as _ from gettext import gettext as _
import cfg import cfg
import django.conf
if not os.path.join(cfg.file_root, "vendor") in sys.path: if not os.path.join(cfg.file_root, "vendor") in sys.path:
sys.path.append(os.path.join(cfg.file_root, "vendor")) sys.path.append(os.path.join(cfg.file_root, "vendor"))
@ -32,7 +33,7 @@ __status__ = "Development"
import urlparse import urlparse
def error_page(status, dynamic_msg, stock_msg): def error_page(status, dynamic_msg, stock_msg):
return u.page_template(template="err", title=status, main="<p>%s</p>%s" % (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): def error_page_404(status, message, traceback, version):
return error_page(status, message, """<p>If you believe this return error_page(status, message, """<p>If you believe this
@ -88,6 +89,26 @@ def load_modules():
else: else:
cfg.log("skipping %s" % name) cfg.log("skipping %s" % name)
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'):
if not name.endswith(".py") or name.startswith('.'):
continue
real_name = os.path.realpath(os.path.join('modules', name))
directory = os.path.dirname(real_name)
directories.add(os.path.join(directory, 'templates'))
cfg.log.info('Template directories - %s' % directories)
return directories
def parse_arguments(): def parse_arguments():
parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.')
parser.add_argument('--pidfile', parser.add_argument('--pidfile',
@ -146,7 +167,6 @@ def setup():
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0] cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
cfg.page_plugins = plugin_mount.PagePlugin.get_plugins() cfg.page_plugins = plugin_mount.PagePlugin.get_plugins()
cfg.log("Loaded %d page plugins" % len(cfg.page_plugins)) cfg.log("Loaded %d page plugins" % len(cfg.page_plugins))
cfg.forms = plugin_mount.FormPlugin.get_plugins()
# Add an extra server # Add an extra server
server = _cpserver.Server() server = _cpserver.Server()
@ -178,13 +198,18 @@ def setup():
cherrypy.engine.signal_handler.subscribe() cherrypy.engine.signal_handler.subscribe()
def main(): def main():
# Initialize basic services # Initialize basic services
service.init() service.init()
setup() setup()
cherrypy.engine.start() # Configure Django
cherrypy.engine.block() template_directories = get_template_directories()
django.conf.settings.configure(TEMPLATE_DIRS=template_directories,
INSTALLED_APPS=['bootstrapform'])
cherrypy.engine.start()
cherrypy.engine.block()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,8 +1,8 @@
import cherrypy import cherrypy
from modules.auth import require from modules.auth import require
import cfg import cfg
from util import * import util
import util as u
class PluginMount(type): class PluginMount(type):
"""See http://martyalchin.com/2008/jan/10/simple-plugin-framework/ for documentation""" """See http://martyalchin.com/2008/jan/10/simple-plugin-framework/ for documentation"""
@ -34,27 +34,6 @@ class PluginMountSingular(PluginMount):
cls.plugins.append(cls) cls.plugins.append(cls)
def get_parts(obj, parts=None, *args, **kwargs):
if parts == None:
parts={}
fields = ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css', 'title']
for v in fields:
if not v in parts:
parts[v] = ''
try:
method = getattr(obj, v)
if callable(method):
parts[v] = method(*args, **kwargs)
else:
parts[v] = method
except AttributeError:
pass
return parts
def _setattr_deep(obj, path, value): def _setattr_deep(obj, path, value):
"""If path is 'x.y.z' or ['x', 'y', 'z'] then perform obj.x.y.z = value""" """If path is 'x.y.z' or ['x', 'y', 'z'] then perform obj.x.y.z = value"""
if isinstance(path, basestring): if isinstance(path, basestring):
@ -87,89 +66,6 @@ class PagePlugin:
cfg.log.info("Registering page: %s" % url) cfg.log.info("Registering page: %s" % url)
_setattr_deep(cfg.html_root, url, self) _setattr_deep(cfg.html_root, url, self)
def fill_template(self, *args, **kwargs):
return u.page_template(*args, **kwargs)
def forms(self, url, *args, **kwargs):
for form in cfg.forms:
if url in form.url:
cfg.log('Pulling together form for url %s (which matches %s)' % (url, form.url))
parts = get_parts(form, None, *args, **kwargs)
return parts
return {'sidebar_left':left, 'sidebar_right':right, 'main':main}
class FormPlugin():
"""
Mount point for plugins that provide forms at specific URLs.
Form plugin classes should also inherit from PagePlugin so they
can implement the page that handles the results of the form. The
name of the class will be appended to each url where the form is
displayed to get the urls of the results pages.
Plugins implementing this reference should provide the following attributes:
url - list of URL path strings of pages on which to display this
form, not including the url path that *only* displays this form
(that's handled by the index method)
order - How high up on the page should this content be displayed?
Lower order is higher up.
The following attributes are optional (though without at least one
of them, this plugin won't do much):
sidebar_right - text to be displayed in the right column (can be attribute or method) (optional)
sidebar_left - text to be displayed in the left column (can be attribute or method) (optional)
main - text to be displayed in the center column (i.e. the form) (can be attribute or method)
js - attribute containing a string that will be placed in the
template head, just below the javascript loads. Use it to load
more javascript files (optional)
Although this plugin is intended for forms, it could just display
some html and skip the form.
"""
__metaclass__ = PluginMount
order = 50
url = []
js = ''
def __init__(self, *args, **kwargs):
for u in self.url:
path = u.split("/")[1:] + [self.__class__.__name__]
_setattr_deep(cfg.html_root, path, self)
cfg.log("Registered page: %s" % '.'.join(path))
def main(self, *args, **kwargs):
return "<p>Override this method and replace it with a form.</p>"
@cherrypy.expose
@require()
def index(self, **kwargs):
"""If the user has tried to fill in the form, process it, otherwise, just display a default form."""
if kwargs:
kwargs['message'] = self.process_form(**kwargs)
parts = get_parts(self, **kwargs)
return self.fill_template(**parts)
def process_form(self, **kwargs):
"""Process the form. Return any message as a result of processing."""
pass
def fill_template(self, *args, **kwargs):
if not 'js' in kwargs:
try:
kwargs['js'] = self.js
except AttributeError:
pass
cfg.log("%%%%%%%%%%% %s" % kwargs)
return u.page_template(*args, **kwargs)
class UserStoreModule: class UserStoreModule:
""" """

View File

@ -1,15 +0,0 @@
CHEETAH=cheetah
TEMPLATES=$(wildcard *.tmpl)
TEMPLATE_PYFILES := $(patsubst %.tmpl,%.py,$(TEMPLATES))
## Catch-all tagets
default: $(TEMPLATE_PYFILES)
%.py: %.tmpl
$(CHEETAH) c $<
templates: $(TEMPLATE_PYFILES)
template: templates
clean:
rm -rf .\#* \#* *.pyc *.bak
@rm -f $(TEMPLATE_PYFILES)

View File

View File

@ -1,10 +1,3 @@
#def default($text, $default)
#if $text
$text
#else
$default
#end if
#end def
<!doctype html> <!doctype html>
<!--[if lt IE 7 ]> <html class="ie ie6 no-js" lang="en"> <![endif]--> <!--[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]--> <!--[if IE 7 ]> <html class="ie ie7 no-js" lang="en"> <![endif]-->
@ -31,22 +24,22 @@
<meta name="msnbot" content="noindex, nofollow, noarchive, noodp" /> <meta name="msnbot" content="noindex, nofollow, noarchive, noodp" />
<meta name="slurp" content="noindex, nofollow, noarchive, noodp, noydir" /> <meta name="slurp" content="noindex, nofollow, noarchive, noodp, noydir" />
<meta name="title" content="$default($title, 'FreedomBox Dashboard')" /> <meta name="title" content="{% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %}" />
<meta name="description" content="Plinth administrative interface for the FreedomBox" /> <meta name="description" content="Plinth administrative interface for the FreedomBox" />
<title>$default($title, "FreedomBox Dashboard")</title> <title>{% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %}</title>
<!-- This is the traditional favicon. Size: 16x16 or 32x32, transparency is OK --> <!-- 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="{{ basehref }}/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 <!-- 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 - 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) --> - 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="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="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="114x114" href="{{ basehref }}/static/theme/img/apple-touch-icon-114px-precomposed.png" />
<!-- Bootstrap base CSS --> <!-- Bootstrap base CSS -->
<link rel="stylesheet" href="$basehref/static/theme/css/bootstrap.css" /> <link rel="stylesheet" href="{{ basehref }}/static/theme/css/bootstrap.css" />
<style type="text/css"> <style type="text/css">
/* custom styles, load before bootstrap responsive styles */ /* custom styles, load before bootstrap responsive styles */
body { body {
@ -58,7 +51,7 @@
} }
</style> </style>
<!-- Bootstrap responsive CSS --> <!-- Bootstrap responsive CSS -->
<link rel="stylesheet" href="$basehref/static/theme/css/bootstrap-responsive.css" /> <link rel="stylesheet" href="{{ basehref }}/static/theme/css/bootstrap-responsive.css" />
<style type="text/css"> <style type="text/css">
/* custom styles, load after all bootstrap styles */ /* custom styles, load after all bootstrap styles */
.super-hero { .super-hero {
@ -91,16 +84,16 @@
} }
</style> </style>
<!-- CSS from previous Plinth template, not sure what to keep yet --> <!-- CSS from previous Plinth template, not sure what to keep yet -->
$css {{ css|safe }}
<!-- End Plinth CSS --> <!-- End Plinth CSS -->
<!-- JS from previous Plinth template, not sure what to keep yet --> <!-- JS from previous Plinth template, not sure what to keep yet -->
<script type="text/javascript" src="$basehref/static/theme/js/menu.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/menu.js"></script>
<script type="text/javascript" src="$basehref/static/theme/js/plinth.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/plinth.js"></script>
$main_menu_js {{ main_menu_js|safe }}
$sub_menu_js {{ sub_menu_js|safe }}
<script type="text/javascript"> <script type="text/javascript">
<!-- <!--
$onload {{ onload|safe }}
// --> // -->
</script> </script>
</head> </head>
@ -114,9 +107,9 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</a> </a>
<a href="$basehref/" class="logo-top"><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="{{ basehref }}/static/theme/img/freedombox-logo-32px.png" alt="FreedomBox" /></a><a class="brand" href="{{ basehref }}/">FreedomBox Dashboard</a>
#block add_nav_and_login {% block add_nav_and_login %}
#end block add_nav_and_login {% endblock %}
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</div> </div>
@ -124,35 +117,37 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
#if $nav or $sidebar_right or $sidebar_left {% if nav or sidebar_right or sidebar_left %}
<div class="span3"> <div class="span3">
#if $nav {% if nav %}
<div class="well sidebar-nav"> <div class="well sidebar-nav">
$nav {{ nav|safe }}
</div><!--/.well --> </div><!--/.well -->
#end if {% endif %}
#if $sidebar_left {% if sidebar_left %}
<div class="well sidebar-nav"> <div class="well sidebar-nav">
left $sidebar_left {{ sidebar_left|safe }}
</div><!--/.well --> </div><!--/.well -->
#end if {% endif %}
#if $sidebar_right {% block sidebar_right_block %}
<div class="well sidebar-nav"> {% if sidebar_right %}
$sidebar_right <div class="well sidebar-nav">
</div><!--/.well --> {{ sidebar_right|safe }}
#end if </div><!--/.well -->
{% endif %}
{% endblock %}
</div> </div>
#end if {% endif %}
<div class="span9"> <div class="span9">
<div class="hero-unit" class="pull-left"> <div class="hero-unit" class="pull-left">
<h2> <h2>
#block title_block {% block title_block %}
$title {{ title }}
#end block title_block {% endblock %}
</h2> </h2>
#block main_block {% block main_block %}
$main {{ main|safe }}
#end block main_block {% endblock %}
</div> </div>
</div><!--/span--> </div><!--/span-->
</div><!--/row--> </div><!--/row-->
@ -160,44 +155,46 @@
<hr> <hr>
<footer> <footer>
<p>#block footer_block <p>{% block footer_block %}
<p> <p>
Plinth is &copy; Copyright 2012 <a href="http://hackervisions.org" target="_blank">James Vasile</a>. It is Plinth is &copy; Copyright 2012 <a href="http://hackervisions.org" target="_blank">James Vasile</a>. It is
free software offered to you under the terms of 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 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>. 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>
<p>Current page: $current_url</p> <p>Current page: {{ current_url }}</p>
<p> <p>
</p> </p>
#end block footer_block</p> {% endblock %}</p>
</footer> </footer>
</div><!--/.fluid-container--> </div><!--/.fluid-container-->
<!-- JavaScript <script> tags are placed at the end of the document to speed up initial page loads--> <!-- 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) --> <!-- 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="{{ basehref }}/static/theme/js/libs/modernizr.min.js"></script>
<!-- Local link to system jQuery --> <!-- Local link to system jQuery -->
<script type="text/javascript" src="$basehref/static/theme/js/libs/jquery.min.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/jquery.min.js"></script>
<!-- Bootstrap JS, most files commented out until we know what we need --> <!-- Bootstrap JS, most files commented out until we know what we need -->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/bootstrap.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/bootstrap.js"></script>-->
<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/bootstrap.min.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/bootstrap.min.js"></script>
<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/button.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/button.js"></script>
<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/dropdown.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/dropdown.js"></script>
<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/collapse.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/collapse.js"></script>
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/transition.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/transition.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/alert.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/alert.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/modal.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/modal.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/scrollspy.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/scrollspy.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/tab.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/tab.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/tooltip.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/tooltip.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/popover.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/popover.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/carousel.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/carousel.js"></script>-->
<!--<script type="text/javascript" src="$basehref/static/theme/js/libs/bootstrap/typeahead.js"></script>--> <!--<script type="text/javascript" src="{{ basehref }}/static/theme/js/libs/bootstrap/typeahead.js"></script>-->
<!-- JS plugins --> <!-- JS plugins -->
<script type="text/javascript" src="$basehref/static/theme/js/plugins.js"></script> <script type="text/javascript" src="{{ basehref }}/static/theme/js/plugins.js"></script>
$js {% block js_block %}
{{ js|safe }}
{% endblock %}
</body> </body>
</html> </html>

7
templates/err.html Normal file
View File

@ -0,0 +1,7 @@
{% extends "login_nav.html" %}
{% block title_block %}
<span class="label label-important error-large">Error: {{ title }}</span>
<br />
<br />
{% endblock %}

View File

@ -1,23 +0,0 @@
#extends templates.login_nav
#def title_block
<span class="label label-important error-large">Error: $title</span>
<br />
<br />
#end def
#def sidebar_left_block
$sidebar_left
#end def
#def main_block
$main
#end def
#def sidebar_right_block
$sidebar_right
#end def
#def nav_block
$nav
#end def

37
templates/form.html Normal file
View File

@ -0,0 +1,37 @@
{% 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 %}
{% load bootstrap %}
{% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary"
value="{{ submit_text|default:"Submit" }}"/>
</form>
{% endblock %}

13
templates/login_nav.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block add_nav_and_login %}
<div class="nav-collapse">
<script type="text/javascript">
main_menu(main_menu_items);
</script>
{% if username %}
<p class="navbar-text pull-right"><i class="icon-user icon-white nav-icon"></i>Logged in as <a href="{{ username }}">{{ username }}</a>. <a href="{{ basehref }}/auth/logout" title="Log out">Log out</a>.</p>
{% else %}
<p class="navbar-text pull-right">Not logged in. <i class="icon-user icon-white nav-icon"></i><a href="{{ basehref }}/auth/login" title="Log in">Log in</a>.</p>
{% endif %}
{% endblock %}

View File

@ -1,31 +0,0 @@
#extends templates.base
#def add_nav_and_login
<div class="nav-collapse">
<script type="text/javascript">
<!--
main_menu(main_menu_items);
// -->
</script>
#if $username
<p class="navbar-text pull-right"><i class="icon-user icon-white nav-icon"></i>Logged in as <a href="$username">$username</a>. <a href="$basehref/auth/logout" title="Log out">Log out</a>.</p>
#else
<p class="navbar-text pull-right">Not logged in. <i class="icon-user icon-white nav-icon"></i><a href="$basehref/auth/login" title="Log in">Log in</a>.</p>
#end if
#end def
#def sidebar_left_block
$sidebar_left
#end def
#def main_block
$main
#end def
#def sidebar_right_block
$sidebar_right
#end def
#def nav_block
$nav
#end def

View File

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

25
templates/messages.html Normal file
View File

@ -0,0 +1,25 @@
{% 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 %}
{% for severity, message in messages %}
<div class='alert alert-{{ severity }} alert-dismissable'>
<a class="close" data-dismiss="alert">&times;</a>
{{ message }}
</div>
{% endfor %}

24
templates/two_col.html Normal file
View File

@ -0,0 +1,24 @@
{{ extends "base.html" }}
{% block css %}
<!-- CSS from previous template, not sure what to keep yet -->
<!--<link rel="stylesheet" href="{{ basehref }}/static/theme/style-plinth-2col.css" />-->
{% endblock %}
{% block add_sidebar_left %}
<div class="span3">
<div class="well sidebar-nav">
{% block nav_block %}
{{ nav }}
{% endblock %}
{% block sidebar_left_block %}
{{ sidebar_left }}
{% endblock %}
</div><!--/.well -->
<div class="well sidebar-nav">
{% block sidebar_right_block %}
{{ sidebar_right }}
{% endblock %}
</div><!--/.well -->
</div><!--/span-->
{% endblock %}

View File

@ -1,25 +0,0 @@
#extends templates.base
#def css
<!-- CSS from previous template, not sure what to keep yet -->
<!--<link rel="stylesheet" href="$basehref/static/theme/style-plinth-2col.css" />-->
#end def
#def add_sidebar_left
<div class="span3">
<div class="well sidebar-nav">
#block nav_block
$nav
#end block nav_block
#block sidebar_left_block
$sidebar_left
#end block sidebar_left_block
</div><!--/.well -->
<div class="well sidebar-nav">
#block sidebar_right_block
$sidebar_right
#end block sidebar_right_block
</div><!--/.well -->
</div><!--/span-->
#end def

27
util.py
View File

@ -1,9 +1,10 @@
import os, sys import os
import sys
import cherrypy import cherrypy
import cfg import cfg
import importlib
import sqlite3 import sqlite3
from django.template.loader import render_to_string
from filedict import FileDict from filedict import FileDict
@ -60,18 +61,16 @@ class Message():
def add(self, text): def add(self, text):
self.text += "<br />%s" % text self.text += "<br />%s" % text
def page_template(template='login_nav', **kwargs):
for k in ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css', 'title', 'basehref']: def render_template(template='login_nav', **kwargs):
if not k in kwargs: for key in ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav',
kwargs[k] = '' 'css', 'title', 'basehref']:
if not key in kwargs:
kwargs[key] = ''
if kwargs['basehref'] == '': if kwargs['basehref'] == '':
kwargs['basehref'] = cfg.server_dir kwargs['basehref'] = cfg.server_dir
#if template=='base' and kwargs['sidebar_right']=='':
# template='two_col'
if isinstance(template, basestring):
template_module = importlib.import_module('templates.' + template)
template = getattr(template_module, template)
try: try:
submenu = cfg.main_menu.active_item().encode("sub_menu", render_subs=True) submenu = cfg.main_menu.active_item().encode("sub_menu", render_subs=True)
except AttributeError: except AttributeError:
@ -82,6 +81,7 @@ def page_template(template='login_nav', **kwargs):
kwargs['sub_menu_js'] = submenu kwargs['sub_menu_js'] = submenu
kwargs['current_url'] = cherrypy.url() kwargs['current_url'] = cherrypy.url()
kwargs['username'] = cherrypy.session.get(cfg.session_key) kwargs['username'] = cherrypy.session.get(cfg.session_key)
kwargs['cfg'] = cfg
if not kwargs['nav'] and submenu: if not kwargs['nav'] and submenu:
kwargs['nav'] = """ <script type="text/javascript"> kwargs['nav'] = """ <script type="text/javascript">
@ -90,7 +90,8 @@ def page_template(template='login_nav', **kwargs):
// --> // -->
</script>""" </script>"""
return str(template(searchList=[kwargs])) return render_to_string(template + '.html', kwargs)
def filedict_con(filespec=None, table='dict'): def filedict_con(filespec=None, table='dict'):
"""TODO: better error handling in filedict_con""" """TODO: better error handling in filedict_con"""