mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
...
This commit is contained in:
commit
35071d7212
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
*.pyc
|
||||||
|
*.tiny.css
|
||||||
|
data/*.log
|
||||||
|
data/cherrypy_sessions
|
||||||
|
data/store.sqlite3
|
||||||
|
doc/*.tex
|
||||||
|
doc/*.pdf
|
||||||
|
doc/*.html
|
||||||
|
doc/*.aux
|
||||||
|
doc/*.toc
|
||||||
|
doc/*.log
|
||||||
|
doc/*.out
|
||||||
|
doc/COPYING.mdwn
|
||||||
|
doc/INSTALL.mdwn
|
||||||
|
doc/README.mdwn
|
||||||
|
doc/TODO.mdwn
|
||||||
|
doc/oneline.txt
|
||||||
|
doc/plinth.1
|
||||||
|
templates/*.py
|
||||||
|
TODO
|
||||||
34
INSTALL
Normal file
34
INSTALL
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
## Installing Plinth
|
||||||
|
|
||||||
|
Unzip and untar the source into a directory. Change to the directory
|
||||||
|
containing the program. Run `./plinth.py` and point your web
|
||||||
|
browser at `localhost:8000`. The default username is "admin" and the
|
||||||
|
default password is "secret".
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* cherrypy - python web engine
|
||||||
|
|
||||||
|
* python - tested with version 2.6.6
|
||||||
|
|
||||||
|
* *GNU Make* is used to build the templates and such.
|
||||||
|
|
||||||
|
The documentation has some dependencies too.
|
||||||
|
|
||||||
|
* *Markdown* is used to format and style docs.
|
||||||
|
|
||||||
|
* *Pandoc* converts the markdown to different formats.
|
||||||
|
|
||||||
|
* *PDFLatex* generates pdf versions of the documentation.
|
||||||
|
|
||||||
|
* *GNU Make* processes /doc/Makefile.
|
||||||
|
|
||||||
|
## Building the Documentation
|
||||||
|
|
||||||
|
Documentation has been collected into a pdf that can be built using
|
||||||
|
`make doc`. It also gets built into smaller files and other formats,
|
||||||
|
including one suitable for install as a man page.
|
||||||
|
|
||||||
|
<a name="installing_systemwide" />
|
||||||
36
Makefile
Normal file
36
Makefile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
MAKE=make
|
||||||
|
|
||||||
|
#TODO: add install target
|
||||||
|
|
||||||
|
CSS=$(wildcard *.css)
|
||||||
|
CSS=$(subst .tiny,,$(shell find themes -type f -name '*.css'))
|
||||||
|
COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS))
|
||||||
|
|
||||||
|
## Catch-all tagets
|
||||||
|
default: template docs css
|
||||||
|
all: default
|
||||||
|
|
||||||
|
%.tiny.css: %.css
|
||||||
|
@cat $< | python -c 'import re,sys;print re.sub("\s*([{};,:])\s*", "\\1", re.sub("/\*.*?\*/", "", re.sub("\s+", " ", sys.stdin.read())))' > $@
|
||||||
|
css: $(COMPRESSED_CSS)
|
||||||
|
|
||||||
|
template:
|
||||||
|
@$(MAKE) -s -C templates
|
||||||
|
templates: template
|
||||||
|
|
||||||
|
docs:
|
||||||
|
@$(MAKE) -s -C doc
|
||||||
|
doc: docs
|
||||||
|
|
||||||
|
html:
|
||||||
|
@$(MAKE) -s -C doc html
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@find themes -name "*.tiny.css" -exec rm {} \;
|
||||||
|
@find . -name "*~" -exec rm {} \;
|
||||||
|
@find . -name ".#*" -exec rm {} \;
|
||||||
|
@find . -name "#*" -exec rm {} \;
|
||||||
|
@find . -name "*.pyc" -exec rm {} \;
|
||||||
|
@find . -name "*.bak" -exec rm {} \;
|
||||||
|
@$(MAKE) -s -C doc clean
|
||||||
|
@$(MAKE) -s -C templates clean
|
||||||
53
README
Normal file
53
README
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
% PLINTH(1) Version 0.1 | Plinth User Manual
|
||||||
|
%
|
||||||
|
% February 2011
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
## Name
|
||||||
|
|
||||||
|
plinth - a web front end for administering every aspect of a Freedom Box.
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
plinth.py
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The Freedom Box is a net appliance conceived by Eben Moglen. It
|
||||||
|
contains free software and is designed to allow you to interface with
|
||||||
|
the rest of the net under conditions of protected privacy and data
|
||||||
|
security.
|
||||||
|
|
||||||
|
The Plinth front end is a web interface to administer the functions of
|
||||||
|
the Freedom Box. For example, the Freedom Box is a wireless router,
|
||||||
|
and the front end is where you can adjust its settings.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The front end is an extensible web platform for forms and menus. It
|
||||||
|
allows authenticated users to fill out forms. The interface saves the
|
||||||
|
form data and from them generates configuration files for the various
|
||||||
|
services running on the box.
|
||||||
|
|
||||||
|
The interface is extremely pluggable. Drop modules into place to add
|
||||||
|
new capabilities to Plinth and your Freedom Box. Replace existing modules to get
|
||||||
|
newer, better shinier functions. The modules will automatically
|
||||||
|
integrate into the existing menu system so you can control all of the
|
||||||
|
box's parts from one central location.
|
||||||
|
|
||||||
|
The interface has a 'basic' and an 'expert' mode. In basic mode, much
|
||||||
|
of Plinth's configuration and capability are hidden. Sane defaults
|
||||||
|
are chosen whenever possible. In expert mode, you can get down into
|
||||||
|
the details and configure things the average user never thinks about.
|
||||||
|
For example, experts can turn off ntp or switch ntp servers. Basic
|
||||||
|
users should never even know those options exist.
|
||||||
|
|
||||||
|
One caveat: expert mode is not as powerful as the commandline. We can
|
||||||
|
only do so much when translating free-form configuration files into
|
||||||
|
web forms. And if you manually change your configuration files, the
|
||||||
|
interface will overwrite those changes at first opportunity. This
|
||||||
|
interface is not a tool for super admins facing complex scenarios. It
|
||||||
|
is for home users to do a wide variety of basic tasks.
|
||||||
|
|
||||||
|
|
||||||
16
cfg.py
Normal file
16
cfg.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from menu import Menu
|
||||||
|
import os
|
||||||
|
|
||||||
|
file_root = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
data_dir = os.path.join(file_root, "data")
|
||||||
|
store_file = os.path.join(data_dir, "store.sqlite3")
|
||||||
|
status_log_file = os.path.join(data_dir, "status.log")
|
||||||
|
access_log_file = os.path.join(data_dir, "access.log")
|
||||||
|
users_dir = os.path.join(data_dir, "users")
|
||||||
|
|
||||||
|
product_name = "Plinth"
|
||||||
|
box_name = "Freedom Plug"
|
||||||
|
|
||||||
|
html_root = None
|
||||||
|
main_menu = Menu()
|
||||||
|
|
||||||
19
cherrypy.config
Normal file
19
cherrypy.config
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[global]
|
||||||
|
server.socket_host = '0.0.0.0'
|
||||||
|
server.socket_port = 8000
|
||||||
|
server.thread_pool = 10
|
||||||
|
tools.staticdir.root = "/home/vasile/src/plinth"
|
||||||
|
tools.sessions.on = True
|
||||||
|
tools.auth.on = True
|
||||||
|
tools.sessions.storage_type = "file"
|
||||||
|
tools.sessions.storage_path = "/home/vasile/src/plinth/data/cherrypy_sessions"
|
||||||
|
tools.sessions.timeout = 90
|
||||||
|
|
||||||
|
[/static]
|
||||||
|
tools.staticdir.on = True
|
||||||
|
tools.staticdir.dir = "static"
|
||||||
|
|
||||||
|
[/favicon.ico]
|
||||||
|
tools.staticfile.on = True
|
||||||
|
tools.staticfile.filename = "/home/vasile/src/plinth/static/theme/favicon.ico"
|
||||||
|
|
||||||
1
data/users/admin
Normal file
1
data/users/admin
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"username": "admin", "name": "Admin", "expert": "on", "groups": ["expert"], "passphrase": "5ebe2294ecd0e0f08eab7690d2a6ee69", "password": "", "email": "root@localhost"}
|
||||||
118
doc/Makefile
Normal file
118
doc/Makefile
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
DOCDIR=../dist/doc
|
||||||
|
|
||||||
|
PANDOC=pandoc
|
||||||
|
PDFLATEX=pdflatex
|
||||||
|
|
||||||
|
|
||||||
|
# List text files in the order in which you want them to appear in the
|
||||||
|
# complete manual:
|
||||||
|
SOURCES=README.mdwn INSTALL.mdwn themes.mdwn hacking.mdwn TODO.mdwn modules.mdwn scripts.mdwn design.mdwn roadmap.mdwn faq.mdwn COPYING.mdwn colophon.mdwn
|
||||||
|
TODO_SOURCES=$(patsubst TODO.mdwn,,$(SOURCES))
|
||||||
|
MAN_SOURCES=$(patsubst COPYING.mdwn,copyright_notice00,$(SOURCES))
|
||||||
|
|
||||||
|
NEWLINE_SOURCES=$(patsubst %,% oneline.txt,$(SOURCES))
|
||||||
|
NEWLINE_MAN_SOURCES=$(patsubst %,% oneline.txt,$(MAN_SOURCES))
|
||||||
|
|
||||||
|
HTML=plinth.html $(patsubst %.mdwn,%.html,$(SOURCES))
|
||||||
|
HTML_PART=$(patsubst %.html,%.part.html,$(HTML))
|
||||||
|
LATEX=plinth.tex $(patsubst %.mdwn,%.tex,$(SOURCES))
|
||||||
|
PDF=plinth.pdf $(patsubst %.mdwn,%.pdf,$(SOURCES))
|
||||||
|
MAN=plinth.1
|
||||||
|
|
||||||
|
OUTPUTS=$(HTML) $(LATEX) $(MAN) $(PDF) $(HTML_PART)
|
||||||
|
DIST_OUTPUT=$(patsubst %,$(DOCDIR)/%,$(OUTPUTS))
|
||||||
|
|
||||||
|
# Yes, do it twice. TODO created during the process requires a second run
|
||||||
|
default:
|
||||||
|
make all
|
||||||
|
make all
|
||||||
|
all: oneline.txt $(OUTPUTS) Makefile
|
||||||
|
|
||||||
|
$(DOCDIR)/%: %
|
||||||
|
cp $< $@
|
||||||
|
dist: $(DIST_OUTPUT)
|
||||||
|
###############################################################################
|
||||||
|
oneline.txt: Makefile
|
||||||
|
perl -e 'print "\n"' > oneline.txt
|
||||||
|
|
||||||
|
$(SOURCES):
|
||||||
|
@rm -f $@
|
||||||
|
@ln -s ../$(patsubst %.mdwn,%,$@) $@
|
||||||
|
|
||||||
|
../TODO : $(TODO_SOURCES) ../*.py ../modules/*.py ../Makefile Makefile ../templates/Makefile
|
||||||
|
grep -ro --exclude=.git* --exclude=plinth.1 --exclude=*.tex --exclude=*.html \
|
||||||
|
--exclude=README.mdwn --exclude=INSTALL.mdwn \
|
||||||
|
--exclude=TODO.mdwn --exclude=COPYING.mdwn \
|
||||||
|
"TODO\:.*" ../* 2>/dev/null | \
|
||||||
|
sed -e "s/TODO\://g" | \
|
||||||
|
sed -e "s/^..\//* /g" | \
|
||||||
|
sed -e 's/"""$$//g' | \
|
||||||
|
sed -e 's/<\/p>$$//g' \
|
||||||
|
> ../TODO
|
||||||
|
###############################################################################
|
||||||
|
##
|
||||||
|
## MAN PAGES
|
||||||
|
##
|
||||||
|
$(MAN): $(SOURCES) ../TODO
|
||||||
|
@csplit -s -f copyright_notice COPYING.mdwn '/##/'
|
||||||
|
cat $(NEWLINE_MAN_SOURCES) | perl -pe 'BEGIN { $$/=undef } $$_ =~ s/\n\n#\s.*/\n/gm; $$_ =~ s/\n\n#/\n\n/gm; $$_ =~ s/(\n\n#\s.*)/uc($$1)/gme' > .make_man
|
||||||
|
$(PANDOC) -s -t man -o $@ .make_man
|
||||||
|
@rm -f copyright_notice0? .make_man
|
||||||
|
manpages: $(MAN)
|
||||||
|
###############################################################################
|
||||||
|
##
|
||||||
|
## LaTeX
|
||||||
|
##
|
||||||
|
%.tex: %.mdwn
|
||||||
|
$(PANDOC) -s --toc -f markdown --standalone -o $@ $<
|
||||||
|
|
||||||
|
hacking.tex: hacking.mdwn ../TODO
|
||||||
|
$(PANDOC) -s --toc -f markdown -o $@ hacking.mdwn ../TODO
|
||||||
|
|
||||||
|
plinth.tex: $(NEWLINE_SOURCES) ../TODO
|
||||||
|
$(PANDOC) -s --toc -f markdown -o $@ $(NEWLINE_SOURCES)
|
||||||
|
|
||||||
|
latex: $(LATEX)
|
||||||
|
###############################################################################
|
||||||
|
##
|
||||||
|
## HTML
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
# This gets us the html sections complete with TOC, but without the
|
||||||
|
# HTML and head section boilerplate. /help/view uses the parts.
|
||||||
|
%.part.html: %.html
|
||||||
|
csplit -s -f $@ $< '%.*<body>%'
|
||||||
|
sed '1d' $@00 > $@01
|
||||||
|
csplit -s -f $@ $@01 '/<\/body>/'
|
||||||
|
mv $@00 $@
|
||||||
|
rm $@01
|
||||||
|
%.html: %.mdwn header.html footer.html style.css Makefile
|
||||||
|
$(PANDOC) -s --toc -c style.css -f markdown -o $@ header.html $< footer.html
|
||||||
|
|
||||||
|
hacking.html: hacking.mdwn ../TODO style.css Makefile
|
||||||
|
$(PANDOC) -s --toc -c style.css -o $@ -f markdown hacking.mdwn ../TODO
|
||||||
|
|
||||||
|
#plinth.html: $(NEWLINE_SOURCES) ../TODO style.css Makefile
|
||||||
|
# $(PANDOC) -s --toc -c style.css -o $@ -f markdown $(NEWLINE_SOURCES)
|
||||||
|
|
||||||
|
plinth.html: $(NEWLINE_SOURCES) ../TODO style.css Makefile
|
||||||
|
@csplit -s -f copyright_notice COPYING.mdwn '/##/'
|
||||||
|
$(PANDOC) -s --toc -c style.css -o $@ -f markdown $(NEWLINE_MAN_SOURCES)
|
||||||
|
@rm -f copyright_notice0? .make_man
|
||||||
|
|
||||||
|
html: $(HTML) $(HTML_PART)
|
||||||
|
###############################################################################
|
||||||
|
%.pdf: %.tex
|
||||||
|
$(PDFLATEX) -interaction=batchmode $< >/dev/null
|
||||||
|
$(PDFLATEX) -interaction=batchmode $< >/dev/null # yes, do it twice so the toc works
|
||||||
|
|
||||||
|
pdf: $(PDF)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
clean-latex:
|
||||||
|
rm -f *.log *.out *.aux *.toc
|
||||||
|
|
||||||
|
clean: clean-latex
|
||||||
|
rm -f $(OUTPUTS) README.mdwn INSTALL.mdwn TODO.mdwn COPYING.mdwn \
|
||||||
|
copyright_notice0? \#*\# ../TODO oneline.txt
|
||||||
8
doc/colophon.mdwn
Normal file
8
doc/colophon.mdwn
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Colophon
|
||||||
|
|
||||||
|
This manual was typed in emacs, formatted using markdown and converted
|
||||||
|
to pdf, html, troff and latex using pandoc and pdflatex.
|
||||||
|
|
||||||
|
The complete source code to this manual is available in the 'doc'
|
||||||
|
directory of the Freedom Box front end source distribution.
|
||||||
|
|
||||||
196
doc/design.mdwn
Normal file
196
doc/design.mdwn
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# Freedom Box Design and Architecture
|
||||||
|
|
||||||
|
This article describes and documents architectural considerations for
|
||||||
|
my vision of Freedom Box. It is not specific to the user interface
|
||||||
|
and instead contains thoughts about the Freedom Box as a whole.
|
||||||
|
|
||||||
|
The major immediate design problems that I see are authentication, ip
|
||||||
|
addressing, and backup. I'm sure there are others.
|
||||||
|
|
||||||
|
If the Freedom Box front end pulls together basic pieces and
|
||||||
|
configures them properly, we'll have the start of the freedom stack.
|
||||||
|
Then we can build things like free social networking applications on
|
||||||
|
top.
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
The target hardware for this project is the
|
||||||
|
[GuruPlug](http://www.globalscaletechnologies.com/t-guruplugdetails.aspx).
|
||||||
|
It has low power consumption, two wired network interfaces, one
|
||||||
|
wireless hostapd-capable interface (802.11b), and two usb slots for
|
||||||
|
connecting external drives and peripherals. The hardware target might
|
||||||
|
change if a better unit becomes available (for example, with 802.11n
|
||||||
|
or USB 3 capability). The GuruPlug is reputed to have availability
|
||||||
|
problems involving shipping delays, and targetting a unit that cannot
|
||||||
|
satsify high demand might be an issue.
|
||||||
|
|
||||||
|
Freedom Boxes are not giant honking servers. They are small boxes
|
||||||
|
that do perform a lot of functions. They are not heavily resourced.
|
||||||
|
So keep things small and light.
|
||||||
|
|
||||||
|
By the same token, there is no need to scale up to thousands of users.
|
||||||
|
A box should happily serve a person and her family, maybe an extended
|
||||||
|
group of friends. Call it 100 people at the most.
|
||||||
|
|
||||||
|
The target user for this project is the home consumer of network
|
||||||
|
appliances. It ranges from the most basic user (the kind of person
|
||||||
|
who might have spent all of three minutes setting up her LinkSys
|
||||||
|
WRT54G) to the home enthusiast who wants a local file server for media
|
||||||
|
and backups, print server and dns service.
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Authentication in the context of the Freedom Box is a diffiult
|
||||||
|
problem. You need to be able to trust your box, and it needs to be
|
||||||
|
able to trust you. What's more, security must withstand forgotten
|
||||||
|
passwords, server destruction, changing email addresses and any other
|
||||||
|
possible disaster scenario.
|
||||||
|
|
||||||
|
In addition, your friends (and their boxes) need to trust your box
|
||||||
|
when it acts on your behalf, even if it does so when you're not around
|
||||||
|
to enter passphrases or otherwise confirm agency. But even as it
|
||||||
|
needs to operate in your absence, it can't itself have authority to
|
||||||
|
access all your data, because you might lack exclusive physical
|
||||||
|
control of the box (e.g. if you have a roommate). If the box can act
|
||||||
|
as you without limits, anybody who takes control of the box takes
|
||||||
|
control of your online identity.
|
||||||
|
|
||||||
|
What's more, security should be high. Freedom Boxes might contain
|
||||||
|
emails or other sensitive documents. The box mediates network
|
||||||
|
traffic, which means rooting it would allow an attacker to spy on or
|
||||||
|
even change traffic. Demonstrating control of an email address should
|
||||||
|
not be enough to gain access to the box and its admin functions.
|
||||||
|
|
||||||
|
### Passphrases
|
||||||
|
|
||||||
|
Most users habitually think of passwords. We need to force them to
|
||||||
|
think of passphrases. It starts by using 'passphrase' in the
|
||||||
|
literature. Second, we need to encourage users to pick phrases, and
|
||||||
|
prompts should urge them to do so. We also need minimum password
|
||||||
|
lengths and maybe even a requirement of at least a few spaces.
|
||||||
|
|
||||||
|
Even better than passphrases are passfiles. We should keep a lookout
|
||||||
|
for ways to use files instead of phrases when securing the Freedom
|
||||||
|
Box.
|
||||||
|
|
||||||
|
### A Scheme for Secure Web Login
|
||||||
|
|
||||||
|
Passphrases should
|
||||||
|
[never be stored in plain text](http://www.codinghorror.com/blog/2007/09/rainbow-hash-cracking.html).
|
||||||
|
[MD5 is too fast for passphrases.](http://chargen.matasano.com/chargen/2007/9/7/enough-with-the-rainbow-tables-what-you-need-to-know-about-s.html)
|
||||||
|
Therefore, my current plan for secure website login involves bcrypt:
|
||||||
|
|
||||||
|
The server sends the client a permanent salt, PS, a random session
|
||||||
|
salt, SS and a number of rounds, R. It brcypts the password using PS
|
||||||
|
and stores the result, B.
|
||||||
|
|
||||||
|
The browser
|
||||||
|
[bcrypts the passphrase using javascript](https://code.google.com/p/javascript-bcrypt/)
|
||||||
|
and PS. Then, it bcrypts that result with SS. It does R rounds of
|
||||||
|
bcrypt with S. The browser then sends the result, C, back to the
|
||||||
|
server.
|
||||||
|
|
||||||
|
The server retrieves bcrypts its stored, hashed passphrase and does R
|
||||||
|
rounds of bcrypt on it with SS. If that matches C, the passphrase is
|
||||||
|
a match.
|
||||||
|
|
||||||
|
The server must be able to keep track of sessions, as it needs to
|
||||||
|
remember SS between giving it to the client and getting it back. The
|
||||||
|
Server cannot rely on the client to remind it of SS. This would allow
|
||||||
|
an attacker to dictate SS and leave the server vulnerable to replay
|
||||||
|
attacks.
|
||||||
|
|
||||||
|
This scheme has the dual advantage of not storing any cleartext
|
||||||
|
passphrases and also never sending any cleartext passphrases between
|
||||||
|
client and server.
|
||||||
|
|
||||||
|
TODO: consult a security expert as to the strength of this scheme
|
||||||
|
|
||||||
|
### Other Schemes
|
||||||
|
|
||||||
|
#### Monkeysphere
|
||||||
|
|
||||||
|
[Monkeysphere](http://web.monkeysphere.info/) is a promising project.
|
||||||
|
Monkeysphere's major win is that it avoids the clunkiness of SSL
|
||||||
|
certificate authorities. That's huge, except it replaces the
|
||||||
|
certificate authority structure with the PGP web of trust. Relying on
|
||||||
|
the web of trust is a good idea
|
||||||
|
([much better than relying on SSL certificate authorities](http://www.crypto.com/blog/spycerts/)),
|
||||||
|
except that new users might not have joined the web. It also requires
|
||||||
|
a bunch of GPG infrastructure that might be difficult to setup
|
||||||
|
automatically and with little user intervention.
|
||||||
|
|
||||||
|
As of this writing, it does not appear to support Windows or Macintosh
|
||||||
|
clients, making it unsuitable for this project.
|
||||||
|
|
||||||
|
#### Secure Remote Password
|
||||||
|
|
||||||
|
[SRP](http://srp.stanford.edu/) is an interesting technique. Patents
|
||||||
|
are a concern. What does it buy us that my scheme, above does not?
|
||||||
|
|
||||||
|
There is a
|
||||||
|
[python implementation](http://members.tripod.com/professor_tom/archives/srpsocket.html),
|
||||||
|
although it is not clear that it has been through the crucible. Even
|
||||||
|
if SRP is solid, this implementation might be flawed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Finding Each Other
|
||||||
|
|
||||||
|
### Dynamic DNS
|
||||||
|
|
||||||
|
Each box might need multiple dynamic DNS pointers. Every box is going
|
||||||
|
to require its own set of dynamic names. For example, my Freedom Box
|
||||||
|
might be known as both `fb.hackervisions.org` and
|
||||||
|
`james.softwarefreedom.org`. My work colleagues might know me as
|
||||||
|
`james@james.softwarefreedom.org` while my friends might reach me at
|
||||||
|
`james@fb.hackervisions.org`. By default, when contacts come in via
|
||||||
|
different names, my box might be smart enough to treat those contacts
|
||||||
|
differently.
|
||||||
|
|
||||||
|
If I share this box with my partner, Emily, she might be
|
||||||
|
`emily@jv187.fboxes.columbia.edu`, in which case my box will need
|
||||||
|
another dynamic DNS pointer.
|
||||||
|
|
||||||
|
### Mesh Networking
|
||||||
|
|
||||||
|
Freedom Boxes should be meshed. Rather than run in infrastructure
|
||||||
|
mode, they should route for each other and create strong local meshes
|
||||||
|
that will carry data for anybody that passes through.
|
||||||
|
|
||||||
|
There are some problems, though.
|
||||||
|
|
||||||
|
* I'm not sure how well nodes in ad-hoc mesh mode interact with
|
||||||
|
others in infrastructure mode.
|
||||||
|
|
||||||
|
* The Freedom Box only has one wireless radio, which will drastically
|
||||||
|
hurt speed.
|
||||||
|
|
||||||
|
* Routing can be difficult when connecting mesh network nodes to the WAN.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Everybody should automatically and redundantly store encrypted backups
|
||||||
|
on their friend's freedom boxes. Recovery should not assume you kept
|
||||||
|
your key or know a password. We can recover the decryption key by
|
||||||
|
having a bunch of friends encrypt the key in various combinations. If
|
||||||
|
enough of my friends get together, they can decrypt it for me.
|
||||||
|
Optionally, a passphrase can serve to slow my friends down if they
|
||||||
|
turn against me. Maybe it takes 5 friends acting in concert to
|
||||||
|
recover my key, but only 3 to recover a version protected by the
|
||||||
|
passphrase.
|
||||||
|
|
||||||
|
## Push vs Pull
|
||||||
|
|
||||||
|
For a social network, real time communication is key. Asynch
|
||||||
|
communication is good, but sometimes people just want to bash comments
|
||||||
|
back and forth. For that, you need push, which is just a web fetch
|
||||||
|
meaning "pull my rss feed".
|
||||||
|
|
||||||
|
If, however, you have a lot of friends and they have a lot of friends,
|
||||||
|
your extended network can be large, resulting in many "pull my rss
|
||||||
|
feed" commands. We should only process push requests for friends to
|
||||||
|
reduce load. You really don't need real time updates of the social
|
||||||
|
networking activity of strangers. Friends of friends is as far out as
|
||||||
|
things should ever go.
|
||||||
37
doc/faq.mdwn
Normal file
37
doc/faq.mdwn
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Plinth and Freedom Plug FAQ
|
||||||
|
|
||||||
|
## General Questions
|
||||||
|
|
||||||
|
### What is the Freedom Plug?
|
||||||
|
|
||||||
|
The Freedom Plug is .... insert links...
|
||||||
|
|
||||||
|
The Freedom Plug is based on the GNU/Debian operating system. It is
|
||||||
|
not a Linux distribution. It is a network appliance that depends on a
|
||||||
|
series of Debian packages that configure a plug computer to behave as
|
||||||
|
a Freedom Plug.
|
||||||
|
|
||||||
|
### What is Plinth?
|
||||||
|
|
||||||
|
Plinth is the web-based GUI administration front end for the Freedom Plug.
|
||||||
|
|
||||||
|
### On what hardware is the Freedom Plug based?
|
||||||
|
|
||||||
|
The current targets are the [Guru Plug](http://guruplug) and the
|
||||||
|
[Dream Plug](http://dreamplug).
|
||||||
|
|
||||||
|
## Accessing the Freedom Plug
|
||||||
|
|
||||||
|
### Why does ssh listen on port 2222 instead of 22?
|
||||||
|
|
||||||
|
If ssh listens on port 2222, bots and scripts will forever attempt to
|
||||||
|
guess your username and password. Maybe your password isn't so
|
||||||
|
strong. Maybe the bots get lucky. Either way, if you allow ssh
|
||||||
|
access on port 22, you're taking a chance that can be avoided quite
|
||||||
|
easily by moving your ssh activity to a slightly more obscure port.
|
||||||
|
|
||||||
|
Because ssh activity on these boxes will be limited to programs
|
||||||
|
configured to work specifically with Freedom Plugs as well relatively
|
||||||
|
few people generally using ssh, the coordination necessary to use a
|
||||||
|
non-standard port is easily achieved.
|
||||||
|
|
||||||
0
doc/footer.html
Normal file
0
doc/footer.html
Normal file
97
doc/hacking.mdwn
Normal file
97
doc/hacking.mdwn
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# Hacking
|
||||||
|
|
||||||
|
This codebase could really use a testing framework.
|
||||||
|
|
||||||
|
If you are interested in helping out, writing tests is a great place
|
||||||
|
to start-- you don't need to know much about the code or python to
|
||||||
|
write useful tests.
|
||||||
|
|
||||||
|
In addition to a testing framework, the code is in need of some
|
||||||
|
general cleanup. I've been inconsistent in capitalization conventions
|
||||||
|
as well as in my use of underscores.
|
||||||
|
|
||||||
|
The plugin interface could use some attention as well. Right now,
|
||||||
|
it's a a bit of a free-for-all until I see how the plugins actually
|
||||||
|
code up. Channeling all that into a few plugin interface grooves
|
||||||
|
would be a help.
|
||||||
|
|
||||||
|
If you're feeling more ambitious than that, the best way to improve
|
||||||
|
Plinth is to add modules. More functionality, especially in the
|
||||||
|
router section, could convert the Freedom Box from a good idea to a
|
||||||
|
must-have appliance.
|
||||||
|
|
||||||
|
Beyond that, we need to train some expert eyes on the interaction
|
||||||
|
between Freedom Boxes. Transparent, zero-config box-to-box backup is
|
||||||
|
possible. We just need to build an auth and dns layer. There are
|
||||||
|
lots of theories on how to do this well. The first theory reduced to
|
||||||
|
practice wins!
|
||||||
|
|
||||||
|
There is a list of TODO items below. Some of them are discrete pieces
|
||||||
|
that can be tackled without diving too deep into the code.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
Plinth is available from github at
|
||||||
|
`git://github.com/jvasile/plinth.git`. The [project page on
|
||||||
|
github](https://github.com/jvasile/plinth) is at
|
||||||
|
`https://github.com/jvasile/plinth`.
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
There are lots of bugs. We don't have a spec or tests, so a bug is
|
||||||
|
really just any unexpected behavior. I am not easily surprised, but
|
||||||
|
there are still lots of bugs.
|
||||||
|
|
||||||
|
<a name="hacking_code_practices" />
|
||||||
|
|
||||||
|
## Coding Practices
|
||||||
|
|
||||||
|
I try to stick to [PEP 8](http://www.python.org/dev/peps/pep-0008/)
|
||||||
|
recommendations. That's not to say I don't deviate, just that
|
||||||
|
deviations are usually bugs that should be fixed.
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
|
||||||
|
Every module should `from gettext import gettext as _` and wrap
|
||||||
|
displayed strings with _(). We don't have the language stuff in place
|
||||||
|
yet (we have no translation files), but we need to put the
|
||||||
|
infrastructure in place for it from the start. Use it like this:
|
||||||
|
|
||||||
|
cfg.log.error(_("Couldn't import %s: %s") % (path, e))
|
||||||
|
|
||||||
|
### Variables and Data Stores
|
||||||
|
|
||||||
|
Plinth needs to keep information for short and long term
|
||||||
|
future use, and it can't just store all of that on the stack.
|
||||||
|
|
||||||
|
Global config information can be put in the `cfg` module namespace.
|
||||||
|
Keep it thread and session safe, though, or you'll get undefined
|
||||||
|
behavior as soon as multiple simultaneous users enter the picture.
|
||||||
|
|
||||||
|
Cherrpy has support for session variables. Use those for short term
|
||||||
|
user-specific data.
|
||||||
|
|
||||||
|
For long term storage, the Plinth needs a back end
|
||||||
|
storage solution. Databases are a bit opaque and can be hard for
|
||||||
|
third party software or shell users to manipulate. For now, I've
|
||||||
|
decided that persistent data should be placed in dicts and stored in
|
||||||
|
json format. We'll need a file locking solution too.
|
||||||
|
|
||||||
|
The `user_store.py` module implements the `UserStoreModule` interface
|
||||||
|
specified in `plugin_mount.py`. Any new system that respects that
|
||||||
|
interface can be used. The existing `user_store.py` holds entire user
|
||||||
|
files in memory and caches user files as it goes. This has two
|
||||||
|
downsides: first, if you have lots of users and store big things in
|
||||||
|
the user dict, you'll run out of memory. Second, it's not thread
|
||||||
|
safe. Maybe a database is a good idea after all.
|
||||||
|
|
||||||
|
We do not yet have a means of storing module data for long terms. My
|
||||||
|
current thinking is that modules can store data in their own
|
||||||
|
directories. That makes removal easy.
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
Plinth has a number of open todo items. Please help!
|
||||||
|
|
||||||
|
* Implement the functions in the submenus of router.py
|
||||||
|
* Unify our logging and cherrypy's.
|
||||||
0
doc/header.html
Normal file
0
doc/header.html
Normal file
71
doc/modules.mdwn
Normal file
71
doc/modules.mdwn
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Modules
|
||||||
|
|
||||||
|
Almost all of the front end's functionality is contained in small,
|
||||||
|
separate modules that reside in the directory tree beneath
|
||||||
|
`/modules/installed`. Some are installed by default, some are
|
||||||
|
required for operation, and some are entirely optional.
|
||||||
|
|
||||||
|
## Installing and Loading Modules
|
||||||
|
|
||||||
|
Eventually, the goal is for module to be separate Debian package so
|
||||||
|
installation is as simple as `aptitude install freedombox-foo`. As an
|
||||||
|
intermediate step, we'll start scripting module installation. But
|
||||||
|
until then, we can install them manually.
|
||||||
|
|
||||||
|
Modules are installed by copying them into the tree beneath the
|
||||||
|
`/modules/installed` directory. They are activated by linking their
|
||||||
|
.py files into the modules directory. (Freedom Box will not load
|
||||||
|
modules unless they are symlinks.) If the module provides other
|
||||||
|
resources, they can be linked from wherever in the working tree they
|
||||||
|
need to be. So if a module provides a template, that template can be
|
||||||
|
linked from `/templates`.
|
||||||
|
|
||||||
|
Modules can be organized into subdirectories beneath the installed
|
||||||
|
directory. As an initial matter, I've made separate directories for
|
||||||
|
each major tab. Modules that extend the functionality of the Freedom
|
||||||
|
Box code base (as opposed to being user-visible interface modules
|
||||||
|
under specific major tabs) go in `modules/installed/lib`. This
|
||||||
|
convention can be adjusted or ignored as needed. In fact, modules can
|
||||||
|
be installed anywhere in the file system as long as the symlinks are
|
||||||
|
in `/modules`.
|
||||||
|
|
||||||
|
The names of the symlinks in the `modules` directory can be arbitrary,
|
||||||
|
and if a name is already taken, (e.g. if `router/info.py` and
|
||||||
|
`apps/info.p`y both exist), append a counter to the end (e.g. link
|
||||||
|
`info.py` to `router/info.py` and `info1.py` to `apps/info.py`).
|
||||||
|
|
||||||
|
If a module cannot be imported for any reason (e.g. it's a dead
|
||||||
|
symlink), freedombox.py will log an error but will otherwise just keep
|
||||||
|
going.
|
||||||
|
|
||||||
|
TODO: automatically prune dead links to clear out old module installs.
|
||||||
|
This is something the install scripts should do.
|
||||||
|
|
||||||
|
## Pluggable Module Structure
|
||||||
|
|
||||||
|
Plugin interfaces are contained in `plugin_mount.py`. Each interface
|
||||||
|
is a metaclass. Classes that implement a given interface should
|
||||||
|
inherit the metaclass and then provide the indicated properties. New
|
||||||
|
metaclasses can be created to make new classes of plugins.
|
||||||
|
|
||||||
|
Any place that might be affected by arbitrary addition of modules
|
||||||
|
should have its own metaclass.
|
||||||
|
|
||||||
|
### FromPlugin
|
||||||
|
|
||||||
|
Each form displayed in the interface should inherit from FormPlugin.
|
||||||
|
This will allow the system to display multiple forms on the page in
|
||||||
|
the correct order. Forms will likely also want to inherit from
|
||||||
|
PagePlugin because after the user clicks submit, the plugin usually
|
||||||
|
displays a responsive message or an error (along with the form to fill
|
||||||
|
out again).
|
||||||
|
|
||||||
|
## Coding Practices for Modules
|
||||||
|
|
||||||
|
All the
|
||||||
|
[coding practices for hacking on the front end](#hacking_code_practices)
|
||||||
|
also apply to modules. In addition, I try to stick to the other
|
||||||
|
practices listed in this section.
|
||||||
|
|
||||||
|
* Every module should `from gettext import gettext as _` and wrap
|
||||||
|
displayed strings with _().
|
||||||
64
doc/roadmap.mdwn
Normal file
64
doc/roadmap.mdwn
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Plinth Roadmap
|
||||||
|
|
||||||
|
## 0.1 Basic Wireless Access Point
|
||||||
|
|
||||||
|
* <strike>Basic/Expert mode toggle</strike>
|
||||||
|
* SSH access
|
||||||
|
* Connect to WAN via DHCP and static IP
|
||||||
|
* Offer DHCP on wired and wireless interfaces
|
||||||
|
* WEP
|
||||||
|
* WPA2
|
||||||
|
* Bridge WAN and LAN
|
||||||
|
|
||||||
|
## 0.2 Basic Wireless Router
|
||||||
|
|
||||||
|
* NAT
|
||||||
|
* DMZ
|
||||||
|
* Port forwarding and triggering
|
||||||
|
* dhcp server (on by default, expert can disable)
|
||||||
|
* dns server
|
||||||
|
* MAC address filtering
|
||||||
|
* NTP client
|
||||||
|
|
||||||
|
## 0.3 Advanced Wireless Router
|
||||||
|
|
||||||
|
* dynamic dns - This is special. See the notes in the section on
|
||||||
|
[dynamic DNS](#dynamic-dns) for details.
|
||||||
|
* UPnP
|
||||||
|
* Cron
|
||||||
|
* Tx power management
|
||||||
|
|
||||||
|
## 0.4 File Server, More Routing Features
|
||||||
|
|
||||||
|
* boxbackupd
|
||||||
|
* NFS
|
||||||
|
* Samba
|
||||||
|
* Auto mount usb volumes and create samba shares for them
|
||||||
|
* TOR
|
||||||
|
* Ad blocking web proxy
|
||||||
|
* HTTPS everywhere (on by default, experts can disable)
|
||||||
|
|
||||||
|
## 0.5 Print Server and Backups
|
||||||
|
|
||||||
|
* Social backup to friend's boxes via ddns and boxbackup
|
||||||
|
* printer discovery and sharing via samba and cups
|
||||||
|
|
||||||
|
## 1.0 Plinth Lives!
|
||||||
|
|
||||||
|
* Complete user documentation for every basic and expert menu item
|
||||||
|
* Package management complete
|
||||||
|
|
||||||
|
## Unscheduled Features
|
||||||
|
|
||||||
|
There are a variety of other features to be implemented, but they are
|
||||||
|
of lower priority than all the tasks scheduled above.
|
||||||
|
|
||||||
|
* NTP Server
|
||||||
|
* Provide virtual networks and multiple SSIDs
|
||||||
|
* Radius Server
|
||||||
|
* QoS
|
||||||
|
* file explorer
|
||||||
|
* Mesh networking
|
||||||
|
* Chaining boxes so the furthest upstream knows to route messages down
|
||||||
|
the chain (e.g. if you and your roommate both have your own box, you
|
||||||
|
just plug one into the other).
|
||||||
1
doc/style.css
Symbolic link
1
doc/style.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../static/theme/style.css
|
||||||
59
doc/themes.mdwn
Normal file
59
doc/themes.mdwn
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Themes and Templates
|
||||||
|
|
||||||
|
The visual look and feel of the front end is described in theme files
|
||||||
|
while <a href="http://cheetahtemplate.org">Cheetah templates</a>
|
||||||
|
handle layout.
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
Themes are stored in `/themes`. Themes consist entirely of static
|
||||||
|
files (e.g. css, images and javascript) and templates. The default or
|
||||||
|
active theme is linked from `/static/default` and `templates/default`.
|
||||||
|
If your theme needs to change anything other than these items, you'll
|
||||||
|
need a module (perhaps you'll need both).
|
||||||
|
|
||||||
|
There is not currently any support for dynamically choosing a theme at
|
||||||
|
runtime, but it is theoretically possible.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Plinth uses the Cheetah templating system. Templates are stored in
|
||||||
|
`/templates`. Template requirements are not specified.
|
||||||
|
|
||||||
|
TODO: formalize the template spec so template writers know what they need to implement and where they can deviate.
|
||||||
|
|
||||||
|
In this section, I'll attempt to document some of the assumptions the
|
||||||
|
program has about templates. The goal is that if you write a tempate
|
||||||
|
that implements the spec, it should work just fine.
|
||||||
|
|
||||||
|
### The Template Stack
|
||||||
|
The template is a hierarchical stack, where some templates extend on
|
||||||
|
others. At the base of this stack is `base.tmpl`. It should specify
|
||||||
|
variables as blocks (rather than using the $ notation). This allows
|
||||||
|
other templates to easily override the base template.
|
||||||
|
|
||||||
|
Next up is the `page.tmpl`. It extends `base.tmpl` and simply
|
||||||
|
replaces all the blocks with interpolable variables. Most of the
|
||||||
|
time, `page.tmpl` is what you will actually use to create pages.
|
||||||
|
|
||||||
|
`err.tmpl` builds on top of `page.tmpl` by adding some decoration to
|
||||||
|
the title field.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
Plinth expects a `main` block. This is where the
|
||||||
|
meat of the content goes. It is the center pain in the default
|
||||||
|
layout. There is a `title` block that the program will fill with text
|
||||||
|
describing the current page. `sidebar_left` contains the submenu
|
||||||
|
navigation menu, and `sidebar_right` is where we put all short text
|
||||||
|
that helps the admin fill out forms. They don't have to be sidebars,
|
||||||
|
and they don't have to go on the left and right.
|
||||||
|
|
||||||
|
It is possible to override the `footer`, but I haven't yet found a
|
||||||
|
reason to do so.
|
||||||
|
|
||||||
|
## Cheetah
|
||||||
|
|
||||||
|
This section is for Cheetah hints that are especially useful in this context.
|
||||||
|
|
||||||
|
TODO: add Cheetah hints
|
||||||
150
filedict.py
Normal file
150
filedict.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""filedict.py
|
||||||
|
a Persistent Dictionary in Python
|
||||||
|
|
||||||
|
Author: Erez Shinan
|
||||||
|
Date : 31-May-2009
|
||||||
|
"""
|
||||||
|
|
||||||
|
import simplejson as json ## jlv replaced pickle with json
|
||||||
|
|
||||||
|
import UserDict
|
||||||
|
##import cPickle as pickle
|
||||||
|
|
||||||
|
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
|
||||||
39
logger.py
Normal file
39
logger.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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")
|
||||||
74
menu.py
Normal file
74
menu.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import simplejson as json
|
||||||
|
from urlparse import urlparse
|
||||||
|
import cherrypy
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class Menu():
|
||||||
|
"""One menu item."""
|
||||||
|
def __init__(self, label="", url="#", order=50):
|
||||||
|
"""label is the text that is displayed on the menu.
|
||||||
|
|
||||||
|
url is the url location that will be activated when the menu
|
||||||
|
item is selected
|
||||||
|
|
||||||
|
order is the numerical rank of this item within the menu.
|
||||||
|
Lower order items appear closest to the top/left of the menu.
|
||||||
|
By convention, we use the spectrum between 0 and 100 to rank
|
||||||
|
orders, but feel free to disregard that. If you need more
|
||||||
|
granularity, don't bother renumbering things. Feel free to
|
||||||
|
use fractional orders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.label = label
|
||||||
|
self.order = order
|
||||||
|
self.url = url
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
def sort_items(self):
|
||||||
|
"""Sort the items in self.items by order."""
|
||||||
|
self.items = sorted(self.items, key=lambda x: x.order, reverse=False)
|
||||||
|
def add_item(self, label, url, order=50):
|
||||||
|
"""This method creates a menu item with the parameters, adds
|
||||||
|
that menu item to this menu, and returns the item."""
|
||||||
|
item = Menu(label=label, 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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def active_item(self):
|
||||||
|
"""Return item list (e.g. submenu) of active menu item."""
|
||||||
|
path = urlparse(cherrypy.url()).path
|
||||||
|
for item in self.items:
|
||||||
|
if path.startswith(item.url):
|
||||||
|
return item
|
||||||
|
|
||||||
|
def serializable(self, render_subs=False):
|
||||||
|
"""Return the items in self.items as a serializable object we can pass to json.
|
||||||
|
Note: this doesn't serialize all the data in this object."""
|
||||||
|
|
||||||
|
so = []
|
||||||
|
for item in self.items:
|
||||||
|
i = { 'label':item.label, 'url':item.url}
|
||||||
|
if item.active_p():
|
||||||
|
i['active']=True
|
||||||
|
if item.items and render_subs:
|
||||||
|
i['subs'] = item.serializable()
|
||||||
|
so.append(i)
|
||||||
|
return so
|
||||||
|
def encode(self, name="", render_subs=False):
|
||||||
|
"""return a string containing a javascript data structure
|
||||||
|
assigned to the menu name
|
||||||
|
|
||||||
|
if render_subs is True, we render submenus too"""
|
||||||
|
|
||||||
|
return ('<SCRIPT LANGUAGE="JavaScript">\n <!--\n var %s_items=' % name
|
||||||
|
#+ json.dumps(self.serializable(render_subs=render_subs), separators=(',',':')) # compact
|
||||||
|
+ "\n"+ json.dumps(self.serializable(render_subs=render_subs), sort_keys=True, indent=4) # pretty print
|
||||||
|
+ ';\n // -->\n </SCRIPT>')
|
||||||
14
model.py
Normal file
14
model.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class User(dict):
|
||||||
|
""" Every user must have keys for a username, name, password (this
|
||||||
|
is a md5 hash of the password), 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', 'password', 'email']:
|
||||||
|
self[key] = ''
|
||||||
|
for key in ['groups']:
|
||||||
|
self[key] = []
|
||||||
|
for key in dict:
|
||||||
|
self[key] = dict[key]
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return None
|
||||||
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
1
modules/apps.py
Symbolic link
1
modules/apps.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/apps/apps.py
|
||||||
1
modules/auth.py
Symbolic link
1
modules/auth.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/lib/auth.py
|
||||||
1
modules/auth_page.py
Symbolic link
1
modules/auth_page.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/lib/auth_page.py
|
||||||
1
modules/config.py
Symbolic link
1
modules/config.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/system/config.py
|
||||||
1
modules/expert_mode.py
Symbolic link
1
modules/expert_mode.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/system/expert_mode.py
|
||||||
1
modules/file_explorer.py
Symbolic link
1
modules/file_explorer.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/sharing/file_explorer.py
|
||||||
1
modules/forms.py
Symbolic link
1
modules/forms.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/lib/forms.py
|
||||||
1
modules/help.py
Symbolic link
1
modules/help.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/help/help.py
|
||||||
1
modules/info.py
Symbolic link
1
modules/info.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/router/info.py
|
||||||
35
modules/installed/apps/apps.py
Normal file
35
modules/installed/apps/apps.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import cherrypy
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class Apps(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("apps")
|
||||||
|
self.menu = cfg.main_menu.add_item("User Apps", "/apps", 80)
|
||||||
|
self.menu.add_item("Photo Gallery", "/apps/photos", 35)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
main = """
|
||||||
|
<p>User Applications are web apps hosted on your %s.</p>
|
||||||
|
|
||||||
|
<p>Eventually this box could be your photo sharing site, your
|
||||||
|
instant messaging site, your social networking site, your news
|
||||||
|
site. Remember web portals? We can be one of those too.
|
||||||
|
Many of the services you use on the web could soon be on site
|
||||||
|
and under your control!</p>
|
||||||
|
""" % (cfg.product_name)
|
||||||
|
return self.fill_template(title="User Applications", main=main, sidebar_right='')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def photos(self):
|
||||||
|
return self.fill_template(title="Open ID", main='', sidebar_right="""
|
||||||
|
<h2>Photo Gallery</h2><p>Your photos might well be the most valuable
|
||||||
|
digital property you have, so why trust it to companies that have no
|
||||||
|
investment in the sentimental value of your family snaps? Keep those
|
||||||
|
photos local, backed up, easily accessed and free from the whims of
|
||||||
|
some other websites business model.</p>
|
||||||
|
""")
|
||||||
91
modules/installed/help/help.py
Normal file
91
modules/installed/help/help.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
import cherrypy
|
||||||
|
from gettext import gettext as _
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
import cfg
|
||||||
|
class Help(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("help")
|
||||||
|
self.menu = cfg.main_menu.add_item(_("Documentation and FAQ"), "/help", 101)
|
||||||
|
self.menu.add_item(_("Where to Get Help"), "/help/index", 5)
|
||||||
|
self.menu.add_item(_("Developer's Manual"), "/help/view/plinth", 10)
|
||||||
|
self.menu.add_item(_("FAQ"), "/help/view/faq", 20)
|
||||||
|
self.menu.add_item(_("%s Wiki" % cfg.box_name), "http://wiki.debian.org/FreedomBox", 30)
|
||||||
|
self.menu.add_item(_("Design and Architecture"), "/help/view/design", 40)
|
||||||
|
self.menu.add_item(_("About"), "/help/about", 100)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
main="""
|
||||||
|
<p>There are a variety of places to go for help with Plinth
|
||||||
|
and the box it runs on.</p>
|
||||||
|
|
||||||
|
<p>This front end has a <a
|
||||||
|
href="/help/view/plinth">developer's manual</a>. It isn't
|
||||||
|
complete, but it is the first place to look. Feel free to
|
||||||
|
offer suggestions, edits, and screenshots for completing
|
||||||
|
it!</p>
|
||||||
|
|
||||||
|
<p><a href="http://wiki.debian.org/FreedomBox">A section of
|
||||||
|
the Debian wiki</a> is devoted to the %(box)s. At some
|
||||||
|
point the documentation in the wiki and the documentation in
|
||||||
|
the manual should dovetail.</p>
|
||||||
|
|
||||||
|
<p>I have collected some of my thoughts about the %(box)s
|
||||||
|
in a <a href="/help/view/design">document focused on its
|
||||||
|
design and architecture</a>.</p>
|
||||||
|
|
||||||
|
<p>There
|
||||||
|
are Debian gurus in the \#debian channels of both
|
||||||
|
irc.freenode.net and irc.oftc.net. They probably don't know
|
||||||
|
much about the %(box)s and almost surely know nothing of
|
||||||
|
this front end, but they are an incredible resource for
|
||||||
|
general Debian issues.</p>
|
||||||
|
|
||||||
|
<p>There is no <a href="/help/view/faq">FAQ</a> because
|
||||||
|
the question frequency is currently zero for all
|
||||||
|
questions.</p>
|
||||||
|
""" % {'box':cfg.box_name}
|
||||||
|
return self.fill_template(title="Documentation and FAQ", main=main)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def about(self):
|
||||||
|
return self.fill_template(title=_("About the %s" % cfg.box_name), main="""
|
||||||
|
<p> We live in a world where our use of the network is
|
||||||
|
mediated by organizations that often do not have our best
|
||||||
|
interests at heart. By building software that does not rely on
|
||||||
|
a central service, we can regain control and privacy. By
|
||||||
|
keeping our data in our homes, we gain useful legal
|
||||||
|
protections over it. By giving back power to the users over
|
||||||
|
their networks and machines, we are returning the Internet to
|
||||||
|
its intended peer-to-peer architecture.</p>
|
||||||
|
|
||||||
|
<p>In order to bring about the new network order, it is
|
||||||
|
paramount that it is easy to convert to it. The hardware it
|
||||||
|
runs on must be cheap. The software it runs on must be easy to
|
||||||
|
install and administrate by anybody. It must be easy to
|
||||||
|
transition from existing services.</p>
|
||||||
|
|
||||||
|
<p>There are a number of projects working to realize a future
|
||||||
|
of distributed services; we aim to bring them all together in
|
||||||
|
a convenient package.</p>
|
||||||
|
|
||||||
|
<p>For more information about the Freedom Box project, see the
|
||||||
|
<a href="http://wiki.debian.org/FreedomBox">Debian
|
||||||
|
Wiki</a>.</p>""")
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self.fill_template(template="err", main="<p>Sorry, as much as I would like to show you that page, I don't seem to have a page named %s!</p>" % page)
|
||||||
|
IF = open(os.path.join("doc", "%s.part.html" % page), 'r')
|
||||||
|
main = IF.read()
|
||||||
|
IF.close()
|
||||||
|
return self.fill_template(template="two_col", title=_("%s Documentation" % cfg.product_name), main=main)
|
||||||
118
modules/installed/lib/auth.py
Normal file
118
modules/installed/lib/auth.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# TODO: DeprecationWarning: the md5 module is deprecated; use hashlib instead import md5
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import urllib, hashlib
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
cfg.session_key = '_cp_username'
|
||||||
|
|
||||||
|
def check_credentials(username, passphrase):
|
||||||
|
"""Verifies credentials for username and passphrase.
|
||||||
|
Returns None on success or a string describing the error on failure"""
|
||||||
|
|
||||||
|
u = cfg.users.get(username)
|
||||||
|
if u is None:
|
||||||
|
cfg.log("Unknown user: %s" % username)
|
||||||
|
return u"Username %s is unknown to me." % username
|
||||||
|
if u['passphrase'] != hashlib.md5(passphrase).hexdigest():
|
||||||
|
return u"Incorrect passphrase."
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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():
|
||||||
|
raise cherrypy.HTTPRedirect("/auth/login")
|
||||||
|
else:
|
||||||
|
raise cherrypy.HTTPRedirect("/auth/login")
|
||||||
|
|
||||||
|
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("/auth/login?from_page=%s" % get_params)
|
||||||
|
else:
|
||||||
|
# Send old page as from_page parameter
|
||||||
|
raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_params)
|
||||||
|
|
||||||
|
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
|
||||||
|
|
||||||
|
def require(*conditions):
|
||||||
|
"""A decorator that appends conditions to the auth.require config
|
||||||
|
variable."""
|
||||||
|
def decorate(f):
|
||||||
|
if not hasattr(f, '_cp_config'):
|
||||||
|
f._cp_config = dict()
|
||||||
|
if 'auth.require' not in f._cp_config:
|
||||||
|
f._cp_config['auth.require'] = []
|
||||||
|
f._cp_config['auth.require'].extend(conditions)
|
||||||
|
return f
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
# Conditions are callables that return True
|
||||||
|
# if the user fulfills the conditions they define, False otherwise
|
||||||
|
#
|
||||||
|
# They can access the current username as cherrypy.request.login
|
||||||
|
#
|
||||||
|
# Define those at will however suits the application.
|
||||||
|
|
||||||
|
def member_of(groupname):
|
||||||
|
def check():
|
||||||
|
# replace with actual check if <username> is in <groupname>
|
||||||
|
return cherrypy.request.login == 'joe' and groupname == 'admin'
|
||||||
|
return check
|
||||||
|
|
||||||
|
def name_is(reqd_username):
|
||||||
|
return lambda: reqd_username == cherrypy.request.login
|
||||||
|
|
||||||
|
# These might be handy
|
||||||
|
|
||||||
|
def any_of(*conditions):
|
||||||
|
"""Returns True if any of the conditions match"""
|
||||||
|
def check():
|
||||||
|
for c in conditions:
|
||||||
|
if c():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return check
|
||||||
|
|
||||||
|
# By default all conditions are required, but this might still be
|
||||||
|
# needed if you want to use it inside of an any_of(...) condition
|
||||||
|
def all_of(*conditions):
|
||||||
|
"""Returns True if all of the conditions match"""
|
||||||
|
def check():
|
||||||
|
for c in conditions:
|
||||||
|
if not c():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return check
|
||||||
|
|
||||||
|
|
||||||
49
modules/installed/lib/auth_page.py
Normal file
49
modules/installed/lib/auth_page.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import cherrypy
|
||||||
|
import cfg
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
from modules.forms import Form
|
||||||
|
from auth import *
|
||||||
|
# Controller to provide login and logout actions
|
||||||
|
|
||||||
|
class AuthController(PagePlugin):
|
||||||
|
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"""
|
||||||
|
|
||||||
|
def get_loginform(self, username, msg='', from_page="/"):
|
||||||
|
form = Form(title="Login", action="/auth/login", message=msg)
|
||||||
|
form.text_input(name="from_page", value=from_page, type="hidden")
|
||||||
|
form.text_input("Username", name="username", value=username)
|
||||||
|
form.text_input("Passphrase", name="passphrase", type="password")
|
||||||
|
form.submit(label="Login")
|
||||||
|
|
||||||
|
return self.fill_template(main=form.render(), sidebar_right=" ")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def login(self, username=None, passphrase=None, from_page="/", **kwargs):
|
||||||
|
if username is None or passphrase is None:
|
||||||
|
return self.get_loginform("", from_page=from_page)
|
||||||
|
|
||||||
|
error_msg = check_credentials(username, passphrase)
|
||||||
|
if error_msg:
|
||||||
|
return self.get_loginform(username, error_msg, from_page)
|
||||||
|
else:
|
||||||
|
cherrypy.session[cfg.session_key] = cherrypy.request.login = username
|
||||||
|
self.on_login(username)
|
||||||
|
raise cherrypy.HTTPRedirect(from_page or "/")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def logout(self, from_page="/"):
|
||||||
|
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 "/")
|
||||||
123
modules/installed/lib/forms.py
Normal file
123
modules/installed/lib/forms.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
class Form():
|
||||||
|
def __init__(self, action=None, cls='form', title=None, onsubmit=None, name=None, message='', method="get"):
|
||||||
|
"""Note that there appears to be a bug in cherrypy whereby
|
||||||
|
forms submitted via post don't have their fields included in
|
||||||
|
kwargs for the default index method. So we use get by
|
||||||
|
default, though it's not as neat."""
|
||||||
|
|
||||||
|
action = self.get_form_attrib_text('action', action)
|
||||||
|
onsubmit = self.get_form_attrib_text('onsubmit', onsubmit)
|
||||||
|
name = self.get_form_attrib_text('name', name)
|
||||||
|
|
||||||
|
self.pretext = ' <form class="%s" method="%s" %s%s%s>\n' % (cls, method, action, onsubmit, name)
|
||||||
|
if title:
|
||||||
|
self.pretext += ' <h2>%s</h2>\n' % title
|
||||||
|
|
||||||
|
if message:
|
||||||
|
self.message = "<h3>%s</h3>" % message
|
||||||
|
else:
|
||||||
|
self.message = ''
|
||||||
|
self.text = ''
|
||||||
|
self.end_text = "</form>\n"
|
||||||
|
def get_form_attrib_text(self, field, val):
|
||||||
|
if val:
|
||||||
|
return ' %s="%s"' % (field, val)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
def html(self, html):
|
||||||
|
self.text += html
|
||||||
|
def dropdown(self, label='', name=None, id=None, vals=None, select=None, onchange=''):
|
||||||
|
"""vals is a list of values.
|
||||||
|
select is the index in vals of the selected item. None means no item is selected yet."""
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
self.text += (""" <label>
|
||||||
|
<span>%(label)s</span>
|
||||||
|
<select name="%(name)s" id="%(id)s" onchange="%(onchange)s">\n"""
|
||||||
|
% {'label':label, 'name':name, 'id':id, 'onchange':onchange})
|
||||||
|
for i in range(len(vals)):
|
||||||
|
v = vals[i]
|
||||||
|
if i == select:
|
||||||
|
selected = "SELECTED"
|
||||||
|
else:
|
||||||
|
selected = ''
|
||||||
|
self.text +=" <option value=\"%s\" %s>%s</option>\n" % (v, selected, v)
|
||||||
|
self.text += """ </select>
|
||||||
|
</label>\n"""
|
||||||
|
|
||||||
|
def dotted_quad(self, label='', name=None, id=None, quad=None):
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
if not quad:
|
||||||
|
quad = [0,0,0,0]
|
||||||
|
self.text += """ <label>
|
||||||
|
<span>%(label)s</span>
|
||||||
|
<input type="text" class="inputtextnowidth" name="%(name)s0" id="%(id)s0" value="%(q0)s" maxlength="3" size="1"/><b>.</b>
|
||||||
|
<input type="text" class="inputtextnowidth" name="%(name)s1" id="%(id)s1" value="%(q1)s" maxlength="3" size="1"/><b>.</b>
|
||||||
|
<input type="text" class="inputtextnowidth" name="%(name)s2" id="%(id)s2" value="%(q2)s" maxlength="3" size="1"/><b>.</b>
|
||||||
|
<input type="text" class="inputtextnowidth" name="%(name)s3" id="%(id)s3" value="%(q3)s" maxlength="3" size="1"/>
|
||||||
|
</label>""" % {'label':label, 'name':name, 'id':id, 'q0':quad[0], 'q1':quad[1], 'q2':quad[2], 'q3':quad[3]}
|
||||||
|
|
||||||
|
def text_input(self, label='', name=None, id=None, type='text', value='', size=20):
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
if type=="hidden":
|
||||||
|
self.text += '<input type="%s" class="inputtext" name="%s" id="%s" value="%s"/>' % (type, name, id, value)
|
||||||
|
else:
|
||||||
|
self.text += """ <label>
|
||||||
|
<span>%s</span>
|
||||||
|
<input type="%s" class="inputtext" name="%s" id="%s" value="%s" size="%s"/>
|
||||||
|
</label>""" % (label, type, name, id, value, size)
|
||||||
|
def text_box(self, label='', name=None, id=None):
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
self.text += """
|
||||||
|
<label>
|
||||||
|
<span>%s</span>
|
||||||
|
<textarea class="textbox" name="%s" id="%s"></textarea>
|
||||||
|
</label>""" % (label, name, id)
|
||||||
|
def submit(self, label='', name=None, id=None):
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
self.text += """
|
||||||
|
<div class="submit">
|
||||||
|
<label><span></span>
|
||||||
|
<input type="submit" class="button" value="%s" name="%s" id="%s" />
|
||||||
|
</label></div>\n""" % (label, name, id)
|
||||||
|
def submit_row(self, buttons):
|
||||||
|
"""buttons is a list of tuples, each containing label, name, id. Name and id are optional."""
|
||||||
|
self.text += '<div class="submit"><label>'
|
||||||
|
button_text = ''
|
||||||
|
for button in buttons:
|
||||||
|
label = button[0]
|
||||||
|
try:
|
||||||
|
name = button[1]
|
||||||
|
except:
|
||||||
|
name = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
id = button[2]
|
||||||
|
except:
|
||||||
|
id = None
|
||||||
|
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
|
||||||
|
if button_text != '':
|
||||||
|
button_text += " "
|
||||||
|
button_text += '<input type="submit" class="button" value="%s" name="%s" id="%s" />\n' % (label, name, id)
|
||||||
|
self.text += '%s</div></label>' % button_text
|
||||||
|
def name_or_id(self, name, id):
|
||||||
|
if not name: name = id
|
||||||
|
if not id: id = name
|
||||||
|
if not name: name = ''
|
||||||
|
if not id: id = ''
|
||||||
|
return name, id
|
||||||
|
def checkbox(self, label='', name='', id='', checked=''):
|
||||||
|
name, id = self.name_or_id(name, id)
|
||||||
|
if checked:
|
||||||
|
checked = 'checked="on"'
|
||||||
|
self.text += """
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<span>%s</span>
|
||||||
|
<input type=checkbox name="%s" id="%s" %s/>
|
||||||
|
</label></div>\n""" % (label, name, id, checked)
|
||||||
|
def get_checkbox(self, name='', id=''):
|
||||||
|
return '<input type=checkbox name="%s" id="%s" />\n' % self.name_or_id(name, id)
|
||||||
|
def render(self):
|
||||||
|
return self.pretext+self.message+self.text+self.end_text
|
||||||
112
modules/installed/lib/user_store.py
Normal file
112
modules/installed/lib/user_store.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import os
|
||||||
|
import simplejson as json
|
||||||
|
import cherrypy
|
||||||
|
import cfg
|
||||||
|
from model import User
|
||||||
|
from plugin_mount import UserStoreModule
|
||||||
|
|
||||||
|
class UserStore(UserStoreModule):
|
||||||
|
"""The user storage is on disk. Rather than slurp the entire
|
||||||
|
thing, we read from the disk as needed. Writes are immediate,
|
||||||
|
though.
|
||||||
|
|
||||||
|
TODO: file locking"""
|
||||||
|
def __init__(self):
|
||||||
|
self.data_dir = cfg.users_dir
|
||||||
|
self.users = {}
|
||||||
|
def sanitize(username):
|
||||||
|
"""Return username with nonalphanumeric/underscore chars
|
||||||
|
removed.
|
||||||
|
|
||||||
|
TODO: allow international chars in usernames."""
|
||||||
|
pass
|
||||||
|
def attr(self, key, username):
|
||||||
|
"""Return field from username's record. If key does not
|
||||||
|
exist, don't raise attribute error, just return None.
|
||||||
|
|
||||||
|
User defaults to current. If no current user and none
|
||||||
|
specified, return none."""
|
||||||
|
try:
|
||||||
|
return self.get(username)[key]
|
||||||
|
except AttributeError:
|
||||||
|
return 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):
|
||||||
|
"""Return True if user username is an expert, else False.
|
||||||
|
|
||||||
|
If username is None, use the current user. If no such user exists, return False."""
|
||||||
|
user = self.get(username)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
return 'expert' in user['groups']
|
||||||
|
|
||||||
|
def get(self, username=None, reload=False):
|
||||||
|
"""Returns a user instance with the user's info or else None if the user couldn't be found.
|
||||||
|
|
||||||
|
If reload is true, reload from disk, regardless of dict's contents
|
||||||
|
|
||||||
|
If username is None, try current user. If no current user, return None.
|
||||||
|
|
||||||
|
TODO: implement user_store.get reload"""
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
username = self.current(name=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.users[username]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
IF = open(os.path.join(self.data_dir, username), "r")
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
data = IF.read()
|
||||||
|
IF.close()
|
||||||
|
|
||||||
|
# We cache the result, and since we assume a system with
|
||||||
|
# relatively few users and small user data files, we never
|
||||||
|
# expire those results. If we revisit that assumption, we
|
||||||
|
# might need some cache expiry.
|
||||||
|
self.users[username] = User(json.loads(data))
|
||||||
|
|
||||||
|
return self.users[username]
|
||||||
|
def exists(self, username):
|
||||||
|
"""Return True if username exists, else False."""
|
||||||
|
return username in self.users or os.path.exists(os.path.join(cfg.users_dir, username))
|
||||||
|
def get_all(self):
|
||||||
|
"Returns a list of all the user objects"
|
||||||
|
usernames = os.listdir(self.data_dir)
|
||||||
|
for name in usernames:
|
||||||
|
self.get(name)
|
||||||
|
return self.users
|
||||||
|
def set(self, user):
|
||||||
|
"""Set the user data, both in memory and as needed in storage."""
|
||||||
|
OF = open(os.path.join(self.data_dir, user['username']), 'w')
|
||||||
|
OF.write(json.dumps(user))
|
||||||
|
OF.close()
|
||||||
|
def remove(self, user):
|
||||||
|
"""Delete the user from storage and RAM. User can be a user instance or a username."""
|
||||||
|
try:
|
||||||
|
name = user['name']
|
||||||
|
except TypeError:
|
||||||
|
if isinstance(user, basestring):
|
||||||
|
name = user
|
||||||
|
else:
|
||||||
|
raise TypeError
|
||||||
|
os.unlink(os.path.join(cfg.users_dir, name))
|
||||||
|
try:
|
||||||
|
del self.users[name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
cfg.log.info("%s deleted %s" % (cherrypy.session.get(cfg.session_key), name))
|
||||||
41
modules/installed/privacy/privacy.py
Normal file
41
modules/installed/privacy/privacy.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import cherrypy
|
||||||
|
from gettext import gettext as _
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
from modules.auth import require
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class Privacy(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("privacy")
|
||||||
|
self.menu = cfg.main_menu.add_item("Privacy Controls", "/privacy", 12)
|
||||||
|
self.menu.add_item("General Config", "/privacy/config", 10)
|
||||||
|
self.menu.add_item("Ad Blocking", "/privacy/adblock", 20)
|
||||||
|
self.menu.add_item("TOR", "/privacy/TOR", 30)
|
||||||
|
self.menu.add_item("HTTPS Everywhere", "/privacy/https_everywhere", 30)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
#raise cherrypy.InternalRedirect('/privacy/config')
|
||||||
|
return self.config()
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def config(self):
|
||||||
|
main="""
|
||||||
|
<p>Privacy controls are not yet implemented. This page is a
|
||||||
|
placeholder and a promise: privacy is important enough that it
|
||||||
|
is a founding consideration, not an afterthought.</p>
|
||||||
|
"""
|
||||||
|
return self.fill_template(title=_("Privacy Control Panel"), main=main,
|
||||||
|
sidebar_right=_("""<h2>Statement of Principles</h2><p>When we say your
|
||||||
|
privacy is important, it's not just an empty pleasantry. We really
|
||||||
|
mean it. Your privacy control panel should give you fine-grained
|
||||||
|
control over exactly who can access your %s and the
|
||||||
|
information on it.</p>
|
||||||
|
|
||||||
|
<p>Your personal information should not leave this box without your
|
||||||
|
knowledge and direction. And if companies or government wants this
|
||||||
|
information, they have to ask <b>you</b> for it. This gives you a
|
||||||
|
change to refuse and also tells you who wants your data.</p>
|
||||||
|
""") % cfg.product_name)
|
||||||
18
modules/installed/router/info.py
Normal file
18
modules/installed/router/info.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import cherrypy
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
from modules.auth import require
|
||||||
|
|
||||||
|
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 self.fill_template(title="Router Information", main="""
|
||||||
|
<p> Eventually we will display a bunch of info, graphs and logs about the routing functions here.</p>
|
||||||
|
""")
|
||||||
160
modules/installed/router/router.py
Normal file
160
modules/installed/router/router.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
from urlparse import urlparse
|
||||||
|
import os, cherrypy
|
||||||
|
from gettext import gettext as _
|
||||||
|
from plugin_mount import PagePlugin, PluginMount, FormPlugin
|
||||||
|
from modules.auth import require
|
||||||
|
from forms import Form
|
||||||
|
from util import *
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class router(PagePlugin):
|
||||||
|
order = 10 # order of running init in PagePlugins
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.register_page("router")
|
||||||
|
self.menu = cfg.main_menu.add_item("Router Admin", "/router", 10)
|
||||||
|
self.menu.add_item("Wireless", "/router/wireless", 12)
|
||||||
|
self.menu.add_item("Firewall", "/router/firewall", 18)
|
||||||
|
self.menu.add_item("Hotspot and Mesh", "/router/hotspot")
|
||||||
|
self.menu.add_item("Info", "/router/info", 100)
|
||||||
|
|
||||||
|
@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('/router/setup')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def wireless(self):
|
||||||
|
return self.fill_template(title="Wireless", main="<p>wireless setup: essid, etc.</p>")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def firewall(self):
|
||||||
|
return self.fill_template(title="Firewall", main="<p>Iptables twiddling.</p>")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def hotspot(self):
|
||||||
|
return self.fill_template(title="Hotspot and Mesh", main="<p>connection sharing setup.</p>")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class setup(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.register_page("router.setup")
|
||||||
|
self.menu = cfg.html_root.router.menu.add_item("General Setup", "/router/setup", 10)
|
||||||
|
self.menu.add_item("Dynamic DNS", "/router/setup/ddns", 20)
|
||||||
|
self.menu.add_item("MAC Address Clone", "/router/setup/mac_address", 30)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self):
|
||||||
|
parts = self.forms('/router/setup')
|
||||||
|
parts['title'] = "General Router Setup"
|
||||||
|
parts['sidebar_right']="""<h2>Introduction</h2><p>Your %s is a replacement for your
|
||||||
|
wireless router. By default, it should do everything your usual
|
||||||
|
router does. With the addition of some extra modules, its abilities
|
||||||
|
can rival those of high-end routers costing hundreds of dollars.</p>
|
||||||
|
""" % cfg.box_name + parts['sidebar_right']
|
||||||
|
if not cfg.users.expert():
|
||||||
|
main += """<p>In basic mode, you don't need to do any
|
||||||
|
router setup before you can go online. Just plug your
|
||||||
|
%(product)s in to your cable or DSL modem and the router
|
||||||
|
will try to get you on the internet using DHCP.</p>
|
||||||
|
|
||||||
|
<p>If that fails, you might need to resort to the expert
|
||||||
|
options. Enable expert mode in the "%(product)s System /
|
||||||
|
Configure" menu.</p>""" % {'product':cfg.box_name}
|
||||||
|
else:
|
||||||
|
parts['main'] += "<p>router name, domain name, router IP, dhcp</p>"
|
||||||
|
return self.fill_template(**parts)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def ddns(self):
|
||||||
|
return self.fill_template(title="Dynamic DNS", main="<p>Masquerade setup</p>")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def mac_address(self):
|
||||||
|
return self.fill_template(title="MAC Address Cloning",
|
||||||
|
main="<p>Your router can pretend to have a different MAC address on any interface.</p>")
|
||||||
|
|
||||||
|
|
||||||
|
class wan(FormPlugin, PagePlugin):
|
||||||
|
url = ["/router/setup"]
|
||||||
|
order = 10
|
||||||
|
|
||||||
|
js = """<script LANGUAGE="JavaScript">
|
||||||
|
<!--
|
||||||
|
function hideshow_static() {
|
||||||
|
var d = document.getElementById('connect_type');
|
||||||
|
connect_type = d.value;
|
||||||
|
if (connect_type != 'Static IP') {
|
||||||
|
hide("static_ip_form");
|
||||||
|
} else {
|
||||||
|
show("static_ip_form");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -->
|
||||||
|
</script>"""
|
||||||
|
|
||||||
|
def sidebar_right(self, *args, **kwargs):
|
||||||
|
side=''
|
||||||
|
if cfg.users.expert():
|
||||||
|
side += """<h2>WAN Connection Type</h2>
|
||||||
|
<h3>DHCP</h3><p>DHCP allows your router to automatically
|
||||||
|
connect with the upstream network. If you are unsure what
|
||||||
|
option to choose, stick with DHCP. It is usually
|
||||||
|
correct.
|
||||||
|
|
||||||
|
<h3>Static IP</h3><p>If you want to setup your connection
|
||||||
|
manually, you can enter static IP information. This option is
|
||||||
|
for those who know what they're doing. As such, it is only
|
||||||
|
available in expert mode.</p>"""
|
||||||
|
return side
|
||||||
|
|
||||||
|
def main(self, wan_ip0=0, wan_ip1=0, wan_ip2=0, wan_ip3=0,
|
||||||
|
subnet0=0, subnet1=0, subnet2=0, subnet3=0,
|
||||||
|
gateway0=0, gateway1=0, gateway2=0, gateway3=0,
|
||||||
|
dns10=0, dns11=0, dns12=0, dns13=0,
|
||||||
|
dns20=0, dns21=0, dns22=0, dns23=0,
|
||||||
|
dns30=0, dns31=0, dns32=0, dns33=0,
|
||||||
|
message=None, **kwargs):
|
||||||
|
if not cfg.users.expert():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
store = filedict_con(cfg.store_file, 'router')
|
||||||
|
defaults = {'connect_type': "'DHCP'",
|
||||||
|
}
|
||||||
|
for k,c in defaults.items():
|
||||||
|
if not k in kwargs:
|
||||||
|
try:
|
||||||
|
kwargs[k] = store[k]
|
||||||
|
except KeyError:
|
||||||
|
exec("if not '%(k)s' in kwargs: store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
|
||||||
|
|
||||||
|
form = Form(title="WAN Connection",
|
||||||
|
action="/router/setup/wan/index",
|
||||||
|
name="wan_connection_form",
|
||||||
|
message=message)
|
||||||
|
form.dropdown('Connection Type', vals=["DHCP", "Static IP"], id="connect_type", onchange="hideshow_static()")
|
||||||
|
form.html('<div id="static_ip_form">')
|
||||||
|
form.dotted_quad("WAN IP Address", name="wan_ip", quad=[wan_ip0, wan_ip1, wan_ip2, wan_ip3])
|
||||||
|
form.dotted_quad("Subnet Mask", name="subnet", quad=[subnet0, subnet1, subnet2, subnet3])
|
||||||
|
form.dotted_quad("Gateway", name="gateway", quad=[gateway0, gateway1, gateway2, gateway3])
|
||||||
|
form.dotted_quad("Static DNS 1", name="dns1", quad=[dns10, dns11, dns12, dns13])
|
||||||
|
form.dotted_quad("Static DNS 2", name="dns2", quad=[dns20, dns21, dns22, dns23])
|
||||||
|
form.dotted_quad("Static DNS 3", name="dns3", quad=[dns30, dns31, dns32, dns33])
|
||||||
|
form.html('</div>')
|
||||||
|
form.html(""" <script LANGUAGE="JavaScript">
|
||||||
|
<!--
|
||||||
|
hideshow_static();
|
||||||
|
// -->
|
||||||
|
</script>""")
|
||||||
|
form.submit("Set Wan")
|
||||||
|
return form.render()
|
||||||
|
|
||||||
25
modules/installed/services/services.py
Normal file
25
modules/installed/services/services.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import cherrypy
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class Services(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("services")
|
||||||
|
self.menu = cfg.main_menu.add_item("Other Services", "/services", 90)
|
||||||
|
self.menu.add_item("Open ID", "/services/openid", 35)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
return self.openid()
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def openid(self):
|
||||||
|
return self.fill_template(title="Open ID", main='', sidebar_right="""
|
||||||
|
<h2>One Login for Every Site</h2><p>Your %s is also an OpenID
|
||||||
|
machine. It can generate credentials that allow you to log in to many
|
||||||
|
websites without the need to remember or enter a separate username and
|
||||||
|
password at each one.</p>
|
||||||
|
""" % cfg.box_name)
|
||||||
35
modules/installed/sharing/file_explorer.py
Normal file
35
modules/installed/sharing/file_explorer.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import cherrypy
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
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", "/sharing/explorer", 30)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self):
|
||||||
|
main = """
|
||||||
|
<p>File explorer for users that also have shell accounts.</p> <p>Until
|
||||||
|
that is written (and it will be a while), we should install <a
|
||||||
|
href="http://www.mollify.org/demo.php">mollify</a> or <a
|
||||||
|
href="http://www.ajaxplorer.info/wordpress/demo/">ajaxplorer</a>, but
|
||||||
|
of which seem to have some support for playing media files in the
|
||||||
|
browser (as opposed to forcing users to download and play them
|
||||||
|
locally). The downsides to third-party explorers are: they're don't
|
||||||
|
fit within our theme system, they require separate login, and they're
|
||||||
|
written in php, which will make integrating them hard.</p>
|
||||||
|
|
||||||
|
<p>There are, of course, many other options for php-based file
|
||||||
|
explorers. These were the ones I saw that might do built-in media
|
||||||
|
players.</p>
|
||||||
|
|
||||||
|
<p>For python-friendly options, check out <a
|
||||||
|
href="http://blogfreakz.com/jquery/web-based-filemanager/">FileManager</a>.
|
||||||
|
It appears to be mostly javascript with some bindings to make it
|
||||||
|
python-friendly.</p>
|
||||||
|
"""
|
||||||
|
return self.fill_template(title="File Explorer", main=main, sidebar_right='')
|
||||||
51
modules/installed/sharing/sharing.py
Normal file
51
modules/installed/sharing/sharing.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import cherrypy
|
||||||
|
from gettext import gettext as _
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
import cfg
|
||||||
|
|
||||||
|
class Sharing(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("sharing")
|
||||||
|
self.menu = cfg.main_menu.add_item("Resource Sharing", "/sharing", 35)
|
||||||
|
self.menu.add_item("File Server", "/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('/sharing/files')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def files(self):
|
||||||
|
return self.fill_template(title="File Server", main='', sidebar_right=_("""
|
||||||
|
<h2>Freedom NAS</h2><p> The %s can make your spare hard drives accessible to your
|
||||||
|
local network, thus acting as a NAS server. We currently support
|
||||||
|
sharing files via NFS and SMB.
|
||||||
|
|
||||||
|
TODO: this is not true. We currently support no sharing.</p>
|
||||||
|
""" % cfg.box_name))
|
||||||
|
|
||||||
|
#TODO: move PrinterSharing to another file, as it should be an optional module (most people don't care about printer sharing)
|
||||||
|
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", "/sharing/printer", 35)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self):
|
||||||
|
main = """
|
||||||
|
<p>TODO: Setup and install SAMBA</p>
|
||||||
|
<p>TODO: Setup and install CUPS</p>
|
||||||
|
"""
|
||||||
|
return self.fill_template(title="Printer Sharing", main=main, sidebar_right="""
|
||||||
|
<h2>Share Your Printer</h2><p> The %s can share your printer via Samba and CUPS.</p>
|
||||||
|
""" % cfg.box_name)
|
||||||
|
|
||||||
|
|
||||||
135
modules/installed/system/config.py
Normal file
135
modules/installed/system/config.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import os, shutil, subprocess
|
||||||
|
from socket import gethostname
|
||||||
|
import cherrypy
|
||||||
|
import simplejson as json
|
||||||
|
from gettext import gettext as _
|
||||||
|
from filedict import FileDict
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
|
import cfg
|
||||||
|
from forms import Form
|
||||||
|
from model import User
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
class Config(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.register_page("sys.config")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self):
|
||||||
|
parts = self.forms('/sys/config')
|
||||||
|
parts['title']=_("Configure this %s" % cfg.box_name)
|
||||||
|
return self.fill_template(**parts)
|
||||||
|
|
||||||
|
def valid_hostname(name):
|
||||||
|
"""Return '' if name is a valid hostname by our standards (not
|
||||||
|
just by RFC 952 and RFC 1123. We're more conservative than the
|
||||||
|
standard. If hostname isn't valid, return message explaining why."""
|
||||||
|
|
||||||
|
message = ''
|
||||||
|
if len(name) > 63:
|
||||||
|
message += "<br />Hostname too long (max is 63 characters)"
|
||||||
|
|
||||||
|
if not is_alphanumeric(name):
|
||||||
|
message += "<br />Hostname must be alphanumeric"
|
||||||
|
|
||||||
|
if not name[0] in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
||||||
|
message += "<br />Hostname must start with a letter"
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def set_hostname(hostname):
|
||||||
|
"Sets machine hostname to hostname"
|
||||||
|
cfg.log.info("Writing '%s' to /etc/hostname" % hostname)
|
||||||
|
unslurp("/etc/hostname", hostname+"\n")
|
||||||
|
try:
|
||||||
|
retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True)
|
||||||
|
if retcode < 0:
|
||||||
|
cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode)
|
||||||
|
else:
|
||||||
|
cfg.log.debug("Hostname restart returned %s" % retcode)
|
||||||
|
except OSError, e:
|
||||||
|
raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e)
|
||||||
|
|
||||||
|
sys_store = filedict_con(cfg.store_file, 'sys')
|
||||||
|
sys_store['hostname'] = hostname
|
||||||
|
|
||||||
|
class general(FormPlugin, PagePlugin):
|
||||||
|
url = ["/sys/config"]
|
||||||
|
order = 30
|
||||||
|
|
||||||
|
def help(self, *args, **kwargs):
|
||||||
|
|
||||||
|
## only expert users are going to get deep enough to see any timestamps
|
||||||
|
if not cfg.users.expert():
|
||||||
|
return ''
|
||||||
|
return _(#"""<h2>Time Zone</h2>
|
||||||
|
"""<p>Set your timezone to get accurate
|
||||||
|
timestamps. %(product)s will use this information to set your
|
||||||
|
%(box)s's systemwide timezone.</p>
|
||||||
|
""" % {'product':cfg.product_name, 'box':cfg.box_name})
|
||||||
|
|
||||||
|
def main(self, message='', **kwargs):
|
||||||
|
sys_store = filedict_con(cfg.store_file, 'sys')
|
||||||
|
defaults = {'time_zone': "slurp('/etc/timezone').rstrip()",
|
||||||
|
'hostname': "gethostname()",
|
||||||
|
}
|
||||||
|
for k,c in defaults.items():
|
||||||
|
if not k in kwargs:
|
||||||
|
try:
|
||||||
|
kwargs[k] = sys_store[k]
|
||||||
|
except KeyError:
|
||||||
|
exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
|
||||||
|
|
||||||
|
## Get the list of supported timezones and the index in that list of the current one
|
||||||
|
module_file = __file__
|
||||||
|
if module_file.endswith(".pyc"):
|
||||||
|
module_file = module_file[:-1]
|
||||||
|
time_zones = json.loads(slurp(os.path.join(os.path.dirname(os.path.realpath(module_file)), "time_zones")))
|
||||||
|
for i in range(len(time_zones)):
|
||||||
|
if kwargs['time_zone'] == time_zones[i]:
|
||||||
|
time_zone_id = i
|
||||||
|
break
|
||||||
|
|
||||||
|
## A little sanity checking. Make sure the current timezone is in the list.
|
||||||
|
try:
|
||||||
|
cfg.log('kwargs tz: %s, from_table: %s' % (kwargs['time_zone'], time_zones[time_zone_id]))
|
||||||
|
except NameError:
|
||||||
|
cfg.log.critical("Unknown Time Zone: %s" % kwargs['time_zone'])
|
||||||
|
raise cherrypy.HTTPError(500, "Unknown Time Zone: %s" % kwargs['time_zone'])
|
||||||
|
|
||||||
|
## And now, the form.
|
||||||
|
form = Form(title=_("General Config"),
|
||||||
|
action="/sys/config/general/index",
|
||||||
|
name="config_general_form",
|
||||||
|
message=message )
|
||||||
|
form.html(self.help())
|
||||||
|
form.dropdown(_("Time Zone"), name="time_zone", vals=time_zones, select=time_zone_id)
|
||||||
|
form.html("<p>Your hostname is the local name by which other machines on your LAN can reach you.</p>")
|
||||||
|
form.text_input('Hostname', name='hostname', value=kwargs['hostname'])
|
||||||
|
form.submit(_("Submit"))
|
||||||
|
return form.render()
|
||||||
|
|
||||||
|
def process_form(self, time_zone='', hostname='', *args, **kwargs):
|
||||||
|
sys_store = filedict_con(cfg.store_file, 'sys')
|
||||||
|
message = ''
|
||||||
|
if hostname != sys_store['hostname']:
|
||||||
|
msg = valid_hostname(hostname)
|
||||||
|
if msg == '':
|
||||||
|
old_val = sys_store['hostname']
|
||||||
|
try:
|
||||||
|
set_hostname(hostname)
|
||||||
|
except:
|
||||||
|
cfg.log.info("Trying to restore old hostname value.")
|
||||||
|
set_hostname(old_val)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
message += msg
|
||||||
|
if time_zone != sys_store['time_zone']:
|
||||||
|
src = os.path.join("/usr/share/zoneinfo", time_zone)
|
||||||
|
cfg.log.info("Copying %s to /etc/localtime" % src)
|
||||||
|
shutil.copy(src, "/etc/localtime")
|
||||||
|
sys_store['time_zone'] = time_zone
|
||||||
|
return message or "Settings updated."
|
||||||
|
|
||||||
70
modules/installed/system/expert_mode.py
Normal file
70
modules/installed/system/expert_mode.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import os
|
||||||
|
import cherrypy
|
||||||
|
import simplejson as json
|
||||||
|
from gettext import gettext as _
|
||||||
|
from filedict import FileDict
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
|
import cfg
|
||||||
|
from forms import Form
|
||||||
|
from model import User
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
class experts(FormPlugin, PagePlugin):
|
||||||
|
url = ["/sys/config"]
|
||||||
|
order = 10
|
||||||
|
|
||||||
|
def help(self, *args, **kwargs):
|
||||||
|
side = _(#"""<h2>Expert Mode</h2>
|
||||||
|
"""
|
||||||
|
<p>The %(box)s can be administered in two modes, 'basic'
|
||||||
|
and 'expert'. Basic mode hides a lot of features and
|
||||||
|
configuration options that most users will never need to think
|
||||||
|
about. Expert mode allows you to get into the details.</p>
|
||||||
|
|
||||||
|
<p>Most users can operate the %(box)s by configuring the
|
||||||
|
limited number of options visible in Basic mode. For the sake
|
||||||
|
of simplicity and ease of use, we hid most of %(product)s's
|
||||||
|
less frequently used options. But if you want more
|
||||||
|
sophisticated features, you can enable Expert mode, and
|
||||||
|
%(product)s will present more advanced menu options.</p>
|
||||||
|
|
||||||
|
<p>You should be aware that it might be possible to render
|
||||||
|
your %(box)s inaccessible via Expert mode options.</p>
|
||||||
|
""" % {'box':cfg.box_name, 'product':cfg.product_name})
|
||||||
|
|
||||||
|
return side
|
||||||
|
|
||||||
|
def main(self, expert=None, message='', **kwargs):
|
||||||
|
"""Note that kwargs contains '':"submit" if this is coming
|
||||||
|
from a submitted form. If kwargs is empty, it's a fresh form
|
||||||
|
with no user input, which means it should just reflect the
|
||||||
|
state of the stored data."""
|
||||||
|
if not kwargs and expert == None:
|
||||||
|
expert = cfg.users.expert()
|
||||||
|
cfg.log("Expert mode is %s" % expert)
|
||||||
|
form = Form(title=_("Expert Mode"),
|
||||||
|
action="/sys/config/experts",
|
||||||
|
name="expert_mode_form",
|
||||||
|
message=message )
|
||||||
|
form.html(self.help())
|
||||||
|
form.checkbox(_("Expert Mode"), name="expert", checked=expert)
|
||||||
|
form.submit(_("Submit"))
|
||||||
|
return form.render()
|
||||||
|
|
||||||
|
def process_form(self, expert='', *args, **kwargs):
|
||||||
|
user = cfg.users.get()
|
||||||
|
|
||||||
|
message = 'settings unchanged'
|
||||||
|
|
||||||
|
if expert:
|
||||||
|
if not 'expert' in user['groups']:
|
||||||
|
user['groups'].append('expert')
|
||||||
|
message = "enabled"
|
||||||
|
else:
|
||||||
|
if 'expert' in user['groups']:
|
||||||
|
user['groups'].remove('expert')
|
||||||
|
message = "disabled"
|
||||||
|
|
||||||
|
cfg.users.set(user)
|
||||||
|
return "Expert mode %s." % message
|
||||||
50
modules/installed/system/system.py
Normal file
50
modules/installed/system/system.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
import cherrypy
|
||||||
|
import simplejson as json
|
||||||
|
from gettext import gettext as _
|
||||||
|
from filedict import FileDict
|
||||||
|
from auth import require
|
||||||
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
|
import cfg
|
||||||
|
from forms import Form
|
||||||
|
from model import User
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
sys_dir = "modules/installed/sys"
|
||||||
|
|
||||||
|
|
||||||
|
class Sys(PagePlugin):
|
||||||
|
order = 10
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
cfg.log("!!!!!!!!!!!!!!!!!!!!")
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("sys")
|
||||||
|
self.menu = cfg.main_menu.add_item(_("%s System" % cfg.product_name), "/sys", 100)
|
||||||
|
self.menu.add_item(_("Configure"), "/sys/config", 10)
|
||||||
|
self.menu.add_item(_("Package Manager"), "/sys/packages", 20)
|
||||||
|
self.menu.add_item(_("Users and Groups"), "/sys/users", 15)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
return self.fill_template(title=_("System Configuration"), main=_("""
|
||||||
|
<p>In this section, you can control the %(product)s's
|
||||||
|
underlying system, as opposed to its various applications and
|
||||||
|
services. These options affect the %(product)s at its most
|
||||||
|
general level. This is where you add/remove users, install
|
||||||
|
applications, reboot, etc.</p>
|
||||||
|
""" % {'product':cfg.product_name}))
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def packages(self):
|
||||||
|
side=_("""
|
||||||
|
<h2>Help</h2>
|
||||||
|
<p>On this page, you can add or remove %s plugins to your %s.</p>
|
||||||
|
<p>Plugins are just Debian packages, so Debian's usual package management features should make this job fairly easy.</p>
|
||||||
|
""" % (cfg.product_name, cfg.box_name))
|
||||||
|
return self.fill_template(title=_("Add/Remove Plugins"), main=_("""
|
||||||
|
<p>aptitude purge modules</p>
|
||||||
|
<p>aptitude install modules</p>
|
||||||
|
<p>The modules should depend on the appropriate Debian packages.</p>"""),
|
||||||
|
sidebar_right=side)
|
||||||
|
|
||||||
572
modules/installed/system/time_zones
Normal file
572
modules/installed/system/time_zones
Normal file
@ -0,0 +1,572 @@
|
|||||||
|
[
|
||||||
|
"Africa/Abidjan",
|
||||||
|
"Africa/Accra",
|
||||||
|
"Africa/Addis_Ababa",
|
||||||
|
"Africa/Algiers",
|
||||||
|
"Africa/Asmara",
|
||||||
|
"Africa/Asmera",
|
||||||
|
"Africa/Bamako",
|
||||||
|
"Africa/Bangui",
|
||||||
|
"Africa/Banjul",
|
||||||
|
"Africa/Bissau",
|
||||||
|
"Africa/Blantyre",
|
||||||
|
"Africa/Brazzaville",
|
||||||
|
"Africa/Bujumbura",
|
||||||
|
"Africa/Cairo",
|
||||||
|
"Africa/Casablanca",
|
||||||
|
"Africa/Ceuta",
|
||||||
|
"Africa/Conakry",
|
||||||
|
"Africa/Dakar",
|
||||||
|
"Africa/Dar_es_Salaam",
|
||||||
|
"Africa/Djibouti",
|
||||||
|
"Africa/Douala",
|
||||||
|
"Africa/El_Aaiun",
|
||||||
|
"Africa/Freetown",
|
||||||
|
"Africa/Gaborone",
|
||||||
|
"Africa/Harare",
|
||||||
|
"Africa/Johannesburg",
|
||||||
|
"Africa/Kampala",
|
||||||
|
"Africa/Khartoum",
|
||||||
|
"Africa/Kigali",
|
||||||
|
"Africa/Kinshasa",
|
||||||
|
"Africa/Lagos",
|
||||||
|
"Africa/Libreville",
|
||||||
|
"Africa/Lome",
|
||||||
|
"Africa/Luanda",
|
||||||
|
"Africa/Lubumbashi",
|
||||||
|
"Africa/Lusaka",
|
||||||
|
"Africa/Malabo",
|
||||||
|
"Africa/Maputo",
|
||||||
|
"Africa/Maseru",
|
||||||
|
"Africa/Mbabane",
|
||||||
|
"Africa/Mogadishu",
|
||||||
|
"Africa/Monrovia",
|
||||||
|
"Africa/Nairobi",
|
||||||
|
"Africa/Ndjamena",
|
||||||
|
"Africa/Niamey",
|
||||||
|
"Africa/Nouakchott",
|
||||||
|
"Africa/Ouagadougou",
|
||||||
|
"Africa/Porto-Novo",
|
||||||
|
"Africa/Sao_Tome",
|
||||||
|
"Africa/Timbuktu",
|
||||||
|
"Africa/Tripoli",
|
||||||
|
"Africa/Tunis",
|
||||||
|
"Africa/Windhoek",
|
||||||
|
"America/Adak",
|
||||||
|
"America/Anchorage",
|
||||||
|
"America/Anguilla",
|
||||||
|
"America/Antigua",
|
||||||
|
"America/Araguaina",
|
||||||
|
"America/Argentina/Buenos_Aires",
|
||||||
|
"America/Argentina/Catamarca",
|
||||||
|
"America/Argentina/ComodRivadavia",
|
||||||
|
"America/Argentina/Cordoba",
|
||||||
|
"America/Argentina/Jujuy",
|
||||||
|
"America/Argentina/La_Rioja",
|
||||||
|
"America/Argentina/Mendoza",
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
"America/Argentina/Salta",
|
||||||
|
"America/Argentina/San_Juan",
|
||||||
|
"America/Argentina/San_Luis",
|
||||||
|
"America/Argentina/Tucuman",
|
||||||
|
"America/Argentina/Ushuaia",
|
||||||
|
"America/Aruba",
|
||||||
|
"America/Asuncion",
|
||||||
|
"America/Atikokan",
|
||||||
|
"America/Atka",
|
||||||
|
"America/Bahia",
|
||||||
|
"America/Bahia_Banderas",
|
||||||
|
"America/Barbados",
|
||||||
|
"America/Belem",
|
||||||
|
"America/Belize",
|
||||||
|
"America/Blanc-Sablon",
|
||||||
|
"America/Boa_Vista",
|
||||||
|
"America/Bogota",
|
||||||
|
"America/Boise",
|
||||||
|
"America/Buenos_Aires",
|
||||||
|
"America/Cambridge_Bay",
|
||||||
|
"America/Campo_Grande",
|
||||||
|
"America/Cancun",
|
||||||
|
"America/Caracas",
|
||||||
|
"America/Catamarca",
|
||||||
|
"America/Cayenne",
|
||||||
|
"America/Cayman",
|
||||||
|
"America/Chicago",
|
||||||
|
"America/Chihuahua",
|
||||||
|
"America/Coral_Harbour",
|
||||||
|
"America/Cordoba",
|
||||||
|
"America/Costa_Rica",
|
||||||
|
"America/Cuiaba",
|
||||||
|
"America/Curacao",
|
||||||
|
"America/Danmarkshavn",
|
||||||
|
"America/Dawson",
|
||||||
|
"America/Dawson_Creek",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Detroit",
|
||||||
|
"America/Dominica",
|
||||||
|
"America/Edmonton",
|
||||||
|
"America/Eirunepe",
|
||||||
|
"America/El_Salvador",
|
||||||
|
"America/Ensenada",
|
||||||
|
"America/Fort_Wayne",
|
||||||
|
"America/Fortaleza",
|
||||||
|
"America/Glace_Bay",
|
||||||
|
"America/Godthab",
|
||||||
|
"America/Goose_Bay",
|
||||||
|
"America/Grand_Turk",
|
||||||
|
"America/Grenada",
|
||||||
|
"America/Guadeloupe",
|
||||||
|
"America/Guatemala",
|
||||||
|
"America/Guayaquil",
|
||||||
|
"America/Guyana",
|
||||||
|
"America/Halifax",
|
||||||
|
"America/Havana",
|
||||||
|
"America/Hermosillo",
|
||||||
|
"America/Indiana/Indianapolis",
|
||||||
|
"America/Indiana/Knox",
|
||||||
|
"America/Indiana/Marengo",
|
||||||
|
"America/Indiana/Petersburg",
|
||||||
|
"America/Indiana/Tell_City",
|
||||||
|
"America/Indiana/Vevay",
|
||||||
|
"America/Indiana/Vincennes",
|
||||||
|
"America/Indiana/Winamac",
|
||||||
|
"America/Indianapolis",
|
||||||
|
"America/Inuvik",
|
||||||
|
"America/Iqaluit",
|
||||||
|
"America/Jamaica",
|
||||||
|
"America/Jujuy",
|
||||||
|
"America/Juneau",
|
||||||
|
"America/Kentucky/Louisville",
|
||||||
|
"America/Kentucky/Monticello",
|
||||||
|
"America/Knox_IN",
|
||||||
|
"America/La_Paz",
|
||||||
|
"America/Lima",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
"America/Louisville",
|
||||||
|
"America/Maceio",
|
||||||
|
"America/Managua",
|
||||||
|
"America/Manaus",
|
||||||
|
"America/Marigot",
|
||||||
|
"America/Martinique",
|
||||||
|
"America/Matamoros",
|
||||||
|
"America/Mazatlan",
|
||||||
|
"America/Mendoza",
|
||||||
|
"America/Menominee",
|
||||||
|
"America/Merida",
|
||||||
|
"America/Mexico_City",
|
||||||
|
"America/Miquelon",
|
||||||
|
"America/Moncton",
|
||||||
|
"America/Monterrey",
|
||||||
|
"America/Montevideo",
|
||||||
|
"America/Montreal",
|
||||||
|
"America/Montserrat",
|
||||||
|
"America/Nassau",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Nipigon",
|
||||||
|
"America/Nome",
|
||||||
|
"America/Noronha",
|
||||||
|
"America/North_Dakota/Center",
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
"America/Ojinaga",
|
||||||
|
"America/Panama",
|
||||||
|
"America/Pangnirtung",
|
||||||
|
"America/Paramaribo",
|
||||||
|
"America/Phoenix",
|
||||||
|
"America/Port-au-Prince",
|
||||||
|
"America/Port_of_Spain",
|
||||||
|
"America/Porto_Acre",
|
||||||
|
"America/Porto_Velho",
|
||||||
|
"America/Puerto_Rico",
|
||||||
|
"America/Rainy_River",
|
||||||
|
"America/Rankin_Inlet",
|
||||||
|
"America/Recife",
|
||||||
|
"America/Regina",
|
||||||
|
"America/Resolute",
|
||||||
|
"America/Rio_Branco",
|
||||||
|
"America/Rosario",
|
||||||
|
"America/Santa_Isabel",
|
||||||
|
"America/Santarem",
|
||||||
|
"America/Santiago",
|
||||||
|
"America/Santo_Domingo",
|
||||||
|
"America/Sao_Paulo",
|
||||||
|
"America/Scoresbysund",
|
||||||
|
"America/Shiprock",
|
||||||
|
"America/St_Barthelemy",
|
||||||
|
"America/St_Johns",
|
||||||
|
"America/St_Kitts",
|
||||||
|
"America/St_Lucia",
|
||||||
|
"America/St_Thomas",
|
||||||
|
"America/St_Vincent",
|
||||||
|
"America/Swift_Current",
|
||||||
|
"America/Tegucigalpa",
|
||||||
|
"America/Thule",
|
||||||
|
"America/Thunder_Bay",
|
||||||
|
"America/Tijuana",
|
||||||
|
"America/Toronto",
|
||||||
|
"America/Tortola",
|
||||||
|
"America/Vancouver",
|
||||||
|
"America/Virgin",
|
||||||
|
"America/Whitehorse",
|
||||||
|
"America/Winnipeg",
|
||||||
|
"America/Yakutat",
|
||||||
|
"America/Yellowknife",
|
||||||
|
"Antarctica/Casey",
|
||||||
|
"Antarctica/Davis",
|
||||||
|
"Antarctica/DumontDUrville",
|
||||||
|
"Antarctica/Macquarie",
|
||||||
|
"Antarctica/Mawson",
|
||||||
|
"Antarctica/McMurdo",
|
||||||
|
"Antarctica/Palmer",
|
||||||
|
"Antarctica/Rothera",
|
||||||
|
"Antarctica/South_Pole",
|
||||||
|
"Antarctica/Syowa",
|
||||||
|
"Antarctica/Vostok",
|
||||||
|
"Arctic/Longyearbyen",
|
||||||
|
"Asia/Aden",
|
||||||
|
"Asia/Almaty",
|
||||||
|
"Asia/Amman",
|
||||||
|
"Asia/Anadyr",
|
||||||
|
"Asia/Aqtau",
|
||||||
|
"Asia/Aqtobe",
|
||||||
|
"Asia/Ashgabat",
|
||||||
|
"Asia/Ashkhabad",
|
||||||
|
"Asia/Baghdad",
|
||||||
|
"Asia/Bahrain",
|
||||||
|
"Asia/Baku",
|
||||||
|
"Asia/Bangkok",
|
||||||
|
"Asia/Beirut",
|
||||||
|
"Asia/Bishkek",
|
||||||
|
"Asia/Brunei",
|
||||||
|
"Asia/Calcutta",
|
||||||
|
"Asia/Choibalsan",
|
||||||
|
"Asia/Chongqing",
|
||||||
|
"Asia/Chungking",
|
||||||
|
"Asia/Colombo",
|
||||||
|
"Asia/Dacca",
|
||||||
|
"Asia/Damascus",
|
||||||
|
"Asia/Dhaka",
|
||||||
|
"Asia/Dili",
|
||||||
|
"Asia/Dubai",
|
||||||
|
"Asia/Dushanbe",
|
||||||
|
"Asia/Gaza",
|
||||||
|
"Asia/Harbin",
|
||||||
|
"Asia/Ho_Chi_Minh",
|
||||||
|
"Asia/Hong_Kong",
|
||||||
|
"Asia/Hovd",
|
||||||
|
"Asia/Irkutsk",
|
||||||
|
"Asia/Istanbul",
|
||||||
|
"Asia/Jakarta",
|
||||||
|
"Asia/Jayapura",
|
||||||
|
"Asia/Jerusalem",
|
||||||
|
"Asia/Kabul",
|
||||||
|
"Asia/Kamchatka",
|
||||||
|
"Asia/Karachi",
|
||||||
|
"Asia/Kashgar",
|
||||||
|
"Asia/Kathmandu",
|
||||||
|
"Asia/Katmandu",
|
||||||
|
"Asia/Kolkata",
|
||||||
|
"Asia/Krasnoyarsk",
|
||||||
|
"Asia/Kuala_Lumpur",
|
||||||
|
"Asia/Kuching",
|
||||||
|
"Asia/Kuwait",
|
||||||
|
"Asia/Macao",
|
||||||
|
"Asia/Macau",
|
||||||
|
"Asia/Magadan",
|
||||||
|
"Asia/Makassar",
|
||||||
|
"Asia/Manila",
|
||||||
|
"Asia/Muscat",
|
||||||
|
"Asia/Nicosia",
|
||||||
|
"Asia/Novokuznetsk",
|
||||||
|
"Asia/Novosibirsk",
|
||||||
|
"Asia/Omsk",
|
||||||
|
"Asia/Oral",
|
||||||
|
"Asia/Phnom_Penh",
|
||||||
|
"Asia/Pontianak",
|
||||||
|
"Asia/Pyongyang",
|
||||||
|
"Asia/Qatar",
|
||||||
|
"Asia/Qyzylorda",
|
||||||
|
"Asia/Rangoon",
|
||||||
|
"Asia/Riyadh",
|
||||||
|
"Asia/Riyadh87",
|
||||||
|
"Asia/Riyadh88",
|
||||||
|
"Asia/Riyadh89",
|
||||||
|
"Asia/Saigon",
|
||||||
|
"Asia/Sakhalin",
|
||||||
|
"Asia/Samarkand",
|
||||||
|
"Asia/Seoul",
|
||||||
|
"Asia/Shanghai",
|
||||||
|
"Asia/Singapore",
|
||||||
|
"Asia/Taipei",
|
||||||
|
"Asia/Tashkent",
|
||||||
|
"Asia/Tbilisi",
|
||||||
|
"Asia/Tehran",
|
||||||
|
"Asia/Tel_Aviv",
|
||||||
|
"Asia/Thimbu",
|
||||||
|
"Asia/Thimphu",
|
||||||
|
"Asia/Tokyo",
|
||||||
|
"Asia/Ujung_Pandang",
|
||||||
|
"Asia/Ulaanbaatar",
|
||||||
|
"Asia/Ulan_Bator",
|
||||||
|
"Asia/Urumqi",
|
||||||
|
"Asia/Vientiane",
|
||||||
|
"Asia/Vladivostok",
|
||||||
|
"Asia/Yakutsk",
|
||||||
|
"Asia/Yekaterinburg",
|
||||||
|
"Asia/Yerevan",
|
||||||
|
"Atlantic/Azores",
|
||||||
|
"Atlantic/Bermuda",
|
||||||
|
"Atlantic/Canary",
|
||||||
|
"Atlantic/Cape_Verde",
|
||||||
|
"Atlantic/Faeroe",
|
||||||
|
"Atlantic/Faroe",
|
||||||
|
"Atlantic/Jan_Mayen",
|
||||||
|
"Atlantic/Madeira",
|
||||||
|
"Atlantic/Reykjavik",
|
||||||
|
"Atlantic/South_Georgia",
|
||||||
|
"Atlantic/St_Helena",
|
||||||
|
"Atlantic/Stanley",
|
||||||
|
"Australia/ACT",
|
||||||
|
"Australia/Adelaide",
|
||||||
|
"Australia/Brisbane",
|
||||||
|
"Australia/Broken_Hill",
|
||||||
|
"Australia/Canberra",
|
||||||
|
"Australia/Currie",
|
||||||
|
"Australia/Darwin",
|
||||||
|
"Australia/Eucla",
|
||||||
|
"Australia/Hobart",
|
||||||
|
"Australia/LHI",
|
||||||
|
"Australia/Lindeman",
|
||||||
|
"Australia/Lord_Howe",
|
||||||
|
"Australia/Melbourne",
|
||||||
|
"Australia/NSW",
|
||||||
|
"Australia/North",
|
||||||
|
"Australia/Perth",
|
||||||
|
"Australia/Queensland",
|
||||||
|
"Australia/South",
|
||||||
|
"Australia/Sydney",
|
||||||
|
"Australia/Tasmania",
|
||||||
|
"Australia/Victoria",
|
||||||
|
"Australia/West",
|
||||||
|
"Australia/Yancowinna",
|
||||||
|
"Brazil/Acre",
|
||||||
|
"Brazil/DeNoronha",
|
||||||
|
"Brazil/East",
|
||||||
|
"Brazil/West",
|
||||||
|
"CET",
|
||||||
|
"CST6CDT",
|
||||||
|
"Canada/Atlantic",
|
||||||
|
"Canada/Central",
|
||||||
|
"Canada/East-Saskatchewan",
|
||||||
|
"Canada/Eastern",
|
||||||
|
"Canada/Mountain",
|
||||||
|
"Canada/Newfoundland",
|
||||||
|
"Canada/Pacific",
|
||||||
|
"Canada/Saskatchewan",
|
||||||
|
"Canada/Yukon",
|
||||||
|
"Chile/Continental",
|
||||||
|
"Chile/EasterIsland",
|
||||||
|
"Cuba",
|
||||||
|
"EET",
|
||||||
|
"EST",
|
||||||
|
"EST5EDT",
|
||||||
|
"Egypt",
|
||||||
|
"Eire",
|
||||||
|
"Etc/GMT",
|
||||||
|
"Etc/GMT+0",
|
||||||
|
"Etc/GMT+1",
|
||||||
|
"Etc/GMT+10",
|
||||||
|
"Etc/GMT+11",
|
||||||
|
"Etc/GMT+12",
|
||||||
|
"Etc/GMT+2",
|
||||||
|
"Etc/GMT+3",
|
||||||
|
"Etc/GMT+4",
|
||||||
|
"Etc/GMT+5",
|
||||||
|
"Etc/GMT+6",
|
||||||
|
"Etc/GMT+7",
|
||||||
|
"Etc/GMT+8",
|
||||||
|
"Etc/GMT+9",
|
||||||
|
"Etc/GMT-0",
|
||||||
|
"Etc/GMT-1",
|
||||||
|
"Etc/GMT-10",
|
||||||
|
"Etc/GMT-11",
|
||||||
|
"Etc/GMT-12",
|
||||||
|
"Etc/GMT-13",
|
||||||
|
"Etc/GMT-14",
|
||||||
|
"Etc/GMT-2",
|
||||||
|
"Etc/GMT-3",
|
||||||
|
"Etc/GMT-4",
|
||||||
|
"Etc/GMT-5",
|
||||||
|
"Etc/GMT-6",
|
||||||
|
"Etc/GMT-7",
|
||||||
|
"Etc/GMT-8",
|
||||||
|
"Etc/GMT-9",
|
||||||
|
"Etc/GMT0",
|
||||||
|
"Etc/Greenwich",
|
||||||
|
"Etc/UCT",
|
||||||
|
"Etc/UTC",
|
||||||
|
"Etc/Universal",
|
||||||
|
"Etc/Zulu",
|
||||||
|
"Europe/Amsterdam",
|
||||||
|
"Europe/Andorra",
|
||||||
|
"Europe/Athens",
|
||||||
|
"Europe/Belfast",
|
||||||
|
"Europe/Belgrade",
|
||||||
|
"Europe/Berlin",
|
||||||
|
"Europe/Bratislava",
|
||||||
|
"Europe/Brussels",
|
||||||
|
"Europe/Bucharest",
|
||||||
|
"Europe/Budapest",
|
||||||
|
"Europe/Chisinau",
|
||||||
|
"Europe/Copenhagen",
|
||||||
|
"Europe/Dublin",
|
||||||
|
"Europe/Gibraltar",
|
||||||
|
"Europe/Guernsey",
|
||||||
|
"Europe/Helsinki",
|
||||||
|
"Europe/Isle_of_Man",
|
||||||
|
"Europe/Istanbul",
|
||||||
|
"Europe/Jersey",
|
||||||
|
"Europe/Kaliningrad",
|
||||||
|
"Europe/Kiev",
|
||||||
|
"Europe/Lisbon",
|
||||||
|
"Europe/Ljubljana",
|
||||||
|
"Europe/London",
|
||||||
|
"Europe/Luxembourg",
|
||||||
|
"Europe/Madrid",
|
||||||
|
"Europe/Malta",
|
||||||
|
"Europe/Mariehamn",
|
||||||
|
"Europe/Minsk",
|
||||||
|
"Europe/Monaco",
|
||||||
|
"Europe/Moscow",
|
||||||
|
"Europe/Nicosia",
|
||||||
|
"Europe/Oslo",
|
||||||
|
"Europe/Paris",
|
||||||
|
"Europe/Podgorica",
|
||||||
|
"Europe/Prague",
|
||||||
|
"Europe/Riga",
|
||||||
|
"Europe/Rome",
|
||||||
|
"Europe/Samara",
|
||||||
|
"Europe/San_Marino",
|
||||||
|
"Europe/Sarajevo",
|
||||||
|
"Europe/Simferopol",
|
||||||
|
"Europe/Skopje",
|
||||||
|
"Europe/Sofia",
|
||||||
|
"Europe/Stockholm",
|
||||||
|
"Europe/Tallinn",
|
||||||
|
"Europe/Tirane",
|
||||||
|
"Europe/Tiraspol",
|
||||||
|
"Europe/Uzhgorod",
|
||||||
|
"Europe/Vaduz",
|
||||||
|
"Europe/Vatican",
|
||||||
|
"Europe/Vienna",
|
||||||
|
"Europe/Vilnius",
|
||||||
|
"Europe/Volgograd",
|
||||||
|
"Europe/Warsaw",
|
||||||
|
"Europe/Zagreb",
|
||||||
|
"Europe/Zaporozhye",
|
||||||
|
"Europe/Zurich",
|
||||||
|
"GB",
|
||||||
|
"GB-Eire",
|
||||||
|
"GMT",
|
||||||
|
"GMT+0",
|
||||||
|
"Greenwich",
|
||||||
|
"HST",
|
||||||
|
"Hongkong",
|
||||||
|
"Iceland",
|
||||||
|
"Indian/Antananarivo",
|
||||||
|
"Indian/Chagos",
|
||||||
|
"Indian/Christmas",
|
||||||
|
"Indian/Cocos",
|
||||||
|
"Indian/Comoro",
|
||||||
|
"Indian/Kerguelen",
|
||||||
|
"Indian/Mahe",
|
||||||
|
"Indian/Maldives",
|
||||||
|
"Indian/Mauritius",
|
||||||
|
"Indian/Mayotte",
|
||||||
|
"Indian/Reunion",
|
||||||
|
"Iran",
|
||||||
|
"Israel",
|
||||||
|
"Jamaica",
|
||||||
|
"Japan",
|
||||||
|
"Kwajalein",
|
||||||
|
"Libya",
|
||||||
|
"MET",
|
||||||
|
"MST",
|
||||||
|
"MST7MDT",
|
||||||
|
"Mexico/BajaNorte",
|
||||||
|
"Mexico/BajaSur",
|
||||||
|
"Mexico/General",
|
||||||
|
"Mideast/Riyadh87",
|
||||||
|
"Mideast/Riyadh88",
|
||||||
|
"Mideast/Riyadh89",
|
||||||
|
"NZ",
|
||||||
|
"NZ-CHAT",
|
||||||
|
"Navajo",
|
||||||
|
"PRC",
|
||||||
|
"PST8PDT",
|
||||||
|
"Pacific/Apia",
|
||||||
|
"Pacific/Auckland",
|
||||||
|
"Pacific/Chatham",
|
||||||
|
"Pacific/Chuuk",
|
||||||
|
"Pacific/Easter",
|
||||||
|
"Pacific/Efate",
|
||||||
|
"Pacific/Enderbury",
|
||||||
|
"Pacific/Fakaofo",
|
||||||
|
"Pacific/Fiji",
|
||||||
|
"Pacific/Funafuti",
|
||||||
|
"Pacific/Galapagos",
|
||||||
|
"Pacific/Gambier",
|
||||||
|
"Pacific/Guadalcanal",
|
||||||
|
"Pacific/Guam",
|
||||||
|
"Pacific/Honolulu",
|
||||||
|
"Pacific/Johnston",
|
||||||
|
"Pacific/Kiritimati",
|
||||||
|
"Pacific/Kosrae",
|
||||||
|
"Pacific/Kwajalein",
|
||||||
|
"Pacific/Majuro",
|
||||||
|
"Pacific/Marquesas",
|
||||||
|
"Pacific/Midway",
|
||||||
|
"Pacific/Nauru",
|
||||||
|
"Pacific/Niue",
|
||||||
|
"Pacific/Norfolk",
|
||||||
|
"Pacific/Noumea",
|
||||||
|
"Pacific/Pago_Pago",
|
||||||
|
"Pacific/Palau",
|
||||||
|
"Pacific/Pitcairn",
|
||||||
|
"Pacific/Pohnpei",
|
||||||
|
"Pacific/Ponape",
|
||||||
|
"Pacific/Port_Moresby",
|
||||||
|
"Pacific/Rarotonga",
|
||||||
|
"Pacific/Saipan",
|
||||||
|
"Pacific/Samoa",
|
||||||
|
"Pacific/Tahiti",
|
||||||
|
"Pacific/Tarawa",
|
||||||
|
"Pacific/Tongatapu",
|
||||||
|
"Pacific/Truk",
|
||||||
|
"Pacific/Wake",
|
||||||
|
"Pacific/Wallis",
|
||||||
|
"Pacific/Yap",
|
||||||
|
"Poland",
|
||||||
|
"Portugal",
|
||||||
|
"ROC",
|
||||||
|
"ROK",
|
||||||
|
"Singapore",
|
||||||
|
"Turkey",
|
||||||
|
"UCT",
|
||||||
|
"US/Alaska",
|
||||||
|
"US/Aleutian",
|
||||||
|
"US/Arizona",
|
||||||
|
"US/Central",
|
||||||
|
"US/East-Indiana",
|
||||||
|
"US/Eastern",
|
||||||
|
"US/Hawaii",
|
||||||
|
"US/Indiana-Starke",
|
||||||
|
"US/Michigan",
|
||||||
|
"US/Mountain",
|
||||||
|
"US/Pacific",
|
||||||
|
"US/Pacific-New",
|
||||||
|
"US/Samoa",
|
||||||
|
"UTC",
|
||||||
|
"Universal",
|
||||||
|
"W-SU",
|
||||||
|
"Zulu"
|
||||||
|
]
|
||||||
127
modules/installed/system/users.py
Normal file
127
modules/installed/system/users.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import os, cherrypy
|
||||||
|
from gettext import gettext as _
|
||||||
|
from auth import require
|
||||||
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
|
import cfg
|
||||||
|
from forms import Form
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
class users(PagePlugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
|
self.register_page("sys.users")
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self):
|
||||||
|
parts = self.forms('/sys/config')
|
||||||
|
parts['title']=_("Manage Users and Groups")
|
||||||
|
return self.fill_template(**parts)
|
||||||
|
|
||||||
|
class add(FormPlugin, PagePlugin):
|
||||||
|
url = ["/sys/users"]
|
||||||
|
order = 30
|
||||||
|
|
||||||
|
sidebar_left = ''
|
||||||
|
sidebar_right = _("""<h2>Add User</h2><p>Adding a user via this
|
||||||
|
administrative interface <b>might</b> create a system user.
|
||||||
|
For example, if you provide a user with ssh access, she will
|
||||||
|
need a system account. If you don't know what that means,
|
||||||
|
don't worry about it.</p>""")
|
||||||
|
|
||||||
|
def main(self, username='', name='', email='', message=None, *args, **kwargs):
|
||||||
|
form = Form(title="Add User",
|
||||||
|
action="/sys/users/add/index",
|
||||||
|
onsubmit="return md5ify('add_user_form', 'password')",
|
||||||
|
name="add_user_form",
|
||||||
|
message=message)
|
||||||
|
form.text = '<script type="text/javascript" src="/static/js/md5.js"></script>\n'+form.text
|
||||||
|
form.text_input(_("Username"), name="username", value=username)
|
||||||
|
form.text_input(_("Full name"), name="name", value=name)
|
||||||
|
form.text_input(_("Email"), name="email", value=email)
|
||||||
|
form.text_input(_("Password"), name="password")
|
||||||
|
form.text_input(name="md5_password", type="hidden")
|
||||||
|
form.submit(label=_("Create User"), name="create")
|
||||||
|
return form.render()
|
||||||
|
|
||||||
|
def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs):
|
||||||
|
msg = ''
|
||||||
|
|
||||||
|
if not username: msg = add_message(msg, _("Must specify a username!"))
|
||||||
|
if not md5_password: msg = add_message(msg, _("Must specify a password!"))
|
||||||
|
|
||||||
|
if cfg.users.get(username):
|
||||||
|
msg = add_message(msg, _("User already exists!"))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cfg.users.set(User(dict={'username':username, 'name':name, 'email':email, 'password':md5_password}))
|
||||||
|
except:
|
||||||
|
msg = add_message(msg, _("Error storing user!"))
|
||||||
|
|
||||||
|
if not msg:
|
||||||
|
msg = add_message(msg, "%s saved." % username)
|
||||||
|
|
||||||
|
main = self.make_form(username, name, email, message=msg)
|
||||||
|
return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
|
||||||
|
|
||||||
|
class edit(FormPlugin, PagePlugin):
|
||||||
|
url = ["/sys/users"]
|
||||||
|
order = 35
|
||||||
|
|
||||||
|
sidebar_left = ''
|
||||||
|
sidebar_right = _("""<h2>Edit Users</h2><p>Click on a user's name to
|
||||||
|
go to a screen for editing that user's account.</p><h2>Delete
|
||||||
|
Users</h2><p>Check the box next to a users' names and then click
|
||||||
|
"Delete User" to remove users from %s and the %s
|
||||||
|
system.</p><p>Deleting users is permanent!</p>""" % (cfg.product_name, cfg.box_name))
|
||||||
|
|
||||||
|
def main(self, msg=''):
|
||||||
|
users = cfg.users.get_all()
|
||||||
|
add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg)
|
||||||
|
add_form.html('<span class="indent"><b>Delete</b><br /></span>')
|
||||||
|
for uname in sorted(users.keys()):
|
||||||
|
add_form.html('<span class="indent"> %s ' %
|
||||||
|
add_form.get_checkbox(name=uname) +
|
||||||
|
'<a href="/sys/users/edit?username=%s">%s (%s)</a><br /></span>' %
|
||||||
|
(uname, users[uname]['name'], uname))
|
||||||
|
add_form.submit(label=_("Delete User"), name="delete")
|
||||||
|
return add_form.render()
|
||||||
|
|
||||||
|
def process_form(self, **kwargs):
|
||||||
|
if 'delete' in kwargs:
|
||||||
|
msg = Message()
|
||||||
|
usernames = find_keys(kwargs, 'on')
|
||||||
|
cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames))
|
||||||
|
if usernames:
|
||||||
|
for username in usernames:
|
||||||
|
if cfg.users.exists(username):
|
||||||
|
try:
|
||||||
|
cfg.users.remove(username)
|
||||||
|
msg.add(_("Deleted user %s." % username))
|
||||||
|
except IOError, e:
|
||||||
|
if cfg.users.get('username', reload=True):
|
||||||
|
m = _("Error on deletion, user %s not fully deleted: %s" % (username, e))
|
||||||
|
cfg.log.error(m)
|
||||||
|
msg.add(m)
|
||||||
|
else:
|
||||||
|
m = _('Deletion failed on %s: %s' % (username, e))
|
||||||
|
cfg.log.error(m)
|
||||||
|
msg.add(m)
|
||||||
|
else:
|
||||||
|
cfg.log.warning(_("Can't delete %s. User does not exist." % username))
|
||||||
|
msg.add(_("User %s does not exist." % username))
|
||||||
|
else:
|
||||||
|
msg.add = _("Must specify at least one valid, existing user.")
|
||||||
|
main = self.make_form(msg=msg.text)
|
||||||
|
return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right)
|
||||||
|
|
||||||
|
sidebar_right = ''
|
||||||
|
u = cfg.users.get(kwargs['username'])
|
||||||
|
if not u:
|
||||||
|
main = _("<p>Could not find a user with username of %s!</p>" % kwargs['username'])
|
||||||
|
return self.fill_template(template="err", title=_("Unnown User"), main=main,
|
||||||
|
sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
|
||||||
|
|
||||||
|
main = _("""<h2>Edit User '%s'</h2>""" % u['username'])
|
||||||
|
sidebar_right = ''
|
||||||
|
return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right)
|
||||||
73
modules/installed/system/wan.py
Normal file
73
modules/installed/system/wan.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import os
|
||||||
|
import cherrypy
|
||||||
|
import simplejson as json
|
||||||
|
from gettext import gettext as _
|
||||||
|
from filedict import FileDict
|
||||||
|
from modules.auth import require
|
||||||
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
|
import cfg
|
||||||
|
from forms import Form
|
||||||
|
from model import User
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
class wan(FormPlugin, PagePlugin):
|
||||||
|
url = ["/sys/config"]
|
||||||
|
order = 20
|
||||||
|
|
||||||
|
def help(self, *args, **kwargs):
|
||||||
|
if not cfg.users.expert():
|
||||||
|
return ''
|
||||||
|
return _(#"""<h4>Admin from WAN</h4>
|
||||||
|
"""<p>If you check this box, this front
|
||||||
|
end will be reachable from the WAN. If your %(box)s
|
||||||
|
connects you to the internet, that means you'll be able to log
|
||||||
|
in to the front end from the internet. This might be
|
||||||
|
convenient, but it is also <b>dangerous</b>, since it can
|
||||||
|
enable attackers to gain access to your %(box)s from the
|
||||||
|
outside world. All they'll need is your username and
|
||||||
|
passphrase, which they might guess or they might simply try
|
||||||
|
every posible combination of letters and numbers until they
|
||||||
|
get in. If you enable the WAN administration option, you
|
||||||
|
<b>must</b> use long and complex passphrases.</p>
|
||||||
|
|
||||||
|
<p>For security reasons, neither WAN Administration nor WAN
|
||||||
|
SSH is available to the `admin` user account.</p>
|
||||||
|
|
||||||
|
<p>TODO: in expert mode, tell user they can ssh in to enable
|
||||||
|
admin from WAN, do their business, then disable it. It would
|
||||||
|
be good to enable the option and autodisable it when the ssh
|
||||||
|
connection dies.</p>
|
||||||
|
""" % {'product':cfg.product_name, 'box':cfg.box_name})
|
||||||
|
|
||||||
|
def main(self, message='', **kwargs):
|
||||||
|
store = filedict_con(cfg.store_file, 'sys')
|
||||||
|
|
||||||
|
defaults = {'wan_admin': "''",
|
||||||
|
'wan_ssh': "''",
|
||||||
|
'lan_ssh': "''",
|
||||||
|
}
|
||||||
|
for k,c in defaults.items():
|
||||||
|
if not k in kwargs:
|
||||||
|
try:
|
||||||
|
kwargs[k] = store[k]
|
||||||
|
except KeyError:
|
||||||
|
exec("if not '%(k)s' in kwargs: store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c})
|
||||||
|
|
||||||
|
form = Form(title=_("Accessing the %s" % cfg.box_name),
|
||||||
|
action="/sys/config/wan",
|
||||||
|
name="admin_wan_form",
|
||||||
|
message=message )
|
||||||
|
form.html(self.help())
|
||||||
|
if cfg.users.expert():
|
||||||
|
form.checkbox(_("Allow access to Plinth from WAN"), name="wan_admin", checked=kwargs['wan_admin'])
|
||||||
|
form.checkbox(_("Allow SSH access from LAN"), name="lan_ssh", checked=kwargs['lan_ssh'])
|
||||||
|
form.checkbox(_("Allow SSH access from WAN"), name="wan_ssh", checked=kwargs['wan_ssh'])
|
||||||
|
form.submit(_("Submit"))
|
||||||
|
return form.render()
|
||||||
|
|
||||||
|
def process_form(self, wan_admin='', wan_ssh='', lan_ssh='', *args, **kwargs):
|
||||||
|
store = filedict_con(cfg.store_file, 'sys')
|
||||||
|
for field in ['wan_admin', 'wan_ssh', 'lan_ssh']:
|
||||||
|
exec("store['%s'] = %s" % (field, field))
|
||||||
|
return "Settings updated."
|
||||||
|
|
||||||
1
modules/privacy.py
Symbolic link
1
modules/privacy.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/privacy/privacy.py
|
||||||
1
modules/router.py
Symbolic link
1
modules/router.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/router/router.py
|
||||||
1
modules/services.py
Symbolic link
1
modules/services.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/services/services.py
|
||||||
1
modules/sharing.py
Symbolic link
1
modules/sharing.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/sharing/sharing.py
|
||||||
1
modules/system.py
Symbolic link
1
modules/system.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/system/system.py
|
||||||
1
modules/user_store.py
Symbolic link
1
modules/user_store.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/lib/user_store.py
|
||||||
1
modules/users.py
Symbolic link
1
modules/users.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/system/users.py
|
||||||
1
modules/wan.py
Symbolic link
1
modules/wan.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
installed/system/wan.py
|
||||||
71
plinth.py
Executable file
71
plinth.py
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Start listener, just for testing
|
||||||
|
import os, sys
|
||||||
|
#import logging
|
||||||
|
from gettext import gettext as _
|
||||||
|
import cherrypy
|
||||||
|
import cfg, plugin_mount
|
||||||
|
from util import *
|
||||||
|
from logger import Logger
|
||||||
|
#from modules.auth import AuthController, require, member_of, name_is
|
||||||
|
|
||||||
|
def error_page(status, dynamic_msg, stock_msg):
|
||||||
|
return page_template(template="err", title=status, main="<p>%s</p>%s" % (dynamic_msg, stock_msg))
|
||||||
|
|
||||||
|
def error_page_404(status, message, traceback, version):
|
||||||
|
return error_page(status, message, """<p>If you believe this missing page should exist, please file a
|
||||||
|
bug with either the Plinth project or the people responsible for
|
||||||
|
the module you are trying to access.</p>
|
||||||
|
|
||||||
|
<p>Sorry for the mistake.</p>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def error_page_500(status, message, traceback, version):
|
||||||
|
cfg.log.error("500 Internal Server Error. Trackback is above.")
|
||||||
|
more="""<p>This is an internal error and not something you caused or can fix. You are welcome to report it.
|
||||||
|
TODO: add link to bug tracker.</p>"""
|
||||||
|
return error_page(status, message, "<p>%s</p><pre>%s</pre>" % (more, "\n".join(traceback.split("\n"))))
|
||||||
|
|
||||||
|
class Root(plugin_mount.PagePlugin):
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
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:
|
||||||
|
exec("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 setup():
|
||||||
|
os.chdir(cfg.file_root)
|
||||||
|
cherrypy.config.update({'error_page.404': error_page_404})
|
||||||
|
cherrypy.config.update({'error_page.500': error_page_500})
|
||||||
|
cfg.log = Logger()
|
||||||
|
load_modules()
|
||||||
|
cfg.html_root = Root()
|
||||||
|
cfg.page_plugins = plugin_mount.PagePlugin.get_plugins()
|
||||||
|
cfg.log("Loaded %d page plugins" % len(cfg.page_plugins))
|
||||||
|
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
|
||||||
|
cfg.forms = plugin_mount.FormPlugin.get_plugins()
|
||||||
|
def main():
|
||||||
|
setup()
|
||||||
|
cherrypy.quickstart(cfg.html_root, script_name='/', config="cherrypy.config")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
169
plugin_mount.py
Normal file
169
plugin_mount.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import cherrypy
|
||||||
|
from modules.auth import require
|
||||||
|
import cfg
|
||||||
|
from util import *
|
||||||
|
|
||||||
|
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 get_parts(obj, parts=None, *args, **kwargs):
|
||||||
|
if parts == None:
|
||||||
|
parts={}
|
||||||
|
|
||||||
|
fields = ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css', 'title']
|
||||||
|
for v in fields:
|
||||||
|
if not v in parts:
|
||||||
|
parts[v] = ''
|
||||||
|
exec("""
|
||||||
|
try:
|
||||||
|
if str(type(obj.%(v)s))=="<type 'instancemethod'>":
|
||||||
|
parts[v] += obj.%(v)s(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
parts[v] += obj.%(v)s
|
||||||
|
except AttributeError:
|
||||||
|
pass""" % {'v':v})
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
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.html_root = self
|
||||||
|
|
||||||
|
def register_page(self, url):
|
||||||
|
cfg.log.info("Registering page: %s" % url)
|
||||||
|
exec "cfg.html_root.%s = self" % (url)
|
||||||
|
def fill_template(self, *args, **kwargs):
|
||||||
|
return page_template(*args, **kwargs)
|
||||||
|
|
||||||
|
def forms(self, url, *args, **kwargs):
|
||||||
|
for form in cfg.forms:
|
||||||
|
if url in form.url:
|
||||||
|
cfg.log('Pulling together form for url %s (which matches %s)' % (url, form.url))
|
||||||
|
|
||||||
|
parts = get_parts(form, None, *args, **kwargs)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
return {'sidebar_left':left, 'sidebar_right':right, 'main':main}
|
||||||
|
|
||||||
|
class FormPlugin():
|
||||||
|
"""
|
||||||
|
Mount point for plugins that provide forms at specific URLs.
|
||||||
|
|
||||||
|
Form plugin classes should also inherit from PagePlugin so they
|
||||||
|
can implement the page that handles the results of the form. The
|
||||||
|
name of the class will be appended to each url where the form is
|
||||||
|
displayed to get the urls of the results pages.
|
||||||
|
|
||||||
|
Plugins implementing this reference should provide the following attributes:
|
||||||
|
|
||||||
|
url - list of URL path strings of pages on which to display this
|
||||||
|
form, not including the url path that *only* displays this form
|
||||||
|
(that's handled by the index method)
|
||||||
|
|
||||||
|
order - How high up on the page should this content be displayed?
|
||||||
|
Lower order is higher up.
|
||||||
|
|
||||||
|
The following attributes are optional (though without at least one
|
||||||
|
of them, this plugin won't do much):
|
||||||
|
|
||||||
|
sidebar_right - text to be displayed in the right column (can be attribute or method) (optional)
|
||||||
|
|
||||||
|
sidebar_left - text to be displayed in the left column (can be attribute or method) (optional)
|
||||||
|
|
||||||
|
main - text to be displayed in the center column (i.e. the form) (can be attribute or method)
|
||||||
|
|
||||||
|
js - attribute containing a string that will be placed in the
|
||||||
|
template head, just below the javascript loads. Use it to load
|
||||||
|
more javascript files (optional)
|
||||||
|
|
||||||
|
Although this plugin is intended for forms, it could just display
|
||||||
|
some html and skip the form.
|
||||||
|
"""
|
||||||
|
__metaclass__ = PluginMount
|
||||||
|
|
||||||
|
order = 50
|
||||||
|
url = []
|
||||||
|
js = ''
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
for u in self.url:
|
||||||
|
exec "cfg.html_root.%s = self" % "%s.%s" % ('.'.join(u.split("/")[1:]), self.__class__.__name__)
|
||||||
|
cfg.log("Registered page: %s.%s" % ('.'.join(u.split("/")[1:]), self.__class__.__name__))
|
||||||
|
|
||||||
|
def main(self, *args, **kwargs):
|
||||||
|
return "<p>Override this method and replace it with a form.</p>"
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require()
|
||||||
|
def index(self, **kwargs):
|
||||||
|
"""If the user has tried to fill in the form, process it, otherwise, just display a default form."""
|
||||||
|
if kwargs:
|
||||||
|
kwargs['message'] = self.process_form(**kwargs)
|
||||||
|
parts = get_parts(self)
|
||||||
|
return self.fill_template(**parts)
|
||||||
|
|
||||||
|
def process_form(self, **kwargs):
|
||||||
|
"""Process the form. Return any message as a result of processing."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fill_template(self, *args, **kwargs):
|
||||||
|
if not 'js' in kwargs:
|
||||||
|
try:
|
||||||
|
kwargs['js'] = self.js
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
cfg.log("%%%%%%%%%%% %s" % kwargs)
|
||||||
|
return page_template(*args, **kwargs)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__metaclass__ = PluginMountSingular
|
||||||
|
|
||||||
1
static/doc
Symbolic link
1
static/doc
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../doc
|
||||||
409
static/js/md5.js
Normal file
409
static/js/md5.js
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
/*
|
||||||
|
* md5.js 1.0b 27/06/96
|
||||||
|
*
|
||||||
|
* Javascript implementation of the RSA Data Security, Inc. MD5
|
||||||
|
* Message-Digest Algorithm.
|
||||||
|
*
|
||||||
|
* Copyright (c) 1996 Henri Torgemane. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software
|
||||||
|
* and its documentation for any purposes and without
|
||||||
|
* fee is hereby granted provided that this copyright notice
|
||||||
|
* appears in all copies.
|
||||||
|
*
|
||||||
|
* Of course, this soft is provided "as is" without express or implied
|
||||||
|
* warranty of any kind.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Modified with german comments and some information about collisions.
|
||||||
|
* (Ralf Mieke, ralf@miekenet.de, http://mieke.home.pages.de)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function array(n) {
|
||||||
|
for(i=0;i<n;i++) this[i]=0;
|
||||||
|
this.length=n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Einige grundlegenden Funktionen müssen wegen
|
||||||
|
* Javascript Fehlern umgeschrieben werden.
|
||||||
|
* Man versuche z.B. 0xffffffff >> 4 zu berechnen..
|
||||||
|
* Die nun verwendeten Funktionen sind zwar langsamer als die Originale,
|
||||||
|
* aber sie funktionieren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function integer(n) { return n%(0xffffffff+1); }
|
||||||
|
|
||||||
|
function shr(a,b) {
|
||||||
|
a=integer(a);
|
||||||
|
b=integer(b);
|
||||||
|
if (a-0x80000000>=0) {
|
||||||
|
a=a%0x80000000;
|
||||||
|
a>>=b;
|
||||||
|
a+=0x40000000>>(b-1);
|
||||||
|
} else
|
||||||
|
a>>=b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shl1(a) {
|
||||||
|
a=a%0x80000000;
|
||||||
|
if (a&0x40000000==0x40000000)
|
||||||
|
{
|
||||||
|
a-=0x40000000;
|
||||||
|
a*=2;
|
||||||
|
a+=0x80000000;
|
||||||
|
} else
|
||||||
|
a*=2;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shl(a,b) {
|
||||||
|
a=integer(a);
|
||||||
|
b=integer(b);
|
||||||
|
for (var i=0;i<b;i++) a=shl1(a);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function and(a,b) {
|
||||||
|
a=integer(a);
|
||||||
|
b=integer(b);
|
||||||
|
var t1=(a-0x80000000);
|
||||||
|
var t2=(b-0x80000000);
|
||||||
|
if (t1>=0)
|
||||||
|
if (t2>=0)
|
||||||
|
return ((t1&t2)+0x80000000);
|
||||||
|
else
|
||||||
|
return (t1&b);
|
||||||
|
else
|
||||||
|
if (t2>=0)
|
||||||
|
return (a&t2);
|
||||||
|
else
|
||||||
|
return (a&b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function or(a,b) {
|
||||||
|
a=integer(a);
|
||||||
|
b=integer(b);
|
||||||
|
var t1=(a-0x80000000);
|
||||||
|
var t2=(b-0x80000000);
|
||||||
|
if (t1>=0)
|
||||||
|
if (t2>=0)
|
||||||
|
return ((t1|t2)+0x80000000);
|
||||||
|
else
|
||||||
|
return ((t1|b)+0x80000000);
|
||||||
|
else
|
||||||
|
if (t2>=0)
|
||||||
|
return ((a|t2)+0x80000000);
|
||||||
|
else
|
||||||
|
return (a|b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function xor(a,b) {
|
||||||
|
a=integer(a);
|
||||||
|
b=integer(b);
|
||||||
|
var t1=(a-0x80000000);
|
||||||
|
var t2=(b-0x80000000);
|
||||||
|
if (t1>=0)
|
||||||
|
if (t2>=0)
|
||||||
|
return (t1^t2);
|
||||||
|
else
|
||||||
|
return ((t1^b)+0x80000000);
|
||||||
|
else
|
||||||
|
if (t2>=0)
|
||||||
|
return ((a^t2)+0x80000000);
|
||||||
|
else
|
||||||
|
return (a^b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function not(a) {
|
||||||
|
a=integer(a);
|
||||||
|
return (0xffffffff-a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Beginn des Algorithmus */
|
||||||
|
|
||||||
|
var state = new array(4);
|
||||||
|
var count = new array(2);
|
||||||
|
count[0] = 0;
|
||||||
|
count[1] = 0;
|
||||||
|
var buffer = new array(64);
|
||||||
|
var transformBuffer = new array(16);
|
||||||
|
var digestBits = new array(16);
|
||||||
|
|
||||||
|
var S11 = 7;
|
||||||
|
var S12 = 12;
|
||||||
|
var S13 = 17;
|
||||||
|
var S14 = 22;
|
||||||
|
var S21 = 5;
|
||||||
|
var S22 = 9;
|
||||||
|
var S23 = 14;
|
||||||
|
var S24 = 20;
|
||||||
|
var S31 = 4;
|
||||||
|
var S32 = 11;
|
||||||
|
var S33 = 16;
|
||||||
|
var S34 = 23;
|
||||||
|
var S41 = 6;
|
||||||
|
var S42 = 10;
|
||||||
|
var S43 = 15;
|
||||||
|
var S44 = 21;
|
||||||
|
|
||||||
|
function F(x,y,z) {
|
||||||
|
return or(and(x,y),and(not(x),z));
|
||||||
|
}
|
||||||
|
|
||||||
|
function G(x,y,z) {
|
||||||
|
return or(and(x,z),and(y,not(z)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function H(x,y,z) {
|
||||||
|
return xor(xor(x,y),z);
|
||||||
|
}
|
||||||
|
|
||||||
|
function I(x,y,z) {
|
||||||
|
return xor(y ,or(x , not(z)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateLeft(a,n) {
|
||||||
|
return or(shl(a, n),(shr(a,(32 - n))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function FF(a,b,c,d,x,s,ac) {
|
||||||
|
a = a+F(b, c, d) + x + ac;
|
||||||
|
a = rotateLeft(a, s);
|
||||||
|
a = a+b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GG(a,b,c,d,x,s,ac) {
|
||||||
|
a = a+G(b, c, d) +x + ac;
|
||||||
|
a = rotateLeft(a, s);
|
||||||
|
a = a+b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HH(a,b,c,d,x,s,ac) {
|
||||||
|
a = a+H(b, c, d) + x + ac;
|
||||||
|
a = rotateLeft(a, s);
|
||||||
|
a = a+b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function II(a,b,c,d,x,s,ac) {
|
||||||
|
a = a+I(b, c, d) + x + ac;
|
||||||
|
a = rotateLeft(a, s);
|
||||||
|
a = a+b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transform(buf,offset) {
|
||||||
|
var a=0, b=0, c=0, d=0;
|
||||||
|
var x = transformBuffer;
|
||||||
|
|
||||||
|
a = state[0];
|
||||||
|
b = state[1];
|
||||||
|
c = state[2];
|
||||||
|
d = state[3];
|
||||||
|
|
||||||
|
for (i = 0; i < 16; i++) {
|
||||||
|
x[i] = and(buf[i*4+offset],0xff);
|
||||||
|
for (j = 1; j < 4; j++) {
|
||||||
|
x[i]+=shl(and(buf[i*4+j+offset] ,0xff), j * 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Runde 1 */
|
||||||
|
a = FF ( a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
|
||||||
|
d = FF ( d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
|
||||||
|
c = FF ( c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
|
||||||
|
b = FF ( b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
|
||||||
|
a = FF ( a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
|
||||||
|
d = FF ( d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
|
||||||
|
c = FF ( c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
|
||||||
|
b = FF ( b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
|
||||||
|
a = FF ( a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
|
||||||
|
d = FF ( d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
|
||||||
|
c = FF ( c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
|
||||||
|
b = FF ( b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
|
||||||
|
a = FF ( a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
|
||||||
|
d = FF ( d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
|
||||||
|
c = FF ( c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
|
||||||
|
b = FF ( b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
|
||||||
|
|
||||||
|
/* Runde 2 */
|
||||||
|
a = GG ( a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
|
||||||
|
d = GG ( d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
|
||||||
|
c = GG ( c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
|
||||||
|
b = GG ( b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
|
||||||
|
a = GG ( a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
|
||||||
|
d = GG ( d, a, b, c, x[10], S22, 0x2441453); /* 22 */
|
||||||
|
c = GG ( c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
|
||||||
|
b = GG ( b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
|
||||||
|
a = GG ( a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
|
||||||
|
d = GG ( d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
|
||||||
|
c = GG ( c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
|
||||||
|
b = GG ( b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
|
||||||
|
a = GG ( a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
|
||||||
|
d = GG ( d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
|
||||||
|
c = GG ( c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
|
||||||
|
b = GG ( b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
|
||||||
|
|
||||||
|
/* Runde 3 */
|
||||||
|
a = HH ( a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
|
||||||
|
d = HH ( d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
|
||||||
|
c = HH ( c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
|
||||||
|
b = HH ( b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
|
||||||
|
a = HH ( a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
|
||||||
|
d = HH ( d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
|
||||||
|
c = HH ( c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
|
||||||
|
b = HH ( b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
|
||||||
|
a = HH ( a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
|
||||||
|
d = HH ( d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
|
||||||
|
c = HH ( c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
|
||||||
|
b = HH ( b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */
|
||||||
|
a = HH ( a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
|
||||||
|
d = HH ( d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
|
||||||
|
c = HH ( c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
|
||||||
|
b = HH ( b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */
|
||||||
|
|
||||||
|
/* Runde 4 */
|
||||||
|
a = II ( a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
|
||||||
|
d = II ( d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
|
||||||
|
c = II ( c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
|
||||||
|
b = II ( b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
|
||||||
|
a = II ( a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
|
||||||
|
d = II ( d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
|
||||||
|
c = II ( c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
|
||||||
|
b = II ( b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
|
||||||
|
a = II ( a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
|
||||||
|
d = II ( d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
|
||||||
|
c = II ( c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
|
||||||
|
b = II ( b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
|
||||||
|
a = II ( a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
|
||||||
|
d = II ( d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
|
||||||
|
c = II ( c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
|
||||||
|
b = II ( b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */
|
||||||
|
|
||||||
|
state[0] +=a;
|
||||||
|
state[1] +=b;
|
||||||
|
state[2] +=c;
|
||||||
|
state[3] +=d;
|
||||||
|
|
||||||
|
}
|
||||||
|
/* Mit der Initialisierung von Dobbertin:
|
||||||
|
state[0] = 0x12ac2375;
|
||||||
|
state[1] = 0x3b341042;
|
||||||
|
state[2] = 0x5f62b97c;
|
||||||
|
state[3] = 0x4ba763ed;
|
||||||
|
gibt es eine Kollision:
|
||||||
|
|
||||||
|
begin 644 Message1
|
||||||
|
M7MH=JO6_>MG!X?!51$)W,CXV!A"=(!AR71,<X`Y-IIT9^Z&8L$2N'Y*Y:R.;
|
||||||
|
39GIK9>TF$W()/MEHR%C4:G1R:Q"=
|
||||||
|
`
|
||||||
|
end
|
||||||
|
|
||||||
|
begin 644 Message2
|
||||||
|
M7MH=JO6_>MG!X?!51$)W,CXV!A"=(!AR71,<X`Y-IIT9^Z&8L$2N'Y*Y:R.;
|
||||||
|
39GIK9>TF$W()/MEHREC4:G1R:Q"=
|
||||||
|
`
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
count[0]=count[1] = 0;
|
||||||
|
state[0] = 0x67452301;
|
||||||
|
state[1] = 0xefcdab89;
|
||||||
|
state[2] = 0x98badcfe;
|
||||||
|
state[3] = 0x10325476;
|
||||||
|
for (i = 0; i < digestBits.length; i++)
|
||||||
|
digestBits[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(b) {
|
||||||
|
var index,i;
|
||||||
|
|
||||||
|
index = and(shr(count[0],3) , 0x3f);
|
||||||
|
if (count[0]<0xffffffff-7)
|
||||||
|
count[0] += 8;
|
||||||
|
else {
|
||||||
|
count[1]++;
|
||||||
|
count[0]-=0xffffffff+1;
|
||||||
|
count[0]+=8;
|
||||||
|
}
|
||||||
|
buffer[index] = and(b,0xff);
|
||||||
|
if (index >= 63) {
|
||||||
|
transform(buffer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
var bits = new array(8);
|
||||||
|
var padding;
|
||||||
|
var i=0, index=0, padLen=0;
|
||||||
|
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
bits[i] = and(shr(count[0],(i * 8)), 0xff);
|
||||||
|
}
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
bits[i+4]=and(shr(count[1],(i * 8)), 0xff);
|
||||||
|
}
|
||||||
|
index = and(shr(count[0], 3) ,0x3f);
|
||||||
|
padLen = (index < 56) ? (56 - index) : (120 - index);
|
||||||
|
padding = new array(64);
|
||||||
|
padding[0] = 0x80;
|
||||||
|
for (i=0;i<padLen;i++)
|
||||||
|
update(padding[i]);
|
||||||
|
for (i=0;i<8;i++)
|
||||||
|
update(bits[i]);
|
||||||
|
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
for (j = 0; j < 4; j++) {
|
||||||
|
digestBits[i*4+j] = and(shr(state[i], (j * 8)) , 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ende des MD5 Algorithmus */
|
||||||
|
|
||||||
|
function hexa(n) {
|
||||||
|
var hexa_h = "0123456789abcdef";
|
||||||
|
var hexa_c="";
|
||||||
|
var hexa_m=n;
|
||||||
|
for (hexa_i=0;hexa_i<8;hexa_i++) {
|
||||||
|
hexa_c=hexa_h.charAt(Math.abs(hexa_m)%16)+hexa_c;
|
||||||
|
hexa_m=Math.floor(hexa_m/16);
|
||||||
|
}
|
||||||
|
return hexa_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ascii="01234567890123456789012345678901" +
|
||||||
|
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
|
||||||
|
"[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
|
||||||
|
|
||||||
|
function MD5(nachricht)
|
||||||
|
{
|
||||||
|
var l,s,k,ka,kb,kc,kd;
|
||||||
|
|
||||||
|
init();
|
||||||
|
for (k=0;k<nachricht.length;k++) {
|
||||||
|
l=nachricht.charAt(k);
|
||||||
|
update(ascii.lastIndexOf(l));
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
ka=kb=kc=kd=0;
|
||||||
|
for (i=0;i<4;i++) ka+=shl(digestBits[15-i], (i*8));
|
||||||
|
for (i=4;i<8;i++) kb+=shl(digestBits[15-i], ((i-4)*8));
|
||||||
|
for (i=8;i<12;i++) kc+=shl(digestBits[15-i], ((i-8)*8));
|
||||||
|
for (i=12;i<16;i++) kd+=shl(digestBits[15-i], ((i-12)*8));
|
||||||
|
s=hexa(kd)+hexa(kc)+hexa(kb)+hexa(ka);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5ify(form, field) {
|
||||||
|
/* replace the field in the form with the md5 version of the contents of that field
|
||||||
|
|
||||||
|
The form must already have a field (preferably hidden) called md5_[field name]*/
|
||||||
|
|
||||||
|
var x=document.forms[form][field].value;
|
||||||
|
document.forms[form]["md5_"+field].value = MD5(x);
|
||||||
|
document.forms[form][field].value = '';
|
||||||
|
}
|
||||||
1
static/theme
Symbolic link
1
static/theme
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../themes/default
|
||||||
15
templates/Makefile
Normal file
15
templates/Makefile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CHEETAH=cheetah
|
||||||
|
TEMPLATES=$(wildcard *.tmpl)
|
||||||
|
TEMPLATE_PYFILES := $(patsubst %.tmpl,%.py,$(TEMPLATES))
|
||||||
|
|
||||||
|
## Catch-all tagets
|
||||||
|
default: $(TEMPLATE_PYFILES)
|
||||||
|
|
||||||
|
%.py: %.tmpl
|
||||||
|
$(CHEETAH) c $<
|
||||||
|
templates: $(TEMPLATE_PYFILES)
|
||||||
|
template: templates
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .\#* \#* *.pyz *.bak
|
||||||
|
@rm -f $(TEMPLATE_PYFILES)
|
||||||
0
templates/__init__.py
Normal file
0
templates/__init__.py
Normal file
BIN
templates/__init__.pyc
Normal file
BIN
templates/__init__.pyc
Normal file
Binary file not shown.
112
templates/base.tmpl
Normal file
112
templates/base.tmpl
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
#def default($text, $default)
|
||||||
|
#if $text
|
||||||
|
$text
|
||||||
|
#else
|
||||||
|
$default
|
||||||
|
#end if
|
||||||
|
#end def
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB">
|
||||||
|
<head>
|
||||||
|
<title>$default($title, "Plinth Front End to the Freedom Box")
|
||||||
|
</title>
|
||||||
|
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
||||||
|
<meta name="description" content="Plint admin frontend for Freedom Box" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/theme/style.tiny.css" media="screen" />
|
||||||
|
$css
|
||||||
|
<script type="text/javascript" src="/static/theme/menu.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/theme/plinth.js"></script>
|
||||||
|
$js
|
||||||
|
$main_menu_js
|
||||||
|
$sub_menu_js
|
||||||
|
<script LANGUAGE="JavaScript">
|
||||||
|
<!--
|
||||||
|
$onload
|
||||||
|
// -->
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onLoad="javascript:onload_handler();">
|
||||||
|
<div id="header">
|
||||||
|
<div id="headerleft">
|
||||||
|
<a href="/"><img src="/static/theme/freedombox.png" /></a>
|
||||||
|
</div>
|
||||||
|
<div id="headerright">
|
||||||
|
<br /><br /><br />
|
||||||
|
<h1><a href="/">Freedom Box</a></h1>
|
||||||
|
<h2><a href="/">Plinth Administration Control Panel</a></h2>
|
||||||
|
<SCRIPT LANGUAGE="JavaScript">
|
||||||
|
<!--
|
||||||
|
main_menu(main_menu_items);
|
||||||
|
// -->
|
||||||
|
</SCRIPT>
|
||||||
|
</div>
|
||||||
|
#if $username
|
||||||
|
<p id="layoutdims">Logged in as $username. <a href="/auth/logout" title="Log out">Logout.</a></p>
|
||||||
|
#else
|
||||||
|
<p id="layoutdims">Not logged in. <a href="/auth/login" title="Log in">Log in.</a></p>
|
||||||
|
#end if
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="colmask threecol">
|
||||||
|
<div class="colmid">
|
||||||
|
<div class="colleft">
|
||||||
|
<div class="col1">
|
||||||
|
<!-- Column 1 start -->
|
||||||
|
<h2>
|
||||||
|
#block title_block
|
||||||
|
$title
|
||||||
|
#end block title_block
|
||||||
|
</h2>
|
||||||
|
#block main_block
|
||||||
|
$main
|
||||||
|
#end block main_block
|
||||||
|
<!-- Column 1 end -->
|
||||||
|
</div>
|
||||||
|
<div class="col2">
|
||||||
|
<!-- Column 2 start -->
|
||||||
|
#block nav_block
|
||||||
|
$nav
|
||||||
|
#end block nav_block
|
||||||
|
#block sidebar_left_block
|
||||||
|
$sidebar_left
|
||||||
|
#end block sidebar_left_block
|
||||||
|
<!-- Column 2 end -->
|
||||||
|
</div>
|
||||||
|
<div class="col3">
|
||||||
|
<!-- Column 3 start -->
|
||||||
|
<div id="ads">
|
||||||
|
<!--<a href="http://matthewjamestaylor.com">
|
||||||
|
<img src="mjt-125x125.gif" width="125" border="0" height="125" alt="Art and Design by Matthew James Taylor" />
|
||||||
|
</a>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
#block sidebar_right_block
|
||||||
|
$sidebar_right
|
||||||
|
#end block sidebar_right_block
|
||||||
|
<!-- Column 3 end -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
#block footer_block
|
||||||
|
<p>
|
||||||
|
Plinth is copyright 2011 <a href="http://hackervisions.org">James Vasile</a>. It is
|
||||||
|
free software offered to you under the terms of
|
||||||
|
the <a href="http://www.gnu.org/licenses/agpl.html">GNU Affero General Public
|
||||||
|
License</a>, Version 3 or later.
|
||||||
|
</p>
|
||||||
|
<!--<p>Current page: $current_url</p>-->
|
||||||
|
<p>
|
||||||
|
This page uses
|
||||||
|
the <a href="http://matthewjamestaylor.com/blog/perfect-3-column.htm">Perfect
|
||||||
|
'Holy Grail' 3 Column Liquid Layout</a>
|
||||||
|
by <a href="http://matthewjamestaylor.com">Matthew James Taylor</a>.
|
||||||
|
</p>
|
||||||
|
#end block footer_block
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
templates/err.tmpl
Normal file
23
templates/err.tmpl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#extends templates.two_col
|
||||||
|
|
||||||
|
#def title_block
|
||||||
|
<span class="err">Error: $title</span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
#end def
|
||||||
|
|
||||||
|
#def sidebar_left_block
|
||||||
|
$sidebar_left
|
||||||
|
#end def
|
||||||
|
|
||||||
|
#def main_block
|
||||||
|
$main
|
||||||
|
#end def
|
||||||
|
|
||||||
|
#def sidebar_right_block
|
||||||
|
$sidebar_right
|
||||||
|
#end def
|
||||||
|
|
||||||
|
#def nav_block
|
||||||
|
$nav
|
||||||
|
#end def
|
||||||
5
templates/two_col.tmpl
Normal file
5
templates/two_col.tmpl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#extends templates.base
|
||||||
|
|
||||||
|
#def css
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/theme/2col.tiny.css" media="screen" />
|
||||||
|
#end def
|
||||||
30
themes/default/2col.css
Normal file
30
themes/default/2col.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* 2 Column settings */
|
||||||
|
|
||||||
|
.colright {
|
||||||
|
float:left;
|
||||||
|
width:0%; /* width of page */
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threecol .colmid {
|
||||||
|
right:5%; /* width of the right column */
|
||||||
|
}
|
||||||
|
.threecol .colleft {
|
||||||
|
right:70%; /* width of the middle column */
|
||||||
|
}
|
||||||
|
.threecol .col1 {
|
||||||
|
width:66%; /* width of center column content (column width minus padding on either side) */
|
||||||
|
left:102%; /* 100% plus left padding of center column */
|
||||||
|
}
|
||||||
|
.threecol .col2 {
|
||||||
|
width:21%; /* Width of left column content (column width minus padding on either side) */
|
||||||
|
left:11%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */
|
||||||
|
}
|
||||||
|
.threecol .col3 {
|
||||||
|
width:21%; /* Width of right column content (column width minus padding on either side) */
|
||||||
|
|
||||||
|
/* Note this used to be 85%, but I subtracted 1% for padding to pull stuff closer to the margin */
|
||||||
|
left:84%; /* Please make note of the brackets here:
|
||||||
|
(100% - left column width) plus (center column left and right padding) plus (left column left and right padding) plus (right column left padding) */
|
||||||
|
}
|
||||||
|
|
||||||
BIN
themes/default/favicon.ico
Normal file
BIN
themes/default/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
themes/default/freedombox.png
Normal file
BIN
themes/default/freedombox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
themes/default/images/freedombox.png
Normal file
BIN
themes/default/images/freedombox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
54
themes/default/menu.js
Normal file
54
themes/default/menu.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
function main_menu(items) {
|
||||||
|
output = "<ul>"
|
||||||
|
for (item in items) {
|
||||||
|
i = items[item];
|
||||||
|
|
||||||
|
// Handle active page
|
||||||
|
if (i["active"]) {
|
||||||
|
active = 'class = "active"';
|
||||||
|
} else {
|
||||||
|
active = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line break labels
|
||||||
|
label = i["label"];
|
||||||
|
if (label.search(" ") != -1) {
|
||||||
|
label = label.replace(" ", "<br />");
|
||||||
|
} else {
|
||||||
|
label = " <br />" + label;
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output +'<li><a href="' + i["url"] + '" ' + active + '>' + label + "</a></li>";
|
||||||
|
}
|
||||||
|
output = output + "</ul>";
|
||||||
|
document.write(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_items(items) {
|
||||||
|
output = "<ul>";
|
||||||
|
for (item in items) {
|
||||||
|
i = items[item];
|
||||||
|
|
||||||
|
// Handle active page
|
||||||
|
if (i["active"]) {
|
||||||
|
active = 'class = "active"';
|
||||||
|
} else {
|
||||||
|
active = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output +'<li><a href="' + i["url"] + '" ' + active + '>' + i['label'] + "</a></li>";
|
||||||
|
if (i['subs']) {
|
||||||
|
output += render_items(i['subs']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output = output + "</ul>";
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
function side_menu(items) {
|
||||||
|
if (items.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
output = "<h2>Menu</h2>" + render_items(items);
|
||||||
|
document.write(output);
|
||||||
|
}
|
||||||
16
themes/default/plinth.js
Normal file
16
themes/default/plinth.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
function toggle_visibility(id) {
|
||||||
|
var d = document.getElementById(id);
|
||||||
|
if(d.style.display == 'block')
|
||||||
|
d.style.display = 'none';
|
||||||
|
else
|
||||||
|
d.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(id) {
|
||||||
|
var d = document.getElementById(id);
|
||||||
|
d.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(id) {
|
||||||
|
document.getElementById(id).style.display = 'none';
|
||||||
|
}
|
||||||
328
themes/default/style.css
Normal file
328
themes/default/style.css
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
body {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
border:0; /* This removes the border around the viewport in old versions of IE */
|
||||||
|
width:100%;
|
||||||
|
background:lightgoldenrodyellow;
|
||||||
|
min-width:600px; /* Minimum width of layout - remove line if not required */
|
||||||
|
/* The min-width property does not work in old versions of Internet Explorer */
|
||||||
|
font-size:90%;
|
||||||
|
font-family:Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color:#369;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color:#fff;
|
||||||
|
background:GoldenRod;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin:.8em 0 .2em 0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin:.4em 0 .8em 0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
margin:10px 0 5px;
|
||||||
|
}
|
||||||
|
#ads img {
|
||||||
|
display:block;
|
||||||
|
padding-top:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles */
|
||||||
|
#header {
|
||||||
|
clear:both;
|
||||||
|
float:left;
|
||||||
|
width:100%;
|
||||||
|
border-bottom:1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerleft {
|
||||||
|
float: left;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
#headerleft a { color:lightgoldenrodyellow; text-decoration:none; outline: none;}
|
||||||
|
|
||||||
|
#headerright {
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
#headerright p,
|
||||||
|
#headerright h1,
|
||||||
|
#headerright h2 {
|
||||||
|
padding:.4em 15px 0 15px;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerright h1 a,
|
||||||
|
#headerright h2 a {color:black; text-decoration:none; outline: none;}
|
||||||
|
#headerright ul {
|
||||||
|
clear:left;
|
||||||
|
float:left;
|
||||||
|
width:100%;
|
||||||
|
list-style:none;
|
||||||
|
margin:10px 0 0 0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
#headerright ul li {
|
||||||
|
display:inline;
|
||||||
|
list-style:none;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
#headerright ul li a {
|
||||||
|
display:block;
|
||||||
|
float:left;
|
||||||
|
margin:0 0 0 1px;
|
||||||
|
padding:3px 10px;
|
||||||
|
text-align:center;
|
||||||
|
background:Khaki;
|
||||||
|
color:#000;
|
||||||
|
text-decoration:none;
|
||||||
|
position:relative;
|
||||||
|
left:15px;
|
||||||
|
line-height:1.3em;
|
||||||
|
}
|
||||||
|
#headerright ul li a:hover {
|
||||||
|
background:GoldenRod;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
#headerright ul li a.active,
|
||||||
|
#headerright ul li a.active:hover {
|
||||||
|
color:#fff;
|
||||||
|
background:#000;
|
||||||
|
font-weight:bold;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
#headerright ul li a span {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 'widths' sub menu */
|
||||||
|
#layoutdims {
|
||||||
|
clear:both;
|
||||||
|
background:#eee;
|
||||||
|
background:GoldenRod;
|
||||||
|
border-top:4px solid #000;
|
||||||
|
margin:0;
|
||||||
|
padding:6px 15px !important;
|
||||||
|
text-align:right;
|
||||||
|
}
|
||||||
|
/* column container */
|
||||||
|
.colmask {
|
||||||
|
position:relative; /* This fixes the IE7 overflow hidden bug */
|
||||||
|
clear:both;
|
||||||
|
float:left;
|
||||||
|
width:100%; /* width of whole page */
|
||||||
|
overflow:hidden; /* This chops off any overhanging divs */
|
||||||
|
}
|
||||||
|
/* common column settings */
|
||||||
|
.colright,
|
||||||
|
.colmid,
|
||||||
|
.colleft {
|
||||||
|
float:left;
|
||||||
|
width:100%; /* width of page */
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.col1,
|
||||||
|
.col2,
|
||||||
|
.col3 {
|
||||||
|
float:left;
|
||||||
|
position:relative;
|
||||||
|
padding:0 0em 1em 0; /* no left and right padding on columns, we just make them narrower instead
|
||||||
|
only padding top and bottom is included here, make it whatever value you need */
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col2 h2,
|
||||||
|
.col3 h2 {color:black;
|
||||||
|
font-weight:bold;
|
||||||
|
font-size:100%;
|
||||||
|
text-align:center;
|
||||||
|
background:Goldenrod;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.4em;}
|
||||||
|
.col2 h3,
|
||||||
|
.col3 h3 {color:black;
|
||||||
|
font-weight:bold;
|
||||||
|
font-size:100%;
|
||||||
|
text-align:left;
|
||||||
|
padding-top: 0em;
|
||||||
|
margin-top: 0em;
|
||||||
|
margin-bottom: 0em;}
|
||||||
|
.col3 p {
|
||||||
|
margin: 0.4em 0.5em .8em 0.5em;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
.col2 li {padding-bottom: 0.2em;}
|
||||||
|
.col2 a {padding-left: 0.2em;}
|
||||||
|
.col2 a.active,
|
||||||
|
.col2 a.active:hover {
|
||||||
|
color:#fff;
|
||||||
|
background:#000;
|
||||||
|
font-weight:bold;
|
||||||
|
text-decoration:none;
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3 Column settings */
|
||||||
|
.threecol {
|
||||||
|
background:#eee; /* right column background colour */
|
||||||
|
background:Khaki;
|
||||||
|
}
|
||||||
|
.threecol .colmid {
|
||||||
|
right:25%; /* width of the right column */
|
||||||
|
background:lightgoldenrodyellow; /* center column background colour */
|
||||||
|
}
|
||||||
|
.threecol .colleft {
|
||||||
|
right:50%; /* width of the middle column */
|
||||||
|
background:#f4f4f4; /* left column background colour */
|
||||||
|
background:Khaki;
|
||||||
|
}
|
||||||
|
.threecol .col1 {
|
||||||
|
width:46%; /* width of center column content (column width minus padding on either side) */
|
||||||
|
left:102%; /* 100% plus left padding of center column */
|
||||||
|
}
|
||||||
|
.threecol .col2 {
|
||||||
|
width:21%; /* Width of left column content (column width minus padding on either side) */
|
||||||
|
left:31%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */
|
||||||
|
}
|
||||||
|
.threecol .col3 {
|
||||||
|
width:21%; /* Width of right column content (column width minus padding on either side) */
|
||||||
|
|
||||||
|
/* Note this used to be 85%, but I subtracted 1% for padding to pull stuff closer to the margin */
|
||||||
|
left:84%; /* Please make note of the brackets here:
|
||||||
|
(100% - left column width) plus (center column left and right padding) plus (left column left and right padding) plus (right column left padding) */
|
||||||
|
}
|
||||||
|
/* Footer styles */
|
||||||
|
#footer {
|
||||||
|
clear:both;
|
||||||
|
float:left;
|
||||||
|
width:100%;
|
||||||
|
border-top:1px solid #000;
|
||||||
|
padding: 1em;
|
||||||
|
font-size:75%;
|
||||||
|
background:GoldenRod;
|
||||||
|
}
|
||||||
|
#footer p {
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
form.form {
|
||||||
|
margin:0 auto;
|
||||||
|
width:100%;
|
||||||
|
background:khaki;
|
||||||
|
position:relative;
|
||||||
|
margin-top:2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form h2 {
|
||||||
|
color:#ffffff;
|
||||||
|
font-size:1.2em;
|
||||||
|
background:goldenrod;
|
||||||
|
text-transform:uppercase;
|
||||||
|
padding:0.5em 0em 0.5em 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form h3 {
|
||||||
|
color:red;
|
||||||
|
font-weight:bold;
|
||||||
|
font-size:1em;
|
||||||
|
padding:0.5em 0 0.5em 0.5em;
|
||||||
|
text-align:center;
|
||||||
|
background:khaki;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .line,
|
||||||
|
form.form p {
|
||||||
|
width:94%;
|
||||||
|
display: block;
|
||||||
|
background:khaki;
|
||||||
|
padding:0.5em 0em 1em 1em;
|
||||||
|
|
||||||
|
padding-right:0.5em;
|
||||||
|
margin-left:0.5em;
|
||||||
|
background:lightgoldenrodyellow;
|
||||||
|
margin-top: 0em;
|
||||||
|
margin-bottom: 0em;
|
||||||
|
}
|
||||||
|
form.form label {
|
||||||
|
width:100%;
|
||||||
|
display: block;
|
||||||
|
background:khaki;
|
||||||
|
padding:1em 0 0.5em 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .submit {
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .indent {
|
||||||
|
padding: 0px;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form label span {
|
||||||
|
display: block;
|
||||||
|
color:black;
|
||||||
|
font-size:12px;
|
||||||
|
float:left;
|
||||||
|
width:30%;
|
||||||
|
text-align:right;
|
||||||
|
padding:0.5em 2em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .inputtext {
|
||||||
|
padding:0.2em 0.3em 0.2em 0.3em;
|
||||||
|
background:lightgoldenrodyellow;
|
||||||
|
border-bottom: 1px double #171717;
|
||||||
|
border-top: 1px double #171717;
|
||||||
|
border-left:1px double #333333;
|
||||||
|
border-right:1px double #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .checkbox { text-align: left;}
|
||||||
|
|
||||||
|
form.form .inputtextnowidth {
|
||||||
|
padding:0.5em 0.5em 0em 0em;
|
||||||
|
background:lightgoldenrodyellow;
|
||||||
|
border-bottom: 1px double #171717;
|
||||||
|
border-top: 1px double #171717;
|
||||||
|
border-left:1px double #333333;
|
||||||
|
border-right:1px double #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .textbox{
|
||||||
|
padding:7px 7px;
|
||||||
|
width:60%;
|
||||||
|
background:lightgoldenrodyellow;
|
||||||
|
border-bottom: 1px double #171717;
|
||||||
|
border-top: 1px double #171717;
|
||||||
|
border-left:1px double #333333;
|
||||||
|
border-right:1px double #333333;
|
||||||
|
overflow:hidden;
|
||||||
|
height:150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.form .button
|
||||||
|
{
|
||||||
|
margin:0 0 10px 0;
|
||||||
|
padding:4px 7px;
|
||||||
|
background:goldenrod;
|
||||||
|
border:0px;
|
||||||
|
position: relative;
|
||||||
|
top:10px;
|
||||||
|
width:100px;
|
||||||
|
border-bottom: 1px double Goldenrod;
|
||||||
|
border-top: 1px double Gold;
|
||||||
|
border-left:1px double Goldenrod;
|
||||||
|
border-right:1px double Gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err {color:red;}
|
||||||
76
util.py
Normal file
76
util.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import sys
|
||||||
|
import cherrypy
|
||||||
|
import cfg
|
||||||
|
import sqlite3
|
||||||
|
from filedict import FileDict
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 += "<br />%s" % text
|
||||||
|
|
||||||
|
def page_template(template='base', **kwargs):
|
||||||
|
for k in ['sidebar_left', 'sidebar_right', 'main', 'js', 'onload', 'nav', 'css']:
|
||||||
|
if not k in kwargs:
|
||||||
|
kwargs[k] = ''
|
||||||
|
|
||||||
|
if template=='base' and kwargs['sidebar_right']=='':
|
||||||
|
template='two_col'
|
||||||
|
if isinstance(template, basestring):
|
||||||
|
exec ("from templates.%s import %s as template" % (template, template))
|
||||||
|
try:
|
||||||
|
submenu = cfg.main_menu.active_item().encode("sub_menu", render_subs=True)
|
||||||
|
except AttributeError:
|
||||||
|
submenu = ""
|
||||||
|
|
||||||
|
kwargs['template'] = template
|
||||||
|
kwargs['main_menu_js'] = cfg.main_menu.encode("main_menu")
|
||||||
|
kwargs['sub_menu_js'] = submenu
|
||||||
|
kwargs['current_url'] = cherrypy.url()
|
||||||
|
kwargs['username'] = cherrypy.session.get(cfg.session_key)
|
||||||
|
|
||||||
|
if not kwargs['nav']: kwargs['nav'] = """ <SCRIPT LANGUAGE="JavaScript">
|
||||||
|
<!--
|
||||||
|
side_menu(sub_menu_items);
|
||||||
|
// -->
|
||||||
|
</SCRIPT>"""
|
||||||
|
|
||||||
|
return str(template(searchList=[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)
|
||||||
Loading…
x
Reference in New Issue
Block a user