Merge pull request #1 from fonfon/master

Update to include several django-changes by Sunil and some url-changes by fonfon
This commit is contained in:
fonfon 2014-07-14 13:15:21 +02:00
commit 8ebb88fd68
168 changed files with 2933 additions and 3286 deletions

5
.gitignore vendored
View File

@ -1,9 +1,11 @@
current-*.tar.gz
*.pyc
*.py.bak
*.swp
*.tiny.css
data/*.log
data/cherrypy_sessions
data/sessions
data/store.sqlite3
doc/*.tex
doc/*.pdf
@ -23,7 +25,8 @@ TODO
\#*
.#*
*~
data/users.sqlite3
data/plinth.sqlite3
predepend
build/
*.pid
.emacs.desktop*

View File

@ -1,4 +1,4 @@
# License to Copy Plinth
# License to Copy Plinth
Plinth is Copyright 2011-2013 James Vasile (<james@hackervisions.org>). It
is distributed under the GNU Affero General Public License, Version 3
@ -16,10 +16,6 @@ The documentation to this software is also distributed under the [GNU
Free Documentation License](http://www.gnu.org/licenses/fdl.html),
version 1.3 or later.
In default form, Plinth incorporates FileDict, a Python module
released under a "MIT/BSD/Python" license, as per [its blog
page](https://erezsh.wordpress.com/2009/05/31/filedict-bug-fixes-and-updates/).
## GNU Affero General Public License, Version 3
GNU AFFERO GENERAL PUBLIC LICENSE

View File

@ -9,18 +9,15 @@ specified and linked otherwise.
- COPYING :: N/A
- COPYRIGHTS :: N/A
- fabfile.py :: -
- filedict.py :: [[http://erez.wikidot.com/filedict-0-1-code][CC-BY-SA 3.0]]
- INSTALL :: -
- logger.py :: -
- Makefile :: -
- menu.py :: -
- model.py :: -
- NOTES :: -
- plinth :: -
- plinth.config :: -
- plinth.py :: [[file:plinth.py::__license__%20%3D%20"GPLv3%20or%20later"]["GPLv3 or later"]]
- plinth.sample.config :: -
- plugin_mount.py :: [[http://martyalchin.com/2008/jan/10/simple-plugin-framework/][CC-BY-SA 3.0]]
- README :: -
- start.sh :: -
- test.sh :: -
@ -45,38 +42,26 @@ specified and linked otherwise.
- doc/scripts.mdwn :: -
- doc/security.mdwn :: -
- doc/themes.mdwn :: -
- modules/installed/first_boot.py :: -
- modules/installed/apps/apps.py :: -
- modules/installed/apps/owncloud.py :: -
- modules/installed/help/help.py :: -
- modules/installed/lib/auth_page.py :: -
- modules/installed/lib/auth.py :: Derived from [[http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions][Arnar Birisson's CherryPy wiki code]].
- modules/installed/lib/forms.py :: [[file:modules/installed/lib/forms.py::Copyright%202011-2013%20James%20Vasile][Copyright James Vasile]]
- modules/installed/lib/user_store.py :: -
- modules/installed/privacy/privacy.py :: -
- modules/installed/router/info.py :: -
- modules/installed/router/router.py :: -
- modules/installed/santiago/santiago.py :: -
- modules/installed/services/services.py :: -
- modules/installed/services/xmpp.py :: -
- modules/installed/sharing/file_explorer.py :: -
- modules/installed/sharing/sharing.py :: -
- modules/installed/system/config.py :: -
- modules/installed/system/diagnostics.py :: -
- modules/installed/system/expert_mode.py :: -
- modules/installed/system/system.py :: -
- modules/installed/system/time_zones :: -
- modules/installed/system/users.py :: -
- modules/installed/system/wan.py :: -
- modules/apps/apps.py :: -
- modules/config/config.py :: -
- modules/diagnostics/diagnostics.py :: -
- modules/expert_mode/expert_mode.py :: -
- modules/first_boot/first_boot.py :: -
- modules/help/help.py :: -
- modules/lib/auth.py :: -
- modules/owncloud/owncloud.py :: -
- modules/packages/packages.py :: -
- modules/santiago/santiago.py :: -
- modules/system/system.py :: -
- modules/tor/tor.py :: -
- modules/users/users.py :: -
- modules/xmpp/xmpp.py :: -
- setup/86_plinth :: -
- share/apache2/plinth.conf :: -
- share/apache2/plinth-ssl.conf :: -
- share/init.d/plinth :: -
- sudoers/plinth :: -
- 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.html :: -
- templates/login_nav.html :: -
- templates/two_col.html :: -
- tests/actions_test.py :: -
- tests/auth_test.py :: -
- tests/testdata/users.sqlite3 :: -

View File

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

91
cfg.py
View File

@ -4,38 +4,65 @@ import os
import ConfigParser
from ConfigParser import SafeConfigParser
def get_item(parser, section, name):
try:
return parser.get(section, name)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
print ("Configuration does not contain the {}.{} option.".format(
section, name))
raise
product_name = None
box_name = None
root = None
file_root = None
python_root = None
data_dir = None
store_file = None
user_db = None
status_log_file = None
access_log_file = None
pidfile = None
host = None
port = None
debug = False
no_daemon = False
server_dir = '/'
parser = SafeConfigParser(
defaults={
'root':os.path.dirname(os.path.realpath(__file__)),
})
parser.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plinth.config'))
product_name = get_item(parser, 'Name', 'product_name')
box_name = get_item(parser, 'Name', 'box_name')
root = get_item(parser, 'Path', 'root')
file_root = get_item(parser, 'Path', 'file_root')
python_root = get_item(parser, 'Path', 'python_root')
data_dir = get_item(parser, 'Path', 'data_dir')
store_file = get_item(parser, 'Path', 'store_file')
user_db = get_item(parser, 'Path', 'user_db')
status_log_file = get_item(parser, 'Path', 'status_log_file')
access_log_file = get_item(parser, 'Path', 'access_log_file')
pidfile = get_item(parser, 'Path', 'pidfile')
host = get_item(parser, 'Network', 'host')
port = int(get_item(parser, 'Network', 'port'))
html_root = None
main_menu = Menu()
if store_file.endswith(".sqlite3"):
store_file = os.path.splitext(store_file)[0]
if user_db.endswith(".sqlite3"):
user_db = os.path.splitext(user_db)[0]
def read():
"""Read configuration"""
directory = os.path.dirname(os.path.realpath(__file__))
parser = SafeConfigParser(
defaults={
'root': directory,
})
parser.read(os.path.join(directory, 'plinth.config'))
config_items = {('Name', 'product_name'),
('Name', 'box_name'),
('Path', 'root'),
('Path', 'file_root'),
('Path', 'python_root'),
('Path', 'data_dir'),
('Path', 'store_file'),
('Path', 'user_db'),
('Path', 'status_log_file'),
('Path', 'access_log_file'),
('Path', 'pidfile'),
('Network', 'host'),
('Network', 'port')}
for section, name in config_items:
try:
value = parser.get(section, name)
globals()[name] = value
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
print ('Configuration does not contain the {}.{} option.'
.format(section, name))
raise
global port # pylint: disable-msg=W0603
port = int(port)
global store_file # pylint: disable-msg=W0603
if store_file.endswith(".sqlite3"):
store_file = os.path.splitext(store_file)[0]
global user_db # pylint: disable-msg=W0603
if user_db.endswith(".sqlite3"):
user_db = os.path.splitext(user_db)[0]

View File

@ -1,149 +0,0 @@
"""filedict.py
a Persistent Dictionary in Python
Author: Erez Shinan
Date : 31-May-2009
"""
import json
import UserDict
import sqlite3
class DefaultArg:
pass
class Solutions:
Sqlite3 = 0
class FileDict(UserDict.DictMixin):
"A dictionary that stores its data persistantly in a file"
def __init__(self, solution=Solutions.Sqlite3, **options):
assert solution == Solutions.Sqlite3
try:
self.__conn = options.pop('connection')
except KeyError:
filename = options.pop('filename')
self.__conn = sqlite3.connect(filename)
self.__tablename = options.pop('table', 'dict')
self._nocommit = False
assert not options, "Unrecognized options: %s" % options
self.__conn.execute('create table if not exists %s (id integer primary key, hash integer, key blob, value blob);'%self.__tablename)
self.__conn.execute('create index if not exists %s_index ON %s(hash);' % (self.__tablename, self.__tablename))
self.__conn.commit()
def _commit(self):
if self._nocommit:
return
self.__conn.commit()
def __pack(self, value):
return sqlite3.Binary(json.dumps(value))
##return sqlite3.Binary(pickle.dumps(value, -1))
def __unpack(self, value):
return json.loads(str(value))
##return pickle.loads(str(value))
def __get_id(self, key):
cursor = self.__conn.execute('select key,id from %s where hash=?;'%self.__tablename, (hash(key),))
for k,id in cursor:
if self.__unpack(k) == key:
return id
raise KeyError(key)
def __getitem__(self, key):
cursor = self.__conn.execute('select key,value from %s where hash=?;'%self.__tablename, (hash(key),))
for k,v in cursor:
if self.__unpack(k) == key:
return self.__unpack(v)
raise KeyError(key)
def __setitem(self, key, value):
value_pickle = self.__pack(value)
try:
id = self.__get_id(key)
cursor = self.__conn.execute('update %s set value=? where id=?;'%self.__tablename, (value_pickle, id) )
except KeyError:
key_pickle = self.__pack(key)
cursor = self.__conn.execute('insert into %s (hash, key, value) values (?, ?, ?);'
%self.__tablename, (hash(key), key_pickle, value_pickle) )
assert cursor.rowcount == 1
def __setitem__(self, key, value):
self.__setitem(key, value)
self._commit()
def __delitem__(self, key):
id = self.__get_id(key)
cursor = self.__conn.execute('delete from %s where id=?;'%self.__tablename, (id,))
if cursor.rowcount <= 0:
raise KeyError(key)
self._commit()
def update(self, d):
for k,v in d.iteritems():
self.__setitem(k, v)
self._commit()
def __iter__(self):
return (self.__unpack(x[0]) for x in self.__conn.execute('select key from %s;'%self.__tablename) )
def keys(self):
return iter(self)
def values(self):
return (self.__unpack(x[0]) for x in self.__conn.execute('select value from %s;'%self.__tablename) )
def items(self):
return (map(self.__unpack, x) for x in self.__conn.execute('select key,value from %s;'%self.__tablename) )
def iterkeys(self):
return self.keys()
def itervalues(self):
return self.values()
def iteritems(self):
return self.items()
def __contains__(self, key):
try:
self.__get_id(key)
return True
except KeyError:
return False
def __len__(self):
return self.__conn.execute('select count(*) from %s;' % self.__tablename).fetchone()[0]
def __del__(self):
try:
self.__conn
except AttributeError:
pass
else:
self.__conn.commit()
@property
def batch(self):
return self._Batch(self)
class _Batch:
def __init__(self, d):
self.__d = d
def __enter__(self):
self.__old_nocommit = self.__d._nocommit
self.__d._nocommit = True
return self.__d
def __exit__(self, type, value, traceback):
self.__d._nocommit = self.__old_nocommit
self.__d._commit()
return True

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Privoxy - A Privacy Enhancing Proxy Server</short>
<description>Privoxy is a web proxy for enhancing privacy by filtering web page content, managing cookies, controlling access, removing ads, banners, pop-ups and other obnoxious Internet junk. It does not cache web content. Enable this if you run Privoxy and would like to configure your web browser to browse the Internet via Privoxy.</description>
<port protocol="tcp" port="8118"/>
</service>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Tor - SOCKS Proxy</short>
<description>Tor enables online anonymity and censorship resistance by directing Internet traffic through a network of relays. It conceals user's location from anyone conducting network surveillance and traffic analysis. A user wishing to use Tor for anonymity can configure a program such as a web browser to direct traffic to a Tor client using its SOCKS proxy port. Enable this if you run Tor and would like to configure your web browser or other programs to channel their traffic through the Tor SOCKS proxy port. It is recommended that you make this service available only for your computer or your internal networks.</description>
<port protocol="tcp" port="9050"/>
</service>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>XMPP (Jabber) web client</short>
<description>Extensible Messaging and Presence Protocol (XMPP) web client protocol allows web based chat clients such as JWChat to connect to the XMPP (Jabber) server. This is also know as the Bidirectional-streams Over Synchronous HTTP (BOSH) protocol. Enable this if you run an XMPP (Jabber) server and you wish web clients to connect to your server.</description>
<port protocol="tcp" port="5280"/>
</service>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>XMPP (Jabber) client</short>
<description>Extensible Messaging and Presence Protocol (XMPP) client connection protocol allows XMPP (Jabber) clients such as Empathy, Pidgin, Kopete and Jitsi to connect to an XMPP (Jabber) server. Enable this if you run an XMPP (Jabber) server and you wish clients to be able to connect to the server and communicate with each other.</description>
<port protocol="tcp" port="5222"/>
</service>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>XMPP (Jabber) server</short>
<description>Extensible Messaging and Presence Protocol (XMPP) server connection protocols allows multiple XMPP (Jabber) servers to work in a federated fashion. Users on one server will be able to see the presence of and communicate with users on another servers. Enable this if you run an XMPP (Jabber) server and you wish users on your server to communicate with users on other XMPP servers.</description>
<port protocol="tcp" port="5269"/>
</service>

View File

@ -1,39 +0,0 @@
import cherrypy
import inspect
import cfg
cherrypy.log.error_file = cfg.status_log_file
cherrypy.log.access_file = cfg.access_log_file
cherrypy.log.screen = False
class Logger():
"""By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL."""
def log(self, msg, level="DEBUG"):
try:
username = cherrypy.session.get(cfg.session_key)
except AttributeError:
username = ''
cherrypy.log.error("%s %s %s" % (username, level, msg), inspect.stack()[2][3], 20)
def __call__(self, *args):
self.log(*args)
def debug(self, msg):
self.log(msg)
def info(self, msg):
self.log(msg, "INFO")
def warn(self, msg):
self.log(msg, "WARNING")
def warning(self, msg):
self.log(msg, "WARNING")
def error(self, msg):
self.log(msg, "ERROR")
def err(self, msg):
self.error(msg)
def critical(self, msg):
self.log(msg, "CRITICAL")

36
menu.py
View File

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

View File

@ -1,15 +0,0 @@
class User(dict):
""" Every user must have keys for a username, name, passphrase (this
is a bcrypt hash of the password), salt, groups, and an email address.
They can be blank or None, but the keys must exist. """
def __init__(self, dict=None):
for key in ['username', 'name', 'passphrase', 'salt', 'email']:
self[key] = ''
for key in ['groups']:
self[key] = []
if dict:
for key in dict:
self[key] = dict[key]
def __getattr__(self, attr):
return None

135
module_loader.py Normal file
View File

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

View File

@ -1 +0,0 @@
installed/apps/apps.py

25
modules/apps/__init__.py Normal file
View File

@ -0,0 +1,25 @@
#
# 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/>.
#
"""
Plinth module for Apps section page
"""
from . import apps
from .apps import init
__all__ = ['apps', 'init']

14
modules/apps/apps.py Normal file
View File

@ -0,0 +1,14 @@
from django.template.response import TemplateResponse
from gettext import gettext as _
import cfg
def init():
"""Initailize the apps module"""
cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
def index(request):
"""Serve the apps index page"""
return TemplateResponse(request, 'apps.html', {'title': _('Applications')})

View File

@ -1,4 +1,4 @@
{% extends 'login_nav.html' %}
{% extends 'base.html' %}
{% comment %}
#
# This file is part of Plinth.

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

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

View File

@ -1 +0,0 @@
installed/lib/auth.py

View File

@ -1 +0,0 @@
installed/lib/auth_page.py

View File

@ -1 +0,0 @@
installed/system/config.py

View File

@ -0,0 +1,27 @@
#
# 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/>.
#
"""
Plinth module for basic system configuration
"""
from . import config
from .config import init
__all__ = ['config', 'init']
DEPENDS = ['system']

View File

@ -19,20 +19,24 @@
Plinth module for configuring timezone, hostname etc.
"""
import cherrypy
from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core import validators
from django.template.response import TemplateResponse
from gettext import gettext as _
import logging
import re
import socket
import actions
import cfg
from modules.auth import require
from plugin_mount import PagePlugin
import util
LOGGER = logging.getLogger(__name__)
def get_hostname():
"""Return the hostname"""
return socket.gethostname()
@ -89,67 +93,64 @@ and must not be greater than 63 characters in length.'),
return time_zones
class Configuration(PagePlugin):
"""System configuration page"""
def __init__(self, *args, **kwargs):
del args # Unused
del kwargs # Unused
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Configure'), 'icon-cog', '/sys/config', 10)
self.register_page('sys.config')
self.menu = cfg.html_root.sys.menu.add_item(_('Configure'), 'icon-cog',
'/sys/config', 10)
@login_required
def index(request):
"""Serve the configuration form"""
status = get_status()
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
form = None
messages = []
is_expert = request.user.groups.filter(name='Expert').exists()
if request.method == 'POST' and is_expert:
form = ConfigurationForm(request.POST, prefix='configuration')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(request, status, form.cleaned_data)
status = get_status()
form = ConfigurationForm(initial=status,
prefix='configuration')
else:
form = ConfigurationForm(initial=status, prefix='configuration')
if kwargs and cfg.users.expert():
form = ConfigurationForm(kwargs, prefix='configuration')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigurationForm(initial=status,
prefix='configuration')
return TemplateResponse(request, 'config.html',
{'title': _('General Configuration'),
'form': form,
'is_expert': is_expert})
def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
'time_zone': util.slurp('/etc/timezone').rstrip()}
def _apply_changes(request, old_status, new_status):
"""Apply the form changes"""
if old_status['hostname'] != new_status['hostname']:
if not set_hostname(new_status['hostname']):
messages.error(request, _('Setting hostname failed'))
else:
form = ConfigurationForm(initial=status, prefix='configuration')
messages.success(request, _('Hostname set'))
else:
messages.info(request, _('Hostname is unchanged'))
return util.render_template(template='config',
title=_('General Configuration'),
form=form, messages=messages)
@staticmethod
def get_status():
"""Return the current status"""
return {'hostname': get_hostname(),
'time_zone': util.slurp('/etc/timezone').rstrip()}
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
if old_status['hostname'] != new_status['hostname']:
set_hostname(new_status['hostname'])
messages.append(('success', _('Hostname set')))
if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change',
[new_status['time_zone']])
del output # Unused
if error:
messages.error(request,
_('Error setting time zone - %s') % error)
else:
messages.append(('info', _('Hostname is unchanged')))
if old_status['time_zone'] != new_status['time_zone']:
output, error = actions.superuser_run('timezone-change',
[new_status['time_zone']])
del output # Unused
if error:
messages.append(('error',
_('Error setting time zone - %s') % error))
else:
messages.append(('success', _('Time zone set')))
else:
messages.append(('info', _('Time zone is unchanged')))
messages.success(request, _('Time zone set'))
else:
messages.info(request, _('Time zone is unchanged'))
def set_hostname(hostname):
@ -158,14 +159,12 @@ def set_hostname(hostname):
# valid_hostname check, convert to ASCII.
hostname = str(hostname)
cfg.log.info("Changing hostname to '%s'" % hostname)
LOGGER.info('Changing hostname to - %s', hostname)
try:
actions.superuser_run("xmpp-pre-hostname-change")
actions.superuser_run("hostname-change", hostname)
actions.superuser_run("xmpp-hostname-change", hostname, async=True)
# don't persist/cache change unless it was saved successfuly
sys_store = util.filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
except OSError as exception:
raise cherrypy.HTTPError(500,
'Updating hostname failed: %s' % exception)
actions.superuser_run('xmpp-pre-hostname-change')
actions.superuser_run('hostname-change', hostname)
actions.superuser_run('xmpp-hostname-change', hostname, async=True)
except OSError:
return False
return True

View File

@ -1,4 +1,4 @@
{% extends "login_nav.html" %}
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -22,9 +22,7 @@
{% block main_block %}
{% if cfg.users.expert %}
{% include 'messages.html' %}
{% if is_expert %}
<form class="form" method="post">
{% csrf_token %}

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

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

View File

@ -1 +0,0 @@
installed/system/diagnostics.py

View File

@ -0,0 +1,27 @@
#
# 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/>.
#
"""
Plinth module for system diagnostics
"""
from . import diagnostics
from .diagnostics import init
__all__ = ['diagnostics', 'init']
DEPENDS = ['system']

View File

@ -0,0 +1,50 @@
#
# 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/>.
#
"""
Plinth module for running diagnostics
"""
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse
from gettext import gettext as _
import actions
import cfg
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30)
@login_required
def index(request):
"""Serve the index page"""
return TemplateResponse(request, 'diagnostics.html',
{'title': _('System Diagnostics')})
@login_required
def test(request):
"""Run diagnostics and the output page"""
output, error = actions.superuser_run("diagnostic-test")
return TemplateResponse(request, 'diagnostics_test.html',
{'title': _('Diagnostic Test'),
'diagnostics_output': output,
'diagnostics_error': error})

View File

@ -1,4 +1,4 @@
{% extends 'login_nav.html' %}
{% extends 'base.html' %}
{% comment %}
#
# This file is part of Plinth.
@ -24,8 +24,8 @@
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>
<p><a class="btn btn-primary btn-large" href="{% url 'diagnostics:test' %}">
Run diagnostic test &raquo;
</a></p>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'login_nav.html' %}
{% extends 'base.html' %}
{% comment %}
#
# This file is part of Plinth.

View File

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

1
modules/enabled/apps Symbolic link
View File

@ -0,0 +1 @@
../apps/

1
modules/enabled/config Symbolic link
View File

@ -0,0 +1 @@
../config/

1
modules/enabled/diagnostics Symbolic link
View File

@ -0,0 +1 @@
../diagnostics/

1
modules/enabled/expert_mode Symbolic link
View File

@ -0,0 +1 @@
../expert_mode/

1
modules/enabled/firewall Symbolic link
View File

@ -0,0 +1 @@
../firewall/

1
modules/enabled/first_boot Symbolic link
View File

@ -0,0 +1 @@
../first_boot/

1
modules/enabled/help Symbolic link
View File

@ -0,0 +1 @@
../help/

1
modules/enabled/lib Symbolic link
View File

@ -0,0 +1 @@
../lib/

1
modules/enabled/owncloud Symbolic link
View File

@ -0,0 +1 @@
../owncloud/

1
modules/enabled/packages Symbolic link
View File

@ -0,0 +1 @@
../packages/

1
modules/enabled/pagekite Symbolic link
View File

@ -0,0 +1 @@
../pagekite/

1
modules/enabled/system Symbolic link
View File

@ -0,0 +1 @@
../system/

1
modules/enabled/tor Symbolic link
View File

@ -0,0 +1 @@
../tor/

1
modules/enabled/users Symbolic link
View File

@ -0,0 +1 @@
../users/

1
modules/enabled/xmpp Symbolic link
View File

@ -0,0 +1 @@
../xmpp/

View File

@ -1 +0,0 @@
installed/system/expert_mode.py

View File

@ -0,0 +1,27 @@
#
# 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/>.
#
"""
Plinth module for expert mode configuration
"""
from . import expert_mode
from .expert_mode import init
__all__ = ['expert_mode', 'init']
DEPENDS = ['system']

View File

@ -0,0 +1,64 @@
from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse
from gettext import gettext as _
import cfg
from ..lib.auth import get_group
class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
"""Form to configure expert mode"""
expert_mode = forms.BooleanField(
label=_('Expert Mode'), required=False)
def init():
"""Initialize the module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Expert Mode'), 'icon-cog', '/sys/expert', 10)
@login_required
def index(request):
"""Serve the configuration form"""
status = get_status(request)
form = None
if request.method == 'POST':
form = ExpertsForm(request.POST, prefix='experts')
# pylint: disable-msg=E1101
if form.is_valid():
_apply_changes(request, form.cleaned_data)
status = get_status(request)
form = ExpertsForm(initial=status, prefix='experts')
else:
form = ExpertsForm(initial=status, prefix='experts')
return TemplateResponse(request, 'expert_mode.html',
{'title': _('Expert Mode'),
'form': form})
def get_status(request):
"""Return the current status"""
return {'expert_mode': request.user.groups.filter(name='Expert').exists()}
def _apply_changes(request, new_status):
"""Apply expert mode configuration"""
message = (messages.info, _('Settings unchanged'))
expert_group = get_group('Expert')
if new_status['expert_mode']:
if not expert_group in request.user.groups.all():
request.user.groups.add(expert_group)
message = (messages.success, _('Expert mode enabled'))
else:
if expert_group in request.user.groups.all():
request.user.groups.remove(expert_group)
message = (messages.success, _('Expert mode disabled'))
message[0](request, message[1])

View File

@ -1,4 +1,4 @@
{% extends "login_nav.html" %}
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
@ -22,8 +22,6 @@
{% 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

View File

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

View File

@ -1 +0,0 @@
installed/sharing/file_explorer.py

View File

@ -1 +0,0 @@
installed/system/firewall.py

View File

@ -0,0 +1,27 @@
#
# 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/>.
#
"""
Plinth module to configure a firewall
"""
from . import firewall
from .firewall import init
__all__ = ['firewall', 'init']
DEPENDS = ['system']

View File

@ -0,0 +1,155 @@
#
# 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/>.
#
"""
Plinth module to configure a firewall
"""
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse
from gettext import gettext as _
import logging
import actions
import cfg
import service as service_module
LOGGER = logging.getLogger(__name__)
def init():
"""Initailze firewall module"""
menu = cfg.main_menu.find('/sys')
menu.add_item(_('Firewall'), 'icon-flag', '/sys/firewall', 50)
service_module.ENABLED.connect(on_service_enabled)
@login_required
def index(request):
"""Serve introcution page"""
if not get_installed_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_installed'})
if not get_enabled_status():
return TemplateResponse(request, 'firewall.html',
{'title': _('Firewall'),
'firewall_status': 'not_running'})
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
return TemplateResponse(
request, 'firewall.html',
{'title': _('Firewall'),
'services': service_module.SERVICES.values(),
'internal_enabled_services': internal_enabled_services,
'external_enabled_services': external_enabled_services})
def get_installed_status():
"""Return whether firewall is installed"""
output = _run(['get-installed'], superuser=True)
return output.split()[0] == 'installed'
def get_enabled_status():
"""Return whether firewall is installed"""
output = _run(['get-status'], superuser=True)
return output.split()[0] == 'running'
def get_enabled_services(zone):
"""Return the status of various services currently enabled"""
output = _run(['get-enabled-services', '--zone', zone], superuser=True)
return output.split()
def add_service(port, zone):
"""Enable a service in firewall"""
_run(['add-service', port, '--zone', zone], superuser=True)
def remove_service(port, zone):
"""Remove a service in firewall"""
_run(['remove-service', port, '--zone', zone], superuser=True)
def on_service_enabled(sender, service_id, enabled, **kwargs):
"""
Enable/disable firewall ports when a service is
enabled/disabled.
"""
del sender # Unused
del kwargs # Unused
internal_enabled_services = get_enabled_services(zone='internal')
external_enabled_services = get_enabled_services(zone='external')
LOGGER.info('Service enabled - %s, %s', service_id, enabled)
service = service_module.SERVICES[service_id]
for port in service.ports:
if enabled:
if port not in internal_enabled_services:
add_service(port, zone='internal')
if (service.is_external and
port not in external_enabled_services):
add_service(port, zone='external')
else:
# service already configured.
pass
else:
if port in internal_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id]
if not any(enabled_services_on_port):
remove_service(port, zone='internal')
if port in external_enabled_services:
enabled_services_on_port = [
service_.is_enabled()
for service_ in service_module.SERVICES.values()
if port in service_.ports and
service_id != service_.service_id and
service_.is_external]
if not any(enabled_services_on_port):
remove_service(port, zone='external')
def _run(arguments, superuser=False):
"""Run an given command and raise exception if there was an error"""
command = 'firewall'
LOGGER.info('Running command - %s, %s, %s', command, arguments, superuser)
if superuser:
output, error = actions.superuser_run(command, arguments)
else:
output, error = actions.run(command, arguments)
if error:
raise Exception('Error setting/getting firewalld confguration - %s'
% error)
return output

View File

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

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

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

View File

@ -1 +0,0 @@
installed/first_boot.py

View File

@ -0,0 +1,24 @@
#
# 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/>.
#
"""
Plinth module for first boot wizard
"""
from . import first_boot
__all__ = ['first_boot']

View File

@ -0,0 +1,188 @@
"""
First Boot: Initial Plinth Configuration.
See docs/design/first-connection.mdwn for details.
The Plinth first-connection process has several stages:
0. The user connects to Plinth for the first time and is redirected from
the home page to the Hello page.
1. The user sets the Box's name, the administrator's name and
password, and the box's PGP key (optional).
2. The box generates and the user receives any PGP keys.
3. The box detects the network's configuration and restarts networking.
4. The user interacts with the box normally.
"""
from django import forms
from django.contrib import messages
from django.core import validators
from django.core.urlresolvers import reverse
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from gettext import gettext as _
from ..lib.auth import add_user
from ..config import config
from withsqlite.withsqlite import sqlite_db
import cfg
## 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!'))
def index(request):
"""Serve the index first boot page"""
return state0(request)
def generate_box_key():
"""Generate a box key"""
return "fake key"
def state0(request):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
All the parameters are form inputs. They get passed in when
the form is submitted. This method checks the inputs and if
they validate, uses them to take action. If they do not
validate, it displays the form to give the user a chance to
input correct values. It might display an error message (in
the message parameter).
message is an optional string that we can display to the
user. It's a good place to put error messages.
"""
try:
if _read_state() >= 5:
return HttpResponseRedirect(reverse('index'))
except KeyError:
pass
## Until LDAP is in place, we'll put the box key in the cfg.store_file
status = get_state0()
form = None
if request.method == 'POST':
form = State0Form(request.POST, prefix='firstboot')
# pylint: disable-msg=E1101
if form.is_valid():
success = _apply_state0(request, status, form.cleaned_data)
if success:
# Everything is good, permanently mark and move to page 2
_write_state(1)
return HttpResponseRedirect(reverse('first_boot:state1'))
else:
form = State0Form(initial=status, prefix='firstboot')
return TemplateResponse(request, 'firstboot_state0.html',
{'title': _('First Boot!'),
'form': form})
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(request, old_state, new_state):
"""Apply changes in state 0 form"""
success = True
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
database:
database['about'] = 'Information about this FreedomBox'
if new_state['box_key']:
database['box_key'] = new_state['box_key']
elif not old_state['box_key']:
database['box_key'] = generate_box_key()
if old_state['hostname'] != new_state['hostname']:
config.set_hostname(new_state['hostname'])
error = add_user(new_state['username'], new_state['password'],
'First user, please change', '', True)
if error:
messages.error(
request, _('User account creation failed: %s') % error)
success = False
else:
messages.success(request, _('User account created'))
return success
def state1(request):
"""
State 1 is when we have a box name and key. In this state,
our task is to provide a certificate and maybe to guide the
user through installing it. We automatically move to State 2,
which is an HTTPS connection.
TODO: HTTPS failure in State 2 should returns to state 1.
"""
# TODO complete first_boot handling
# Make sure the user is not stuck on a dead end for now.
_write_state(5)
return TemplateResponse(request, 'firstboot_state1.html',
{'title': _('Installing the Certificate')})
def _read_state():
"""Read the current state from database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
return database['state']
def _write_state(state):
"""Write state to database"""
with sqlite_db(cfg.store_file, table='firstboot',
autocommit=True) as database:
database['state'] = state

View File

@ -24,8 +24,6 @@
<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>

View File

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

View File

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

View File

@ -1 +0,0 @@
installed/lib/forms.py

View File

@ -1 +0,0 @@
installed/help/help.py

25
modules/help/__init__.py Normal file
View File

@ -0,0 +1,25 @@
#
# 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/>.
#
"""
Plinth module for help pages
"""
from . import help # pylint: disable-msg=W0622
from .help import init
__all__ = ['help', 'init']

44
modules/help/help.py Normal file
View File

@ -0,0 +1,44 @@
import os
from gettext import gettext as _
from django.http import Http404
from django.template.response import TemplateResponse
import cfg
def init():
"""Initialize the Help module"""
menu = cfg.main_menu.add_item(_('Documentation'), 'icon-book', '/help',
101)
menu.add_item(_("Where to Get Help"), "icon-search", "/help/index/", 5)
menu.add_item(_('Developer\'s Manual'), 'icon-info-sign',
'/help/page/plinth', 10)
menu.add_item(_('FAQ'), 'icon-question-sign', '/help/page/faq', 20)
menu.add_item(_('%s Wiki' % cfg.box_name), 'icon-pencil',
'http://wiki.debian.org/FreedomBox', 30)
menu.add_item(_('About'), 'icon-star', '/help/about', 100)
def index(request):
"""Serve the index page"""
return TemplateResponse(request, 'help.html',
{'title': _('Documentation and FAQ')})
def about(request):
"""Serve the about page"""
title = _('About the {box_name}').format(box_name=cfg.box_name)
return TemplateResponse(request, 'about.html', {'title': title})
def helppage(request, page):
"""Serve a help page from the 'doc' directory"""
try:
input_file = open(os.path.join('doc', '%s.part.html' % page), 'r')
except IOError:
raise Http404
main = input_file.read()
title = _('%s Documentation') % cfg.product_name
return TemplateResponse(request, 'base.html',
{'title': title, 'main': main})

View File

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

View File

@ -1,4 +1,4 @@
{% extends 'login_nav.html' %}
{% extends 'base.html' %}
{% comment %}
#
# This file is part of Plinth.
@ -23,7 +23,7 @@
<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">
<p>This front end has a <a href="{% url 'help:helppage' 'plinth' %}">
developer's manual</a>. It isn't complete, but it is the first place
to look. Feel free to offer suggestions, edits, and screenshots for
completing it!</p>
@ -39,7 +39,7 @@ about the {{ cfg.box_name }} and almost surely know nothing of this
front end, but they are an incredible resource for general Debian
issues.</p>
<p>There is no <a href="{{ basehref }}/help/view/faq">FAQ</a> because
<p>There is no <a href="{% url 'help:helppage' 'faq' %}">FAQ</a> because
the question frequency is currently zero for all questions.</p>
{% endblock %}

33
modules/help/urls.py Normal file
View File

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

View File

@ -1 +0,0 @@
installed/router/info.py

View File

@ -1,26 +0,0 @@
import cherrypy
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import util
class Apps(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("apps")
self.menu = cfg.main_menu.add_item("Apps", "icon-download-alt", "/apps", 80)
self.menu.add_item("Chat", "icon-comment", "/../jwchat", 30)
self.menu.add_item("Photo Gallery", "icon-picture", "/apps/photos", 35)
@cherrypy.expose
def index(self):
return util.render_template(template='apps',
title=_('User Applications'))
@cherrypy.expose
@require()
def photos(self):
return util.render_template(template='photos',
title=_('Photo Gallery'))

View File

@ -1,85 +0,0 @@
import cherrypy
from django import forms
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import actions
import cfg
import service
import util
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
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('apps.owncloud')
cfg.html_root.apps.menu.add_item('Owncloud', 'icon-picture',
'/apps/owncloud', 35)
status = self.get_status()
self.service = service.Service('owncloud', _('ownCloud'),
['http', 'https'], is_external=True,
enabled=status['enabled'])
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the ownCloud configuration page"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = OwnCloudForm(kwargs, prefix='owncloud')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = OwnCloudForm(initial=status, prefix='owncloud')
else:
form = OwnCloudForm(initial=status, prefix='owncloud')
return util.render_template(template='owncloud', title=_('ownCloud'),
form=form, messages=messages)
@staticmethod
def get_status():
"""Return the current status"""
output, error = actions.run('owncloud-setup', 'status')
if error:
raise Exception('Error getting ownCloud status: %s' % error)
return {'enabled': 'enable' in output.split()}
def _apply_changes(self, old_status, new_status, messages):
"""Apply the changes"""
if old_status['enabled'] == new_status['enabled']:
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
# enabled/disabled
self.service.notify_enabled(self, new_status['enabled'])

View File

@ -1,184 +0,0 @@
"""
First Boot: Initial Plinth Configuration.
See docs/design/first-connection.mdwn for details.
The Plinth first-connection process has several stages:
0. The user connects to Plinth for the first time and is redirected from
the home page to the Hello page.
1. The user sets the Box's name, the administrator's name and
password, and the box's PGP key (optional).
2. The box generates and the user receives any PGP keys.
3. The box detects the network's configuration and restarts networking.
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):
"""First boot wizard"""
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
# this is the url this page will hang off of (/firstboot)
self.register_page('firstboot')
self.register_page('firstboot/state0')
self.register_page('firstboot/state1')
@cherrypy.expose
def index(self, *args, **kwargs):
return self.state0(*args, **kwargs)
def generate_box_key(self):
return "fake key"
@cherrypy.expose
def state0(self, **kwargs):
"""
In this state, we do time config over HTTP, name the box and
server key selection.
All the parameters are form inputs. They get passed in when
the form is submitted. This method checks the inputs and if
they validate, uses them to take action. If they do not
validate, it displays the form to give the user a chance to
input correct values. It might display an error message (in
the message parameter).
message is an optional string that we can display to the
user. It's a good place to put error messages.
"""
# 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()
form = None
messages = []
if kwargs:
form = State0Form(kwargs, prefix='firstboot')
# pylint: disable-msg=E1101
if form.is_valid():
success = self._apply_state0(status, form.cleaned_data,
messages)
if success:
# Everything is good, permanently mark and move to page 2
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:
messages.append(('success', _('User account created')))
return success
@staticmethod
@cherrypy.expose
def state1():
"""
State 1 is when we have a box name and key. In this state,
our task is to provide a certificate and maybe to guide the
user through installing it. We automatically move to State 2,
which is an HTTPS connection.
TODO: HTTPS failure in State 2 should returns to state 1.
"""
# TODO complete first_boot handling
# Make sure the user is not stuck on a dead end for now.
with sqlite_db(cfg.store_file, table='firstboot', autocommit=True) as \
database:
database['state'] = 5
return util.render_template(template='firstboot_state1',
title=_('Installing the Certificate'))

View File

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

View File

@ -1,165 +0,0 @@
# Form based authentication for CherryPy. Requires the
# Session tool to be loaded.
#
# Thanks for this code is owed to Arnar Birgisson -at - gmail.com. It
# is based on code he wrote that was retrieved from
# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
# on 1 February 2011.
import cherrypy
import urllib
from passlib.hash import bcrypt
from passlib.exc import PasswordSizeError
import cfg
import random
from model import User
cfg.session_key = '_cp_username'
def add_user(username, passphrase, name='', email='', expert=False):
"""Add a new user with specified username and passphrase.
"""
error = None
if not username: error = "Must specify a username!"
if not passphrase: error = "Must specify a passphrase!"
if error is None:
if username in map(lambda x: x[0], cfg.users.get_all()):
error = "User already exists!"
else:
try:
pass_hash = bcrypt.encrypt(passphrase)
except PasswordSizeError:
error = "Password is too long."
if error is None:
di = {
'username':username,
'name':name,
'email':email,
'expert':'on' if expert else 'off',
'groups':['expert'] if expert else [],
'passphrase':pass_hash,
'salt':pass_hash[7:29], # for bcrypt
}
new_user = User(di)
cfg.users.set(username,new_user)
if error:
cfg.log(error)
return error
def check_credentials(username, passphrase):
"""Verifies credentials for username and passphrase.
Returns None on success or a string describing the error on failure.
Handles passwords up to 4096 bytes:
>>> len("A" * 4096)
4096
>>> len(u"u|2603" * 682)
4092
"""
if not username or not passphrase:
error = "No username or password."
cfg.log(error)
return error
bad_authentication = "Bad username or password."
hashed_password = None
if username not in cfg.users or 'passphrase' not in cfg.users[username]:
cfg.log(bad_authentication)
return bad_authentication
hashed_password = cfg.users[username]['passphrase']
try:
# time-dependent comparison when non-ASCII characters are used.
if not bcrypt.verify(passphrase, hashed_password):
error = bad_authentication
else:
error = None
except PasswordSizeError:
error = bad_authentication
if error:
cfg.log(error)
return error
def check_auth(*args, **kwargs):
"""A tool that looks in config for 'auth.require'. If found and it
is not None, a login is required and the entry is evaluated as a
list of conditions that the user must fulfill"""
conditions = cherrypy.request.config.get('auth.require', None)
# format GET params
get_params = urllib.quote(cherrypy.request.request_line.split()[1])
if conditions is not None:
username = cherrypy.session.get(cfg.session_key)
if username:
cherrypy.request.login = username
for condition in conditions:
# A condition is just a callable that returns true or false
if not condition():
# Send old page as from_page parameter
raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params)
else:
# Send old page as from_page parameter
raise cherrypy.HTTPRedirect(cfg.server_dir + "/auth/login?from_page=%s" % get_params)
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
def require(*conditions):
"""A decorator that appends conditions to the auth.require config
variable."""
def decorate(f):
if not hasattr(f, '_cp_config'):
f._cp_config = dict()
if 'auth.require' not in f._cp_config:
f._cp_config['auth.require'] = []
f._cp_config['auth.require'].extend(conditions)
return f
return decorate
# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current username as cherrypy.request.login
#
# Define those at will however suits the application.
def member_of(groupname):
def check():
# replace with actual check if <username> is in <groupname>
return cherrypy.request.login == 'joe' and groupname == 'admin'
return check
def name_is(reqd_username):
return lambda: reqd_username == cherrypy.request.login
# These might be handy
def any_of(*conditions):
"""Returns True if any of the conditions match"""
def check():
for c in conditions:
if c():
return True
return False
return check
# By default all conditions are required, but this might still be
# needed if you want to use it inside of an any_of(...) condition
def all_of(*conditions):
"""Returns True if all of the conditions match"""
def check():
for c in conditions:
if not c():
return False
return True
return check

View File

@ -1,80 +0,0 @@
"""
Controller to provide login and logout actions
"""
import cherrypy
import cfg
from django import forms
from gettext import gettext as _
from plugin_mount import PagePlugin
import auth
import util
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):
"""Login and logout pages"""
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('auth')
def on_login(self, username):
"""Called on successful login"""
def on_logout(self, username):
"""Called on logout"""
@cherrypy.expose
def login(self, from_page=cfg.server_dir+"/", **kwargs):
"""Serve the login page"""
form = None
if kwargs:
form = LoginForm(kwargs, prefix='auth')
# pylint: disable-msg=E1101
if form.is_valid():
username = form.cleaned_data['username']
cherrypy.session[cfg.session_key] = username
cherrypy.request.login = username
self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or
(cfg.server_dir + "/"))
else:
form = LoginForm(prefix='auth')
return util.render_template(template='form', title=_('Login'),
form=form, submit_text=_('Login'))
@cherrypy.expose
def logout(self, from_page=cfg.server_dir+"/"):
sess = cherrypy.session
username = sess.get(cfg.session_key, None)
sess[cfg.session_key] = None
if username:
cherrypy.request.login = None
self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page or (cfg.server_dir + "/"))

View File

@ -1,60 +0,0 @@
import cherrypy
import cfg
from model import User
from plugin_mount import UserStoreModule
from withsqlite.withsqlite import sqlite_db
class UserStore(UserStoreModule, sqlite_db):
def __init__(self):
self.db_file = cfg.user_db
sqlite_db.__init__(self, self.db_file, autocommit=True, check_same_thread=False)
self.__enter__()
def close(self):
self.__exit__(None,None,None)
def current(self, name=False):
"""Return current user, if there is one, else None.
If name = True, return the username instead of the user."""
try:
username = cherrypy.session.get(cfg.session_key)
if name:
return username
else:
return self.get(username)
except AttributeError:
return None
def expert(self, username=None):
if not username:
username = self.current(name=True)
groups = self.attr(username,"groups")
if not groups:
return False
return 'expert' in groups
def attr(self, username=None, field=None):
return self.get(username)[field]
def get(self,username=None):
return User(sqlite_db.get(self,username))
def exists(self, username=None):
try:
user = self.get(username)
if not user:
return False
elif user["username"]=='':
return False
return True
except TypeError:
return False
def remove(self,username=None):
self.__delitem__(username)
def get_all(self):
return self.items()
def set(self,username=None,user=None):
sqlite_db.__setitem__(self,username, user)

View File

@ -1,28 +0,0 @@
import cherrypy
from gettext import gettext as _
from plugin_mount import PagePlugin
from modules.auth import require
import cfg
import util
class Privacy(PagePlugin):
order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("privacy")
self.menu = cfg.main_menu.add_item("Privacy", "icon-eye-open", "/privacy", 12)
self.menu.add_item("General Config", "icon-asterisk", "/privacy/config", 10)
self.menu.add_item("Ad Blocking", "icon-ban-circle", "/privacy/adblock", 20)
self.menu.add_item("HTTPS Everywhere", "icon-lock", "/privacy/https_everywhere", 30)
@cherrypy.expose
def index(self):
#raise cherrypy.InternalRedirect('/privacy/config')
return self.config()
@cherrypy.expose
@require()
def config(self):
return util.render_template(template='privacy_config',
title=_('Privacy Control Panel'))

View File

@ -1,46 +0,0 @@
{% 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

@ -1,53 +0,0 @@
#
# 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/>.
#
"""
Plinth module for configuring Tor
"""
import cherrypy
from gettext import gettext as _
from plugin_mount import PagePlugin
from modules.auth import require
import actions
import cfg
import util
class tor(PagePlugin):
order = 30 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("privacy.tor")
cfg.html_root.privacy.menu.add_item("Tor", "icon-eye-close", "/privacy/tor", 30)
@cherrypy.expose
@require()
def index(self):
output, error = actions.superuser_run("tor-get-ports")
port_info = output.split("\n")
tor_ports = {}
for line in port_info:
try:
(key, val) = line.split()
tor_ports[key] = val
except ValueError:
continue
return util.render_template(template='tor',
title=_('Tor Control Panel'),
tor_ports=tor_ports)

View File

@ -1,21 +0,0 @@
import cherrypy
from plugin_mount import PagePlugin
from modules.auth import require
import util
class Info(PagePlugin):
title = 'Info'
order = 10
url = 'info'
def __init__(self, *args, **kwargs):
self.register_page("router.info")
@cherrypy.expose
@require()
def index(self):
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>
""")

View File

@ -1,225 +0,0 @@
#
# 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/>.
#
"""
Plinth module for configuring PageKite service
"""
import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _
import actions
import cfg
from modules.auth import require
from plugin_mount import PagePlugin
import util
class PageKite(PagePlugin):
"""PageKite menu entry and introduction page"""
order = 60
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("router.setup.pagekite")
cfg.html_root.router.setup.menu.add_item(
_("Public Visibility (PageKite)"), "icon-flag",
"/router/setup/pagekite", 50)
@staticmethod
@cherrypy.expose
@require()
def index(**kwargs):
"""Serve introdution page"""
del kwargs # Unused
menu = {'title': _('PageKite'),
'items': [{'url': '/router/setup/pagekite/configure',
'text': _('Configure PageKite')}]}
sidebar_right = util.render_template(template='menu_block', menu=menu)
return util.render_template(template='pagekite_introduction',
title=_("Public Visibility (PageKite)"),
sidebar_right=sidebar_right)
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"""
order = 65
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("router.setup.pagekite.configure")
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = ConfigureForm(kwargs, prefix='pagekite')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigureForm(initial=status, prefix='pagekite')
else:
form = ConfigureForm(initial=status, prefix='pagekite')
return util.render_template(template='pagekite_configure',
title=_('Configure PageKite'), form=form,
messages=messages)
def get_status(self):
"""
Return the current status of PageKite configuration by
executing various actions.
"""
status = {}
# Check if PageKite is installed
output = self._run(['get-installed'])
cfg.log('Output - %s' % output)
if output.split()[0] != 'installed':
return None
# PageKite service enabled/disabled
output = self._run(['get-status'])
status['enabled'] = (output.split()[0] == 'enabled')
# PageKite kite details
output = self._run(['get-kite'])
kite_details = output.split()
status['kite_name'] = kite_details[0]
status['kite_secret'] = kite_details[1]
# Service status
status['service'] = {}
for service in ('http', 'ssh'):
output = self._run(['get-service-status', service])
status[service + '_enabled'] = (output.split()[0] == 'enabled')
return status
def _apply_changes(self, old_status, new_status, messages):
"""Apply the changes to PageKite configuration"""
cfg.log.info('New status is - %s' % new_status)
if old_status != new_status:
self._run(['stop'])
if old_status['enabled'] != new_status['enabled']:
if new_status['enabled']:
self._run(['set-status', 'enable'])
messages.append(('success', _('PageKite enabled')))
else:
self._run(['set-status', 'disable'])
messages.append(('success', _('PageKite disabled')))
if old_status['kite_name'] != new_status['kite_name'] or \
old_status['kite_secret'] != new_status['kite_secret']:
self._run(['set-kite', '--kite-name', new_status['kite_name'],
'--kite-secret', new_status['kite_secret']])
messages.append(('success', _('Kite details set')))
for service in ['http', 'ssh']:
if old_status[service + '_enabled'] != \
new_status[service + '_enabled']:
if new_status[service + '_enabled']:
self._run(['set-service-status', service, 'enable'])
messages.append(('success', _('Service enabled: {service}')
.format(service=service)))
else:
self._run(['set-service-status', service, 'disable'])
messages.append(('success',
_('Service disabled: {service}')
.format(service=service)))
if old_status != new_status:
self._run(['start'])
@staticmethod
def _run(arguments, superuser=True):
"""Run an given command and raise exception if there was an error"""
command = 'pagekite-configure'
cfg.log.info('Running command - %s, %s, %s' % (command, arguments,
superuser))
if superuser:
output, error = actions.superuser_run(command, arguments)
else:
output, error = actions.run(command, arguments)
if error:
raise Exception('Error setting/getting PageKite confguration - %s'
% error)
return output

View File

@ -1,150 +0,0 @@
import cherrypy
from django import forms
from gettext import gettext as _
from plugin_mount import PagePlugin
from modules.auth import require
import cfg
import util
class Router(PagePlugin):
"""Router page"""
order = 9 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, args, kwargs)
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
def index():
"""This isn't an internal redirect, because we need the url to
reflect that we've moved down into the submenu hierarchy.
Otherwise, it's hard to know which menu portion to make active
or expand or contract."""
raise cherrypy.HTTPRedirect(cfg.server_dir + '/router/setup')
@staticmethod
@cherrypy.expose
@require()
def wireless():
"""Serve the wireless page"""
return util.render_template(title="Wireless",
main="<p>wireless setup: essid, etc.</p>")
@staticmethod
@cherrypy.expose
@require()
def firewall():
"""Serve the firewall page"""
return util.render_template(title="Firewall",
main="<p>Iptables twiddling.</p>")
@staticmethod
@cherrypy.expose
@require()
def hotspot():
"""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'))])
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):
PagePlugin.__init__(self, args, kwargs)
self.register_page('router.setup')
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
@require()
def index(self, **kwargs):
"""Return the setup page"""
status = self.get_status()
form = None
messages = []
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:
form = WANForm(initial=status, prefix='router')
return util.render_template(template='router_setup',
title=_('General Router Setup'),
form=form, messages=messages)
@staticmethod
@cherrypy.expose
@require()
def ddns():
"""Return the DDNS page"""
return util.render_template(title="Dynamic DNS",
main="<p>Masquerade setup</p>")
@staticmethod
@cherrypy.expose
@require()
def mac_address():
"""Return the MAC address page"""
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')}
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply the changes"""
print 'Apply changes - %s, %s', old_status, new_status
if old_status['connection_type'] == new_status['connection_type']:
return
store = util.filedict_con(cfg.store_file, 'router')
store['connection_type'] = new_status['connection_type']
messages.append(('success', _('Connection type set')))
messages.append(('info', _('IP address settings unimplemented')))

View File

@ -1,117 +0,0 @@
{% 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 %}
<div class="well sidebar-nav">
<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 %}
</div>
{% 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

@ -1,23 +0,0 @@
import cherrypy
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import util
class Services(PagePlugin):
order = 9 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("services")
self.menu = cfg.main_menu.add_item("Services", "icon-list", "/services", 90)
self.menu.add_item("Open ID", "icon-user", "/services/openid", 35)
@cherrypy.expose
def index(self):
return self.openid()
@cherrypy.expose
@require()
def openid(self):
return util.render_template(template='openid', title="Open ID")

View File

@ -1,189 +0,0 @@
import cherrypy
from django import forms
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import actions
import service
import util
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):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('services.xmpp')
cfg.html_root.services.menu.add_item('XMPP', 'icon-comment',
'/services/xmpp', 40)
self.client_service = service.Service(
'xmpp-client', _('Chat Server - client connections'),
is_external=True, enabled=True)
self.server_service = service.Service(
'xmpp-server', _('Chat Server - server connections'),
is_external=True, enabled=True)
self.bosh_service = service.Service(
'xmpp-bosh', _('Chat Server - web interface'), is_external=True,
enabled=True)
@staticmethod
@cherrypy.expose
@require()
def index(**kwargs):
"""Serve XMPP page"""
del kwargs # Unused
main = "<p>XMPP Server Accounts and Configuration</p>"
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
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
form = None
messages = []
if kwargs:
form = ConfigureForm(kwargs, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = ConfigureForm(initial=status, prefix='xmpp')
else:
form = ConfigureForm(initial=status, prefix='xmpp')
sidebar_right = util.render_template(template='menu_block',
menu=SIDE_MENU)
return util.render_template(template='xmpp_configure',
title=_('Configure XMPP Server'),
form=form, messages=messages,
sidebar_right=sidebar_right)
@staticmethod
def get_status():
"""Return the current status"""
output, error = actions.run('xmpp-setup', 'status')
if error:
raise Exception('Error getting status: %s' % error)
return {'inband_enabled': 'inband_enable' in output.split()}
@staticmethod
def sidebar_right(**kwargs):
"""Return rendered string for sidebar on the right"""
del kwargs # Unused
return util.render_template(template='menu_block', menu=SIDE_MENU)
@staticmethod
def _apply_changes(old_status, new_status, messages):
"""Apply the form changes"""
cfg.log.info('Status - %s, %s' % (old_status, new_status))
if old_status['inband_enabled'] == new_status['inband_enabled']:
messages.append(('info', _('Setting unchanged')))
return
if new_status['inband_enabled']:
messages.append(('success', _('Inband registration enabled')))
option = 'inband_enable'
else:
messages.append(('success', _('Inband registration disabled')))
option = 'noinband_enable'
cfg.log.info('Option - %s' % option)
_output, error = actions.superuser_run('xmpp-setup', [option])
del _output
if error:
raise Exception('Error running command - %s' % error)
class RegisterForm(forms.Form): # pylint: disable-msg=W0232
"""Configuration form"""
username = forms.CharField(label=_('Username'))
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput())
class Register(PagePlugin):
"""User registration page"""
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('services.xmpp.register')
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the registration form"""
form = None
messages = []
if kwargs:
form = RegisterForm(kwargs, prefix='xmpp')
# pylint: disable-msg=E1101
if form.is_valid():
self._register_user(form.cleaned_data, messages)
form = RegisterForm(prefix='xmpp')
else:
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,19 +0,0 @@
import cherrypy
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import util
class FileExplorer(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sharing.explorer")
cfg.html_root.sharing.menu.add_item("File Explorer", "icon-folder-open", "/sharing/explorer", 30)
@cherrypy.expose
@require()
def index(self):
return util.render_template(template='file_explorer',
title=_('File Explorer'))

View File

@ -1,44 +0,0 @@
import cherrypy
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import util
class Sharing(PagePlugin):
order = 9 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sharing")
self.menu = cfg.main_menu.add_item("Sharing", "icon-share-alt", "/sharing", 35)
self.menu.add_item("File Server", "icon-inbox", "/sharing/files", 10)
@cherrypy.expose
def index(self):
"""This isn't an internal redirect, because we need the url to
reflect that we've moved down into the submenu hierarchy.
Otherwise, it's hard to know which menu portion to make active
or expand or contract."""
raise cherrypy.HTTPRedirect(cfg.server_dir + '/sharing/files')
@cherrypy.expose
@require()
def files(self):
return util.render_template(template='sharing',
title=_('File Server'))
#TODO: move PrinterSharing to another file, as it should be an optional module (most people don't care about printer sharing)
class PrinterSharing(PagePlugin):
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sharing.printer")
cfg.html_root.sharing.menu.add_item("Printer Sharing", "icon-print", "/sharing/printer", 35)
@cherrypy.expose
@require()
def index(self):
return util.render_template(template='sharing_printer',
title=_('Printer Sharing'))

View File

@ -1,42 +0,0 @@
{% 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

@ -1,57 +0,0 @@
#
# 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/>.
#
"""
Plinth module for running diagnostics
"""
import cherrypy
from gettext import gettext as _
from auth import require
from plugin_mount import PagePlugin
import actions
import cfg
import util
class diagnostics(PagePlugin):
order = 30
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.diagnostics")
cfg.html_root.sys.menu.add_item("Diagnostics", "icon-screenshot", "/sys/diagnostics", 30)
@cherrypy.expose
@require()
def index(self):
return util.render_template(template='diagnostics',
title=_('System Diagnostics'))
class test(PagePlugin):
order = 31
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.diagnostics.test")
@cherrypy.expose
@require()
def index(self):
output, error = actions.superuser_run("diagnostic-test")
return util.render_template(template='diagnostics_test',
title=_('Diagnostic Test'),
diagnostics_output=output,
diagnostics_error=error)

View File

@ -1,79 +0,0 @@
import cherrypy
from django import forms
from gettext import gettext as _
from modules.auth import require
from plugin_mount import PagePlugin
import cfg
import util
class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
"""Form to configure expert mode"""
expert_mode = forms.BooleanField(
label=_('Expert Mode'), required=False)
# XXX: Only present due to issue with submitting empty form
dummy = forms.CharField(label='Dummy', initial='dummy',
widget=forms.HiddenInput())
class Experts(PagePlugin):
"""Expert forms page"""
order = 60
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.config.expert')
cfg.html_root.sys.config.menu.add_item(_('Expert mode'), 'icon-cog',
'/sys/config/expert', 10)
@cherrypy.expose
@require()
def index(self, **kwargs):
"""Serve the configuration form"""
status = self.get_status()
cfg.log.info('Args - %s' % kwargs)
form = None
messages = []
if kwargs:
form = ExpertsForm(kwargs, prefix='experts')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(form.cleaned_data, messages)
status = self.get_status()
form = ExpertsForm(initial=status, prefix='experts')
else:
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']:
user['groups'].append('expert')
message = ('success', _('Expert mode enabled'))
else:
if 'expert' in user['groups']:
user['groups'].remove('expert')
message = ('success', _('Expert mode disabled'))
cfg.users.set(user['username'], user)
messages.append(message)

View File

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

View File

@ -1,133 +0,0 @@
import cherrypy
from django import forms
from gettext import gettext as _
from auth import require
from plugin_mount import PagePlugin
import actions
import cfg
import util
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
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page('sys.packages')
cfg.html_root.sys.menu.add_item('Package Manager', 'icon-gift',
'/sys/packages', 20)
@cherrypy.expose
@require()
def index(self, *args, **kwargs):
"""Serve the form"""
del args # Unused
status = self.get_status()
form = None
messages = []
if kwargs:
form = PackagesForm(kwargs, prefix='packages')
# pylint: disable-msg=E1101
if form.is_valid():
self._apply_changes(status, form.cleaned_data, messages)
status = self.get_status()
form = PackagesForm(initial=status, prefix='packages')
else:
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:
messages.append(
('success', _('Module enabled - {module}').format(
module=module)))
else:
output, error = actions.superuser_run(
'module-manager', ['disable', cfg.python_root, module])
del output # Unused
# TODO: need a smoother way for plinth to unload the
# module
if error:
messages.append(
('error',
_('Error disabling module - {module}').format(
module=module)))
else:
messages.append(
('success', _('Module disabled - {module}').format(
module=module)))

View File

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

View File

@ -1,52 +0,0 @@
{% 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,172 +0,0 @@
import cherrypy
from django import forms
from django.core import validators
from gettext import gettext as _
import auth
from auth import require
from plugin_mount import PagePlugin
import cfg
from model import User
import util
class Users(PagePlugin):
order = 20 # order of running init in PagePlugins
def __init__(self, *args, **kwargs):
PagePlugin.__init__(self, *args, **kwargs)
self.register_page("sys.users")
@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
@require()
def index(self, **kwargs):
"""Serve the form"""
form = None
messages = []
if kwargs:
form = UserAddForm(kwargs, prefix='user')
# pylint: disable-msg=E1101
if form.is_valid():
self._add_user(form.cleaned_data, messages)
form = UserAddForm(prefix='user')
else:
form = UserAddForm(prefix='user')
return util.render_template(template='users_add', title=_('Add User'),
form=form, messages=messages)
@staticmethod
def _add_user(data, messages):
"""Add a user"""
if cfg.users.exists(data['username']):
messages.append(
('error', _('User "{username}" already exists').format(
username=data['username'])))
return
auth.add_user(data['username'], data['password'], data['full_name'],
data['email'], False)
messages.append(
('success', _('User "{username}" added').format(
username=data['username'])))
class UserEditForm(forms.Form): # pylint: disable-msg=W0232
"""Form to edit/delete a user"""
def __init__(self, *args, **kwargs):
# pylint: disable-msg=E1002
super(forms.Form, self).__init__(*args, **kwargs)
users = cfg.users.get_all()
for uname in users:
user = User(uname[1])
label = '%s (%s)' % (user['name'], user['username'])
field = forms.BooleanField(label=label, required=False)
# pylint: disable-msg=E1101
self.fields['delete_user_' + user['username']] = field
class UserEdit(PagePlugin):
"""User edit page"""
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)))

Some files were not shown because too many files have changed in this diff Show More