diff --git a/.gitignore b/.gitignore index a30ed9e0c..3b9739d04 100644 --- a/.gitignore +++ b/.gitignore @@ -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* diff --git a/COPYING b/COPYING index 694d4547f..dc63b35dc 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -# License to Copy Plinth +# License to Copy Plinth Plinth is Copyright 2011-2013 James Vasile (). 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 diff --git a/LICENSES b/LICENSES index 27ecd15dd..06d7d8122 100644 --- a/LICENSES +++ b/LICENSES @@ -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%20GNU%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 :: - diff --git a/Makefile b/Makefile index bf6f8c021..59c53dab5 100644 --- a/Makefile +++ b/Makefile @@ -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 {} \; diff --git a/cfg.py b/cfg.py index 01aeee2d7..efda8373d 100644 --- a/cfg.py +++ b/cfg.py @@ -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] diff --git a/filedict.py b/filedict.py deleted file mode 100644 index 94f779759..000000000 --- a/filedict.py +++ /dev/null @@ -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 diff --git a/lib/firewalld/services/privoxy.xml b/lib/firewalld/services/privoxy.xml deleted file mode 100644 index e456a8f9c..000000000 --- a/lib/firewalld/services/privoxy.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Privoxy - A Privacy Enhancing Proxy Server - 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. - - diff --git a/lib/firewalld/services/tor-socks.xml b/lib/firewalld/services/tor-socks.xml deleted file mode 100644 index b6f7cad4f..000000000 --- a/lib/firewalld/services/tor-socks.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Tor - SOCKS Proxy - 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. - - diff --git a/lib/firewalld/services/xmpp-bosh.xml b/lib/firewalld/services/xmpp-bosh.xml deleted file mode 100644 index 039f9cc3b..000000000 --- a/lib/firewalld/services/xmpp-bosh.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - XMPP (Jabber) web client - 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. - - diff --git a/lib/firewalld/services/xmpp-client.xml b/lib/firewalld/services/xmpp-client.xml deleted file mode 100644 index d31bd537c..000000000 --- a/lib/firewalld/services/xmpp-client.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - XMPP (Jabber) client - 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. - - diff --git a/lib/firewalld/services/xmpp-server.xml b/lib/firewalld/services/xmpp-server.xml deleted file mode 100644 index bf8a2ea6f..000000000 --- a/lib/firewalld/services/xmpp-server.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - XMPP (Jabber) server - 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. - - diff --git a/logger.py b/logger.py deleted file mode 100644 index 6fd8171fa..000000000 --- a/logger.py +++ /dev/null @@ -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") diff --git a/menu.py b/menu.py index 84b26c19d..bcda4fd73 100644 --- a/menu.py +++ b/menu.py @@ -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 diff --git a/model.py b/model.py deleted file mode 100644 index 6ad424e23..000000000 --- a/model.py +++ /dev/null @@ -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 diff --git a/module_loader.py b/module_loader.py new file mode 100644 index 000000000..ad5214776 --- /dev/null +++ b/module_loader.py @@ -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 . +# + +""" +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 diff --git a/modules/apps.py b/modules/apps.py deleted file mode 120000 index d69ec8059..000000000 --- a/modules/apps.py +++ /dev/null @@ -1 +0,0 @@ -installed/apps/apps.py \ No newline at end of file diff --git a/modules/apps/__init__.py b/modules/apps/__init__.py new file mode 100644 index 000000000..28d22ea0b --- /dev/null +++ b/modules/apps/__init__.py @@ -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 . +# + +""" +Plinth module for Apps section page +""" + +from . import apps +from .apps import init + +__all__ = ['apps', 'init'] diff --git a/modules/apps/apps.py b/modules/apps/apps.py new file mode 100644 index 000000000..a5ae5596f --- /dev/null +++ b/modules/apps/apps.py @@ -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')}) diff --git a/modules/installed/apps/templates/apps.html b/modules/apps/templates/apps.html similarity index 97% rename from modules/installed/apps/templates/apps.html rename to modules/apps/templates/apps.html index a05062282..9dd2a0df2 100644 --- a/modules/installed/apps/templates/apps.html +++ b/modules/apps/templates/apps.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/apps/urls.py b/modules/apps/urls.py new file mode 100644 index 000000000..b47f44cde --- /dev/null +++ b/modules/apps/urls.py @@ -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 . +# + +""" +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') + ) diff --git a/modules/auth.py b/modules/auth.py deleted file mode 120000 index 7a4c6eccd..000000000 --- a/modules/auth.py +++ /dev/null @@ -1 +0,0 @@ -installed/lib/auth.py \ No newline at end of file diff --git a/modules/auth_page.py b/modules/auth_page.py deleted file mode 120000 index 7ce3ca22a..000000000 --- a/modules/auth_page.py +++ /dev/null @@ -1 +0,0 @@ -installed/lib/auth_page.py \ No newline at end of file diff --git a/modules/config.py b/modules/config.py deleted file mode 120000 index 2e37164c3..000000000 --- a/modules/config.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/config.py \ No newline at end of file diff --git a/modules/config/__init__.py b/modules/config/__init__.py new file mode 100644 index 000000000..9e8ba0667 --- /dev/null +++ b/modules/config/__init__.py @@ -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 . +# + +""" +Plinth module for basic system configuration +""" + +from . import config +from .config import init + +__all__ = ['config', 'init'] + +DEPENDS = ['system'] diff --git a/modules/installed/system/config.py b/modules/config/config.py similarity index 50% rename from modules/installed/system/config.py rename to modules/config/config.py index 808681f2e..4aa6f299e 100644 --- a/modules/installed/system/config.py +++ b/modules/config/config.py @@ -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 diff --git a/modules/installed/system/templates/config.html b/modules/config/templates/config.html similarity index 92% rename from modules/installed/system/templates/config.html rename to modules/config/templates/config.html index 5e5310481..9f4e4f68a 100644 --- a/modules/installed/system/templates/config.html +++ b/modules/config/templates/config.html @@ -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 %}
{% csrf_token %} diff --git a/modules/config/urls.py b/modules/config/urls.py new file mode 100644 index 000000000..e80f518dc --- /dev/null +++ b/modules/config/urls.py @@ -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 . +# + +""" +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'), + ) diff --git a/modules/diagnostics.py b/modules/diagnostics.py deleted file mode 120000 index 31b7abf86..000000000 --- a/modules/diagnostics.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/diagnostics.py \ No newline at end of file diff --git a/modules/diagnostics/__init__.py b/modules/diagnostics/__init__.py new file mode 100644 index 000000000..5346b6ae7 --- /dev/null +++ b/modules/diagnostics/__init__.py @@ -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 . +# + +""" +Plinth module for system diagnostics +""" + +from . import diagnostics +from .diagnostics import init + +__all__ = ['diagnostics', 'init'] + +DEPENDS = ['system'] diff --git a/modules/diagnostics/diagnostics.py b/modules/diagnostics/diagnostics.py new file mode 100644 index 000000000..854bdf2ed --- /dev/null +++ b/modules/diagnostics/diagnostics.py @@ -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 . +# + +""" +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}) diff --git a/modules/installed/system/templates/diagnostics.html b/modules/diagnostics/templates/diagnostics.html similarity index 86% rename from modules/installed/system/templates/diagnostics.html rename to modules/diagnostics/templates/diagnostics.html index afb597e34..c85203f08 100644 --- a/modules/installed/system/templates/diagnostics.html +++ b/modules/diagnostics/templates/diagnostics.html @@ -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.

-

Run diagnostic test -»

+

+ Run diagnostic test » +

{% endblock %} diff --git a/modules/installed/system/templates/diagnostics_test.html b/modules/diagnostics/templates/diagnostics_test.html similarity index 97% rename from modules/installed/system/templates/diagnostics_test.html rename to modules/diagnostics/templates/diagnostics_test.html index 93012ab24..04f5b0036 100644 --- a/modules/installed/system/templates/diagnostics_test.html +++ b/modules/diagnostics/templates/diagnostics_test.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/diagnostics/urls.py b/modules/diagnostics/urls.py new file mode 100644 index 000000000..4ed974f9b --- /dev/null +++ b/modules/diagnostics/urls.py @@ -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 . +# + +""" +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'), + ) diff --git a/modules/enabled/apps b/modules/enabled/apps new file mode 120000 index 000000000..ee655a64d --- /dev/null +++ b/modules/enabled/apps @@ -0,0 +1 @@ +../apps/ \ No newline at end of file diff --git a/modules/enabled/config b/modules/enabled/config new file mode 120000 index 000000000..408852685 --- /dev/null +++ b/modules/enabled/config @@ -0,0 +1 @@ +../config/ \ No newline at end of file diff --git a/modules/enabled/diagnostics b/modules/enabled/diagnostics new file mode 120000 index 000000000..44f76bee7 --- /dev/null +++ b/modules/enabled/diagnostics @@ -0,0 +1 @@ +../diagnostics/ \ No newline at end of file diff --git a/modules/enabled/expert_mode b/modules/enabled/expert_mode new file mode 120000 index 000000000..0006eedc0 --- /dev/null +++ b/modules/enabled/expert_mode @@ -0,0 +1 @@ +../expert_mode/ \ No newline at end of file diff --git a/modules/enabled/firewall b/modules/enabled/firewall new file mode 120000 index 000000000..ecb364d05 --- /dev/null +++ b/modules/enabled/firewall @@ -0,0 +1 @@ +../firewall/ \ No newline at end of file diff --git a/modules/enabled/first_boot b/modules/enabled/first_boot new file mode 120000 index 000000000..b977d6c94 --- /dev/null +++ b/modules/enabled/first_boot @@ -0,0 +1 @@ +../first_boot/ \ No newline at end of file diff --git a/modules/enabled/help b/modules/enabled/help new file mode 120000 index 000000000..76c213eda --- /dev/null +++ b/modules/enabled/help @@ -0,0 +1 @@ +../help/ \ No newline at end of file diff --git a/modules/enabled/lib b/modules/enabled/lib new file mode 120000 index 000000000..5bf80bf13 --- /dev/null +++ b/modules/enabled/lib @@ -0,0 +1 @@ +../lib/ \ No newline at end of file diff --git a/modules/enabled/owncloud b/modules/enabled/owncloud new file mode 120000 index 000000000..b9d942937 --- /dev/null +++ b/modules/enabled/owncloud @@ -0,0 +1 @@ +../owncloud/ \ No newline at end of file diff --git a/modules/enabled/packages b/modules/enabled/packages new file mode 120000 index 000000000..f314f73fa --- /dev/null +++ b/modules/enabled/packages @@ -0,0 +1 @@ +../packages/ \ No newline at end of file diff --git a/modules/enabled/pagekite b/modules/enabled/pagekite new file mode 120000 index 000000000..1c4c9c144 --- /dev/null +++ b/modules/enabled/pagekite @@ -0,0 +1 @@ +../pagekite/ \ No newline at end of file diff --git a/modules/enabled/system b/modules/enabled/system new file mode 120000 index 000000000..16f8cc2b2 --- /dev/null +++ b/modules/enabled/system @@ -0,0 +1 @@ +../system/ \ No newline at end of file diff --git a/modules/enabled/tor b/modules/enabled/tor new file mode 120000 index 000000000..f7495e4ab --- /dev/null +++ b/modules/enabled/tor @@ -0,0 +1 @@ +../tor/ \ No newline at end of file diff --git a/modules/enabled/users b/modules/enabled/users new file mode 120000 index 000000000..db9284fbb --- /dev/null +++ b/modules/enabled/users @@ -0,0 +1 @@ +../users/ \ No newline at end of file diff --git a/modules/enabled/xmpp b/modules/enabled/xmpp new file mode 120000 index 000000000..686474fe7 --- /dev/null +++ b/modules/enabled/xmpp @@ -0,0 +1 @@ +../xmpp/ \ No newline at end of file diff --git a/modules/expert_mode.py b/modules/expert_mode.py deleted file mode 120000 index aaa140ed1..000000000 --- a/modules/expert_mode.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/expert_mode.py \ No newline at end of file diff --git a/modules/expert_mode/__init__.py b/modules/expert_mode/__init__.py new file mode 100644 index 000000000..071bd92a6 --- /dev/null +++ b/modules/expert_mode/__init__.py @@ -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 . +# + +""" +Plinth module for expert mode configuration +""" + +from . import expert_mode +from .expert_mode import init + +__all__ = ['expert_mode', 'init'] + +DEPENDS = ['system'] diff --git a/modules/expert_mode/expert_mode.py b/modules/expert_mode/expert_mode.py new file mode 100644 index 000000000..0c213625f --- /dev/null +++ b/modules/expert_mode/expert_mode.py @@ -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]) diff --git a/modules/installed/system/templates/expert_mode.html b/modules/expert_mode/templates/expert_mode.html similarity index 96% rename from modules/installed/system/templates/expert_mode.html rename to modules/expert_mode/templates/expert_mode.html index c6df98b36..8e447ffbb 100644 --- a/modules/installed/system/templates/expert_mode.html +++ b/modules/expert_mode/templates/expert_mode.html @@ -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' %} -

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 diff --git a/modules/expert_mode/urls.py b/modules/expert_mode/urls.py new file mode 100644 index 000000000..ffb97da53 --- /dev/null +++ b/modules/expert_mode/urls.py @@ -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 . +# + +""" +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'), + ) diff --git a/modules/file_explorer.py b/modules/file_explorer.py deleted file mode 120000 index f19191667..000000000 --- a/modules/file_explorer.py +++ /dev/null @@ -1 +0,0 @@ -installed/sharing/file_explorer.py \ No newline at end of file diff --git a/modules/firewall.py b/modules/firewall.py deleted file mode 120000 index 1d198303f..000000000 --- a/modules/firewall.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/firewall.py \ No newline at end of file diff --git a/modules/firewall/__init__.py b/modules/firewall/__init__.py new file mode 100644 index 000000000..3e3718c6c --- /dev/null +++ b/modules/firewall/__init__.py @@ -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 . +# + +""" +Plinth module to configure a firewall +""" + +from . import firewall +from .firewall import init + +__all__ = ['firewall', 'init'] + +DEPENDS = ['system'] diff --git a/modules/firewall/firewall.py b/modules/firewall/firewall.py new file mode 100644 index 000000000..e75b9fdb2 --- /dev/null +++ b/modules/firewall/firewall.py @@ -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 . +# + +""" +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 diff --git a/modules/installed/system/templates/firewall.html b/modules/firewall/templates/firewall.html similarity index 97% rename from modules/installed/system/templates/firewall.html rename to modules/firewall/templates/firewall.html index 19c165a46..34684d595 100644 --- a/modules/installed/system/templates/firewall.html +++ b/modules/firewall/templates/firewall.html @@ -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.

-

The following the current status:

+

The following is the current status:

{% if firewall_status = 'not_installed' %}

Firewall is not installed. Please install it. Firewall comes diff --git a/modules/firewall/urls.py b/modules/firewall/urls.py new file mode 100644 index 000000000..be2f1494a --- /dev/null +++ b/modules/firewall/urls.py @@ -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 . +# + +""" +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') + ) diff --git a/modules/first_boot.py b/modules/first_boot.py deleted file mode 120000 index ae5c55471..000000000 --- a/modules/first_boot.py +++ /dev/null @@ -1 +0,0 @@ -installed/first_boot.py \ No newline at end of file diff --git a/modules/first_boot/__init__.py b/modules/first_boot/__init__.py new file mode 100644 index 000000000..b9a2a74f5 --- /dev/null +++ b/modules/first_boot/__init__.py @@ -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 . +# + +""" +Plinth module for first boot wizard +""" + +from . import first_boot + +__all__ = ['first_boot'] diff --git a/modules/first_boot/first_boot.py b/modules/first_boot/first_boot.py new file mode 100644 index 000000000..f784c507b --- /dev/null +++ b/modules/first_boot/first_boot.py @@ -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 diff --git a/modules/installed/templates/firstboot_sidebar.html b/modules/first_boot/templates/firstboot_sidebar.html similarity index 100% rename from modules/installed/templates/firstboot_sidebar.html rename to modules/first_boot/templates/firstboot_sidebar.html diff --git a/modules/installed/templates/firstboot_state0.html b/modules/first_boot/templates/firstboot_state0.html similarity index 98% rename from modules/installed/templates/firstboot_state0.html rename to modules/first_boot/templates/firstboot_state0.html index 656fe406f..02d47cde6 100644 --- a/modules/installed/templates/firstboot_state0.html +++ b/modules/first_boot/templates/firstboot_state0.html @@ -24,8 +24,6 @@

Welcome to Your FreedomBox!

- {% include 'messages.html' %} -

Welcome. It looks like this FreedomBox isn't set up yet. We'll need to ask you a just few questions to get started.

diff --git a/modules/installed/templates/firstboot_state1.html b/modules/first_boot/templates/firstboot_state1.html similarity index 94% rename from modules/installed/templates/firstboot_state1.html rename to modules/first_boot/templates/firstboot_state1.html index 5ea907ff9..fdc675f39 100644 --- a/modules/installed/templates/firstboot_state1.html +++ b/modules/first_boot/templates/firstboot_state1.html @@ -23,7 +23,7 @@ {% block main_block %}

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

    diff --git a/modules/first_boot/urls.py b/modules/first_boot/urls.py new file mode 100644 index 000000000..68669e6e9 --- /dev/null +++ b/modules/first_boot/urls.py @@ -0,0 +1,30 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +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') + ) diff --git a/modules/forms.py b/modules/forms.py deleted file mode 120000 index 4b0a50792..000000000 --- a/modules/forms.py +++ /dev/null @@ -1 +0,0 @@ -installed/lib/forms.py \ No newline at end of file diff --git a/modules/help.py b/modules/help.py deleted file mode 120000 index 3fc0799d2..000000000 --- a/modules/help.py +++ /dev/null @@ -1 +0,0 @@ -installed/help/help.py \ No newline at end of file diff --git a/modules/help/__init__.py b/modules/help/__init__.py new file mode 100644 index 000000000..27adfedfb --- /dev/null +++ b/modules/help/__init__.py @@ -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 . +# + +""" +Plinth module for help pages +""" + +from . import help # pylint: disable-msg=W0622 +from .help import init + +__all__ = ['help', 'init'] diff --git a/modules/help/help.py b/modules/help/help.py new file mode 100644 index 000000000..30ca646d4 --- /dev/null +++ b/modules/help/help.py @@ -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}) diff --git a/modules/installed/help/templates/about.html b/modules/help/templates/about.html similarity index 95% rename from modules/installed/help/templates/about.html rename to modules/help/templates/about.html index 2f4a39b81..973751fcf 100644 --- a/modules/installed/help/templates/about.html +++ b/modules/help/templates/about.html @@ -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 %} -

    We live in a world where our use of the network is mediated by diff --git a/modules/installed/help/templates/help.html b/modules/help/templates/help.html similarity index 90% rename from modules/installed/help/templates/help.html rename to modules/help/templates/help.html index 17aaedea4..6269ecfa7 100644 --- a/modules/installed/help/templates/help.html +++ b/modules/help/templates/help.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. @@ -23,7 +23,7 @@

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

    -

    This front end has a +

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

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

    -

    There is no FAQ because +

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

    {% endblock %} diff --git a/modules/help/urls.py b/modules/help/urls.py new file mode 100644 index 000000000..6d61537f2 --- /dev/null +++ b/modules/help/urls.py @@ -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 . +# + +""" +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'), +) diff --git a/modules/info.py b/modules/info.py deleted file mode 120000 index 0cc405209..000000000 --- a/modules/info.py +++ /dev/null @@ -1 +0,0 @@ -installed/router/info.py \ No newline at end of file diff --git a/modules/installed/apps/apps.py b/modules/installed/apps/apps.py deleted file mode 100644 index 43f8fcafd..000000000 --- a/modules/installed/apps/apps.py +++ /dev/null @@ -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')) diff --git a/modules/installed/apps/owncloud.py b/modules/installed/apps/owncloud.py deleted file mode 100644 index 7ed4dfcdb..000000000 --- a/modules/installed/apps/owncloud.py +++ /dev/null @@ -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']) diff --git a/modules/installed/first_boot.py b/modules/installed/first_boot.py deleted file mode 100644 index 8fdde7b31..000000000 --- a/modules/installed/first_boot.py +++ /dev/null @@ -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')) diff --git a/modules/installed/help/help.py b/modules/installed/help/help.py deleted file mode 100644 index 927f9e6c6..000000000 --- a/modules/installed/help/help.py +++ /dev/null @@ -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) diff --git a/modules/installed/lib/auth.py b/modules/installed/lib/auth.py deleted file mode 100644 index 974ecef93..000000000 --- a/modules/installed/lib/auth.py +++ /dev/null @@ -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 is in - 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 - - diff --git a/modules/installed/lib/auth_page.py b/modules/installed/lib/auth_page.py deleted file mode 100644 index 1d9b49ca3..000000000 --- a/modules/installed/lib/auth_page.py +++ /dev/null @@ -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 + "/")) diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py deleted file mode 100644 index 41d13f947..000000000 --- a/modules/installed/lib/user_store.py +++ /dev/null @@ -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) diff --git a/modules/installed/privacy/privacy.py b/modules/installed/privacy/privacy.py deleted file mode 100644 index 8de533f88..000000000 --- a/modules/installed/privacy/privacy.py +++ /dev/null @@ -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')) diff --git a/modules/installed/privacy/templates/privacy_config.html b/modules/installed/privacy/templates/privacy_config.html deleted file mode 100644 index cc3becb15..000000000 --- a/modules/installed/privacy/templates/privacy_config.html +++ /dev/null @@ -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 . -# -{% endcomment %} - -{% block main_block %} - -

    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.

    - -{% endblock %} - -{% block sidebar_right_block %} - - - -{% endblock %} diff --git a/modules/installed/privacy/tor.py b/modules/installed/privacy/tor.py deleted file mode 100644 index 404553629..000000000 --- a/modules/installed/privacy/tor.py +++ /dev/null @@ -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 . -# - -""" -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) diff --git a/modules/installed/router/info.py b/modules/installed/router/info.py deleted file mode 100644 index cc9c5539a..000000000 --- a/modules/installed/router/info.py +++ /dev/null @@ -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=""" -

    Eventually we will display a bunch of info, graphs and logs about -the routing functions here.

    -""") diff --git a/modules/installed/router/pagekite.py b/modules/installed/router/pagekite.py deleted file mode 100644 index 799a8860d..000000000 --- a/modules/installed/router/pagekite.py +++ /dev/null @@ -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 . -# - -""" -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 \ -http://mybox1-myacc.pagekite.me \ -')) - - ssh_enabled = forms.BooleanField( - label=_('Secure Shell (SSH)'), required=False, - help_text=_('See SSH client setup instructions')) - - -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 diff --git a/modules/installed/router/router.py b/modules/installed/router/router.py deleted file mode 100644 index 2cc75d980..000000000 --- a/modules/installed/router/router.py +++ /dev/null @@ -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="

    wireless setup: essid, etc.

    ") - - @staticmethod - @cherrypy.expose - @require() - def firewall(): - """Serve the firewall page""" - return util.render_template(title="Firewall", - main="

    Iptables twiddling.

    ") - - @staticmethod - @cherrypy.expose - @require() - def hotspot(): - """Serve the hotspot page""" - return util.render_template(title="Hotspot and Mesh", - main="

    connection sharing setup.

    ") - - -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="

    Masquerade setup

    ") - - @staticmethod - @cherrypy.expose - @require() - def mac_address(): - """Return the MAC address page""" - return util.render_template( - title="MAC Address Cloning", - main="

    Your router can pretend to have a different MAC address \ -on any interface.

    ") - - @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'))) diff --git a/modules/installed/router/templates/router_setup.html b/modules/installed/router/templates/router_setup.html deleted file mode 100644 index c484c5585..000000000 --- a/modules/installed/router/templates/router_setup.html +++ /dev/null @@ -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 . -# -{% endcomment %} - -{% block main_block %} - -{% if cfg.users.expert %} - -

    WAN Connection

    - - {% include 'messages.html' %} - - - {% csrf_token %} - - {% include 'bootstrapform/field.html' with field=form.connection_type %} - -
    - {% 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 %} -
    - - - - - -{% else %} - -

    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.

    - -

    If that fails, you might need to resort to the expert options. - Enable expert mode in the "{{ cfg.product_name }} / System / - Configure" menu.

    - -{% endif %} - -{% endblock %} - -{% block sidebar_right_block %} - - - -{% endblock %} - -{% block js_block %} - {{ js|safe }} - - - -{% endblock %} diff --git a/modules/installed/services/services.py b/modules/installed/services/services.py deleted file mode 100644 index 0bf5fb698..000000000 --- a/modules/installed/services/services.py +++ /dev/null @@ -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") diff --git a/modules/installed/services/xmpp.py b/modules/installed/services/xmpp.py deleted file mode 100644 index 767411432..000000000 --- a/modules/installed/services/xmpp.py +++ /dev/null @@ -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 = "

    XMPP Server Accounts and Configuration

    " - - 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))) diff --git a/modules/installed/sharing/file_explorer.py b/modules/installed/sharing/file_explorer.py deleted file mode 100644 index c56cdb504..000000000 --- a/modules/installed/sharing/file_explorer.py +++ /dev/null @@ -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')) diff --git a/modules/installed/sharing/sharing.py b/modules/installed/sharing/sharing.py deleted file mode 100644 index 81e004c20..000000000 --- a/modules/installed/sharing/sharing.py +++ /dev/null @@ -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')) diff --git a/modules/installed/sharing/templates/file_explorer.html b/modules/installed/sharing/templates/file_explorer.html deleted file mode 100644 index a7f98b232..000000000 --- a/modules/installed/sharing/templates/file_explorer.html +++ /dev/null @@ -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 . -# -{% endcomment %} - -{% block main_block %} - -

    File explorer for users that also have shell accounts.

    Until -that is written (and it will be a while), we should -install mollify -or ajaxplorer, -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.

    - -

    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.

    - -

    For python-friendly options, check out FileManager. -It appears to be mostly javascript with some bindings to make it -python-friendly.

    - -{% endblock %} diff --git a/modules/installed/system/diagnostics.py b/modules/installed/system/diagnostics.py deleted file mode 100644 index db05738a2..000000000 --- a/modules/installed/system/diagnostics.py +++ /dev/null @@ -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 . -# - -""" -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) diff --git a/modules/installed/system/expert_mode.py b/modules/installed/system/expert_mode.py deleted file mode 100644 index 041ce0793..000000000 --- a/modules/installed/system/expert_mode.py +++ /dev/null @@ -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) diff --git a/modules/installed/system/firewall.py b/modules/installed/system/firewall.py deleted file mode 100644 index e736627f1..000000000 --- a/modules/installed/system/firewall.py +++ /dev/null @@ -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 . -# - -""" -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 diff --git a/modules/installed/system/packages.py b/modules/installed/system/packages.py deleted file mode 100644 index 63df8cab0..000000000 --- a/modules/installed/system/packages.py +++ /dev/null @@ -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))) diff --git a/modules/installed/system/system.py b/modules/installed/system/system.py deleted file mode 100644 index cfdf92515..000000000 --- a/modules/installed/system/system.py +++ /dev/null @@ -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")) diff --git a/modules/installed/system/templates/wan.html b/modules/installed/system/templates/wan.html deleted file mode 100644 index ea83226d0..000000000 --- a/modules/installed/system/templates/wan.html +++ /dev/null @@ -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 . -# -{% endcomment %} - -{% load bootstrap %} - -{% block main_block %} - -{% if cfg.users.expert %} - - {% include 'messages.html' %} - -

    For security reasons, neither WAN Administration nor WAN SSH is - available to the `admin` user account.

    - -

    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.

    - -
    - {% csrf_token %} - - {{ form|bootstrap }} - - - -
    - -{% else %} - -

    This page is available only in expert mode.

    - -{% endif %} - -{% endblock %} diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py deleted file mode 100644 index 1b7550405..000000000 --- a/modules/installed/system/users.py +++ /dev/null @@ -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))) diff --git a/modules/installed/system/wan.py b/modules/installed/system/wan.py deleted file mode 100644 index 962fe806f..000000000 --- a/modules/installed/system/wan.py +++ /dev/null @@ -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 cfg -import util - - -class WanForm(forms.Form): # pylint: disable-msg=W0232 - """Form to configure wan settings""" - - wan_admin = forms.BooleanField( - label=_('Allow access to Plinth from WAN'), - required=False, - help_text=_('If you check this box, this front end will be reachable \ -from the WAN. If your {{ box_name }} connects you to the internet, that \ -means you\'ll be able to log in to the front end from the internet. This \ -might be convenient, but it is also dangerous, since it can \ -enable attackers to gain access to your {{ box_name }} from the outside \ -world. All they\'ll need is your username and passphrase, which they might \ -guess or they might simply try every posible combination of letters and \ -numbers until they get in. If you enable the WAN administration option, you \ -must use long and complex passphrases.').format( - box_name=cfg.box_name)) - - lan_ssh = forms.BooleanField( - label=_('Allow SSH access from LAN'), - required=False) - - wan_ssh = forms.BooleanField( - label=_('Allow SSH access from WAN'), - required=False) - - # XXX: Only present due to issue with submitting empty form - dummy = forms.CharField(label='Dummy', initial='dummy', - widget=forms.HiddenInput()) - - -class Wan(PagePlugin): - order = 60 - - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.register_page('sys.config.wan') - - cfg.html_root.sys.config.menu.add_item(_('WAN'), 'icon-cog', - '/sys/config/wan', 20) - - @cherrypy.expose - @require() - def index(self, **kwargs): - """Serve the configuration form""" - status = self.get_status() - - form = None - messages = [] - - if kwargs and cfg.users.expert(): - form = WanForm(kwargs, prefix='wan') - # pylint: disable-msg=E1101 - if form.is_valid(): - self._apply_changes(form.cleaned_data, messages) - status = self.get_status() - form = WanForm(initial=status, prefix='wan') - else: - form = WanForm(initial=status, prefix='wan') - - title = _('Accessing the {box_name}').format(box_name=cfg.box_name) - return util.render_template(template='wan', title=title, form=form, - messages=messages) - - @staticmethod - def get_status(): - """Return the current status""" - return util.filedict_con(cfg.store_file, 'sys') - - @staticmethod - def _apply_changes(new_status, messages): - """Apply the changes after form submission""" - store = util.filedict_con(cfg.store_file, 'sys') - for field in ['wan_admin', 'wan_ssh', 'lan_ssh']: - store[field] = new_status[field] - - messages.append(('success', _('Setting updated'))) diff --git a/modules/lib/__init__.py b/modules/lib/__init__.py new file mode 100644 index 000000000..61826142a --- /dev/null +++ b/modules/lib/__init__.py @@ -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 . +# + +""" +Plinth library modules +""" + +from . import auth + +__all__ = ['auth'] diff --git a/modules/lib/auth.py b/modules/lib/auth.py new file mode 100644 index 000000000..0ff7bb82b --- /dev/null +++ b/modules/lib/auth.py @@ -0,0 +1,29 @@ +from django.contrib.auth.models import Group, User + + +def add_user(username, passphrase, name='', email='', expert=False): + """Add a new user with specified username and passphrase""" + if not username: + return 'Must specify a username!' + + if not passphrase: + return 'Must specify a passphrase!' + + user = User.objects.create_user(username, email=email, + password=passphrase) + user.first_name = name + user.save() + + if expert: + user.groups.add(get_group('Expert')) + + +def get_group(name): + """Return an existing or newly created group with given name""" + try: + group = Group.objects.get(name__exact=name) + except Group.DoesNotExist: + group = Group(name=name) + group.save() + + return group diff --git a/modules/installed/sharing/templates/sharing.html b/modules/lib/urls.py similarity index 62% rename from modules/installed/sharing/templates/sharing.html rename to modules/lib/urls.py index 020c3edfe..2414b705a 100644 --- a/modules/installed/sharing/templates/sharing.html +++ b/modules/lib/urls.py @@ -1,5 +1,3 @@ -{% extends "login_nav.html" %} -{% comment %} # # This file is part of Plinth. # @@ -16,18 +14,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -{% endcomment %} -{% block sidebar_right_block %} +""" +URLs for the Lib module +""" - - -{% endblock %} +urlpatterns = patterns( # pylint: disable-msg=C0103 + '', + url(r'^accounts/login/$', 'django.contrib.auth.views.login', + {'template_name': 'login.html'}, name='login'), + url(r'^accounts/logout/$', 'django.contrib.auth.views.logout', + {'next_page': cfg.server_dir}, name='logout') + ) diff --git a/modules/owncloud.py b/modules/owncloud.py deleted file mode 120000 index 4ae17c8be..000000000 --- a/modules/owncloud.py +++ /dev/null @@ -1 +0,0 @@ -installed/apps/owncloud.py \ No newline at end of file diff --git a/modules/owncloud/__init__.py b/modules/owncloud/__init__.py new file mode 100644 index 000000000..e77100b70 --- /dev/null +++ b/modules/owncloud/__init__.py @@ -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 . +# + +""" +Plinth module to configure ownCloud +""" + +from . import owncloud +from .owncloud import init + +__all__ = ['owncloud', 'init'] + +DEPENDS = ['apps'] diff --git a/modules/owncloud/owncloud.py b/modules/owncloud/owncloud.py new file mode 100644 index 000000000..1507569b6 --- /dev/null +++ b/modules/owncloud/owncloud.py @@ -0,0 +1,80 @@ +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 actions +import cfg +import service + + +SERVICE = None + + +class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232 + """ownCloud configuration form""" + enabled = forms.BooleanField(label=_('Enable ownCloud'), required=False) + + +def init(): + """Initialize the ownCloud module""" + menu = cfg.main_menu.find('/apps') + menu.add_item('Owncloud', 'icon-picture', '/apps/owncloud', 35) + + status = get_status() + + global SERVICE # pylint: disable-msg=W0603 + SERVICE = service.Service('owncloud', _('ownCloud'), ['http', 'https'], + is_external=True, enabled=status['enabled']) + + +@login_required +def index(request): + """Serve the ownCloud configuration page""" + status = get_status() + + form = None + + if request.method == 'POST': + form = OwnCloudForm(request.POST, prefix='owncloud') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status() + form = OwnCloudForm(initial=status, prefix='owncloud') + else: + form = OwnCloudForm(initial=status, prefix='owncloud') + + return TemplateResponse(request, 'owncloud.html', + {'title': _('ownCloud'), + 'form': form}) + + +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(request, old_status, new_status): + """Apply the changes""" + if old_status['enabled'] == new_status['enabled']: + messages.info(request, _('Setting unchanged')) + return + + if new_status['enabled']: + messages.success(request, _('ownCloud enabled')) + option = 'enable' + else: + messages.success(request, _('ownCloud disabled')) + option = 'noenable' + + actions.superuser_run('owncloud-setup', [option], async=True) + + # Send a signal to other modules that the service is + # enabled/disabled + SERVICE.notify_enabled(None, new_status['enabled']) diff --git a/modules/installed/apps/templates/owncloud.html b/modules/owncloud/templates/owncloud.html similarity index 93% rename from modules/installed/apps/templates/owncloud.html rename to modules/owncloud/templates/owncloud.html index 1e7efebd0..dad44f08e 100644 --- a/modules/installed/apps/templates/owncloud.html +++ b/modules/owncloud/templates/owncloud.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. @@ -22,15 +22,13 @@ {% block main_block %} - {% include 'messages.html' %} -
    {% csrf_token %} {{ form|bootstrap }}

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

    diff --git a/modules/owncloud/urls.py b/modules/owncloud/urls.py new file mode 100644 index 000000000..faa23d43b --- /dev/null +++ b/modules/owncloud/urls.py @@ -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 . +# + +""" +URLs for the ownCloud module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.owncloud.owncloud', + url(r'^apps/owncloud/$', 'index', name='index'), + ) diff --git a/modules/packages.py b/modules/packages.py deleted file mode 120000 index fa4dedfc6..000000000 --- a/modules/packages.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/packages.py \ No newline at end of file diff --git a/modules/packages/__init__.py b/modules/packages/__init__.py new file mode 100644 index 000000000..f448daaab --- /dev/null +++ b/modules/packages/__init__.py @@ -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 . +# + +""" +Plinth module to manage packages +""" + +from . import packages +from .packages import init + +__all__ = ['packages', 'init'] + +DEPENDS = ['system'] diff --git a/modules/packages/packages.py b/modules/packages/packages.py new file mode 100644 index 000000000..cbae9198a --- /dev/null +++ b/modules/packages/packages.py @@ -0,0 +1,119 @@ +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 actions +import cfg + + +def get_modules_available(): + """Return list of all modules""" + output, error = actions.run('module-manager', ['list-available']) + if error: + raise Exception('Error getting modules: %s' % error) + + 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""" + 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) + + +def init(): + """Initialize the Packages module""" + menu = cfg.main_menu.find('/sys') + menu.add_item('Package Manager', 'icon-gift', '/sys/packages', 20) + + +@login_required +def index(request): + """Serve the form""" + status = get_status() + + form = None + + if request.method == 'POST': + form = PackagesForm(request.POST, prefix='packages') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status() + form = PackagesForm(initial=status, prefix='packages') + else: + form = PackagesForm(initial=status, prefix='packages') + + return TemplateResponse(request, 'packages.html', + {'title': _('Add/Remove Plugins'), + 'form': form}) + + +def get_status(): + """Return the current status""" + modules_available = get_modules_available() + modules_enabled = get_modules_enabled() + + return {module + '_enabled': module in modules_enabled + for module in modules_available} + + +def _apply_changes(request, old_status, new_status): + """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.error( + request, _('Error enabling module - {module}').format( + module=module)) + else: + messages.success( + request, _('Module enabled - {module}').format( + module=module)) + else: + output, error = actions.superuser_run( + 'module-manager', ['disable', cfg.python_root, module]) + del output # Unused + + # TODO: need a smoother way for plinth to unload the + # module + if error: + messages.error( + request, _('Error disabling module - {module}').format( + module=module)) + else: + messages.success( + request, _('Module disabled - {module}').format( + module=module)) diff --git a/modules/installed/system/templates/packages.html b/modules/packages/templates/packages.html similarity index 96% rename from modules/installed/system/templates/packages.html rename to modules/packages/templates/packages.html index d8d7d6164..90dd247a6 100644 --- a/modules/installed/system/templates/packages.html +++ b/modules/packages/templates/packages.html @@ -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' %} -

    aptitude purge modules

    aptitude install modules

    diff --git a/modules/packages/urls.py b/modules/packages/urls.py new file mode 100644 index 000000000..60faf35bf --- /dev/null +++ b/modules/packages/urls.py @@ -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 . +# + +""" +URLs for the Packages module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.packages.packages', + url(r'^sys/packages/$', 'index', name='index'), + ) diff --git a/modules/pagekite.py b/modules/pagekite.py deleted file mode 120000 index 2981955ca..000000000 --- a/modules/pagekite.py +++ /dev/null @@ -1 +0,0 @@ -installed/router/pagekite.py \ No newline at end of file diff --git a/modules/pagekite/__init__.py b/modules/pagekite/__init__.py new file mode 100644 index 000000000..3a017a1cd --- /dev/null +++ b/modules/pagekite/__init__.py @@ -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 . +# + +""" +Plinth module to configure PageKite +""" + +from . import pagekite +from .pagekite import init + +__all__ = ['pagekite', 'init'] + +DEPENDS = ['apps'] diff --git a/modules/pagekite/pagekite.py b/modules/pagekite/pagekite.py new file mode 100644 index 000000000..9856f9da1 --- /dev/null +++ b/modules/pagekite/pagekite.py @@ -0,0 +1,211 @@ +# +# 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 . +# + +""" +Plinth module for configuring PageKite service +""" + +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 import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse +from gettext import gettext as _ +import logging + +import actions +import cfg + + +LOGGER = logging.getLogger(__name__) + + +def init(): + """Intialize the PageKite module""" + menu = cfg.main_menu.find('/apps') + menu.add_item(_('Public Visibility (PageKite)'), 'icon-flag', + '/apps/pagekite', 50) + + +@login_required +def index(request): + """Serve introdution page""" + menu = {'title': _('PageKite'), + 'items': [{'url': '/apps/pagekite/configure', + 'text': _('Configure PageKite')}]} + + sidebar_right = render_to_string('menu_block.html', {'menu': menu}, + RequestContext(request)) + + return TemplateResponse(request, 'pagekite_introduction.html', + {'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 \ +http://mybox1-myacc.pagekite.me \ +')) + + ssh_enabled = forms.BooleanField( + label=_('Secure Shell (SSH)'), required=False, + help_text=_('See SSH client setup instructions')) + + +@login_required +def configure(request): + """Serve the configuration form""" + status = get_status() + + form = None + + if request.method == 'POST': + form = ConfigureForm(request.POST, prefix='pagekite') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status() + form = ConfigureForm(initial=status, prefix='pagekite') + else: + form = ConfigureForm(initial=status, prefix='pagekite') + + return TemplateResponse(request, 'pagekite_configure.html', + {'title': _('Configure PageKite'), + 'form': form}) + + +def get_status(): + """ + Return the current status of PageKite configuration by + executing various actions. + """ + status = {} + + # Check if PageKite is installed + output = _run(['get-installed']) + if output.split()[0] != 'installed': + return None + + # PageKite service enabled/disabled + output = _run(['get-status']) + status['enabled'] = (output.split()[0] == 'enabled') + + # PageKite kite details + output = _run(['get-kite']) + kite_details = output.split() + status['kite_name'] = kite_details[0] + status['kite_secret'] = kite_details[1] + + # Service status + status['service'] = {} + for service in ('http', 'ssh'): + output = _run(['get-service-status', service]) + status[service + '_enabled'] = (output.split()[0] == 'enabled') + + return status + + +def _apply_changes(request, old_status, new_status): + """Apply the changes to PageKite configuration""" + LOGGER.info('New status is - %s', new_status) + + if old_status != new_status: + _run(['stop']) + + if old_status['enabled'] != new_status['enabled']: + if new_status['enabled']: + _run(['set-status', 'enable']) + messages.success(request, _('PageKite enabled')) + else: + _run(['set-status', 'disable']) + messages.success(request, _('PageKite disabled')) + + if old_status['kite_name'] != new_status['kite_name'] or \ + old_status['kite_secret'] != new_status['kite_secret']: + _run(['set-kite', '--kite-name', new_status['kite_name'], + '--kite-secret', new_status['kite_secret']]) + messages.success(request, _('Kite details set')) + + for service in ['http', 'ssh']: + if old_status[service + '_enabled'] != \ + new_status[service + '_enabled']: + if new_status[service + '_enabled']: + _run(['set-service-status', service, 'enable']) + messages.success(request, _('Service enabled: {service}') + .format(service=service)) + else: + _run(['set-service-status', service, 'disable']) + messages.success(request, _('Service disabled: {service}') + .format(service=service)) + + if old_status != new_status: + _run(['start']) + + +def _run(arguments, superuser=True): + """Run an given command and raise exception if there was an error""" + command = 'pagekite-configure' + + 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 PageKite confguration - %s' + % error) + + return output diff --git a/modules/installed/router/templates/pagekite_configure.html b/modules/pagekite/templates/pagekite_configure.html similarity index 97% rename from modules/installed/router/templates/pagekite_configure.html rename to modules/pagekite/templates/pagekite_configure.html index dd31c47de..e8544420b 100644 --- a/modules/installed/router/templates/pagekite_configure.html +++ b/modules/pagekite/templates/pagekite_configure.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. @@ -31,8 +31,6 @@ {% else %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/installed/router/templates/pagekite_introduction.html b/modules/pagekite/templates/pagekite_introduction.html similarity index 95% rename from modules/installed/router/templates/pagekite_introduction.html rename to modules/pagekite/templates/pagekite_introduction.html index f6619c9a1..d1b675aea 100644 --- a/modules/installed/router/templates/pagekite_introduction.html +++ b/modules/pagekite/templates/pagekite_introduction.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. @@ -49,7 +49,7 @@ there. In future, it might be possible to use your buddy's

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

    diff --git a/modules/pagekite/urls.py b/modules/pagekite/urls.py new file mode 100644 index 000000000..3db1d2f98 --- /dev/null +++ b/modules/pagekite/urls.py @@ -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 . +# + +""" +URLs for the PageKite module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.pagekite.pagekite', + url(r'^apps/pagekite/$', 'index', name='index'), + url(r'^apps/pagekite/configure/$', 'configure', name='configure'), + ) diff --git a/modules/privacy.py b/modules/privacy.py deleted file mode 120000 index 07f303ee7..000000000 --- a/modules/privacy.py +++ /dev/null @@ -1 +0,0 @@ -installed/privacy/privacy.py \ No newline at end of file diff --git a/modules/router.py b/modules/router.py deleted file mode 120000 index 7239c9f3b..000000000 --- a/modules/router.py +++ /dev/null @@ -1 +0,0 @@ -installed/router/router.py \ No newline at end of file diff --git a/modules/santiago/__init__.py b/modules/santiago/__init__.py new file mode 100644 index 000000000..abe7409d6 --- /dev/null +++ b/modules/santiago/__init__.py @@ -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 . +# + +""" +Plinth module to configure santiago port +""" + +from . import santiago + + +__all__ = ['santiago'] diff --git a/modules/installed/santiago/santiago.py b/modules/santiago/santiago.py similarity index 86% rename from modules/installed/santiago/santiago.py rename to modules/santiago/santiago.py index b052daab7..e9339fe55 100644 --- a/modules/installed/santiago/santiago.py +++ b/modules/santiago/santiago.py @@ -23,14 +23,15 @@ santiago_port = 52854 # return True class Santiago(PagePlugin): - order = 90 # order of running init in PagePlugins - def __init__(self, *args, **kwargs): + order = 90 # order of running init in PagePlugins + def __init__(self): + super(Santiago, self).__init__() self.register_page("santiago") self.santiago_address = self.get_santiago_address() #TODO: multiple santiago ports #set a listener on the santiago address - def get_santiago_address(self): + def get_santiago_address(self): if 'santiago' in cfg.users['admin'] and 'address' in cfg.users['admin']['santiago']: return cfg.users['admin']['santiago']['address'] else: @@ -54,11 +55,11 @@ class Santiago(PagePlugin): print "Need to add these two lines to /etc/torrc:\n%s" % hidden_service_config return "" - def check_for_hidden_service(self): + def check_for_hidden_service(self): pass - @cherrypy.expose - def index(self, *args, **kw): + @cherrypy.expose + def index(self, *args, **kw): """ A request is a dict with some required keys: @@ -111,12 +112,13 @@ class Santiago(PagePlugin): ## Plinth page to config santiago class santiago(PagePlugin): - def __init__(self, *args, **kwargs): - PagePlugin.__init__(self, *args, **kwargs) - self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10) - self.register_page("privacy.santiago") + def __init__(self): + super(Santiago, self).__init__(self) - @cherrypy.expose - @require() - def index(self): - return "Santiago's config goes here." + self.menu = cfg.html_root.privacy.menu.add_item("Santiago", "icon-leaf", "/privacy/santiago", 10) + self.register_page("privacy.santiago") + + @cherrypy.expose + @require() + def index(self): + return "Santiago's config goes here." diff --git a/modules/services.py b/modules/services.py deleted file mode 120000 index 806ae4f9f..000000000 --- a/modules/services.py +++ /dev/null @@ -1 +0,0 @@ -installed/services/services.py \ No newline at end of file diff --git a/modules/sharing.py b/modules/sharing.py deleted file mode 120000 index e2323fb26..000000000 --- a/modules/sharing.py +++ /dev/null @@ -1 +0,0 @@ -installed/sharing/sharing.py \ No newline at end of file diff --git a/modules/system.py b/modules/system.py deleted file mode 120000 index 6eddf3e2d..000000000 --- a/modules/system.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/system.py \ No newline at end of file diff --git a/modules/system/__init__.py b/modules/system/__init__.py new file mode 100644 index 000000000..1d9349bb0 --- /dev/null +++ b/modules/system/__init__.py @@ -0,0 +1,26 @@ +# +# 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 . +# + +""" +Plinth module for system section page +""" + +from . import system +from system import init + + +__all__ = ['system', 'init'] diff --git a/modules/system/system.py b/modules/system/system.py new file mode 100644 index 000000000..e94c85b85 --- /dev/null +++ b/modules/system/system.py @@ -0,0 +1,15 @@ +from gettext import gettext as _ +from django.template.response import TemplateResponse + +import cfg + + +def init(): + """Initialize the system module""" + cfg.main_menu.add_item(_('System'), 'icon-cog', '/sys', 100) + + +def index(request): + """Serve the index page""" + return TemplateResponse(request, 'system.html', + {'title': _('System Configuration')}) diff --git a/modules/installed/system/templates/system.html b/modules/system/templates/system.html similarity index 97% rename from modules/installed/system/templates/system.html rename to modules/system/templates/system.html index 45d842319..24f53b350 100644 --- a/modules/installed/system/templates/system.html +++ b/modules/system/templates/system.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/system/urls.py b/modules/system/urls.py new file mode 100644 index 000000000..5982ce850 --- /dev/null +++ b/modules/system/urls.py @@ -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 . +# + +""" +URLs for the System module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.system.system', + url(r'^sys/$', 'index', name='index'), + ) diff --git a/modules/tor.py b/modules/tor.py deleted file mode 120000 index 28c310568..000000000 --- a/modules/tor.py +++ /dev/null @@ -1 +0,0 @@ -installed/privacy/tor.py \ No newline at end of file diff --git a/modules/tor/__init__.py b/modules/tor/__init__.py new file mode 100644 index 000000000..0a806432a --- /dev/null +++ b/modules/tor/__init__.py @@ -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 . +# + +""" +Plinth module to configure Tor +""" + +from . import tor +from .tor import init + +__all__ = ['tor', 'init'] + +DEPENDS = ['apps'] diff --git a/modules/installed/privacy/templates/tor.html b/modules/tor/templates/tor.html similarity index 98% rename from modules/installed/privacy/templates/tor.html rename to modules/tor/templates/tor.html index 85ebe5745..c97aafded 100644 --- a/modules/installed/privacy/templates/tor.html +++ b/modules/tor/templates/tor.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/tor/tor.py b/modules/tor/tor.py new file mode 100644 index 000000000..ee3cdfdb9 --- /dev/null +++ b/modules/tor/tor.py @@ -0,0 +1,53 @@ +# +# 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 . +# + +""" +Plinth module for configuring Tor +""" + +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 Tor module""" + menu = cfg.main_menu.find('/apps') + menu.add_item("Tor", "icon-eye-close", "/apps/tor", 30) + + +@login_required +def index(request): + """Service the index page""" + output, error = actions.superuser_run("tor-get-ports") + del error # Unused + + port_info = output.split("\n") + tor_ports = {} + for line in port_info: + try: + (key, val) = line.split() + tor_ports[key] = val + except ValueError: + continue + + return TemplateResponse(request, 'tor.html', + {'title': _('Tor Control Panel'), + 'tor_ports': tor_ports}) diff --git a/modules/tor/urls.py b/modules/tor/urls.py new file mode 100644 index 000000000..4c13cb138 --- /dev/null +++ b/modules/tor/urls.py @@ -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 . +# + +""" +URLs for the Tor module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.tor.tor', + url(r'^apps/tor/$', 'index', name='index') + ) diff --git a/modules/user_store.py b/modules/user_store.py deleted file mode 120000 index 09e8f90fe..000000000 --- a/modules/user_store.py +++ /dev/null @@ -1 +0,0 @@ -installed/lib/user_store.py \ No newline at end of file diff --git a/modules/users.py b/modules/users.py deleted file mode 120000 index 4b7ce2010..000000000 --- a/modules/users.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/users.py \ No newline at end of file diff --git a/modules/users/__init__.py b/modules/users/__init__.py new file mode 100644 index 000000000..3dc71aeb5 --- /dev/null +++ b/modules/users/__init__.py @@ -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 . +# + +""" +Plinth module to manage users +""" + +from . import users +from .users import init + +__all__ = ['users', 'init'] + +DEPENDS = ['system'] diff --git a/modules/installed/system/templates/users_add.html b/modules/users/templates/users_add.html similarity index 95% rename from modules/installed/system/templates/users_add.html rename to modules/users/templates/users_add.html index 4afbc68c9..1be72d630 100644 --- a/modules/installed/system/templates/users_add.html +++ b/modules/users/templates/users_add.html @@ -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' %} - {% csrf_token %} diff --git a/modules/installed/system/templates/users_edit.html b/modules/users/templates/users_edit.html similarity index 95% rename from modules/installed/system/templates/users_edit.html rename to modules/users/templates/users_edit.html index da1ef4136..4c695679b 100644 --- a/modules/installed/system/templates/users_edit.html +++ b/modules/users/templates/users_edit.html @@ -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' %} - {% csrf_token %} diff --git a/modules/users/urls.py b/modules/users/urls.py new file mode 100644 index 000000000..0c46aa72e --- /dev/null +++ b/modules/users/urls.py @@ -0,0 +1,30 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +URLs for the Users module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.users.users', + url(r'^sys/users/$', 'index', name='index'), + url(r'^sys/users/add/$', 'add', name='add'), + url(r'^sys/users/edit/$', 'edit', name='edit') + ) diff --git a/modules/users/users.py b/modules/users/users.py new file mode 100644 index 000000000..4ce4bc316 --- /dev/null +++ b/modules/users/users.py @@ -0,0 +1,151 @@ +from django import forms +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core import validators +from django.template import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse +from gettext import gettext as _ +import logging + +import cfg +from ..lib.auth import add_user + + +LOGGER = logging.getLogger(__name__) + + +def init(): + """Intialize the module""" + menu = cfg.main_menu.find('/sys') + menu.add_item(_('Users and Groups'), 'icon-user', '/sys/users', 15) + + +@login_required +def index(request): + """Return a rendered users page""" + menu = {'title': _('Users and Groups'), + 'items': [{'url': '/sys/users/add', + 'text': _('Add User')}, + {'url': '/sys/users/edit', + 'text': _('Edit Users')}]} + + sidebar_right = render_to_string('menu_block.html', {'menu': menu}, + RequestContext(request)) + + return TemplateResponse(request, 'base.html', + {'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) + + +@login_required +def add(request): + """Serve the form""" + form = None + + if request.method == 'POST': + form = UserAddForm(request.POST, prefix='user') + # pylint: disable-msg=E1101 + if form.is_valid(): + _add_user(request, form.cleaned_data) + form = UserAddForm(prefix='user') + else: + form = UserAddForm(prefix='user') + + return TemplateResponse(request, 'users_add.html', + {'title': _('Add User'), + 'form': form}) + + +def _add_user(request, data): + """Add a user""" + if User.objects.filter(username=data['username']).exists(): + messages.error(request, _('User "{username}" already exists').format( + username=data['username'])) + return + + add_user(data['username'], data['password'], data['full_name'], + data['email'], False) + messages.success(request, _('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) + + for user in User.objects.all(): + label = '%s (%s)' % (user.first_name, user.username) + field = forms.BooleanField(label=label, required=False) + # pylint: disable-msg=E1101 + self.fields['delete_user_' + user.username] = field + + +@login_required +def edit(request): + """Serve the edit form""" + form = None + + if request.method == 'POST': + form = UserEditForm(request.POST, prefix='user') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_edit_changes(request, form.cleaned_data) + form = UserEditForm(prefix='user') + else: + form = UserEditForm(prefix='user') + + return TemplateResponse(request, 'users_edit.html', + {'title': _('Edit or Delete User'), + 'form': form}) + + +def _apply_edit_changes(request, data): + """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] + + requesting_user = request.user.username + LOGGER.info('%s asked to delete %s', requesting_user, username) + + if username == requesting_user: + messages.error( + request, _('Can not delete current account - "%s"') % username) + continue + + if not User.objects.filter(username=username).exists(): + messages.error(request, _('User "%s" does not exist') % username) + continue + + try: + User.objects.filter(username=username).delete() + messages.success(request, _('User "%s" deleted') % username) + except IOError as exception: + messages.error(request, _('Error deleting "%s" - %s') % + (username, exception)) diff --git a/modules/wan.py b/modules/wan.py deleted file mode 120000 index 5a68efd0d..000000000 --- a/modules/wan.py +++ /dev/null @@ -1 +0,0 @@ -installed/system/wan.py \ No newline at end of file diff --git a/modules/xmpp.py b/modules/xmpp.py deleted file mode 120000 index f31c66cd0..000000000 --- a/modules/xmpp.py +++ /dev/null @@ -1 +0,0 @@ -installed/services/xmpp.py \ No newline at end of file diff --git a/modules/xmpp/__init__.py b/modules/xmpp/__init__.py new file mode 100644 index 000000000..578564afb --- /dev/null +++ b/modules/xmpp/__init__.py @@ -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 . +# + +""" +Plinth module to configure XMPP server +""" + +from . import xmpp +from .xmpp import init + +__all__ = ['xmpp', 'init'] + +DEPENDS = ['apps'] diff --git a/modules/installed/services/templates/xmpp_configure.html b/modules/xmpp/templates/xmpp_configure.html similarity index 93% rename from modules/installed/services/templates/xmpp_configure.html rename to modules/xmpp/templates/xmpp_configure.html index 718d7d353..38c898cbf 100644 --- a/modules/installed/services/templates/xmpp_configure.html +++ b/modules/xmpp/templates/xmpp_configure.html @@ -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' %} - {% csrf_token %} diff --git a/modules/installed/services/templates/xmpp_register.html b/modules/xmpp/templates/xmpp_register.html similarity index 93% rename from modules/installed/services/templates/xmpp_register.html rename to modules/xmpp/templates/xmpp_register.html index bfb7c3de4..9b0620aeb 100644 --- a/modules/installed/services/templates/xmpp_register.html +++ b/modules/xmpp/templates/xmpp_register.html @@ -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' %} - {% csrf_token %} diff --git a/modules/xmpp/urls.py b/modules/xmpp/urls.py new file mode 100644 index 000000000..050026d81 --- /dev/null +++ b/modules/xmpp/urls.py @@ -0,0 +1,30 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +URLs for the XMPP module +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'modules.xmpp.xmpp', + url(r'^apps/xmpp/$', 'index', name='index'), + url(r'^apps/xmpp/configure/$', 'configure', name='configure'), + url(r'^apps/xmpp/register/$', 'register', name='register') + ) diff --git a/modules/xmpp/xmpp.py b/modules/xmpp/xmpp.py new file mode 100644 index 000000000..592dd3b01 --- /dev/null +++ b/modules/xmpp/xmpp.py @@ -0,0 +1,165 @@ +from django import forms +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.template import RequestContext +from django.template.loader import render_to_string +from django.template.response import TemplateResponse +from gettext import gettext as _ +import logging + +import actions +import cfg +import service + + +LOGGER = logging.getLogger(__name__) + +SIDE_MENU = {'title': _('XMPP'), + 'items': [{'url': '/apps/xmpp/configure', + 'text': 'Configure XMPP Server'}, + {'url': '/apps/xmpp/register', + 'text': 'Register XMPP Account'}]} + + +def init(): + """Initialize the XMPP module""" + menu = cfg.main_menu.find('/apps') + menu.add_item('Chat', 'icon-comment', '/../jwchat', 20) + menu.add_item('XMPP', 'icon-comment', '/apps/xmpp', 40) + + service.Service( + 'xmpp-client', _('Chat Server - client connections'), + is_external=True, enabled=True) + service.Service( + 'xmpp-server', _('Chat Server - server connections'), + is_external=True, enabled=True) + service.Service( + 'xmpp-bosh', _('Chat Server - web interface'), is_external=True, + enabled=True) + + +@login_required +def index(request): + """Serve XMPP page""" + main = "

    XMPP Server Accounts and Configuration

    " + + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) + + return TemplateResponse(request, 'base.html', + {'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')) + + +@login_required +def configure(request): + """Serve the configuration form""" + status = get_status() + + form = None + + if request.method == 'POST': + form = ConfigureForm(request.POST, prefix='xmpp') + # pylint: disable-msg=E1101 + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status() + form = ConfigureForm(initial=status, prefix='xmpp') + else: + form = ConfigureForm(initial=status, prefix='xmpp') + + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) + + return TemplateResponse(request, 'xmpp_configure.html', + {'title': _('Configure XMPP Server'), + 'form': form, + 'sidebar_right': sidebar_right}) + + +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()} + + +def _apply_changes(request, old_status, new_status): + """Apply the form changes""" + LOGGER.info('Status - %s, %s', old_status, new_status) + + if old_status['inband_enabled'] == new_status['inband_enabled']: + messages.info(request, _('Setting unchanged')) + return + + if new_status['inband_enabled']: + messages.success(request, _('Inband registration enabled')) + option = 'inband_enable' + else: + messages.success(request, _('Inband registration disabled')) + option = 'noinband_enable' + + LOGGER.info('Option - %s', option) + + _output, error = actions.superuser_run('xmpp-setup', [option]) + del _output # Unused + if error: + raise Exception('Error running command - %s' % error) + + +class RegisterForm(forms.Form): # pylint: disable-msg=W0232 + """Configuration form""" + username = forms.CharField(label=_('Username')) + + password = forms.CharField( + label=_('Password'), widget=forms.PasswordInput()) + + +@login_required +def register(request): + """Serve the registration form""" + form = None + + if request.method == 'POST': + form = RegisterForm(request.POST, prefix='xmpp') + # pylint: disable-msg=E1101 + if form.is_valid(): + _register_user(request, form.cleaned_data) + form = RegisterForm(prefix='xmpp') + else: + form = RegisterForm(prefix='xmpp') + + sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, + RequestContext(request)) + + return TemplateResponse(request, 'xmpp_register.html', + {'title': _('Register XMPP Account'), + 'form': form, + 'sidebar_right': sidebar_right}) + + +def _register_user(request, data): + """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.success(request, _('Registered account for %s') % + data['username']) + else: + messages.error(request, + _('Failed to register account for %s: %s') % + (data['username'], output)) diff --git a/plinth.py b/plinth.py index b5b5af526..eb4ca1edc 100755 --- a/plinth.py +++ b/plinth.py @@ -1,26 +1,21 @@ #!/usr/bin/env python -import os, stat, sys, argparse -from gettext import gettext as _ -import cfg +import argparse import django.conf -if not os.path.join(cfg.file_root, "vendor") in sys.path: - sys.path.append(os.path.join(cfg.file_root, "vendor")) +import django.core.management +import django.core.wsgi +import logging +import os +import stat +import sys import cherrypy from cherrypy import _cpserver from cherrypy.process.plugins import Daemonizer -Daemonizer(cherrypy.engine).subscribe() -import plugin_mount +import cfg +import module_loader import service -import util as u - -from logger import Logger -#from modules.auth import AuthController, require, member_of, name_is - -from withsqlite.withsqlite import sqlite_db -import socket __version__ = "0.2.14" __author__ = "James Vasile" @@ -30,183 +25,197 @@ __maintainer__ = "James Vasile" __email__ = "james@jamesvasile.com" __status__ = "Development" -import urlparse - -def error_page(status, dynamic_msg, stock_msg): - return u.render_template(template="err", title=status, main="

    %s

    %s" % (dynamic_msg, stock_msg)) - -def error_page_404(status, message, traceback, version): - return error_page(status, message, """

    If you believe this - missing page should exist, please file a bug with either the Plinth - project (it has - an issue tracker) or the people responsible for the module you - are trying to access.

    - -

    Sorry for the mistake.

    - """) - -def error_page_500(status, message, traceback, version): - cfg.log.error("500 Internal Server Error. Trackback is above.") - more="""

    This is an internal error and not something you caused - or can fix. Please report the error on the bug tracker so - we can fix it.

    """ - return error_page(status, message, "

    %s

    %s
    " % (more, "\n".join(traceback.split("\n")))) - -class Root(plugin_mount.PagePlugin): - @cherrypy.expose - def index(self): - ## TODO: firstboot hijacking root should probably be in the firstboot module with a hook in plinth.py - with sqlite_db(cfg.store_file, table="firstboot") as db: - if not 'state' in db: - # if we created a new user db, make sure it can't be read by everyone - userdb_fname = '{}.sqlite3'.format(cfg.user_db) - os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - # cherrypy.InternalRedirect throws a 301, causing the - # browser to cache the redirect, preventing the user from - # navigating to /plinth until the browser is restarted. - raise cherrypy.HTTPRedirect('firstboot', 307) - elif db['state'] < 5: - cfg.log("First Boot state = %d" % db['state']) - raise cherrypy.InternalRedirect('firstboot/state%d' % db['state']) - if cherrypy.session.get(cfg.session_key, None): - raise cherrypy.InternalRedirect('router') - else: - raise cherrypy.InternalRedirect('help/about') - -def load_modules(): - """Import all the symlinked .py files in the modules directory and - all the .py files in directories linked in the modules directory - (but don't dive deeper than that). Also, ignore the installed - directory.""" - for name in os.listdir("modules"): - if name.endswith(".py") and not name.startswith('.'): - cfg.log.info("importing modules/%s" % name) - try: - __import__("modules.%s" % (name[:-3])) - except ImportError, e: - cfg.log.error(_("Couldn't import modules/%s: %s") % (name, e)) - else: - cfg.log("skipping %s" % name) - - -def get_template_directories(): - """Return the list of template directories""" - directory = os.path.dirname(os.path.abspath(__file__)) - core_directory = os.path.join(directory, 'templates') - - directories = set((core_directory,)) - for name in os.listdir('modules'): - if not name.endswith(".py") or name.startswith('.'): - continue - - real_name = os.path.realpath(os.path.join('modules', name)) - directory = os.path.dirname(real_name) - directories.add(os.path.join(directory, 'templates')) - - cfg.log.info('Template directories - %s' % directories) - - return directories +LOGGER = logging.getLogger(__name__) def parse_arguments(): - parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') - parser.add_argument('--pidfile', - help='specify a file in which the server may write its pid') - # FIXME make this work with basehref for static files. - parser.add_argument('--server_dir', - help='specify where to host the server.') - parser.add_argument("--debug", action="store_true", - help="Debug flag. Don't use.") + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description='Plinth web interface for FreedomBox') + parser.add_argument( + '--pidfile', default='plinth.pid', + help='specify a file in which the server may write its pid') + # TODO: server_dir is actually a url prefix; use a better variable name + parser.add_argument( + '--server_dir', default='/', + help='web server path under which to serve') + parser.add_argument( + '--debug', action='store_true', default=False, + help='enable debugging and run server *insecurely*') + parser.add_argument( + '--no-daemon', action='store_true', default=False, + help='do not start as a daemon') - args=parser.parse_args() - set_config(args, "pidfile", "plinth.pid") - set_config(args, "server_dir", "/") - set_config(args, "debug", False) + args = parser.parse_args() + cfg.pidfile = args.pidfile + cfg.server_dir = args.server_dir + cfg.debug = args.debug + cfg.no_daemon = args.no_daemon - return cfg -def set_config(args, element, default): - """Sets *cfg* elements based on *args* values, or uses a reasonable default. +def setup_logging(): + """Setup logging framework""" + # Don't propagate cherrypy log messages to root logger + logging.getLogger('cherrypy').propagate = False - - If values are passed in from *args*, use those. - - If values are read from the config file, use those. - - If no values have been given, use default. + cherrypy.log.error_file = cfg.status_log_file + cherrypy.log.access_file = cfg.access_log_file + if not cfg.no_daemon: + cherrypy.log.screen = False - """ - try: - # cfg.(element) = args.(element) - setattr(cfg, element, getattr(args, element)) - except AttributeError: - # if it fails, we didn't receive that argument. - try: - # if not cfg.(element): cfg.(element) = default - if not getattr(cfg, element): - setattr(cfg, element, default) - except AttributeError: - # it wasn't in the config file, but set the default anyway. - setattr(cfg, element, default) -def setup(): - cfg = parse_arguments() +def setup_paths(): + """Setup current directory and python import paths""" + os.chdir(cfg.python_root) + if not os.path.join(cfg.file_root, 'vendor') in sys.path: + sys.path.append(os.path.join(cfg.file_root, 'vendor')) - try: - if cfg.pidfile: - from cherrypy.process.plugins import PIDFile - PIDFile(cherrypy.engine, cfg.pidfile).subscribe() - except AttributeError: - pass - os.chdir(cfg.python_root) - cherrypy.config.update({'error_page.404': error_page_404}) - cherrypy.config.update({'error_page.500': error_page_500}) - cfg.log = Logger() - load_modules() - cfg.html_root = Root() +def setup_server(): + """Setup CherryPy server""" + LOGGER.info('Setting up CherryPy server') - cfg.users = plugin_mount.UserStoreModule.get_plugins()[0] - cfg.page_plugins = plugin_mount.PagePlugin.get_plugins() - cfg.log("Loaded %d page plugins" % len(cfg.page_plugins)) + # Set the PID file path + try: + if cfg.pidfile: + from cherrypy.process.plugins import PIDFile + PIDFile(cherrypy.engine, cfg.pidfile).subscribe() + except AttributeError: + pass - # Add an extra server - server = _cpserver.Server() - server.socket_host = '127.0.0.1' - server.socket_port = 52854 - server.subscribe() + # Add an extra server + server = _cpserver.Server() + server.socket_host = '127.0.0.1' + server.socket_port = 52854 + server.subscribe() - # Configure default server - cherrypy.config.update( - {'server.socket_host': cfg.host, - 'server.socket_port': cfg.port, - 'server.thread_pool':10, - 'tools.staticdir.root': cfg.file_root, - 'tools.sessions.on':True, - 'tools.auth.on':True, - 'tools.sessions.storage_type':"file", - 'tools.sessions.timeout':90, - 'tools.sessions.storage_path':"%s/cherrypy_sessions" % cfg.data_dir,}) + # Configure default server + cherrypy.config.update( + {'server.socket_host': cfg.host, + 'server.socket_port': cfg.port, + 'server.thread_pool': 10}) + + application = django.core.wsgi.get_wsgi_application() + cherrypy.tree.graft(application, cfg.server_dir) + + config = { + '/': {'tools.staticdir.root': '%s/static' % cfg.file_root, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': '.'}} + cherrypy.tree.mount(None, django.conf.settings.STATIC_URL, config) + + if not cfg.no_daemon: + Daemonizer(cherrypy.engine).subscribe() + + cherrypy.engine.signal_handler.subscribe() + + +def context_processor(request): + """Add additional context values to RequestContext for use in templates""" + path_parts = request.path.split('/') + active_menu_urls = ['/'.join(path_parts[:length]) + for length in xrange(1, len(path_parts))] + return { + 'cfg': cfg, + 'main_menu': cfg.main_menu, + 'submenu': cfg.main_menu.active_item(request), + 'request_path': request.path, + 'basehref': cfg.server_dir, + 'active_menu_urls': active_menu_urls + } + + +def configure_django(): + """Setup Django configuration in the absense of .settings file""" + context_processors = [ + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'plinth.context_processor'] + + logging_configuration = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'default': { + 'format': + '[%(asctime)s] %(name)-14s %(levelname)-8s %(message)s', + } + }, + 'handlers': { + 'file': { + 'class': 'logging.FileHandler', + 'filename': cfg.status_log_file, + 'formatter': 'default' + }, + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default' + } + }, + 'root': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG' if cfg.debug else 'INFO' + } + } + + data_file = os.path.join(cfg.data_dir, 'plinth.sqlite3') + + template_directories = module_loader.get_template_directories() + sessions_directory = os.path.join(cfg.data_dir, 'sessions') + django.conf.settings.configure( + ALLOWED_HOSTS=['127.0.0.1', 'localhost'], + CACHES={'default': + {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}, + DATABASES={'default': + {'ENGINE': 'django.db.backends.sqlite3', + 'NAME': data_file}}, + DEBUG=cfg.debug, + INSTALLED_APPS=['bootstrapform', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.messages'], + LOGGING=logging_configuration, + LOGIN_URL=cfg.server_dir + '/accounts/login/', + LOGIN_REDIRECT_URL=cfg.server_dir + '/', + LOGOUT_URL=cfg.server_dir + '/accounts/logout/', + ROOT_URLCONF='urls', + SESSION_ENGINE='django.contrib.sessions.backends.file', + SESSION_FILE_PATH=sessions_directory, + STATIC_URL=os.path.join(cfg.server_dir, 'static/'), + TEMPLATE_CONTEXT_PROCESSORS=context_processors, + TEMPLATE_DIRS=template_directories) + + LOGGER.info('Configured Django') + LOGGER.info('Template directories - %s', template_directories) + + if not os.path.isfile(data_file): + LOGGER.info('Creating and initializing data file') + django.core.management.call_command('syncdb', interactive=False) + os.chmod(data_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) - config = { - '/': {'tools.staticdir.root': '%s/static' % cfg.file_root, - 'tools.proxy.on': True,}, - '/static': {'tools.staticdir.on': True, - 'tools.staticdir.dir': "."}, - '/favicon.ico':{'tools.staticfile.on': True, - 'tools.staticfile.filename': - "%s/static/theme/favicon.ico" % cfg.file_root}} - cherrypy.tree.mount(cfg.html_root, cfg.server_dir, config=config) - cherrypy.engine.signal_handler.subscribe() def main(): - # Initialize basic services + """Intialize and start the application""" + parse_arguments() + + cfg.read() + + setup_logging() + service.init() - setup() + setup_paths() - # Configure Django - template_directories = get_template_directories() - django.conf.settings.configure(TEMPLATE_DIRS=template_directories, - INSTALLED_APPS=['bootstrapform']) + configure_django() + + module_loader.load_modules() + + setup_server() cherrypy.engine.start() cherrypy.engine.block() diff --git a/plugin_mount.py b/plugin_mount.py deleted file mode 100644 index e3ef5db41..000000000 --- a/plugin_mount.py +++ /dev/null @@ -1,86 +0,0 @@ -import cherrypy -from modules.auth import require -import cfg -import util - - -class PluginMount(type): - """See http://martyalchin.com/2008/jan/10/simple-plugin-framework/ for documentation""" - def __init__(cls, name, bases, attrs): - if not hasattr(cls, 'plugins'): - cls.plugins = [] - else: - cls.plugins.append(cls) - - def init_plugins(cls, *args, **kwargs): - try: - cls.plugins = sorted(cls.plugins, key=lambda x: x.order, reverse=False) - except AttributeError: - pass - return [p(*args, **kwargs) for p in cls.plugins] - def get_plugins(cls, *args, **kwargs): - return cls.init_plugins(*args, **kwargs) - -class MultiplePluginViolation: - pass - -class PluginMountSingular(PluginMount): - def __init__(cls, name, bases, attrs): - if not hasattr(cls, 'plugins'): - cls.plugins = [] - else: - if len(cls.plugins) > 0: - raise MultiplePluginViolation - cls.plugins.append(cls) - - -def _setattr_deep(obj, path, value): - """If path is 'x.y.z' or ['x', 'y', 'z'] then perform obj.x.y.z = value""" - if isinstance(path, basestring): - path = path.split('.') - - for part in path[:-1]: - obj = getattr(obj, part) - - setattr(obj, path[-1], value) - - -class PagePlugin: - """ - Mount point for page plugins. Page plugins provide display pages - in the interface (one menu item, for example). - - order - How early should this plugin be loaded? Lower order is earlier. - """ - - order = 50 - - __metaclass__ = PluginMount - def __init__(self, *args, **kwargs): - """If cfg.html_root is none, then this is the html_root.""" - if not cfg.html_root: - cfg.log('Setting html root to %s' % self.__class__.__name__) - cfg.html_root = self - - def register_page(self, url): - cfg.log.info("Registering page: %s" % url) - _setattr_deep(cfg.html_root, url, self) - - -class UserStoreModule: - """ - Mount Point for plugins that will manage the user backend storage, - where we keep a hash for each user. - - Plugins implementing this reference should provide the following - methods, as described in the doc strings of the default - user_store.py: get, get_all, set, exists, remove, attr, expert. - See source code for doc strings. - - This is designed as a plugin so mutiple types of user store can be - supported. But the project is moving towards LDAP for - compatibility with third party software. A future version of - Plinth is likely to require LDAP. - """ - __metaclass__ = PluginMountSingular # singular because we can only use one user store at a time - diff --git a/modules/installed/apps/templates/photos.html b/templates/404.html similarity index 64% rename from modules/installed/apps/templates/photos.html rename to templates/404.html index 5658a54ff..8c0fcc69c 100644 --- a/modules/installed/apps/templates/photos.html +++ b/templates/404.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. @@ -18,12 +18,19 @@ # {% endcomment %} +{% block title_block %} +404 +{% endblock %} + {% block main_block %} -

    Your photos might well be the most valuable digital property you -have, so why trust it to companies that have no investment in the -sentimental value of your family snaps? Keep those photos local, -backed up, easily accessed and free from the whims of some other -websites business model.

    +

    Requested page {{ request_path }} was not found.

    + +

    If you believe this missing page should exist, please file a bug with either + the Plinth project (it + has an issue tracker) or the people responsible for the module you are + trying to access.

    + +

    Sorry for the mistake.

    {% endblock %} diff --git a/modules/installed/sharing/templates/sharing_printer.html b/templates/500.html similarity index 73% rename from modules/installed/sharing/templates/sharing_printer.html rename to templates/500.html index 05ef7fb3e..0863ec630 100644 --- a/modules/installed/sharing/templates/sharing_printer.html +++ b/templates/500.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. @@ -18,20 +18,15 @@ # {% endcomment %} +{% block title_block %} +500 +{% endblock %} + {% block main_block %} -

    TODO: Setup and install SAMBA

    -

    TODO: Setup and install CUPS

    - -{% endblock %} - -{% block sidebar_right_block %} - - +

    This is an internal error and not something you caused or can fix. Please + report the error on + the bug tracker so we + can fix it.

    {% endblock %} diff --git a/templates/base.html b/templates/base.html index ebaea525c..5848f7bd0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,3 +1,4 @@ +{% load static %} @@ -24,25 +25,25 @@ - + - {% if title %} {{ title }} {% else %} FreedomBox Dashboard {% endif %} + {% if title %} {{ title }} {% else %} FreedomBox {% endif %} - + - - - + + + - + - - + + {{ css|safe }} @@ -52,13 +53,50 @@
diff --git a/templates/messages.html b/templates/messages.html index fa5404495..34559a873 100644 --- a/templates/messages.html +++ b/templates/messages.html @@ -17,8 +17,8 @@ # {% endcomment %} -{% for severity, message in messages %} -
+{% for message in messages %} +
× {{ message }}
diff --git a/templates/two_col.html b/templates/two_col.html deleted file mode 100644 index ad8187349..000000000 --- a/templates/two_col.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ extends "base.html" }} - -{% block css %} - - -{% endblock %} - -{% block add_sidebar_left %} -
- - -
-{% endblock %} diff --git a/tests/actions_test.py b/tests/actions_test.py index c78fdce72..09fbae144 100644 --- a/tests/actions_test.py +++ b/tests/actions_test.py @@ -5,7 +5,6 @@ from actions import superuser_run, run import os import shlex import subprocess -import sys import unittest class TestPrivileged(unittest.TestCase): diff --git a/tests/auth_test.py b/tests/auth_test.py index 6ea5d0252..08806748b 100644 --- a/tests/auth_test.py +++ b/tests/auth_test.py @@ -1,18 +1,18 @@ #! /usr/bin/env python # -*- mode: python; mode: auto-fill; fill-column: 80 -*- -import user_store, auth +import auth from logger import Logger import cfg import unittest import cherrypy import plugin_mount import os -from model import User cfg.log = Logger() cherrypy.log.access_file = None + class Auth(unittest.TestCase): """Test check_credentials function of auth to confirm it works as expected""" diff --git a/tests/user_store_test.py b/tests/user_store_test.py index e6a3e8433..def0c0c93 100644 --- a/tests/user_store_test.py +++ b/tests/user_store_test.py @@ -1,7 +1,6 @@ #! /usr/bin/env python # -*- mode: python; mode: auto-fill; fill-column: 80 -*- -import user_store from logger import Logger import cfg import unittest @@ -13,6 +12,7 @@ cfg.log = Logger() cherrypy.log.access_file = None + class UserStore(unittest.TestCase): """Test each function of user_store to confirm they work as expected""" @@ -83,4 +83,4 @@ class UserStore(unittest.TestCase): return user if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/urls.py b/urls.py new file mode 100644 index 000000000..73837133b --- /dev/null +++ b/urls.py @@ -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 . +# + +""" +Django URLconf file containing all urls +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'views', + url(r'^$', 'index', name='index') + ) diff --git a/util.py b/util.py index 39e044e77..3bb0499ce 100644 --- a/util.py +++ b/util.py @@ -1,90 +1,42 @@ import os -import sys -import cherrypy -import cfg -import sqlite3 -from django.template.loader import render_to_string -from filedict import FileDict + +def rel_urljoin(parts, prepend_slash=True): + """ + urllibs' urljoin joins ("foo", "/bar") to "/bar". + Instead concatenate the parts with "/" to i.e. /foo/bar + """ + url = '/'.join(s.strip('/') for s in parts) + if prepend_slash and not url.startswith('/'): + url = '/' + url + return url def mkdir(newdir): - """works the way a good mkdir should :) + """works the way a good mkdir should :) - already exists, silently complete - regular file in the way, raise an exception - parent directory(ies) does not exist, make them as well - """ - if os.path.isdir(newdir): - pass - elif os.path.isfile(newdir): - raise OSError("a file with the same name as the desired " \ + """ + if os.path.isdir(newdir): + pass + elif os.path.isfile(newdir): + raise OSError("a file with the same name as the desired " \ "dir, '%s', already exists." % newdir) - else: - head, tail = os.path.split(newdir) - if head and not os.path.isdir(head): - mkdir(head) - #print "mkdir %s" % repr(newdir) - if tail: - os.mkdir(newdir) -def is_string(obj): - isinstance(obj, basestring) -def is_ascii(s): - return all(ord(c) < 128 for c in s) -def is_alphanumeric(string): - for c in string: - o = ord(c) - if not o in range(48, 58) + range(41, 91) + [95] + range(97, 123): - return False - return True + else: + head, tail = os.path.split(newdir) + if head and not os.path.isdir(head): + mkdir(head) + #print "mkdir %s" % repr(newdir) + if tail: + os.mkdir(newdir) + def slurp(filespec): with open(filespec) as x: f = x.read() return f + def unslurp(filespec, msg): with open(filespec, 'w') as x: x.write(msg) - -def find_in_seq(func, seq): - "Return first item in seq for which func(item) returns True." - for i in seq: - if func(i): - return i - -def find_keys(dic, val): - """return the key of dictionary dic given the value""" - return [k for k, v in dic.iteritems() if v == val] - -class Message(): - def __init__(self, msg=''): - self.text = msg - def add(self, text): - self.text += "
%s" % text - - -def render_template(template='login_nav', **kwargs): - for key in ['sidebar_left', 'sidebar_right', 'main', 'js', 'nav', 'css', - 'title', 'basehref']: - if not key in kwargs: - kwargs[key] = '' - - if kwargs['basehref'] == '': - kwargs['basehref'] = cfg.server_dir - - kwargs['template'] = template - kwargs['main_menu'] = cfg.main_menu - kwargs['submenu'] = cfg.main_menu.active_item() - kwargs['current_url'] = cherrypy.url() - kwargs['username'] = cherrypy.session.get(cfg.session_key) - kwargs['cfg'] = cfg - - return render_to_string(template + '.html', kwargs) - - -def filedict_con(filespec=None, table='dict'): - """TODO: better error handling in filedict_con""" - try: - return FileDict(connection=sqlite3.connect(filespec), table=table) - except IOError as (errno, strerror): - cfg.log.critical("I/O error({0}): {1}".format(errno, strerror)) - sys.exit(-1) diff --git a/views.py b/views.py new file mode 100644 index 000000000..055fa5838 --- /dev/null +++ b/views.py @@ -0,0 +1,51 @@ +# +# 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 . +# + +""" +Main Plinth views +""" + +import logging + +import cfg +from withsqlite.withsqlite import sqlite_db +from django.http.response import HttpResponseRedirect +from django.core.urlresolvers import reverse + + +LOGGER = logging.getLogger(__name__) + + +def index(request): + """Serve the main index page""" + # TODO: Move firstboot handling to firstboot module somehow + with sqlite_db(cfg.store_file, table='firstboot') as database: + if not 'state' in database: + # Permanent redirect causes the browser to cache the redirect, + # preventing the user from navigating to /plinth until the + # browser is restarted. + return HttpResponseRedirect(reverse('first_boot:index')) + + if database['state'] < 5: + LOGGER.info('First boot state - %d', database['state']) + return HttpResponseRedirect(reverse('first_boot:state%d' % + database['state'])) + + if request.user.is_authenticated(): + return HttpResponseRedirect(reverse('apps:index')) + + return HttpResponseRedirect(reverse('apps:about'))