summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--module/config/default.conf2
-rw-r--r--module/lib/bottle.py1934
-rw-r--r--module/lib/wsgiserver/LICENSE.txt25
-rw-r--r--module/lib/wsgiserver/__init__.py1794
-rw-r--r--module/setup.py46
-rw-r--r--module/web/ServerThread.py282
-rw-r--r--module/web/cnl_app.py151
-rw-r--r--module/web/createsuperuser.py43
-rw-r--r--module/web/filters.py63
-rw-r--r--module/web/json_app.py322
-rw-r--r--module/web/locale/cs/LC_MESSAGES/django.mobin0 -> 7652 bytes
-rwxr-xr-xmodule/web/manage.py13
-rw-r--r--module/web/middlewares.py124
-rw-r--r--module/web/pyload_app.py483
-rw-r--r--module/web/run_fcgi.py170
-rwxr-xr-xmodule/web/run_server.py90
-rw-r--r--module/web/settings.py165
-rw-r--r--module/web/syncdb.py152
-rw-r--r--module/web/syncdb_django11.py154
-rw-r--r--module/web/templates/jinja/default/base.html317
-rw-r--r--module/web/templates/jinja/default/captcha.html35
-rw-r--r--module/web/templates/jinja/default/collector.html84
-rw-r--r--module/web/templates/jinja/default/downloads.html50
-rw-r--r--module/web/templates/jinja/default/edit_package.html40
-rw-r--r--module/web/templates/jinja/default/home.html241
-rw-r--r--module/web/templates/jinja/default/login.html35
-rw-r--r--module/web/templates/jinja/default/logout.html9
-rw-r--r--module/web/templates/jinja/default/logs.html61
-rw-r--r--module/web/templates/jinja/default/package_ui.js408
-rw-r--r--module/web/templates/jinja/default/pathchooser.html76
-rw-r--r--module/web/templates/jinja/default/queue.html85
-rw-r--r--module/web/templates/jinja/default/settings.html232
-rw-r--r--module/web/templates/jinja/default/test.html12
-rw-r--r--module/web/templates/jinja/default/window.html45
-rw-r--r--module/web/urls.py26
-rw-r--r--module/web/utils.py86
-rw-r--r--module/web/webinterface.py151
-rw-r--r--systemCheck.py116
38 files changed, 7006 insertions, 1116 deletions
diff --git a/module/config/default.conf b/module/config/default.conf
index 7d7b84854..dfa58608b 100644
--- a/module/config/default.conf
+++ b/module/config/default.conf
@@ -11,7 +11,7 @@ ssl - "SSL":
file key : "SSL Key" = ssl.key
webinterface - "Webinterface":
bool activated : "Activated" = True
- builtin;lighttpd;nginx;fastcgi server : "Server" = builtin
+ builtin;threaded;fastcgi server : "Server" = builtin
bool https : "Use HTTPS" = False
ip host : "IP" = 0.0.0.0
int port : "Port" = 8001
diff --git a/module/lib/bottle.py b/module/lib/bottle.py
new file mode 100644
index 000000000..8f2be9e81
--- /dev/null
+++ b/module/lib/bottle.py
@@ -0,0 +1,1934 @@
+# -*- coding: utf-8 -*-
+"""
+Bottle is a fast and simple micro-framework for small web applications. It
+offers request dispatching (Routes) with url parameter support, templates,
+a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
+template engines - all in a single file and with no dependencies other than the
+Python Standard Library.
+
+Homepage and documentation: http://bottle.paws.de/
+
+Licence (MIT)
+-------------
+
+ Copyright (c) 2009, Marcel Hellkamp.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+
+Example
+-------
+
+This is an example::
+
+ from bottle import route, run, request, response, static_file, abort
+
+ @route('/')
+ def hello_world():
+ return 'Hello World!'
+
+ @route('/hello/:name')
+ def hello_name(name):
+ return 'Hello %s!' % name
+
+ @route('/hello', method='POST')
+ def hello_post():
+ name = request.POST['name']
+ return 'Hello %s!' % name
+
+ @route('/static/:filename#.*#')
+ def static(filename):
+ return static_file(filename, root='/path/to/static/files/')
+
+ run(host='localhost', port=8080)
+"""
+
+from __future__ import with_statement
+
+__author__ = 'Marcel Hellkamp'
+__version__ = '0.8.5'
+__license__ = 'MIT'
+
+import base64
+import cgi
+import email.utils
+import functools
+import hmac
+import inspect
+import itertools
+import mimetypes
+import os
+import re
+import subprocess
+import sys
+import thread
+import threading
+import time
+import tokenize
+import tempfile
+
+from Cookie import SimpleCookie
+from tempfile import TemporaryFile
+from traceback import format_exc
+from urllib import quote as urlquote
+from urlparse import urlunsplit, urljoin
+
+try:
+ from collections import MutableMapping as DictMixin
+except ImportError: # pragma: no cover
+ from UserDict import DictMixin
+
+try:
+ from urlparse import parse_qs
+except ImportError: # pragma: no cover
+ from cgi import parse_qs
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: no cover
+ import pickle
+
+try:
+ try:
+ from json import dumps as json_dumps
+ except ImportError: # pragma: no cover
+ from simplejson import dumps as json_dumps
+except ImportError: # pragma: no cover
+ json_dumps = None
+
+if sys.version_info >= (3,0,0): # pragma: no cover
+ # See Request.POST
+ from io import BytesIO
+ from io import TextIOWrapper
+ class NCTextIOWrapper(TextIOWrapper):
+ ''' Garbage collecting an io.TextIOWrapper(buffer) instance closes the
+ wrapped buffer. This subclass keeps it open. '''
+ def close(self): pass
+ StringType = bytes
+ def touni(x, enc='utf8'): # Convert anything to unicode (py3)
+ return str(x, encoding=enc) if isinstance(x, bytes) else str(x)
+else:
+ from StringIO import StringIO as BytesIO
+ from types import StringType
+ NCTextIOWrapper = None
+ def touni(x, enc='utf8'): # Convert anything to unicode (py2)
+ return x if isinstance(x, unicode) else unicode(str(x), encoding=enc)
+
+def tob(data, enc='utf8'): # Convert strings to bytes (py2 and py3)
+ return data.encode(enc) if isinstance(data, unicode) else data
+
+# Background compatibility
+import warnings
+def depr(message, critical=False):
+ if critical: raise DeprecationWarning(message)
+ warnings.warn(message, DeprecationWarning, stacklevel=3)
+
+
+
+
+
+
+# Exceptions and Events
+
+class BottleException(Exception):
+ """ A base class for exceptions used by bottle. """
+ pass
+
+
+class HTTPResponse(BottleException):
+ """ Used to break execution and immediately finish the response """
+ def __init__(self, output='', status=200, header=None):
+ super(BottleException, self).__init__("HTTP Response %d" % status)
+ self.status = int(status)
+ self.output = output
+ self.headers = HeaderDict(header) if header else None
+
+ def apply(self, response):
+ if self.headers:
+ for key, value in self.headers.iterallitems():
+ response.headers[key] = value
+ response.status = self.status
+
+
+class HTTPError(HTTPResponse):
+ """ Used to generate an error page """
+ def __init__(self, code=500, output='Unknown Error', exception=None, traceback=None, header=None):
+ super(HTTPError, self).__init__(output, code, header)
+ self.exception = exception
+ self.traceback = traceback
+
+ def __repr__(self):
+ return ''.join(ERROR_PAGE_TEMPLATE.render(e=self))
+
+
+
+
+
+
+# Routing
+
+class RouteError(BottleException):
+ """ This is a base class for all routing related exceptions """
+
+
+class RouteSyntaxError(RouteError):
+ """ The route parser found something not supported by this router """
+
+
+class RouteBuildError(RouteError):
+ """ The route could not been build """
+
+
+class Route(object):
+ ''' Represents a single route and can parse the dynamic route syntax '''
+ syntax = re.compile(r'(.*?)(?<!\\):([a-zA-Z_]+)?(?:#(.*?)#)?')
+ default = '[^/]+'
+
+ def __init__(self, route, target=None, name=None, static=False):
+ """ Create a Route. The route string may contain `:key`,
+ `:key#regexp#` or `:#regexp#` tokens for each dynamic part of the
+ route. These can be escaped with a backslash infront of the `:`
+ and are compleately ignored if static is true. A name may be used
+ to refer to this route later (depends on Router)
+ """
+ self.route = route
+ self.target = target
+ self.name = name
+ if static:
+ self.route = self.route.replace(':','\\:')
+ self._tokens = None
+
+ def tokens(self):
+ """ Return a list of (type, value) tokens. """
+ if not self._tokens:
+ self._tokens = list(self.tokenise(self.route))
+ return self._tokens
+
+ @classmethod
+ def tokenise(cls, route):
+ ''' Split a string into an iterator of (type, value) tokens. '''
+ match = None
+ for match in cls.syntax.finditer(route):
+ pre, name, rex = match.groups()
+ if pre: yield ('TXT', pre.replace('\\:',':'))
+ if rex and name: yield ('VAR', (rex, name))
+ elif name: yield ('VAR', (cls.default, name))
+ elif rex: yield ('ANON', rex)
+ if not match:
+ yield ('TXT', route.replace('\\:',':'))
+ elif match.end() < len(route):
+ yield ('TXT', route[match.end():].replace('\\:',':'))
+
+ def group_re(self):
+ ''' Return a regexp pattern with named groups '''
+ out = ''
+ for token, data in self.tokens():
+ if token == 'TXT': out += re.escape(data)
+ elif token == 'VAR': out += '(?P<%s>%s)' % (data[1], data[0])
+ elif token == 'ANON': out += '(?:%s)' % data
+ return out
+
+ def flat_re(self):
+ ''' Return a regexp pattern with non-grouping parentheses '''
+ rf = lambda m: m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
+ return re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', rf, self.group_re())
+
+ def format_str(self):
+ ''' Return a format string with named fields. '''
+ out, i = '', 0
+ for token, value in self.tokens():
+ if token == 'TXT': out += value.replace('%','%%')
+ elif token == 'ANON': out += '%%(anon%d)s' % i; i+=1
+ elif token == 'VAR': out += '%%(%s)s' % value[1]
+ return out
+
+ @property
+ def static(self):
+ return not self.is_dynamic()
+
+ def is_dynamic(self):
+ ''' Return true if the route contains dynamic parts '''
+ for token, value in self.tokens():
+ if token != 'TXT':
+ return True
+ return False
+
+ def __repr__(self):
+ return "<Route(%s) />" % repr(self.route)
+
+ def __eq__(self, other):
+ return self.route == other.route
+
+class Router(object):
+ ''' A route associates a string (e.g. URL) with an object (e.g. function)
+ Some dynamic routes may extract parts of the string and provide them as
+ a dictionary. This router matches a string against multiple routes and
+ returns the associated object along with the extracted data.
+ '''
+
+ def __init__(self):
+ self.routes = [] # List of all installed routes
+ self.named = {} # Cache for named routes and their format strings
+ self.static = {} # Cache for static routes
+ self.dynamic = [] # Search structure for dynamic routes
+
+ def add(self, route, target=None, **ka):
+ """ Add a route->target pair or a :class:`Route` object to the Router.
+ Return the Route object. See :class:`Route` for details.
+ """
+ if not isinstance(route, Route):
+ route = Route(route, target, **ka)
+ if self.get_route(route):
+ return RouteError('Route %s is not uniqe.' % route)
+ self.routes.append(route)
+ return route
+
+ def get_route(self, route, target=None, **ka):
+ ''' Get a route from the router by specifying either the same
+ parameters as in :meth:`add` or comparing to an instance of
+ :class:`Route`. Note that not all parameters are considered by the
+ compare function. '''
+ if not isinstance(route, Route):
+ route = Route(route, **ka)
+ for known in self.routes:
+ if route == known:
+ return known
+ return None
+
+ def match(self, uri):
+ ''' Match an URI and return a (target, urlargs) tuple '''
+ if uri in self.static:
+ return self.static[uri], {}
+ for combined, subroutes in self.dynamic:
+ match = combined.match(uri)
+ if not match: continue
+ target, args_re = subroutes[match.lastindex - 1]
+ args = args_re.match(uri).groupdict() if args_re else {}
+ return target, args
+ return None, {}
+
+ def build(self, _name, **args):
+ ''' Build an URI out of a named route and values for te wildcards. '''
+ try:
+ return self.named[_name] % args
+ except KeyError:
+ raise RouteBuildError("No route found with name '%s'." % _name)
+
+ def compile(self):
+ ''' Build the search structures. Call this before actually using the
+ router.'''
+ self.named = {}
+ self.static = {}
+ self.dynamic = []
+ for route in self.routes:
+ if route.name:
+ self.named[route.name] = route.format_str()
+ if route.static:
+ self.static[route.route] = route.target
+ continue
+ gpatt = route.group_re()
+ fpatt = route.flat_re()
+ try:
+ gregexp = re.compile('^(%s)$' % gpatt) if '(?P' in gpatt else None
+ combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, fpatt)
+ self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
+ self.dynamic[-1][1].append((route.target, gregexp))
+ except (AssertionError, IndexError), e: # AssertionError: Too many groups
+ self.dynamic.append((re.compile('(^%s$)'%fpatt),[(route.target, gregexp)]))
+ except re.error, e:
+ raise RouteSyntaxError("Could not add Route: %s (%s)" % (route, e))
+
+ def __eq__(self, other):
+ return self.routes == other.routes
+
+
+
+
+
+# WSGI abstraction: Application, Request and Response objects
+
+class Bottle(object):
+ """ WSGI application """
+
+ def __init__(self, catchall=True, autojson=True, config=None):
+ """ Create a new bottle instance.
+ You usually don't do that. Use `bottle.app.push()` instead.
+ """
+ self.routes = Router()
+ self.mounts = {}
+ self.error_handler = {}
+ self.catchall = catchall
+ self.config = config or {}
+ self.serve = True
+ self.castfilter = []
+ if autojson and json_dumps:
+ self.add_filter(dict, dict2json)
+
+ def optimize(self, *a, **ka):
+ depr("Bottle.optimize() is obsolete.")
+
+ def mount(self, app, script_path):
+ ''' Mount a Bottle application to a specific URL prefix '''
+ if not isinstance(app, Bottle):
+ raise TypeError('Only Bottle instances are supported for now.')
+ script_path = '/'.join(filter(None, script_path.split('/')))
+ path_depth = script_path.count('/') + 1
+ if not script_path:
+ raise TypeError('Empty script_path. Perhaps you want a merge()?')
+ for other in self.mounts:
+ if other.startswith(script_path):
+ raise TypeError('Conflict with existing mount: %s' % other)
+ @self.route('/%s/:#.*#' % script_path, method="ANY")
+ def mountpoint():
+ request.path_shift(path_depth)
+ return app.handle(request.path, request.method)
+ self.mounts[script_path] = app
+
+ def add_filter(self, ftype, func):
+ ''' Register a new output filter. Whenever bottle hits a handler output
+ matching `ftype`, `func` is applied to it. '''
+ if not isinstance(ftype, type):
+ raise TypeError("Expected type object, got %s" % type(ftype))
+ self.castfilter = [(t, f) for (t, f) in self.castfilter if t != ftype]
+ self.castfilter.append((ftype, func))
+ self.castfilter.sort()
+
+ def match_url(self, path, method='GET'):
+ """ Find a callback bound to a path and a specific HTTP method.
+ Return (callback, param) tuple or raise HTTPError.
+ method: HEAD falls back to GET. All methods fall back to ANY.
+ """
+ path, method = path.strip().lstrip('/'), method.upper()
+ callbacks, args = self.routes.match(path)
+ if not callbacks:
+ raise HTTPError(404, "Not found: " + path)
+ if method in callbacks:
+ return callbacks[method], args
+ if method == 'HEAD' and 'GET' in callbacks:
+ return callbacks['GET'], args
+ if 'ANY' in callbacks:
+ return callbacks['ANY'], args
+ allow = [m for m in callbacks if m != 'ANY']
+ if 'GET' in allow and 'HEAD' not in allow:
+ allow.append('HEAD')
+ raise HTTPError(405, "Method not allowed.",
+ header=[('Allow',",".join(allow))])
+
+ def get_url(self, routename, **kargs):
+ """ Return a string that matches a named route """
+ scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
+ location = self.routes.build(routename, **kargs).lstrip('/')
+ return urljoin(urljoin('/', scriptname), location)
+
+ def route(self, path=None, method='GET', **kargs):
+ """ Decorator: bind a function to a GET request path.
+
+ If the path parameter is None, the signature of the decorated
+ function is used to generate the paths. See yieldroutes()
+ for details.
+
+ The method parameter (default: GET) specifies the HTTP request
+ method to listen to. You can specify a list of methods too.
+ """
+ def wrapper(callback):
+ routes = [path] if path else yieldroutes(callback)
+ methods = method.split(';') if isinstance(method, str) else method
+ for r in routes:
+ for m in methods:
+ r, m = r.strip().lstrip('/'), m.strip().upper()
+ old = self.routes.get_route(r, **kargs)
+ if old:
+ old.target[m] = callback
+ else:
+ self.routes.add(r, {m: callback}, **kargs)
+ self.routes.compile()
+ return callback
+ return wrapper
+
+ def get(self, path=None, method='GET', **kargs):
+ """ Decorator: Bind a function to a GET request path.
+ See :meth:'route' for details. """
+ return self.route(path, method, **kargs)
+
+ def post(self, path=None, method='POST', **kargs):
+ """ Decorator: Bind a function to a POST request path.
+ See :meth:'route' for details. """
+ return self.route(path, method, **kargs)
+
+ def put(self, path=None, method='PUT', **kargs):
+ """ Decorator: Bind a function to a PUT request path.
+ See :meth:'route' for details. """
+ return self.route(path, method, **kargs)
+
+ def delete(self, path=None, method='DELETE', **kargs):
+ """ Decorator: Bind a function to a DELETE request path.
+ See :meth:'route' for details. """
+ return self.route(path, method, **kargs)
+
+ def error(self, code=500):
+ """ Decorator: Registrer an output handler for a HTTP error code"""
+ def wrapper(handler):
+ self.error_handler[int(code)] = handler
+ return handler
+ return wrapper
+
+ def handle(self, url, method):
+ """ Execute the handler bound to the specified url and method and return
+ its output. If catchall is true, exceptions are catched and returned as
+ HTTPError(500) objects. """
+ if not self.serve:
+ return HTTPError(503, "Server stopped")
+ try:
+ handler, args = self.match_url(url, method)
+ return handler(**args)
+ except HTTPResponse, e:
+ return e
+ except Exception, e:
+ if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
+ or not self.catchall:
+ raise
+ return HTTPError(500, 'Unhandled exception', e, format_exc(10))
+
+ def _cast(self, out, request, response, peek=None):
+ """ Try to convert the parameter into something WSGI compatible and set
+ correct HTTP headers when possible.
+ Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
+ iterable of strings and iterable of unicodes
+ """
+ # Filtered types (recursive, because they may return anything)
+ for testtype, filterfunc in self.castfilter:
+ if isinstance(out, testtype):
+ return self._cast(filterfunc(out), request, response)
+
+ # Empty output is done here
+ if not out:
+ response.headers['Content-Length'] = 0
+ return []
+ # Join lists of byte or unicode strings. Mixed lists are NOT supported
+ if isinstance(out, (tuple, list))\
+ and isinstance(out[0], (StringType, unicode)):
+ out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
+ # Encode unicode strings
+ if isinstance(out, unicode):
+ out = out.encode(response.charset)
+ # Byte Strings are just returned
+ if isinstance(out, StringType):
+ response.headers['Content-Length'] = str(len(out))
+ return [out]
+ # HTTPError or HTTPException (recursive, because they may wrap anything)
+ if isinstance(out, HTTPError):
+ out.apply(response)
+ return self._cast(self.error_handler.get(out.status, repr)(out), request, response)
+ if isinstance(out, HTTPResponse):
+ out.apply(response)
+ return self._cast(out.output, request, response)
+
+ # File-like objects.
+ if hasattr(out, 'read'):
+ if 'wsgi.file_wrapper' in request.environ:
+ return request.environ['wsgi.file_wrapper'](out)
+ elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
+ return WSGIFileWrapper(out)
+
+ # Handle Iterables. We peek into them to detect their inner type.
+ try:
+ out = iter(out)
+ first = out.next()
+ while not first:
+ first = out.next()
+ except StopIteration:
+ return self._cast('', request, response)
+ except HTTPResponse, e:
+ first = e
+ except Exception, e:
+ first = HTTPError(500, 'Unhandled exception', e, format_exc(10))
+ if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
+ or not self.catchall:
+ raise
+ # These are the inner types allowed in iterator or generator objects.
+ if isinstance(first, HTTPResponse):
+ return self._cast(first, request, response)
+ if isinstance(first, StringType):
+ return itertools.chain([first], out)
+ if isinstance(first, unicode):
+ return itertools.imap(lambda x: x.encode(response.charset),
+ itertools.chain([first], out))
+ return self._cast(HTTPError(500, 'Unsupported response type: %s'\
+ % type(first)), request, response)
+
+ def __call__(self, environ, start_response):
+ """ The bottle WSGI-interface. """
+ try:
+ environ['bottle.app'] = self
+ request.bind(environ)
+ response.bind(self)
+ out = self.handle(request.path, request.method)
+ out = self._cast(out, request, response)
+ # rfc2616 section 4.3
+ if response.status in (100, 101, 204, 304) or request.method == 'HEAD':
+ out = []
+ status = '%d %s' % (response.status, HTTP_CODES[response.status])
+ start_response(status, response.headerlist)
+ return out
+ except (KeyboardInterrupt, SystemExit, MemoryError):
+ raise
+ except Exception, e:
+ if not self.catchall:
+ raise
+ err = '<h1>Critical error while processing request: %s</h1>' \
+ % environ.get('PATH_INFO', '/')
+ if DEBUG:
+ err += '<h2>Error:</h2>\n<pre>%s</pre>\n' % repr(e)
+ err += '<h2>Traceback:</h2>\n<pre>%s</pre>\n' % format_exc(10)
+ environ['wsgi.errors'].write(err) #TODO: wsgi.error should not get html
+ start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html')])
+ return [tob(err)]
+
+
+class Request(threading.local, DictMixin):
+ """ Represents a single HTTP request using thread-local attributes.
+ The Request object wraps a WSGI environment and can be used as such.
+ """
+ def __init__(self, environ=None, config=None):
+ """ Create a new Request instance.
+
+ You usually don't do this but use the global `bottle.request`
+ instance instead.
+ """
+ self.bind(environ or {}, config)
+
+ def bind(self, environ, config=None):
+ """ Bind a new WSGI enviroment.
+
+ This is done automatically for the global `bottle.request`
+ instance on every request.
+ """
+ self.environ = environ
+ self.config = config or {}
+ # These attributes are used anyway, so it is ok to compute them here
+ self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/')
+ self.method = environ.get('REQUEST_METHOD', 'GET').upper()
+
+ @property
+ def _environ(self):
+ depr("Request._environ renamed to Request.environ")
+ return self.environ
+
+ def copy(self):
+ ''' Returns a copy of self '''
+ return Request(self.environ.copy(), self.config)
+
+ def path_shift(self, shift=1):
+ ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
+
+ :param shift: The number of path fragments to shift. May be negative to
+ change the shift direction. (default: 1)
+ '''
+ script_name = self.environ.get('SCRIPT_NAME','/')
+ self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift)
+ self['PATH_INFO'] = self.path
+
+ def __getitem__(self, key): return self.environ[key]
+ def __delitem__(self, key): self[key] = ""; del(self.environ[key])
+ def __iter__(self): return iter(self.environ)
+ def __len__(self): return len(self.environ)
+ def keys(self): return self.environ.keys()
+ def __setitem__(self, key, value):
+ """ Shortcut for Request.environ.__setitem__ """
+ self.environ[key] = value
+ todelete = []
+ if key in ('PATH_INFO','REQUEST_METHOD'):
+ self.bind(self.environ, self.config)
+ elif key == 'wsgi.input': todelete = ('body','forms','files','params')
+ elif key == 'QUERY_STRING': todelete = ('get','params')
+ elif key.startswith('HTTP_'): todelete = ('headers', 'cookies')
+ for key in todelete:
+ if 'bottle.' + key in self.environ:
+ del self.environ['bottle.' + key]
+
+ @property
+ def query_string(self):
+ """ The content of the QUERY_STRING environment variable. """
+ return self.environ.get('QUERY_STRING', '')
+
+ @property
+ def fullpath(self):
+ """ Request path including SCRIPT_NAME (if present) """
+ return self.environ.get('SCRIPT_NAME', '').rstrip('/') + self.path
+
+ @property
+ def url(self):
+ """ Full URL as requested by the client (computed).
+
+ This value is constructed out of different environment variables
+ and includes scheme, host, port, scriptname, path and query string.
+ """
+ scheme = self.environ.get('wsgi.url_scheme', 'http')
+ host = self.environ.get('HTTP_X_FORWARDED_HOST', self.environ.get('HTTP_HOST', None))
+ if not host:
+ host = self.environ.get('SERVER_NAME')
+ port = self.environ.get('SERVER_PORT', '80')
+ if scheme + port not in ('https443', 'http80'):
+ host += ':' + port
+ parts = (scheme, host, urlquote(self.fullpath), self.query_string, '')
+ return urlunsplit(parts)
+
+ @property
+ def content_length(self):
+ """ Content-Length header as an integer, -1 if not specified """
+ return int(self.environ.get('CONTENT_LENGTH','') or -1)
+
+ @property
+ def header(self):
+ ''' :class:`HeaderDict` filled with request headers.
+
+ HeaderDict keys are case insensitive str.title()d
+ '''
+ if 'bottle.headers' not in self.environ:
+ header = self.environ['bottle.headers'] = HeaderDict()
+ for key, value in self.environ.iteritems():
+ if key.startswith('HTTP_'):
+ key = key[5:].replace('_','-').title()
+ header[key] = value
+ return self.environ['bottle.headers']
+
+ @property
+ def GET(self):
+ """ The QUERY_STRING parsed into a MultiDict.
+
+ Keys and values are strings. Multiple values per key are possible.
+ See MultiDict for details.
+ """
+ if 'bottle.get' not in self.environ:
+ data = parse_qs(self.query_string, keep_blank_values=True)
+ get = self.environ['bottle.get'] = MultiDict()
+ for key, values in data.iteritems():
+ for value in values:
+ get[key] = value
+ return self.environ['bottle.get']
+
+ @property
+ def POST(self):
+ """ Property: The HTTP POST body parsed into a MultiDict.
+
+ This supports urlencoded and multipart POST requests. Multipart
+ is commonly used for file uploads and may result in some of the
+ values being cgi.FieldStorage objects instead of strings.
+
+ Multiple values per key are possible. See MultiDict for details.
+ """
+ if 'bottle.post' not in self.environ:
+ self.environ['bottle.post'] = MultiDict()
+ self.environ['bottle.forms'] = MultiDict()
+ self.environ['bottle.files'] = MultiDict()
+ safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi
+ for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
+ if key in self.environ: safe_env[key] = self.environ[key]
+ if NCTextIOWrapper:
+ fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n')
+ # TODO: Content-Length may be wrong now. Does cgi.FieldStorage
+ # use it at all? I think not, because all tests pass.
+ else:
+ fb = self.body
+ data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True)
+ for item in data.list or []:
+ if item.filename:
+ self.environ['bottle.post'][item.name] = item
+ self.environ['bottle.files'][item.name] = item
+ else:
+ self.environ['bottle.post'][item.name] = item.value
+ self.environ['bottle.forms'][item.name] = item.value
+ return self.environ['bottle.post']
+
+ @property
+ def forms(self):
+ """ Property: HTTP POST form data parsed into a MultiDict. """
+ if 'bottle.forms' not in self.environ: self.POST
+ return self.environ['bottle.forms']
+
+ @property
+ def files(self):
+ """ Property: HTTP POST file uploads parsed into a MultiDict. """
+ if 'bottle.files' not in self.environ: self.POST
+ return self.environ['bottle.files']
+
+ @property
+ def params(self):
+ """ A combined MultiDict with POST and GET parameters. """
+ if 'bottle.params' not in self.environ:
+ self.environ['bottle.params'] = MultiDict(self.GET)
+ self.environ['bottle.params'].update(dict(self.forms))
+ return self.environ['bottle.params']
+
+ @property
+ def body(self):
+ """ The HTTP request body as a seekable buffer object.
+
+ This property returns a copy of the `wsgi.input` stream and should
+ be used instead of `environ['wsgi.input']`.
+ """
+ if 'bottle.body' not in self.environ:
+ maxread = max(0, self.content_length)
+ stream = self.environ['wsgi.input']
+ body = BytesIO() if maxread < MEMFILE_MAX else TemporaryFile(mode='w+b')
+ while maxread > 0:
+ part = stream.read(min(maxread, MEMFILE_MAX))
+ if not part: #TODO: Wrong content_length. Error? Do nothing?
+ break
+ body.write(part)
+ maxread -= len(part)
+ self.environ['wsgi.input'] = body
+ self.environ['bottle.body'] = body
+ self.environ['bottle.body'].seek(0)
+ return self.environ['bottle.body']
+
+ @property
+ def auth(self): #TODO: Tests and docs. Add support for digest. namedtuple?
+ """ HTTP authorisation data as a (user, passwd) tuple. (experimental)
+
+ This implementation currently only supports basic auth and returns
+ None on errors.
+ """
+ return parse_auth(self.environ.get('HTTP_AUTHORIZATION',''))
+
+ @property
+ def COOKIES(self):
+ """ Cookie information parsed into a dictionary.
+
+ Secure cookies are NOT decoded automatically. See
+ Request.get_cookie() for details.
+ """
+ if 'bottle.cookies' not in self.environ:
+ raw_dict = SimpleCookie(self.environ.get('HTTP_COOKIE',''))
+ self.environ['bottle.cookies'] = {}
+ for cookie in raw_dict.itervalues():
+ self.environ['bottle.cookies'][cookie.key] = cookie.value
+ return self.environ['bottle.cookies']
+
+ def get_cookie(self, name, secret=None):
+ """ Return the (decoded) value of a cookie. """
+ value = self.COOKIES.get(name)
+ dec = cookie_decode(value, secret) if secret else None
+ return dec or value
+
+ @property
+ def is_ajax(self):
+ ''' True if the request was generated using XMLHttpRequest '''
+ #TODO: write tests
+ return self.header.get('X-Requested-With') == 'XMLHttpRequest'
+
+
+
+class Response(threading.local):
+ """ Represents a single HTTP response using thread-local attributes.
+ """
+
+ def __init__(self, config=None):
+ self.bind(config)
+
+ def bind(self, config=None):
+ """ Resets the Response object to its factory defaults. """
+ self._COOKIES = None
+ self.status = 200
+ self.headers = HeaderDict()
+ self.content_type = 'text/html; charset=UTF-8'
+ self.config = config or {}
+
+ @property
+ def header(self):
+ depr("Response.header renamed to Response.headers")
+ return self.headers
+
+ def copy(self):
+ ''' Returns a copy of self '''
+ copy = Response(self.config)
+ copy.status = self.status
+ copy.headers = self.headers.copy()
+ copy.content_type = self.content_type
+ return copy
+
+ def wsgiheader(self):
+ ''' Returns a wsgi conform list of header/value pairs. '''
+ for c in self.COOKIES.values():
+ if c.OutputString() not in self.headers.getall('Set-Cookie'):
+ self.headers.append('Set-Cookie', c.OutputString())
+ # rfc2616 section 10.2.3, 10.3.5
+ if self.status in (204, 304) and 'content-type' in self.headers:
+ del self.headers['content-type']
+ if self.status == 304:
+ for h in ('allow', 'content-encoding', 'content-language',
+ 'content-length', 'content-md5', 'content-range',
+ 'content-type', 'last-modified'): # + c-location, expires?
+ if h in self.headers:
+ del self.headers[h]
+ return list(self.headers.iterallitems())
+ headerlist = property(wsgiheader)
+
+ @property
+ def charset(self):
+ """ Return the charset specified in the content-type header.
+
+ This defaults to `UTF-8`.
+ """
+ if 'charset=' in self.content_type:
+ return self.content_type.split('charset=')[-1].split(';')[0].strip()
+ return 'UTF-8'
+
+ @property
+ def COOKIES(self):
+ """ A dict-like SimpleCookie instance. Use Response.set_cookie() instead. """
+ if not self._COOKIES:
+ self._COOKIES = SimpleCookie()
+ return self._COOKIES
+
+ def set_cookie(self, key, value, secret=None, **kargs):
+ """ Add a new cookie with various options.
+
+ If the cookie value is not a string, a secure cookie is created.
+
+ Possible options are:
+ expires, path, comment, domain, max_age, secure, version, httponly
+ See http://de.wikipedia.org/wiki/HTTP-Cookie#Aufbau for details
+ """
+ if not isinstance(value, basestring):
+ if not secret:
+ raise TypeError('Cookies must be strings when secret is not set')
+ value = cookie_encode(value, secret).decode('ascii') #2to3 hack
+ self.COOKIES[key] = value
+ for k, v in kargs.iteritems():
+ self.COOKIES[key][k.replace('_', '-')] = v
+
+ def get_content_type(self):
+ """ Current 'Content-Type' header. """
+ return self.headers['Content-Type']
+
+ def set_content_type(self, value):
+ self.headers['Content-Type'] = value
+
+ content_type = property(get_content_type, set_content_type, None,
+ get_content_type.__doc__)
+
+
+
+
+
+
+# Data Structures
+
+class MultiDict(DictMixin):
+ """ A dict that remembers old values for each key """
+ # collections.MutableMapping would be better for Python >= 2.6
+ def __init__(self, *a, **k):
+ self.dict = dict()
+ for k, v in dict(*a, **k).iteritems():
+ self[k] = v
+
+ def __len__(self): return len(self.dict)
+ def __iter__(self): return iter(self.dict)
+ def __contains__(self, key): return key in self.dict
+ def __delitem__(self, key): del self.dict[key]
+ def keys(self): return self.dict.keys()
+ def __getitem__(self, key): return self.get(key, KeyError, -1)
+ def __setitem__(self, key, value): self.append(key, value)
+
+ def append(self, key, value): self.dict.setdefault(key, []).append(value)
+ def replace(self, key, value): self.dict[key] = [value]
+ def getall(self, key): return self.dict.get(key) or []
+
+ def get(self, key, default=None, index=-1):
+ if key not in self.dict and default != KeyError:
+ return [default][index]
+ return self.dict[key][index]
+
+ def iterallitems(self):
+ for key, values in self.dict.iteritems():
+ for value in values:
+ yield key, value
+
+
+class HeaderDict(MultiDict):
+ """ Same as :class:`MultiDict`, but title()s the keys and overwrites by default. """
+ def __contains__(self, key): return MultiDict.__contains__(self, self.httpkey(key))
+ def __getitem__(self, key): return MultiDict.__getitem__(self, self.httpkey(key))
+ def __delitem__(self, key): return MultiDict.__delitem__(self, self.httpkey(key))
+ def __setitem__(self, key, value): self.replace(key, value)
+ def get(self, key, default=None, index=-1): return MultiDict.get(self, self.httpkey(key), default, index)
+ def append(self, key, value): return MultiDict.append(self, self.httpkey(key), str(value))
+ def replace(self, key, value): return MultiDict.replace(self, self.httpkey(key), str(value))
+ def getall(self, key): return MultiDict.getall(self, self.httpkey(key))
+ def httpkey(self, key): return str(key).replace('_','-').title()
+
+
+class AppStack(list):
+ """ A stack implementation. """
+
+ def __call__(self):
+ """ Return the current default app. """
+ return self[-1]
+
+ def push(self, value=None):
+ """ Add a new Bottle instance to the stack """
+ if not isinstance(value, Bottle):
+ value = Bottle()
+ self.append(value)
+ return value
+
+class WSGIFileWrapper(object):
+
+ def __init__(self, fp, buffer_size=1024*64):
+ self.fp, self.buffer_size = fp, buffer_size
+ for attr in ('fileno', 'close', 'read', 'readlines'):
+ if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr))
+
+ def __iter__(self):
+ read, buff = self.fp.read, self.buffer_size
+ while True:
+ part = read(buff)
+ if not part: break
+ yield part
+
+
+
+# Module level functions
+
+# Output filter
+
+def dict2json(d):
+ response.content_type = 'application/json'
+ return json_dumps(d)
+
+
+def abort(code=500, text='Unknown Error: Appliction stopped.'):
+ """ Aborts execution and causes a HTTP error. """
+ raise HTTPError(code, text)
+
+
+def redirect(url, code=303):
+ """ Aborts execution and causes a 303 redirect """
+ scriptname = request.environ.get('SCRIPT_NAME', '').rstrip('/') + '/'
+ location = urljoin(request.url, urljoin(scriptname, url))
+ raise HTTPResponse("", status=code, header=dict(Location=location))
+
+
+def send_file(*a, **k): #BC 0.6.4
+ """ Raises the output of static_file(). (deprecated) """
+ raise static_file(*a, **k)
+
+
+def static_file(filename, root, guessmime=True, mimetype=None, download=False):
+ """ Opens a file in a safe way and returns a HTTPError object with status
+ code 200, 305, 401 or 404. Sets Content-Type, Content-Length and
+ Last-Modified header. Obeys If-Modified-Since header and HEAD requests.
+ """
+ root = os.path.abspath(root) + os.sep
+ filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
+ header = dict()
+
+ if not filename.startswith(root):
+ return HTTPError(403, "Access denied.")
+ if not os.path.exists(filename) or not os.path.isfile(filename):
+ return HTTPError(404, "File does not exist.")
+ if not os.access(filename, os.R_OK):
+ return HTTPError(403, "You do not have permission to access this file.")
+
+ if not mimetype and guessmime:
+ header['Content-Type'] = mimetypes.guess_type(filename)[0]
+ else:
+ header['Content-Type'] = mimetype if mimetype else 'text/plain'
+
+ if download == True:
+ download = os.path.basename(filename)
+ if download:
+ header['Content-Disposition'] = 'attachment; filename="%s"' % download
+
+ stats = os.stat(filename)
+ lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
+ header['Last-Modified'] = lm
+ ims = request.environ.get('HTTP_IF_MODIFIED_SINCE')
+ if ims:
+ ims = ims.split(";")[0].strip() # IE sends "<date>; length=146"
+ ims = parse_date(ims)
+ if ims is not None and ims >= int(stats.st_mtime):
+ header['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
+ return HTTPResponse(status=304, header=header)
+ header['Content-Length'] = stats.st_size
+ if request.method == 'HEAD':
+ return HTTPResponse('', header=header)
+ else:
+ return HTTPResponse(open(filename, 'rb'), header=header)
+
+
+
+
+
+
+# Utilities
+
+def debug(mode=True):
+ """ Change the debug level.
+ There is only one debug level supported at the moment."""
+ global DEBUG
+ DEBUG = bool(mode)
+
+
+def parse_date(ims):
+ """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
+ try:
+ ts = email.utils.parsedate_tz(ims)
+ return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone
+ except (TypeError, ValueError, IndexError):
+ return None
+
+
+def parse_auth(header):
+ """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
+ try:
+ method, data = header.split(None, 1)
+ if method.lower() == 'basic':
+ name, pwd = base64.b64decode(data).split(':', 1)
+ return name, pwd
+ except (KeyError, ValueError, TypeError):
+ return None
+
+
+def _lscmp(a, b):
+ ''' Compares two strings in a cryptographically save way:
+ Runtime is not affected by a common prefix. '''
+ return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b)
+
+
+def cookie_encode(data, key):
+ ''' Encode and sign a pickle-able object. Return a string '''
+ msg = base64.b64encode(pickle.dumps(data, -1))
+ sig = base64.b64encode(hmac.new(key, msg).digest())
+ return tob('!') + sig + tob('?') + msg
+
+
+def cookie_decode(data, key):
+ ''' Verify and decode an encoded string. Return an object or None'''
+ data = tob(data)
+ if cookie_is_encoded(data):
+ sig, msg = data.split(tob('?'), 1)
+ if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())):
+ return pickle.loads(base64.b64decode(msg))
+ return None
+
+
+def cookie_is_encoded(data):
+ ''' Return True if the argument looks like a encoded cookie.'''
+ return bool(data.startswith(tob('!')) and tob('?') in data)
+
+
+def tonativefunc(enc='utf-8'):
+ ''' Returns a function that turns everything into 'native' strings using enc '''
+ if sys.version_info >= (3,0,0):
+ return lambda x: x.decode(enc) if isinstance(x, bytes) else str(x)
+ return lambda x: x.encode(enc) if isinstance(x, unicode) else str(x)
+
+
+def yieldroutes(func):
+ """ Return a generator for routes that match the signature (name, args)
+ of the func parameter. This may yield more than one route if the function
+ takes optional keyword arguments. The output is best described by example:
+ a() -> '/a'
+ b(x, y) -> '/b/:x/:y'
+ c(x, y=5) -> '/c/:x' and '/c/:x/:y'
+ d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y'
+ """
+ path = func.__name__.replace('__','/').lstrip('/')
+ spec = inspect.getargspec(func)
+ argc = len(spec[0]) - len(spec[3] or [])
+ path += ('/:%s' * argc) % tuple(spec[0][:argc])
+ yield path
+ for arg in spec[0][argc:]:
+ path += '/:%s' % arg
+ yield path
+
+def path_shift(script_name, path_info, shift=1):
+ ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
+
+ :return: The modified paths.
+ :param script_name: The SCRIPT_NAME path.
+ :param script_name: The PATH_INFO path.
+ :param shift: The number of path fragments to shift. May be negative to
+ change ths shift direction. (default: 1)
+ '''
+ if shift == 0: return script_name, path_info
+ pathlist = path_info.strip('/').split('/')
+ scriptlist = script_name.strip('/').split('/')
+ if pathlist and pathlist[0] == '': pathlist = []
+ if scriptlist and scriptlist[0] == '': scriptlist = []
+ if shift > 0 and shift <= len(pathlist):
+ moved = pathlist[:shift]
+ scriptlist = scriptlist + moved
+ pathlist = pathlist[shift:]
+ elif shift < 0 and shift >= -len(scriptlist):
+ moved = scriptlist[shift:]
+ pathlist = moved + pathlist
+ scriptlist = scriptlist[:shift]
+ else:
+ empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO'
+ raise AssertionError("Cannot shift. Nothing left from %s" % empty)
+ new_script_name = '/' + '/'.join(scriptlist)
+ new_path_info = '/' + '/'.join(pathlist)
+ if path_info.endswith('/') and pathlist: new_path_info += '/'
+ return new_script_name, new_path_info
+
+
+
+
+# Decorators
+#TODO: Replace default_app() with app()
+
+def validate(**vkargs):
+ """
+ Validates and manipulates keyword arguments by user defined callables.
+ Handles ValueError and missing arguments by raising HTTPError(403).
+ """
+ def decorator(func):
+ def wrapper(**kargs):
+ for key, value in vkargs.iteritems():
+ if key not in kargs:
+ abort(403, 'Missing parameter: %s' % key)
+ try:
+ kargs[key] = value(kargs[key])
+ except ValueError:
+ abort(403, 'Wrong parameter format for: %s' % key)
+ return func(**kargs)
+ return wrapper
+ return decorator
+
+
+route = functools.wraps(Bottle.route)(lambda *a, **ka: app().route(*a, **ka))
+get = functools.wraps(Bottle.get)(lambda *a, **ka: app().get(*a, **ka))
+post = functools.wraps(Bottle.post)(lambda *a, **ka: app().post(*a, **ka))
+put = functools.wraps(Bottle.put)(lambda *a, **ka: app().put(*a, **ka))
+delete = functools.wraps(Bottle.delete)(lambda *a, **ka: app().delete(*a, **ka))
+error = functools.wraps(Bottle.error)(lambda *a, **ka: app().error(*a, **ka))
+url = functools.wraps(Bottle.get_url)(lambda *a, **ka: app().get_url(*a, **ka))
+mount = functools.wraps(Bottle.mount)(lambda *a, **ka: app().mount(*a, **ka))
+
+def default():
+ depr("The default() decorator is deprecated. Use @error(404) instead.")
+ return error(404)
+
+
+
+
+
+
+# Server adapter
+
+class ServerAdapter(object):
+ quiet = False
+
+ def __init__(self, host='127.0.0.1', port=8080, **kargs):
+ self.options = kargs
+ self.host = host
+ self.port = int(port)
+
+ def run(self, handler): # pragma: no cover
+ pass
+
+ def __repr__(self):
+ args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()])
+ return "%s(%s)" % (self.__class__.__name__, args)
+
+
+class CGIServer(ServerAdapter):
+ quiet = True
+ def run(self, handler): # pragma: no cover
+ from wsgiref.handlers import CGIHandler
+ CGIHandler().run(handler) # Just ignore host and port here
+
+
+class FlupFCGIServer(ServerAdapter):
+ def run(self, handler): # pragma: no cover
+ import flup.server.fcgi
+ flup.server.fcgi.WSGIServer(handler, bindAddress=(self.host, self.port)).run()
+
+
+class WSGIRefServer(ServerAdapter):
+ def run(self, handler): # pragma: no cover
+ from wsgiref.simple_server import make_server, WSGIRequestHandler
+ if self.quiet:
+ class QuietHandler(WSGIRequestHandler):
+ def log_request(*args, **kw): pass
+ self.options['handler_class'] = QuietHandler
+ srv = make_server(self.host, self.port, handler, **self.options)
+ srv.serve_forever()
+
+
+class CherryPyServer(ServerAdapter):
+ def run(self, handler): # pragma: no cover
+ from cherrypy import wsgiserver
+ server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler)
+ server.start()
+
+
+class PasteServer(ServerAdapter):
+ def run(self, handler): # pragma: no cover
+ from paste import httpserver
+ from paste.translogger import TransLogger
+ app = TransLogger(handler)
+ httpserver.serve(app, host=self.host, port=str(self.port), **self.options)
+
+
+class FapwsServer(ServerAdapter):
+ """
+ Extremly fast webserver using libev.
+ See http://william-os4y.livejournal.com/
+ """
+ def run(self, handler): # pragma: no cover
+ import fapws._evwsgi as evwsgi
+ from fapws import base
+ evwsgi.start(self.host, self.port)
+ evwsgi.set_base_module(base)
+ def app(environ, start_response):
+ environ['wsgi.multiprocess'] = False
+ return handler(environ, start_response)
+ evwsgi.wsgi_cb(('',app))
+ evwsgi.run()
+
+
+class TornadoServer(ServerAdapter):
+ """ Untested. As described here:
+ http://github.com/facebook/tornado/blob/master/tornado/wsgi.py#L187 """
+ def run(self, handler): # pragma: no cover
+ import tornado.wsgi
+ import tornado.httpserver
+ import tornado.ioloop
+ container = tornado.wsgi.WSGIContainer(handler)
+ server = tornado.httpserver.HTTPServer(container)
+ server.listen(port=self.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+class AppEngineServer(ServerAdapter):
+ """ Untested. """
+ quiet = True
+ def run(self, handler):
+ from google.appengine.ext.webapp import util
+ util.run_wsgi_app(handler)
+
+
+class TwistedServer(ServerAdapter):
+ """ Untested. """
+ def run(self, handler):
+ from twisted.web import server, wsgi
+ from twisted.python.threadpool import ThreadPool
+ from twisted.internet import reactor
+ thread_pool = ThreadPool()
+ thread_pool.start()
+ reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
+ factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
+ reactor.listenTCP(self.port, factory, interface=self.host)
+ reactor.run()
+
+
+class DieselServer(ServerAdapter):
+ """ Untested. """
+ def run(self, handler):
+ from diesel.protocols.wsgi import WSGIApplication
+ app = WSGIApplication(handler, port=self.port)
+ app.run()
+
+
+class GunicornServer(ServerAdapter):
+ """ Untested. """
+ def run(self, handler):
+ import gunicorn.arbiter
+ gunicorn.arbiter.Arbiter((self.host, self.port), 4, handler).run()
+
+
+class EventletServer(ServerAdapter):
+ """ Untested """
+ def run(self, handler):
+ from eventlet import wsgi, listen
+ wsgi.server(listen((self.host, self.port)), handler)
+
+
+class RocketServer(ServerAdapter):
+ """ Untested. As requested in issue 63
+ http://github.com/defnull/bottle/issues/#issue/63 """
+ def run(self, handler):
+ from rocket import Rocket
+ server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler })
+ server.start()
+
+
+class AutoServer(ServerAdapter):
+ """ Untested. """
+ adapters = [CherryPyServer, PasteServer, TwistedServer, WSGIRefServer]
+ def run(self, handler):
+ for sa in self.adapters:
+ try:
+ return sa(self.host, self.port, **self.options).run(handler)
+ except ImportError:
+ pass
+
+
+def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080,
+ interval=1, reloader=False, quiet=False, **kargs):
+ """ Runs bottle as a web server. """
+ app = app if app else default_app()
+ # Instantiate server, if it is a class instead of an instance
+ if isinstance(server, type):
+ server = server(host=host, port=port, **kargs)
+ if not isinstance(server, ServerAdapter):
+ raise RuntimeError("Server must be a subclass of WSGIAdapter")
+ server.quiet = server.quiet or quiet
+ if not server.quiet and not os.environ.get('BOTTLE_CHILD'):
+ print "Bottle server starting up (using %s)..." % repr(server)
+ print "Listening on http://%s:%d/" % (server.host, server.port)
+ print "Use Ctrl-C to quit."
+ print
+ try:
+ if reloader:
+ interval = min(interval, 1)
+ if os.environ.get('BOTTLE_CHILD'):
+ _reloader_child(server, app, interval)
+ else:
+ _reloader_observer(server, app, interval)
+ else:
+ server.run(app)
+ except KeyboardInterrupt: pass
+ if not server.quiet and not os.environ.get('BOTTLE_CHILD'):
+ print "Shutting down..."
+
+
+class FileCheckerThread(threading.Thread):
+ ''' Thread that periodically checks for changed module files. '''
+
+ def __init__(self, lockfile, interval):
+ threading.Thread.__init__(self)
+ self.lockfile, self.interval = lockfile, interval
+ #1: lockfile to old; 2: lockfile missing
+ #3: module file changed; 5: external exit
+ self.status = 0
+
+ def run(self):
+ exists = os.path.exists
+ mtime = lambda path: os.stat(path).st_mtime
+ files = dict()
+ for module in sys.modules.values():
+ try:
+ path = inspect.getsourcefile(module)
+ if path and exists(path): files[path] = mtime(path)
+ except TypeError: pass
+ while not self.status:
+ for path, lmtime in files.iteritems():
+ if not exists(path) or mtime(path) > lmtime:
+ self.status = 3
+ if not exists(self.lockfile):
+ self.status = 2
+ elif mtime(self.lockfile) < time.time() - self.interval - 5:
+ self.status = 1
+ if not self.status:
+ time.sleep(self.interval)
+ if self.status != 5:
+ thread.interrupt_main()
+
+
+def _reloader_child(server, app, interval):
+ ''' Start the server and check for modified files in a background thread.
+ As soon as an update is detected, KeyboardInterrupt is thrown in
+ the main thread to exit the server loop. The process exists with status
+ code 3 to request a reload by the observer process. If the lockfile
+ is not modified in 2*interval second or missing, we assume that the
+ observer process died and exit with status code 1 or 2.
+ '''
+ lockfile = os.environ.get('BOTTLE_LOCKFILE')
+ bgcheck = FileCheckerThread(lockfile, interval)
+ try:
+ bgcheck.start()
+ server.run(app)
+ except KeyboardInterrupt, e: pass
+ bgcheck.status, status = 5, bgcheck.status
+ bgcheck.join() # bgcheck.status == 5 --> silent exit
+ if status: sys.exit(status)
+
+
+def _reloader_observer(server, app, interval):
+ ''' Start a child process with identical commandline arguments and restart
+ it as long as it exists with status code 3. Also create a lockfile and
+ touch it (update mtime) every interval seconds.
+ '''
+ fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock')
+ os.close(fd) # We only need this file to exist. We never write to it
+ try:
+ while os.path.exists(lockfile):
+ args = [sys.executable] + sys.argv
+ environ = os.environ.copy()
+ environ['BOTTLE_CHILD'] = 'true'
+ environ['BOTTLE_LOCKFILE'] = lockfile
+ p = subprocess.Popen(args, env=environ)
+ while p.poll() is None: # Busy wait...
+ os.utime(lockfile, None) # I am alive!
+ time.sleep(interval)
+ if p.poll() != 3:
+ if os.path.exists(lockfile): os.unlink(lockfile)
+ sys.exit(p.poll())
+ elif not server.quiet:
+ print "Reloading server..."
+ except KeyboardInterrupt: pass
+ if os.path.exists(lockfile): os.unlink(lockfile)
+
+
+
+# Templates
+
+class TemplateError(HTTPError):
+ def __init__(self, message):
+ HTTPError.__init__(self, 500, message)
+
+
+class BaseTemplate(object):
+ """ Base class and minimal API for template adapters """
+ extentions = ['tpl','html','thtml','stpl']
+ settings = {} #used in prepare()
+ defaults = {} #used in render()
+
+ def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings):
+ """ Create a new template.
+ If the source parameter (str or buffer) is missing, the name argument
+ is used to guess a template filename. Subclasses can assume that
+ self.source and/or self.filename are set. Both are strings.
+ The lookup, encoding and settings parameters are stored as instance
+ variables.
+ The lookup parameter stores a list containing directory paths.
+ The encoding parameter should be used to decode byte strings or files.
+ The settings parameter contains a dict for engine-specific settings.
+ """
+ self.name = name
+ self.source = source.read() if hasattr(source, 'read') else source
+ self.filename = source.filename if hasattr(source, 'filename') else None
+ self.lookup = map(os.path.abspath, lookup)
+ self.encoding = encoding
+ self.settings = self.settings.copy() # Copy from class variable
+ self.settings.update(settings) # Apply
+ if not self.source and self.name:
+ self.filename = self.search(self.name, self.lookup)
+ if not self.filename:
+ raise TemplateError('Template %s not found.' % repr(name))
+ if not self.source and not self.filename:
+ raise TemplateError('No template specified.')
+ self.prepare(**self.settings)
+
+ @classmethod
+ def search(cls, name, lookup=[]):
+ """ Search name in all directories specified in lookup.
+ First without, then with common extensions. Return first hit. """
+ if os.path.isfile(name): return name
+ for spath in lookup:
+ fname = os.path.join(spath, name)
+ if os.path.isfile(fname):
+ return fname
+ for ext in cls.extentions:
+ if os.path.isfile('%s.%s' % (fname, ext)):
+ return '%s.%s' % (fname, ext)
+
+ @classmethod
+ def global_config(cls, key, *args):
+ ''' This reads or sets the global settings stored in class.settings. '''
+ if args:
+ cls.settings[key] = args[0]
+ else:
+ return cls.settings[key]
+
+ def prepare(self, **options):
+ """ Run preparations (parsing, caching, ...).
+ It should be possible to call this again to refresh a template or to
+ update settings.
+ """
+ raise NotImplementedError
+
+ def render(self, **args):
+ """ Render the template with the specified local variables and return
+ a single byte or unicode string. If it is a byte string, the encoding
+ must match self.encoding. This method must be thread-safe!
+ """
+ raise NotImplementedError
+
+
+class MakoTemplate(BaseTemplate):
+ def prepare(self, **options):
+ from mako.template import Template
+ from mako.lookup import TemplateLookup
+ options.update({'input_encoding':self.encoding})
+ #TODO: This is a hack... http://github.com/defnull/bottle/issues#issue/8
+ mylookup = TemplateLookup(directories=['.']+self.lookup, **options)
+ if self.source:
+ self.tpl = Template(self.source, lookup=mylookup)
+ else: #mako cannot guess extentions. We can, but only at top level...
+ name = self.name
+ if not os.path.splitext(name)[1]:
+ name += os.path.splitext(self.filename)[1]
+ self.tpl = mylookup.get_template(name)
+
+ def render(self, **args):
+ _defaults = self.defaults.copy()
+ _defaults.update(args)
+ return self.tpl.render(**_defaults)
+
+
+class CheetahTemplate(BaseTemplate):
+ def prepare(self, **options):
+ from Cheetah.Template import Template
+ self.context = threading.local()
+ self.context.vars = {}
+ options['searchList'] = [self.context.vars]
+ if self.source:
+ self.tpl = Template(source=self.source, **options)
+ else:
+ self.tpl = Template(file=self.filename, **options)
+
+ def render(self, **args):
+ self.context.vars.update(self.defaults)
+ self.context.vars.update(args)
+ out = str(self.tpl)
+ self.context.vars.clear()
+ return [out]
+
+
+class Jinja2Template(BaseTemplate):
+ def prepare(self, filters=None, tests=None, **kwargs):
+ from jinja2 import Environment, FunctionLoader
+ if 'prefix' in kwargs: # TODO: to be removed after a while
+ raise RuntimeError('The keyword argument `prefix` has been removed. '
+ 'Use the full jinja2 environment name line_statement_prefix instead.')
+ self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
+ if filters: self.env.filters.update(filters)
+ if tests: self.env.tests.update(tests)
+ if self.source:
+ self.tpl = self.env.from_string(self.source)
+ else:
+ self.tpl = self.env.get_template(self.filename)
+
+ def render(self, **args):
+ _defaults = self.defaults.copy()
+ _defaults.update(args)
+ return self.tpl.render(**_defaults).encode("utf-8")
+
+ def loader(self, name):
+ fname = self.search(name, self.lookup)
+ if fname:
+ with open(fname, "rb") as f:
+ return f.read().decode(self.encoding)
+
+
+class SimpleTemplate(BaseTemplate):
+ blocks = ('if','elif','else','try','except','finally','for','while','with','def','class')
+ dedent_blocks = ('elif', 'else', 'except', 'finally')
+
+ def prepare(self, escape_func=cgi.escape, noescape=False):
+ self.cache = {}
+ if self.source:
+ self.code = self.translate(self.source)
+ self.co = compile(self.code, '<string>', 'exec')
+ else:
+ self.code = self.translate(open(self.filename).read())
+ self.co = compile(self.code, self.filename, 'exec')
+ enc = self.encoding
+ self._str = lambda x: touni(x, enc)
+ self._escape = lambda x: escape_func(touni(x, enc))
+ if noescape:
+ self._str, self._escape = self._escape, self._str
+
+ def translate(self, template):
+ stack = [] # Current Code indentation
+ lineno = 0 # Current line of code
+ ptrbuffer = [] # Buffer for printable strings and token tuple instances
+ codebuffer = [] # Buffer for generated python code
+ touni = functools.partial(unicode, encoding=self.encoding)
+ multiline = dedent = False
+
+ def yield_tokens(line):
+ for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)):
+ if i % 2:
+ if part.startswith('!'): yield 'RAW', part[1:]
+ else: yield 'CMD', part
+ else: yield 'TXT', part
+
+ def split_comment(codeline):
+ """ Removes comments from a line of code. """
+ line = codeline.splitlines()[0]
+ try:
+ tokens = list(tokenize.generate_tokens(iter(line).next))
+ except tokenize.TokenError:
+ return line.rsplit('#',1) if '#' in line else (line, '')
+ for token in tokens:
+ if token[0] == tokenize.COMMENT:
+ start, end = token[2][1], token[3][1]
+ return codeline[:start] + codeline[end:], codeline[start:end]
+ return line, ''
+
+ def flush(): # Flush the ptrbuffer
+ if not ptrbuffer: return
+ cline = ''
+ for line in ptrbuffer:
+ for token, value in line:
+ if token == 'TXT': cline += repr(value)
+ elif token == 'RAW': cline += '_str(%s)' % value
+ elif token == 'CMD': cline += '_escape(%s)' % value
+ cline += ', '
+ cline = cline[:-2] + '\\\n'
+ cline = cline[:-2]
+ if cline[:-1].endswith('\\\\\\\\\\n'):
+ cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr'
+ cline = '_printlist([' + cline + '])'
+ del ptrbuffer[:] # Do this before calling code() again
+ code(cline)
+
+ def code(stmt):
+ for line in stmt.splitlines():
+ codebuffer.append(' ' * len(stack) + line.strip())
+
+ for line in template.splitlines(True):
+ lineno += 1
+ line = line if isinstance(line, unicode)\
+ else unicode(line, encoding=self.encoding)
+ if lineno <= 2:
+ m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line)
+ if m: self.encoding = m.group(1)
+ if m: line = line.replace('coding','coding (removed)')
+ if line.strip()[:2].count('%') == 1:
+ line = line.split('%',1)[1].lstrip() # Full line following the %
+ cline = split_comment(line)[0].strip()
+ cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0]
+ flush() ##encodig (TODO: why?)
+ if cmd in self.blocks or multiline:
+ cmd = multiline or cmd
+ dedent = cmd in self.dedent_blocks # "else:"
+ if dedent and not oneline and not multiline:
+ cmd = stack.pop()
+ code(line)
+ oneline = not cline.endswith(':') # "if 1: pass"
+ multiline = cmd if cline.endswith('\\') else False
+ if not oneline and not multiline:
+ stack.append(cmd)
+ elif cmd == 'end' and stack:
+ code('#end(%s) %s' % (stack.pop(), line.strip()[3:]))
+ elif cmd == 'include':
+ p = cline.split(None, 2)[1:]
+ if len(p) == 2:
+ code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1]))
+ elif p:
+ code("_=_include(%s, _stdout)" % repr(p[0]))
+ else: # Empty %include -> reverse of %rebase
+ code("_printlist(_base)")
+ elif cmd == 'rebase':
+ p = cline.split(None, 2)[1:]
+ if len(p) == 2:
+ code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1]))
+ elif p:
+ code("globals()['_rebase']=(%s, {})" % repr(p[0]))
+ else:
+ code(line)
+ else: # Line starting with text (not '%') or '%%' (escaped)
+ if line.strip().startswith('%%'):
+ line = line.replace('%%', '%', 1)
+ ptrbuffer.append(yield_tokens(line))
+ flush()
+ return '\n'.join(codebuffer) + '\n'
+
+ def subtemplate(self, _name, _stdout, **args):
+ if _name not in self.cache:
+ self.cache[_name] = self.__class__(name=_name, lookup=self.lookup)
+ return self.cache[_name].execute(_stdout, **args)
+
+ def execute(self, _stdout, **args):
+ env = self.defaults.copy()
+ env.update({'_stdout': _stdout, '_printlist': _stdout.extend,
+ '_include': self.subtemplate, '_str': self._str,
+ '_escape': self._escape})
+ env.update(args)
+ eval(self.co, env)
+ if '_rebase' in env:
+ subtpl, rargs = env['_rebase']
+ subtpl = self.__class__(name=subtpl, lookup=self.lookup)
+ rargs['_base'] = _stdout[:] #copy stdout
+ del _stdout[:] # clear stdout
+ return subtpl.execute(_stdout, **rargs)
+ return env
+
+ def render(self, **args):
+ """ Render the template using keyword arguments as local variables. """
+ stdout = []
+ self.execute(stdout, **args)
+ return ''.join(stdout)
+
+
+def template(tpl, template_adapter=SimpleTemplate, **kwargs):
+ '''
+ Get a rendered template as a string iterator.
+ You can use a name, a filename or a template string as first parameter.
+ '''
+ if tpl not in TEMPLATES or DEBUG:
+ settings = kwargs.get('template_settings',{})
+ lookup = kwargs.get('template_lookup', TEMPLATE_PATH)
+ if isinstance(tpl, template_adapter):
+ TEMPLATES[tpl] = tpl
+ if settings: TEMPLATES[tpl].prepare(**settings)
+ elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
+ TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings)
+ else:
+ TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings)
+ if not TEMPLATES[tpl]:
+ abort(500, 'Template (%s) not found' % tpl)
+ return TEMPLATES[tpl].render(**kwargs)
+
+mako_template = functools.partial(template, template_adapter=MakoTemplate)
+cheetah_template = functools.partial(template, template_adapter=CheetahTemplate)
+jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
+
+def view(tpl_name, **defaults):
+ ''' Decorator: renders a template for a handler.
+ The handler can control its behavior like that:
+
+ - return a dict of template vars to fill out the template
+ - return something other than a dict and the view decorator will not
+ process the template, but return the handler result as is.
+ This includes returning a HTTPResponse(dict) to get,
+ for instance, JSON with autojson or other castfilters
+ '''
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ result = func(*args, **kwargs)
+ if isinstance(result, (dict, DictMixin)):
+ tplvars = defaults.copy()
+ tplvars.update(result)
+ return template(tpl_name, **tplvars)
+ return result
+ return wrapper
+ return decorator
+
+mako_view = functools.partial(view, template_adapter=MakoTemplate)
+cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
+jinja2_view = functools.partial(view, template_adapter=Jinja2Template)
+
+
+
+
+
+
+# Modul initialization and configuration
+
+TEMPLATE_PATH = ['./', './views/']
+TEMPLATES = {}
+DEBUG = False
+MEMFILE_MAX = 1024*100
+HTTP_CODES = {
+ 100: 'CONTINUE',
+ 101: 'SWITCHING PROTOCOLS',
+ 200: 'OK',
+ 201: 'CREATED',
+ 202: 'ACCEPTED',
+ 203: 'NON-AUTHORITATIVE INFORMATION',
+ 204: 'NO CONTENT',
+ 205: 'RESET CONTENT',
+ 206: 'PARTIAL CONTENT',
+ 300: 'MULTIPLE CHOICES',
+ 301: 'MOVED PERMANENTLY',
+ 302: 'FOUND',
+ 303: 'SEE OTHER',
+ 304: 'NOT MODIFIED',
+ 305: 'USE PROXY',
+ 306: 'RESERVED',
+ 307: 'TEMPORARY REDIRECT',
+ 400: 'BAD REQUEST',
+ 401: 'UNAUTHORIZED',
+ 402: 'PAYMENT REQUIRED',
+ 403: 'FORBIDDEN',
+ 404: 'NOT FOUND',
+ 405: 'METHOD NOT ALLOWED',
+ 406: 'NOT ACCEPTABLE',
+ 407: 'PROXY AUTHENTICATION REQUIRED',
+ 408: 'REQUEST TIMEOUT',
+ 409: 'CONFLICT',
+ 410: 'GONE',
+ 411: 'LENGTH REQUIRED',
+ 412: 'PRECONDITION FAILED',
+ 413: 'REQUEST ENTITY TOO LARGE',
+ 414: 'REQUEST-URI TOO LONG',
+ 415: 'UNSUPPORTED MEDIA TYPE',
+ 416: 'REQUESTED RANGE NOT SATISFIABLE',
+ 417: 'EXPECTATION FAILED',
+ 500: 'INTERNAL SERVER ERROR',
+ 501: 'NOT IMPLEMENTED',
+ 502: 'BAD GATEWAY',
+ 503: 'SERVICE UNAVAILABLE',
+ 504: 'GATEWAY TIMEOUT',
+ 505: 'HTTP VERSION NOT SUPPORTED',
+}
+""" A dict of known HTTP error and status codes """
+
+
+
+ERROR_PAGE_TEMPLATE = SimpleTemplate("""
+%try:
+ %from bottle import DEBUG, HTTP_CODES, request
+ %status_name = HTTP_CODES.get(e.status, 'Unknown').title()
+ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+ <html>
+ <head>
+ <title>Error {{e.status}}: {{status_name}}</title>
+ <style type="text/css">
+ html {background-color: #eee; font-family: sans;}
+ body {background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 15px;}
+ pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
+ </style>
+ </head>
+ <body>
+ <h1>Error {{e.status}}: {{status_name}}</h1>
+ <p>Sorry, the requested URL <tt>{{request.url}}</tt> caused an error:</p>
+ <pre>{{str(e.output)}}</pre>
+ %if DEBUG and e.exception:
+ <h2>Exception:</h2>
+ <pre>{{repr(e.exception)}}</pre>
+ %end
+ %if DEBUG and e.traceback:
+ <h2>Traceback:</h2>
+ <pre>{{e.traceback}}</pre>
+ %end
+ </body>
+ </html>
+%except ImportError:
+ <b>ImportError:</b> Could not generate the error page. Please add bottle to sys.path
+%end
+""")
+""" The HTML template used for error messages """
+
+request = Request()
+""" Whenever a page is requested, the :class:`Bottle` WSGI handler stores
+metadata about the current request into this instance of :class:`Request`.
+It is thread-safe and can be accessed from within handler functions. """
+
+response = Response()
+""" The :class:`Bottle` WSGI handler uses metadata assigned to this instance
+of :class:`Response` to generate the WSGI response. """
+
+local = threading.local()
+""" Thread-local namespace. Not used by Bottle, but could get handy """
+
+# Initialize app stack (create first empty Bottle app)
+# BC: 0.6.4 and needed for run()
+app = default_app = AppStack()
+app.push()
diff --git a/module/lib/wsgiserver/LICENSE.txt b/module/lib/wsgiserver/LICENSE.txt
new file mode 100644
index 000000000..a15165ee2
--- /dev/null
+++ b/module/lib/wsgiserver/LICENSE.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2004-2007, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the CherryPy Team nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/module/lib/wsgiserver/__init__.py b/module/lib/wsgiserver/__init__.py
new file mode 100644
index 000000000..c380e18b0
--- /dev/null
+++ b/module/lib/wsgiserver/__init__.py
@@ -0,0 +1,1794 @@
+"""A high-speed, production ready, thread pooled, generic WSGI server.
+
+Simplest example on how to use this module directly
+(without using CherryPy's application machinery):
+
+ from cherrypy import wsgiserver
+
+ def my_crazy_app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type','text/plain')]
+ start_response(status, response_headers)
+ return ['Hello world!\n']
+
+ server = wsgiserver.CherryPyWSGIServer(
+ ('0.0.0.0', 8070), my_crazy_app,
+ server_name='www.cherrypy.example')
+
+The CherryPy WSGI server can serve as many WSGI applications
+as you want in one instance by using a WSGIPathInfoDispatcher:
+
+ d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
+ server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
+
+Want SSL support? Just set these attributes:
+
+ server.ssl_certificate = <filename>
+ server.ssl_private_key = <filename>
+
+ if __name__ == '__main__':
+ try:
+ server.start()
+ except KeyboardInterrupt:
+ server.stop()
+
+This won't call the CherryPy engine (application side) at all, only the
+WSGI server, which is independant from the rest of CherryPy. Don't
+let the name "CherryPyWSGIServer" throw you; the name merely reflects
+its origin, not its coupling.
+
+For those of you wanting to understand internals of this module, here's the
+basic call flow. The server's listening thread runs a very tight loop,
+sticking incoming connections onto a Queue:
+
+ server = CherryPyWSGIServer(...)
+ server.start()
+ while True:
+ tick()
+ # This blocks until a request comes in:
+ child = socket.accept()
+ conn = HTTPConnection(child, ...)
+ server.requests.put(conn)
+
+Worker threads are kept in a pool and poll the Queue, popping off and then
+handling each connection in turn. Each connection can consist of an arbitrary
+number of requests and their responses, so we run a nested loop:
+
+ while True:
+ conn = server.requests.get()
+ conn.communicate()
+ -> while True:
+ req = HTTPRequest(...)
+ req.parse_request()
+ -> # Read the Request-Line, e.g. "GET /page HTTP/1.1"
+ req.rfile.readline()
+ req.read_headers()
+ req.respond()
+ -> response = wsgi_app(...)
+ try:
+ for chunk in response:
+ if chunk:
+ req.write(chunk)
+ finally:
+ if hasattr(response, "close"):
+ response.close()
+ if req.close_connection:
+ return
+"""
+
+
+import base64
+import os
+import Queue
+import re
+quoted_slash = re.compile("(?i)%2F")
+import rfc822
+import socket
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
+
+import sys
+import threading
+import time
+import traceback
+from urllib import unquote
+from urlparse import urlparse
+import warnings
+
+try:
+ from OpenSSL import SSL
+ from OpenSSL import crypto
+except ImportError:
+ SSL = None
+
+import errno
+
+def plat_specific_errors(*errnames):
+ """Return error numbers for all errors in errnames on this platform.
+
+ The 'errno' module contains different global constants depending on
+ the specific platform (OS). This function will return the list of
+ numeric values for a given list of potential names.
+ """
+ errno_names = dir(errno)
+ nums = [getattr(errno, k) for k in errnames if k in errno_names]
+ # de-dupe the list
+ return dict.fromkeys(nums).keys()
+
+socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
+
+socket_errors_to_ignore = plat_specific_errors(
+ "EPIPE",
+ "EBADF", "WSAEBADF",
+ "ENOTSOCK", "WSAENOTSOCK",
+ "ETIMEDOUT", "WSAETIMEDOUT",
+ "ECONNREFUSED", "WSAECONNREFUSED",
+ "ECONNRESET", "WSAECONNRESET",
+ "ECONNABORTED", "WSAECONNABORTED",
+ "ENETRESET", "WSAENETRESET",
+ "EHOSTDOWN", "EHOSTUNREACH",
+ )
+socket_errors_to_ignore.append("timed out")
+
+socket_errors_nonblocking = plat_specific_errors(
+ 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
+
+comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
+ 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
+ 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
+ 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
+ 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
+ 'WWW-AUTHENTICATE']
+
+
+class WSGIPathInfoDispatcher(object):
+ """A WSGI dispatcher for dispatch based on the PATH_INFO.
+
+ apps: a dict or list of (path_prefix, app) pairs.
+ """
+
+ def __init__(self, apps):
+ try:
+ apps = apps.items()
+ except AttributeError:
+ pass
+
+ # Sort the apps by len(path), descending
+ apps.sort()
+ apps.reverse()
+
+ # The path_prefix strings must start, but not end, with a slash.
+ # Use "" instead of "/".
+ self.apps = [(p.rstrip("/"), a) for p, a in apps]
+
+ def __call__(self, environ, start_response):
+ path = environ["PATH_INFO"] or "/"
+ for p, app in self.apps:
+ # The apps list should be sorted by length, descending.
+ if path.startswith(p + "/") or path == p:
+ environ = environ.copy()
+ environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
+ environ["PATH_INFO"] = path[len(p):]
+ return app(environ, start_response)
+
+ start_response('404 Not Found', [('Content-Type', 'text/plain'),
+ ('Content-Length', '0')])
+ return ['']
+
+
+class MaxSizeExceeded(Exception):
+ pass
+
+class SizeCheckWrapper(object):
+ """Wraps a file-like object, raising MaxSizeExceeded if too large."""
+
+ def __init__(self, rfile, maxlen):
+ self.rfile = rfile
+ self.maxlen = maxlen
+ self.bytes_read = 0
+
+ def _check_length(self):
+ if self.maxlen and self.bytes_read > self.maxlen:
+ raise MaxSizeExceeded()
+
+ def read(self, size=None):
+ data = self.rfile.read(size)
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+ def readline(self, size=None):
+ if size is not None:
+ data = self.rfile.readline(size)
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+ # User didn't specify a size ...
+ # We read the line in chunks to make sure it's not a 100MB line !
+ res = []
+ while True:
+ data = self.rfile.readline(256)
+ self.bytes_read += len(data)
+ self._check_length()
+ res.append(data)
+ # See http://www.cherrypy.org/ticket/421
+ if len(data) < 256 or data[-1:] == "\n":
+ return ''.join(res)
+
+ def readlines(self, sizehint=0):
+ # Shamelessly stolen from StringIO
+ total = 0
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline()
+ return lines
+
+ def close(self):
+ self.rfile.close()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ data = self.rfile.next()
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+
+class HTTPRequest(object):
+ """An HTTP Request (and response).
+
+ A single HTTP connection may consist of multiple request/response pairs.
+
+ send: the 'send' method from the connection's socket object.
+ wsgi_app: the WSGI application to call.
+ environ: a partial WSGI environ (server and connection entries).
+ The caller MUST set the following entries:
+ * All wsgi.* entries, including .input
+ * SERVER_NAME and SERVER_PORT
+ * Any SSL_* entries
+ * Any custom entries like REMOTE_ADDR and REMOTE_PORT
+ * SERVER_SOFTWARE: the value to write in the "Server" response header.
+ * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of
+ the response. From RFC 2145: "An HTTP server SHOULD send a
+ response version equal to the highest version for which the
+ server is at least conditionally compliant, and whose major
+ version is less than or equal to the one received in the
+ request. An HTTP server MUST NOT send a version for which
+ it is not at least conditionally compliant."
+
+ outheaders: a list of header tuples to write in the response.
+ ready: when True, the request has been parsed and is ready to begin
+ generating the response. When False, signals the calling Connection
+ that the response should not be generated and the connection should
+ close.
+ close_connection: signals the calling Connection that the request
+ should close. This does not imply an error! The client and/or
+ server may each request that the connection be closed.
+ chunked_write: if True, output will be encoded with the "chunked"
+ transfer-coding. This value is set automatically inside
+ send_headers.
+ """
+
+ max_request_header_size = 0
+ max_request_body_size = 0
+
+ def __init__(self, wfile, environ, wsgi_app):
+ self.rfile = environ['wsgi.input']
+ self.wfile = wfile
+ self.environ = environ.copy()
+ self.wsgi_app = wsgi_app
+
+ self.ready = False
+ self.started_response = False
+ self.status = ""
+ self.outheaders = []
+ self.sent_headers = False
+ self.close_connection = False
+ self.chunked_write = False
+
+ def parse_request(self):
+ """Parse the next HTTP request start-line and message-headers."""
+ self.rfile.maxlen = self.max_request_header_size
+ self.rfile.bytes_read = 0
+
+ try:
+ self._parse_request()
+ except MaxSizeExceeded:
+ self.simple_response("413 Request Entity Too Large")
+ return
+
+ def _parse_request(self):
+ # HTTP/1.1 connections are persistent by default. If a client
+ # requests a page, then idles (leaves the connection open),
+ # then rfile.readline() will raise socket.error("timed out").
+ # Note that it does this based on the value given to settimeout(),
+ # and doesn't need the client to request or acknowledge the close
+ # (although your TCP stack might suffer for it: cf Apache's history
+ # with FIN_WAIT_2).
+ request_line = self.rfile.readline()
+ if not request_line:
+ # Force self.ready = False so the connection will close.
+ self.ready = False
+ return
+
+ if request_line == "\r\n":
+ # RFC 2616 sec 4.1: "...if the server is reading the protocol
+ # stream at the beginning of a message and receives a CRLF
+ # first, it should ignore the CRLF."
+ # But only ignore one leading line! else we enable a DoS.
+ request_line = self.rfile.readline()
+ if not request_line:
+ self.ready = False
+ return
+
+ environ = self.environ
+
+ try:
+ method, path, req_protocol = request_line.strip().split(" ", 2)
+ except ValueError:
+ self.simple_response(400, "Malformed Request-Line")
+ return
+
+ environ["REQUEST_METHOD"] = method
+
+ # path may be an abs_path (including "http://host.domain.tld");
+ scheme, location, path, params, qs, frag = urlparse(path)
+
+ if frag:
+ self.simple_response("400 Bad Request",
+ "Illegal #fragment in Request-URI.")
+ return
+
+ if scheme:
+ environ["wsgi.url_scheme"] = scheme
+ if params:
+ path = path + ";" + params
+
+ environ["SCRIPT_NAME"] = ""
+
+ # Unquote the path+params (e.g. "/this%20path" -> "this path").
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
+ #
+ # But note that "...a URI must be separated into its components
+ # before the escaped characters within those components can be
+ # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
+ atoms = [unquote(x) for x in quoted_slash.split(path)]
+ path = "%2F".join(atoms)
+ environ["PATH_INFO"] = path
+
+ # Note that, like wsgiref and most other WSGI servers,
+ # we unquote the path but not the query string.
+ environ["QUERY_STRING"] = qs
+
+ # Compare request and server HTTP protocol versions, in case our
+ # server does not support the requested protocol. Limit our output
+ # to min(req, server). We want the following output:
+ # request server actual written supported response
+ # protocol protocol response protocol feature set
+ # a 1.0 1.0 1.0 1.0
+ # b 1.0 1.1 1.1 1.0
+ # c 1.1 1.0 1.0 1.0
+ # d 1.1 1.1 1.1 1.1
+ # Notice that, in (b), the response will be "HTTP/1.1" even though
+ # the client only understands 1.0. RFC 2616 10.5.6 says we should
+ # only return 505 if the _major_ version is different.
+ rp = int(req_protocol[5]), int(req_protocol[7])
+ server_protocol = environ["ACTUAL_SERVER_PROTOCOL"]
+ sp = int(server_protocol[5]), int(server_protocol[7])
+ if sp[0] != rp[0]:
+ self.simple_response("505 HTTP Version Not Supported")
+ return
+ # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
+ environ["SERVER_PROTOCOL"] = req_protocol
+ self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
+
+ # If the Request-URI was an absoluteURI, use its location atom.
+ if location:
+ environ["SERVER_NAME"] = location
+
+ # then all the http headers
+ try:
+ self.read_headers()
+ except ValueError, ex:
+ self.simple_response("400 Bad Request", repr(ex.args))
+ return
+
+ mrbs = self.max_request_body_size
+ if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs:
+ self.simple_response("413 Request Entity Too Large")
+ return
+
+ # Persistent connection support
+ if self.response_protocol == "HTTP/1.1":
+ # Both server and client are HTTP/1.1
+ if environ.get("HTTP_CONNECTION", "") == "close":
+ self.close_connection = True
+ else:
+ # Either the server or client (or both) are HTTP/1.0
+ if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
+ self.close_connection = True
+
+ # Transfer-Encoding support
+ te = None
+ if self.response_protocol == "HTTP/1.1":
+ te = environ.get("HTTP_TRANSFER_ENCODING")
+ if te:
+ te = [x.strip().lower() for x in te.split(",") if x.strip()]
+
+ self.chunked_read = False
+
+ if te:
+ for enc in te:
+ if enc == "chunked":
+ self.chunked_read = True
+ else:
+ # Note that, even if we see "chunked", we must reject
+ # if there is an extension we don't recognize.
+ self.simple_response("501 Unimplemented")
+ self.close_connection = True
+ return
+
+ # From PEP 333:
+ # "Servers and gateways that implement HTTP 1.1 must provide
+ # transparent support for HTTP 1.1's "expect/continue" mechanism.
+ # This may be done in any of several ways:
+ # 1. Respond to requests containing an Expect: 100-continue request
+ # with an immediate "100 Continue" response, and proceed normally.
+ # 2. Proceed with the request normally, but provide the application
+ # with a wsgi.input stream that will send the "100 Continue"
+ # response if/when the application first attempts to read from
+ # the input stream. The read request must then remain blocked
+ # until the client responds.
+ # 3. Wait until the client decides that the server does not support
+ # expect/continue, and sends the request body on its own.
+ # (This is suboptimal, and is not recommended.)
+ #
+ # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
+ # but it seems like it would be a big slowdown for such a rare case.
+ if environ.get("HTTP_EXPECT", "") == "100-continue":
+ self.simple_response(100)
+
+ self.ready = True
+
+ def read_headers(self):
+ """Read header lines from the incoming stream."""
+ environ = self.environ
+
+ while True:
+ line = self.rfile.readline()
+ if not line:
+ # No more data--illegal end of headers
+ raise ValueError("Illegal end of headers.")
+
+ if line == '\r\n':
+ # Normal end of headers
+ break
+
+ if line[0] in ' \t':
+ # It's a continuation line.
+ v = line.strip()
+ else:
+ k, v = line.split(":", 1)
+ k, v = k.strip().upper(), v.strip()
+ envname = "HTTP_" + k.replace("-", "_")
+
+ if k in comma_separated_headers:
+ existing = environ.get(envname)
+ if existing:
+ v = ", ".join((existing, v))
+ environ[envname] = v
+
+ ct = environ.pop("HTTP_CONTENT_TYPE", None)
+ if ct is not None:
+ environ["CONTENT_TYPE"] = ct
+ cl = environ.pop("HTTP_CONTENT_LENGTH", None)
+ if cl is not None:
+ environ["CONTENT_LENGTH"] = cl
+
+ def decode_chunked(self):
+ """Decode the 'chunked' transfer coding."""
+ cl = 0
+ data = StringIO.StringIO()
+ while True:
+ line = self.rfile.readline().strip().split(";", 1)
+ chunk_size = int(line.pop(0), 16)
+ if chunk_size <= 0:
+ break
+## if line: chunk_extension = line[0]
+ cl += chunk_size
+ data.write(self.rfile.read(chunk_size))
+ crlf = self.rfile.read(2)
+ if crlf != "\r\n":
+ self.simple_response("400 Bad Request",
+ "Bad chunked transfer coding "
+ "(expected '\\r\\n', got %r)" % crlf)
+ return
+
+ # Grab any trailer headers
+ self.read_headers()
+
+ data.seek(0)
+ self.environ["wsgi.input"] = data
+ self.environ["CONTENT_LENGTH"] = str(cl) or ""
+ return True
+
+ def respond(self):
+ """Call the appropriate WSGI app and write its iterable output."""
+ # Set rfile.maxlen to ensure we don't read past Content-Length.
+ # This will also be used to read the entire request body if errors
+ # are raised before the app can read the body.
+ if self.chunked_read:
+ # If chunked, Content-Length will be 0.
+ self.rfile.maxlen = self.max_request_body_size
+ else:
+ cl = int(self.environ.get("CONTENT_LENGTH", 0))
+ if self.max_request_body_size:
+ self.rfile.maxlen = min(cl, self.max_request_body_size)
+ else:
+ self.rfile.maxlen = cl
+ self.rfile.bytes_read = 0
+
+ try:
+ self._respond()
+ except MaxSizeExceeded:
+ if not self.sent_headers:
+ self.simple_response("413 Request Entity Too Large")
+ return
+
+ def _respond(self):
+ if self.chunked_read:
+ if not self.decode_chunked():
+ self.close_connection = True
+ return
+
+ response = self.wsgi_app(self.environ, self.start_response)
+ try:
+ for chunk in response:
+ # "The start_response callable must not actually transmit
+ # the response headers. Instead, it must store them for the
+ # server or gateway to transmit only after the first
+ # iteration of the application return value that yields
+ # a NON-EMPTY string, or upon the application's first
+ # invocation of the write() callable." (PEP 333)
+ if chunk:
+ self.write(chunk)
+ finally:
+ if hasattr(response, "close"):
+ response.close()
+
+ if (self.ready and not self.sent_headers):
+ self.sent_headers = True
+ self.send_headers()
+ if self.chunked_write:
+ self.wfile.sendall("0\r\n\r\n")
+
+ def simple_response(self, status, msg=""):
+ """Write a simple response back to the client."""
+ status = str(status)
+ buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status),
+ "Content-Length: %s\r\n" % len(msg),
+ "Content-Type: text/plain\r\n"]
+
+ if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
+ # Request Entity Too Large
+ self.close_connection = True
+ buf.append("Connection: close\r\n")
+
+ buf.append("\r\n")
+ if msg:
+ buf.append(msg)
+
+ try:
+ self.wfile.sendall("".join(buf))
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
+
+ def start_response(self, status, headers, exc_info = None):
+ """WSGI callable to begin the HTTP response."""
+ # "The application may call start_response more than once,
+ # if and only if the exc_info argument is provided."
+ if self.started_response and not exc_info:
+ raise AssertionError("WSGI start_response called a second "
+ "time with no exc_info.")
+
+ # "if exc_info is provided, and the HTTP headers have already been
+ # sent, start_response must raise an error, and should raise the
+ # exc_info tuple."
+ if self.sent_headers:
+ try:
+ raise exc_info[0], exc_info[1], exc_info[2]
+ finally:
+ exc_info = None
+
+ self.started_response = True
+ self.status = status
+ self.outheaders.extend(headers)
+ return self.write
+
+ def write(self, chunk):
+ """WSGI callable to write unbuffered data to the client.
+
+ This method is also used internally by start_response (to write
+ data from the iterable returned by the WSGI application).
+ """
+ if not self.started_response:
+ raise AssertionError("WSGI write called before start_response.")
+
+ if not self.sent_headers:
+ self.sent_headers = True
+ self.send_headers()
+
+ if self.chunked_write and chunk:
+ buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
+ self.wfile.sendall("".join(buf))
+ else:
+ self.wfile.sendall(chunk)
+
+ def send_headers(self):
+ """Assert, process, and send the HTTP response message-headers."""
+ hkeys = [key.lower() for key, value in self.outheaders]
+ status = int(self.status[:3])
+
+ if status == 413:
+ # Request Entity Too Large. Close conn to avoid garbage.
+ self.close_connection = True
+ elif "content-length" not in hkeys:
+ # "All 1xx (informational), 204 (no content),
+ # and 304 (not modified) responses MUST NOT
+ # include a message-body." So no point chunking.
+ if status < 200 or status in (204, 205, 304):
+ pass
+ else:
+ if (self.response_protocol == 'HTTP/1.1'
+ and self.environ["REQUEST_METHOD"] != 'HEAD'):
+ # Use the chunked transfer-coding
+ self.chunked_write = True
+ self.outheaders.append(("Transfer-Encoding", "chunked"))
+ else:
+ # Closing the conn is the only way to determine len.
+ self.close_connection = True
+
+ if "connection" not in hkeys:
+ if self.response_protocol == 'HTTP/1.1':
+ # Both server and client are HTTP/1.1 or better
+ if self.close_connection:
+ self.outheaders.append(("Connection", "close"))
+ else:
+ # Server and/or client are HTTP/1.0
+ if not self.close_connection:
+ self.outheaders.append(("Connection", "Keep-Alive"))
+
+ if (not self.close_connection) and (not self.chunked_read):
+ # Read any remaining request body data on the socket.
+ # "If an origin server receives a request that does not include an
+ # Expect request-header field with the "100-continue" expectation,
+ # the request includes a request body, and the server responds
+ # with a final status code before reading the entire request body
+ # from the transport connection, then the server SHOULD NOT close
+ # the transport connection until it has read the entire request,
+ # or until the client closes the connection. Otherwise, the client
+ # might not reliably receive the response message. However, this
+ # requirement is not be construed as preventing a server from
+ # defending itself against denial-of-service attacks, or from
+ # badly broken client implementations."
+ size = self.rfile.maxlen - self.rfile.bytes_read
+ if size > 0:
+ self.rfile.read(size)
+
+ if "date" not in hkeys:
+ self.outheaders.append(("Date", rfc822.formatdate()))
+
+ if "server" not in hkeys:
+ self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE']))
+
+ buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"]
+ try:
+ buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
+ except TypeError:
+ if not isinstance(k, str):
+ raise TypeError("WSGI response header key %r is not a string.")
+ if not isinstance(v, str):
+ raise TypeError("WSGI response header value %r is not a string.")
+ else:
+ raise
+ buf.append("\r\n")
+ self.wfile.sendall("".join(buf))
+
+
+class NoSSLError(Exception):
+ """Exception raised when a client speaks HTTP to an HTTPS socket."""
+ pass
+
+
+class FatalSSLAlert(Exception):
+ """Exception raised when the SSL implementation signals a fatal alert."""
+ pass
+
+
+if not _fileobject_uses_str_type:
+ class CP_fileobject(socket._fileobject):
+ """Faux file object attached to a socket object."""
+
+ def sendall(self, data):
+ """Sendall for non-blocking sockets."""
+ while data:
+ try:
+ bytes_sent = self.send(data)
+ data = data[bytes_sent:]
+ except socket.error, e:
+ if e.args[0] not in socket_errors_nonblocking:
+ raise
+
+ def send(self, data):
+ return self._sock.send(data)
+
+ def flush(self):
+ if self._wbuf:
+ buffer = "".join(self._wbuf)
+ self._wbuf = []
+ self.sendall(buffer)
+
+ def recv(self, size):
+ while True:
+ try:
+ return self._sock.recv(size)
+ except socket.error, e:
+ if (e.args[0] not in socket_errors_nonblocking
+ and e.args[0] not in socket_error_eintr):
+ raise
+
+ def read(self, size=-1):
+ # Use max, disallow tiny reads in a loop as they are very inefficient.
+ # We never leave read() with any leftover data from a new recv() call
+ # in our internal buffer.
+ rbufsize = max(self._rbufsize, self.default_bufsize)
+ # Our use of StringIO rather than lists of string objects returned by
+ # recv() minimizes memory usage and fragmentation that occurs when
+ # rbufsize is large compared to the typical return value of recv().
+ buf = self._rbuf
+ buf.seek(0, 2) # seek end
+ if size < 0:
+ # Read until EOF
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(rbufsize)
+ if not data:
+ break
+ buf.write(data)
+ return buf.getvalue()
+ else:
+ # Read until size bytes or EOF seen, whichever comes first
+ buf_len = buf.tell()
+ if buf_len >= size:
+ # Already have size bytes in our buffer? Extract and return.
+ buf.seek(0)
+ rv = buf.read(size)
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return rv
+
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ left = size - buf_len
+ # recv() will malloc the amount of memory given as its
+ # parameter even though it often returns much less data
+ # than that. The returned data string is short lived
+ # as we copy it into a StringIO and free it. This avoids
+ # fragmentation issues on many platforms.
+ data = self.recv(left)
+ if not data:
+ break
+ n = len(data)
+ if n == size and not buf_len:
+ # Shortcut. Avoid buffer data copies when:
+ # - We have no data in our buffer.
+ # AND
+ # - Our call to recv returned exactly the
+ # number of bytes we were asked to read.
+ return data
+ if n == left:
+ buf.write(data)
+ del data # explicit free
+ break
+ assert n <= left, "recv(%d) returned %d bytes" % (left, n)
+ buf.write(data)
+ buf_len += n
+ del data # explicit free
+ #assert buf_len == buf.tell()
+ return buf.getvalue()
+
+ def readline(self, size=-1):
+ buf = self._rbuf
+ buf.seek(0, 2) # seek end
+ if buf.tell() > 0:
+ # check if we already have it in our buffer
+ buf.seek(0)
+ bline = buf.readline(size)
+ if bline.endswith('\n') or len(bline) == size:
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return bline
+ del bline
+ if size < 0:
+ # Read until \n or EOF, whichever comes first
+ if self._rbufsize <= 1:
+ # Speed up unbuffered case
+ buf.seek(0)
+ buffers = [buf.read()]
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ data = None
+ recv = self.recv
+ while data != "\n":
+ data = recv(1)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+
+ buf.seek(0, 2) # seek end
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ buf.write(data[:nl])
+ self._rbuf.write(data[nl:])
+ del data
+ break
+ buf.write(data)
+ return buf.getvalue()
+ else:
+ # Read until size bytes or \n or EOF seen, whichever comes first
+ buf.seek(0, 2) # seek end
+ buf_len = buf.tell()
+ if buf_len >= size:
+ buf.seek(0)
+ rv = buf.read(size)
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return rv
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ left = size - buf_len
+ # did we just receive a newline?
+ nl = data.find('\n', 0, left)
+ if nl >= 0:
+ nl += 1
+ # save the excess data to _rbuf
+ self._rbuf.write(data[nl:])
+ if buf_len:
+ buf.write(data[:nl])
+ break
+ else:
+ # Shortcut. Avoid data copy through buf when returning
+ # a substring of our first recv().
+ return data[:nl]
+ n = len(data)
+ if n == size and not buf_len:
+ # Shortcut. Avoid data copy through buf when
+ # returning exactly all of our first recv().
+ return data
+ if n >= left:
+ buf.write(data[:left])
+ self._rbuf.write(data[left:])
+ break
+ buf.write(data)
+ buf_len += n
+ #assert buf_len == buf.tell()
+ return buf.getvalue()
+
+else:
+ class CP_fileobject(socket._fileobject):
+ """Faux file object attached to a socket object."""
+
+ def sendall(self, data):
+ """Sendall for non-blocking sockets."""
+ while data:
+ try:
+ bytes_sent = self.send(data)
+ data = data[bytes_sent:]
+ except socket.error, e:
+ if e.args[0] not in socket_errors_nonblocking:
+ raise
+
+ def send(self, data):
+ return self._sock.send(data)
+
+ def flush(self):
+ if self._wbuf:
+ buffer = "".join(self._wbuf)
+ self._wbuf = []
+ self.sendall(buffer)
+
+ def recv(self, size):
+ while True:
+ try:
+ return self._sock.recv(size)
+ except socket.error, e:
+ if (e.args[0] not in socket_errors_nonblocking
+ and e.args[0] not in socket_error_eintr):
+ raise
+
+ def read(self, size=-1):
+ if size < 0:
+ # Read until EOF
+ buffers = [self._rbuf]
+ self._rbuf = ""
+ if self._rbufsize <= 1:
+ recv_size = self.default_bufsize
+ else:
+ recv_size = self._rbufsize
+
+ while True:
+ data = self.recv(recv_size)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+ else:
+ # Read until size bytes or EOF seen, whichever comes first
+ data = self._rbuf
+ buf_len = len(data)
+ if buf_len >= size:
+ self._rbuf = data[size:]
+ return data[:size]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ left = size - buf_len
+ recv_size = max(self._rbufsize, left)
+ data = self.recv(recv_size)
+ if not data:
+ break
+ buffers.append(data)
+ n = len(data)
+ if n >= left:
+ self._rbuf = data[left:]
+ buffers[-1] = data[:left]
+ break
+ buf_len += n
+ return "".join(buffers)
+
+ def readline(self, size=-1):
+ data = self._rbuf
+ if size < 0:
+ # Read until \n or EOF, whichever comes first
+ if self._rbufsize <= 1:
+ # Speed up unbuffered case
+ assert data == ""
+ buffers = []
+ while data != "\n":
+ data = self.recv(1)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ return data[:nl]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ buffers.append(data)
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ buffers[-1] = data[:nl]
+ break
+ return "".join(buffers)
+ else:
+ # Read until size bytes or \n or EOF seen, whichever comes first
+ nl = data.find('\n', 0, size)
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ return data[:nl]
+ buf_len = len(data)
+ if buf_len >= size:
+ self._rbuf = data[size:]
+ return data[:size]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ buffers.append(data)
+ left = size - buf_len
+ nl = data.find('\n', 0, left)
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ buffers[-1] = data[:nl]
+ break
+ n = len(data)
+ if n >= left:
+ self._rbuf = data[left:]
+ buffers[-1] = data[:left]
+ break
+ buf_len += n
+ return "".join(buffers)
+
+
+class SSL_fileobject(CP_fileobject):
+ """SSL file object attached to a socket object."""
+
+ ssl_timeout = 3
+ ssl_retry = .01
+
+ def _safe_call(self, is_reader, call, *args, **kwargs):
+ """Wrap the given call with SSL error-trapping.
+
+ is_reader: if False EOF errors will be raised. If True, EOF errors
+ will return "" (to emulate normal sockets).
+ """
+ start = time.time()
+ while True:
+ try:
+ return call(*args, **kwargs)
+ except SSL.WantReadError:
+ # Sleep and try again. This is dangerous, because it means
+ # the rest of the stack has no way of differentiating
+ # between a "new handshake" error and "client dropped".
+ # Note this isn't an endless loop: there's a timeout below.
+ time.sleep(self.ssl_retry)
+ except SSL.WantWriteError:
+ time.sleep(self.ssl_retry)
+ except SSL.SysCallError, e:
+ if is_reader and e.args == (-1, 'Unexpected EOF'):
+ return ""
+
+ errnum = e.args[0]
+ if is_reader and errnum in socket_errors_to_ignore:
+ return ""
+ raise socket.error(errnum)
+ except SSL.Error, e:
+ if is_reader and e.args == (-1, 'Unexpected EOF'):
+ return ""
+
+ thirdarg = None
+ try:
+ thirdarg = e.args[0][0][2]
+ except IndexError:
+ pass
+
+ if thirdarg == 'http request':
+ # The client is talking HTTP to an HTTPS server.
+ raise NoSSLError()
+ raise FatalSSLAlert(*e.args)
+ except:
+ raise
+
+ if time.time() - start > self.ssl_timeout:
+ raise socket.timeout("timed out")
+
+ def recv(self, *args, **kwargs):
+ buf = []
+ r = super(SSL_fileobject, self).recv
+ while True:
+ data = self._safe_call(True, r, *args, **kwargs)
+ buf.append(data)
+ p = self._sock.pending()
+ if not p:
+ return "".join(buf)
+
+ def sendall(self, *args, **kwargs):
+ return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs)
+
+
+class HTTPConnection(object):
+ """An HTTP connection (active socket).
+
+ socket: the raw socket object (usually TCP) for this connection.
+ wsgi_app: the WSGI application for this server/connection.
+ environ: a WSGI environ template. This will be copied for each request.
+
+ rfile: a fileobject for reading from the socket.
+ send: a function for writing (+ flush) to the socket.
+ """
+
+ rbufsize = -1
+ RequestHandlerClass = HTTPRequest
+ environ = {"wsgi.version": (1, 0),
+ "wsgi.url_scheme": "http",
+ "wsgi.multithread": True,
+ "wsgi.multiprocess": False,
+ "wsgi.run_once": False,
+ "wsgi.errors": sys.stderr,
+ }
+
+ def __init__(self, sock, wsgi_app, environ):
+ self.socket = sock
+ self.wsgi_app = wsgi_app
+
+ # Copy the class environ into self.
+ self.environ = self.environ.copy()
+ self.environ.update(environ)
+
+ if SSL and isinstance(sock, SSL.ConnectionType):
+ timeout = sock.gettimeout()
+ self.rfile = SSL_fileobject(sock, "rb", self.rbufsize)
+ self.rfile.ssl_timeout = timeout
+ self.wfile = SSL_fileobject(sock, "wb", -1)
+ self.wfile.ssl_timeout = timeout
+ else:
+ self.rfile = CP_fileobject(sock, "rb", self.rbufsize)
+ self.wfile = CP_fileobject(sock, "wb", -1)
+
+ # Wrap wsgi.input but not HTTPConnection.rfile itself.
+ # We're also not setting maxlen yet; we'll do that separately
+ # for headers and body for each iteration of self.communicate
+ # (if maxlen is 0 the wrapper doesn't check length).
+ self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0)
+
+ def communicate(self):
+ """Read each request and respond appropriately."""
+ try:
+ while True:
+ # (re)set req to None so that if something goes wrong in
+ # the RequestHandlerClass constructor, the error doesn't
+ # get written to the previous request.
+ req = None
+ req = self.RequestHandlerClass(self.wfile, self.environ,
+ self.wsgi_app)
+
+ # This order of operations should guarantee correct pipelining.
+ req.parse_request()
+ if not req.ready:
+ return
+
+ req.respond()
+ if req.close_connection:
+ return
+
+ except socket.error, e:
+ errnum = e.args[0]
+ if errnum == 'timed out':
+ if req and not req.sent_headers:
+ req.simple_response("408 Request Timeout")
+ elif errnum not in socket_errors_to_ignore:
+ if req and not req.sent_headers:
+ req.simple_response("500 Internal Server Error",
+ format_exc())
+ return
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except FatalSSLAlert, e:
+ # Close the connection.
+ return
+ except NoSSLError:
+ if req and not req.sent_headers:
+ # Unwrap our wfile
+ req.wfile = CP_fileobject(self.socket._sock, "wb", -1)
+ req.simple_response("400 Bad Request",
+ "The client sent a plain HTTP request, but "
+ "this server only speaks HTTPS on this port.")
+ self.linger = True
+ except Exception, e:
+ if req and not req.sent_headers:
+ req.simple_response("500 Internal Server Error", format_exc())
+
+ linger = False
+
+ def close(self):
+ """Close the socket underlying this connection."""
+ self.rfile.close()
+
+ if not self.linger:
+ # Python's socket module does NOT call close on the kernel socket
+ # when you call socket.close(). We do so manually here because we
+ # want this server to send a FIN TCP segment immediately. Note this
+ # must be called *before* calling socket.close(), because the latter
+ # drops its reference to the kernel socket.
+ self.socket._sock.close()
+ self.socket.close()
+ else:
+ # On the other hand, sometimes we want to hang around for a bit
+ # to make sure the client has a chance to read our entire
+ # response. Skipping the close() calls here delays the FIN
+ # packet until the socket object is garbage-collected later.
+ # Someday, perhaps, we'll do the full lingering_close that
+ # Apache does, but not today.
+ pass
+
+
+def format_exc(limit=None):
+ """Like print_exc() but return a string. Backport for Python 2.3."""
+ try:
+ etype, value, tb = sys.exc_info()
+ return ''.join(traceback.format_exception(etype, value, tb, limit))
+ finally:
+ etype = value = tb = None
+
+
+_SHUTDOWNREQUEST = None
+
+class WorkerThread(threading.Thread):
+ """Thread which continuously polls a Queue for Connection objects.
+
+ server: the HTTP Server which spawned this thread, and which owns the
+ Queue and is placing active connections into it.
+ ready: a simple flag for the calling server to know when this thread
+ has begun polling the Queue.
+
+ Due to the timing issues of polling a Queue, a WorkerThread does not
+ check its own 'ready' flag after it has started. To stop the thread,
+ it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
+ (one for each running WorkerThread).
+ """
+
+ conn = None
+
+ def __init__(self, server):
+ self.ready = False
+ self.server = server
+ threading.Thread.__init__(self)
+
+ def run(self):
+ try:
+ self.ready = True
+ while True:
+ conn = self.server.requests.get()
+ if conn is _SHUTDOWNREQUEST:
+ return
+
+ self.conn = conn
+ try:
+ conn.communicate()
+ finally:
+ conn.close()
+ self.conn = None
+ except (KeyboardInterrupt, SystemExit), exc:
+ self.server.interrupt = exc
+
+
+class ThreadPool(object):
+ """A Request Queue for the CherryPyWSGIServer which pools threads.
+
+ ThreadPool objects must provide min, get(), put(obj), start()
+ and stop(timeout) attributes.
+ """
+
+ def __init__(self, server, min=10, max=-1):
+ self.server = server
+ self.min = min
+ self.max = max
+ self._threads = []
+ self._queue = Queue.Queue()
+ self.get = self._queue.get
+
+ def start(self):
+ """Start the pool of threads."""
+ for i in xrange(self.min):
+ self._threads.append(WorkerThread(self.server))
+ for worker in self._threads:
+ worker.setName("CP WSGIServer " + worker.getName())
+ worker.start()
+ for worker in self._threads:
+ while not worker.ready:
+ time.sleep(.1)
+
+ def _get_idle(self):
+ """Number of worker threads which are idle. Read-only."""
+ return len([t for t in self._threads if t.conn is None])
+ idle = property(_get_idle, doc=_get_idle.__doc__)
+
+ def put(self, obj):
+ self._queue.put(obj)
+ if obj is _SHUTDOWNREQUEST:
+ return
+
+ def grow(self, amount):
+ """Spawn new worker threads (not above self.max)."""
+ for i in xrange(amount):
+ if self.max > 0 and len(self._threads) >= self.max:
+ break
+ worker = WorkerThread(self.server)
+ worker.setName("CP WSGIServer " + worker.getName())
+ self._threads.append(worker)
+ worker.start()
+
+ def shrink(self, amount):
+ """Kill off worker threads (not below self.min)."""
+ # Grow/shrink the pool if necessary.
+ # Remove any dead threads from our list
+ for t in self._threads:
+ if not t.isAlive():
+ self._threads.remove(t)
+ amount -= 1
+
+ if amount > 0:
+ for i in xrange(min(amount, len(self._threads) - self.min)):
+ # Put a number of shutdown requests on the queue equal
+ # to 'amount'. Once each of those is processed by a worker,
+ # that worker will terminate and be culled from our list
+ # in self.put.
+ self._queue.put(_SHUTDOWNREQUEST)
+
+ def stop(self, timeout=5):
+ # Must shut down threads here so the code that calls
+ # this method can know when all threads are stopped.
+ for worker in self._threads:
+ self._queue.put(_SHUTDOWNREQUEST)
+
+ # Don't join currentThread (when stop is called inside a request).
+ current = threading.currentThread()
+ while self._threads:
+ worker = self._threads.pop()
+ if worker is not current and worker.isAlive():
+ try:
+ if timeout is None or timeout < 0:
+ worker.join()
+ else:
+ worker.join(timeout)
+ if worker.isAlive():
+ # We exhausted the timeout.
+ # Forcibly shut down the socket.
+ c = worker.conn
+ if c and not c.rfile.closed:
+ if SSL and isinstance(c.socket, SSL.ConnectionType):
+ # pyOpenSSL.socket.shutdown takes no args
+ c.socket.shutdown()
+ else:
+ c.socket.shutdown(socket.SHUT_RD)
+ worker.join()
+ except (AssertionError,
+ # Ignore repeated Ctrl-C.
+ # See http://www.cherrypy.org/ticket/691.
+ KeyboardInterrupt), exc1:
+ pass
+
+
+
+class SSLConnection:
+ """A thread-safe wrapper for an SSL.Connection.
+
+ *args: the arguments to create the wrapped SSL.Connection(*args).
+ """
+
+ def __init__(self, *args):
+ self._ssl_conn = SSL.Connection(*args)
+ self._lock = threading.RLock()
+
+ for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
+ 'renegotiate', 'bind', 'listen', 'connect', 'accept',
+ 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list',
+ 'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
+ 'makefile', 'get_app_data', 'set_app_data', 'state_string',
+ 'sock_shutdown', 'get_peer_certificate', 'want_read',
+ 'want_write', 'set_connect_state', 'set_accept_state',
+ 'connect_ex', 'sendall', 'settimeout'):
+ exec """def %s(self, *args):
+ self._lock.acquire()
+ try:
+ return self._ssl_conn.%s(*args)
+ finally:
+ self._lock.release()
+""" % (f, f)
+
+
+try:
+ import fcntl
+except ImportError:
+ try:
+ from ctypes import windll, WinError
+ except ImportError:
+ def prevent_socket_inheritance(sock):
+ """Dummy function, since neither fcntl nor ctypes are available."""
+ pass
+ else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (Windows)."""
+ if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
+ raise WinError()
+else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (POSIX)."""
+ fd = sock.fileno()
+ old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
+
+
+class CherryPyWSGIServer(object):
+ """An HTTP server for WSGI.
+
+ bind_addr: The interface on which to listen for connections.
+ For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+ or IPv6 address, or any valid hostname. The string 'localhost' is a
+ synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+ The string '0.0.0.0' is a special IPv4 entry meaning "any active
+ interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+ IPv6. The empty string or None are not allowed.
+
+ For UNIX sockets, supply the filename as a string.
+ wsgi_app: the WSGI 'application callable'; multiple WSGI applications
+ may be passed as (path_prefix, app) pairs.
+ numthreads: the number of worker threads to create (default 10).
+ server_name: the string to set for WSGI's SERVER_NAME environ entry.
+ Defaults to socket.gethostname().
+ max: the maximum number of queued requests (defaults to -1 = no limit).
+ request_queue_size: the 'backlog' argument to socket.listen();
+ specifies the maximum number of queued connections (default 5).
+ timeout: the timeout in seconds for accepted connections (default 10).
+
+ nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket
+ option.
+
+ protocol: the version string to write in the Status-Line of all
+ HTTP responses. For example, "HTTP/1.1" (the default). This
+ also limits the supported features used in the response.
+
+
+ SSL/HTTPS
+ ---------
+ The OpenSSL module must be importable for SSL functionality.
+ You can obtain it from http://pyopenssl.sourceforge.net/
+
+ ssl_certificate: the filename of the server SSL certificate.
+ ssl_privatekey: the filename of the server's private key file.
+
+ If either of these is None (both are None by default), this server
+ will not use SSL. If both are given and are valid, they will be read
+ on server start and used in the SSL context for the listening socket.
+ """
+
+ protocol = "HTTP/1.1"
+ _bind_addr = "127.0.0.1"
+ version = "CherryPy/3.1.2"
+ ready = False
+ _interrupt = None
+
+ nodelay = True
+
+ ConnectionClass = HTTPConnection
+ environ = {}
+
+ # Paths to certificate and private key files
+ ssl_certificate = None
+ ssl_private_key = None
+
+ def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
+ max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
+ self.requests = ThreadPool(self, min=numthreads or 1, max=max)
+
+ if callable(wsgi_app):
+ # We've been handed a single wsgi_app, in CP-2.1 style.
+ # Assume it's mounted at "".
+ self.wsgi_app = wsgi_app
+ else:
+ # We've been handed a list of (path_prefix, wsgi_app) tuples,
+ # so that the server can call different wsgi_apps, and also
+ # correctly set SCRIPT_NAME.
+ warnings.warn("The ability to pass multiple apps is deprecated "
+ "and will be removed in 3.2. You should explicitly "
+ "include a WSGIPathInfoDispatcher instead.",
+ DeprecationWarning)
+ self.wsgi_app = WSGIPathInfoDispatcher(wsgi_app)
+
+ self.bind_addr = bind_addr
+ if not server_name:
+ server_name = socket.gethostname()
+ self.server_name = server_name
+ self.request_queue_size = request_queue_size
+
+ self.timeout = timeout
+ self.shutdown_timeout = shutdown_timeout
+
+ def _get_numthreads(self):
+ return self.requests.min
+ def _set_numthreads(self, value):
+ self.requests.min = value
+ numthreads = property(_get_numthreads, _set_numthreads)
+
+ def __str__(self):
+ return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
+ self.bind_addr)
+
+ def _get_bind_addr(self):
+ return self._bind_addr
+ def _set_bind_addr(self, value):
+ if isinstance(value, tuple) and value[0] in ('', None):
+ # Despite the socket module docs, using '' does not
+ # allow AI_PASSIVE to work. Passing None instead
+ # returns '0.0.0.0' like we want. In other words:
+ # host AI_PASSIVE result
+ # '' Y 192.168.x.y
+ # '' N 192.168.x.y
+ # None Y 0.0.0.0
+ # None N 127.0.0.1
+ # But since you can get the same effect with an explicit
+ # '0.0.0.0', we deny both the empty string and None as values.
+ raise ValueError("Host values of '' or None are not allowed. "
+ "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
+ "to listen on all active interfaces.")
+ self._bind_addr = value
+ bind_addr = property(_get_bind_addr, _set_bind_addr,
+ doc="""The interface on which to listen for connections.
+
+ For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+ or IPv6 address, or any valid hostname. The string 'localhost' is a
+ synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+ The string '0.0.0.0' is a special IPv4 entry meaning "any active
+ interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+ IPv6. The empty string or None are not allowed.
+
+ For UNIX sockets, supply the filename as a string.""")
+
+ def start(self):
+ """Run the server forever."""
+ # We don't have to trap KeyboardInterrupt or SystemExit here,
+ # because cherrpy.server already does so, calling self.stop() for us.
+ # If you're using this server with another framework, you should
+ # trap those exceptions in whatever code block calls start().
+ self._interrupt = None
+
+ # Select the appropriate socket
+ if isinstance(self.bind_addr, basestring):
+ # AF_UNIX socket
+
+ # So we can reuse the socket...
+ try: os.unlink(self.bind_addr)
+ except: pass
+
+ # So everyone can access the socket...
+ try: os.chmod(self.bind_addr, 0777)
+ except: pass
+
+ info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
+ else:
+ # AF_INET or AF_INET6 socket
+ # Get the correct address family for our host (allows IPv6 addresses)
+ host, port = self.bind_addr
+ try:
+ info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
+ except socket.gaierror:
+ # Probably a DNS issue. Assume IPv4.
+ info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)]
+
+ self.socket = None
+ msg = "No socket could be created"
+ for res in info:
+ af, socktype, proto, canonname, sa = res
+ try:
+ self.bind(af, socktype, proto)
+ except socket.error, msg:
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+ continue
+ break
+ if not self.socket:
+ raise socket.error, msg
+
+ # Timeout so KeyboardInterrupt can be caught on Win32
+ self.socket.settimeout(1)
+ self.socket.listen(self.request_queue_size)
+
+ # Create worker threads
+ self.requests.start()
+
+ self.ready = True
+ while self.ready:
+ self.tick()
+ if self.interrupt:
+ while self.interrupt is True:
+ # Wait for self.stop() to complete. See _set_interrupt.
+ time.sleep(0.1)
+ if self.interrupt:
+ raise self.interrupt
+
+ def bind(self, family, type, proto=0):
+ """Create (or recreate) the actual socket object."""
+ self.socket = socket.socket(family, type, proto)
+ prevent_socket_inheritance(self.socket)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if self.nodelay:
+ self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ if self.ssl_certificate and self.ssl_private_key:
+ if SSL is None:
+ raise ImportError("You must install pyOpenSSL to use HTTPS.")
+
+ # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_privatekey_file(self.ssl_private_key)
+ ctx.use_certificate_file(self.ssl_certificate)
+ self.socket = SSLConnection(ctx, self.socket)
+ self.populate_ssl_environ()
+
+ # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
+ # activate dual-stack. See http://www.cherrypy.org/ticket/871.
+ if (not isinstance(self.bind_addr, basestring)
+ and self.bind_addr[0] == '::' and family == socket.AF_INET6):
+ try:
+ self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
+ except (AttributeError, socket.error):
+ # Apparently, the socket option is not available in
+ # this machine's TCP stack
+ pass
+
+ self.socket.bind(self.bind_addr)
+
+ def tick(self):
+ """Accept a new connection and put it on the Queue."""
+ try:
+ s, addr = self.socket.accept()
+ prevent_socket_inheritance(s)
+ if not self.ready:
+ return
+ if hasattr(s, 'settimeout'):
+ s.settimeout(self.timeout)
+
+ environ = self.environ.copy()
+ # SERVER_SOFTWARE is common for IIS. It's also helpful for
+ # us to pass a default value for the "Server" response header.
+ if environ.get("SERVER_SOFTWARE") is None:
+ environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
+ # set a non-standard environ entry so the WSGI app can know what
+ # the *real* server protocol is (and what features to support).
+ # See http://www.faqs.org/rfcs/rfc2145.html.
+ environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol
+ environ["SERVER_NAME"] = self.server_name
+
+ if isinstance(self.bind_addr, basestring):
+ # AF_UNIX. This isn't really allowed by WSGI, which doesn't
+ # address unix domain sockets. But it's better than nothing.
+ environ["SERVER_PORT"] = ""
+ else:
+ environ["SERVER_PORT"] = str(self.bind_addr[1])
+ # optional values
+ # Until we do DNS lookups, omit REMOTE_HOST
+ environ["REMOTE_ADDR"] = addr[0]
+ environ["REMOTE_PORT"] = str(addr[1])
+
+ conn = self.ConnectionClass(s, self.wsgi_app, environ)
+ self.requests.put(conn)
+ except socket.timeout:
+ # The only reason for the timeout in start() is so we can
+ # notice keyboard interrupts on Win32, which don't interrupt
+ # accept() by default
+ return
+ except socket.error, x:
+ if x.args[0] in socket_error_eintr:
+ # I *think* this is right. EINTR should occur when a signal
+ # is received during the accept() call; all docs say retry
+ # the call, and I *think* I'm reading it right that Python
+ # will then go ahead and poll for and handle the signal
+ # elsewhere. See http://www.cherrypy.org/ticket/707.
+ return
+ if x.args[0] in socket_errors_nonblocking:
+ # Just try again. See http://www.cherrypy.org/ticket/479.
+ return
+ if x.args[0] in socket_errors_to_ignore:
+ # Our socket was closed.
+ # See http://www.cherrypy.org/ticket/686.
+ return
+ raise
+
+ def _get_interrupt(self):
+ return self._interrupt
+ def _set_interrupt(self, interrupt):
+ self._interrupt = True
+ self.stop()
+ self._interrupt = interrupt
+ interrupt = property(_get_interrupt, _set_interrupt,
+ doc="Set this to an Exception instance to "
+ "interrupt the server.")
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ self.ready = False
+
+ sock = getattr(self, "socket", None)
+ if sock:
+ if not isinstance(self.bind_addr, basestring):
+ # Touch our own socket to make accept() return immediately.
+ try:
+ host, port = sock.getsockname()[:2]
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
+ else:
+ # Note that we're explicitly NOT using AI_PASSIVE,
+ # here, because we want an actual IP to touch.
+ # localhost won't work if we've bound to a public IP,
+ # but it will if we bound to '0.0.0.0' (INADDR_ANY).
+ for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ s = None
+ try:
+ s = socket.socket(af, socktype, proto)
+ # See http://groups.google.com/group/cherrypy-users/
+ # browse_frm/thread/bbfe5eb39c904fe0
+ s.settimeout(1.0)
+ s.connect((host, port))
+ s.close()
+ except socket.error:
+ if s:
+ s.close()
+ if hasattr(sock, "close"):
+ sock.close()
+ self.socket = None
+
+ self.requests.stop(self.shutdown_timeout)
+
+ def populate_ssl_environ(self):
+ """Create WSGI environ entries to be merged into each request."""
+ cert = open(self.ssl_certificate, 'rb').read()
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+ ssl_environ = {
+ "wsgi.url_scheme": "https",
+ "HTTPS": "on",
+ # pyOpenSSL doesn't provide access to any of these AFAICT
+## 'SSL_PROTOCOL': 'SSLv2',
+## SSL_CIPHER string The cipher specification name
+## SSL_VERSION_INTERFACE string The mod_ssl program version
+## SSL_VERSION_LIBRARY string The OpenSSL program version
+ }
+
+ # Server certificate attributes
+ ssl_environ.update({
+ 'SSL_SERVER_M_VERSION': cert.get_version(),
+ 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
+## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
+## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
+ })
+
+ for prefix, dn in [("I", cert.get_issuer()),
+ ("S", cert.get_subject())]:
+ # X509Name objects don't seem to have a way to get the
+ # complete DN string. Use str() and slice it instead,
+ # because str(dn) == "<X509Name object '/C=US/ST=...'>"
+ dnstr = str(dn)[18:-2]
+
+ wsgikey = 'SSL_SERVER_%s_DN' % prefix
+ ssl_environ[wsgikey] = dnstr
+
+ # The DN should be of the form: /k1=v1/k2=v2, but we must allow
+ # for any value to contain slashes itself (in a URL).
+ while dnstr:
+ pos = dnstr.rfind("=")
+ dnstr, value = dnstr[:pos], dnstr[pos + 1:]
+ pos = dnstr.rfind("/")
+ dnstr, key = dnstr[:pos], dnstr[pos + 1:]
+ if key and value:
+ wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
+ ssl_environ[wsgikey] = value
+
+ self.environ.update(ssl_environ)
+
diff --git a/module/setup.py b/module/setup.py
index 287580b2d..5618ea8e0 100644
--- a/module/setup.py
+++ b/module/setup.py
@@ -120,11 +120,6 @@ class Setup():
print _("The Graphical User Interface.")
print ""
- if not web:
- print _("no Webinterface available")
- print _("Gives abillity to control pyLoad with your webbrowser.")
- print ""
-
if not js:
print _("no JavaScript engine found")
print _("You will need this for some Click'N'Load links. Install Spidermonkey or ossp-js")
@@ -227,29 +222,14 @@ class Setup():
print ""
- web = self.check_module("django")
-
- try:
- import django
-
- if django.VERSION < (1, 1):
- print _("Your django version is to old, please upgrade to django 1.1")
- web = False
- elif django.VERSION > (1, 3):
- print _("Your django version is to new, please use django 1.2")
- web = False
- except:
- web = False
-
- self.print_dep("django", web)
- web = web and sqlite
+ web = sqlite
from module import JsEngine
js = True if JsEngine.ENGINE else False
self.print_dep(_("JS engine"), js)
- return (basic, ssl, captcha, gui, web, js)
+ return basic, ssl, captcha, gui, web, js
def conf_basic(self):
print ""
@@ -280,7 +260,7 @@ class Setup():
print ""
print _("## Webinterface Setup ##")
- db_path = "pyload.db"
+ db_path = "web.db"
is_db = isfile(db_path)
db_setup = True
@@ -290,31 +270,21 @@ class Setup():
if db_setup:
if is_db: remove(db_path)
- from django import VERSION
import sqlite3
-
- if VERSION[:2] < (1,2):
- from module.web import syncdb_django11 as syncdb
- else:
- from module.web import syncdb
-
- from module.web import createsuperuser
-
-
+ from web.webinterface import setup_database
+ setup_database()
+
print ""
- syncdb.handle_noargs()
- print _("If you see no errors, your db should be fine and we're adding an user now.")
username = self.ask(_("Username"), "User")
- createsuperuser.handle(username, "email@trash-mail.com")
password = self.ask("", "", password=True)
salt = reduce(lambda x, y: x + y, [str(random.randint(0, 9)) for i in range(0, 5)])
hash = sha1(salt + password)
- password = "sha1$%s$%s" % (salt, hash.hexdigest())
+ password = salt + hash.hexdigest()
conn = sqlite3.connect(db_path)
c = conn.cursor()
- c.execute('UPDATE "main"."auth_user" SET "password"=? WHERE "username"=?', (password, username))
+ c.execute('INSERT INTO users(name, password) VALUES (?,?)', (username, password))
conn.commit()
c.close()
diff --git a/module/web/ServerThread.py b/module/web/ServerThread.py
index db5e3be05..7f47f80c6 100644
--- a/module/web/ServerThread.py
+++ b/module/web/ServerThread.py
@@ -1,17 +1,11 @@
#!/usr/bin/env python
from __future__ import with_statement
from os.path import exists
-from os.path import join
-from os.path import abspath
-from os import makedirs
-from subprocess import PIPE
-from subprocess import Popen
-from subprocess import call
-from sys import version_info
-from cStringIO import StringIO
import threading
-import sys
import logging
+import sqlite3
+
+import webinterface
core = None
log = logging.getLogger("log")
@@ -25,188 +19,108 @@ class WebServer(threading.Thread):
self.running = True
self.server = pycore.config['webinterface']['server']
self.https = pycore.config['webinterface']['https']
+ self.cert = pycore.config["ssl"]["cert"]
+ self.key = pycore.config["ssl"]["key"]
+ self.host = pycore.config['webinterface']['host']
+ self.port = pycore.config['webinterface']['port']
+
self.setDaemon(True)
-
+
def run(self):
- sys.path.append(join(pypath, "module", "web"))
- avail = ["builtin"]
- host = self.core.config['webinterface']['host']
- port = self.core.config['webinterface']['port']
- serverpath = join(pypath, "module", "web")
- path = join(abspath(""), "servers")
- out = StringIO()
-
- if not exists("pyload.db"):
- #print "########## IMPORTANT ###########"
- #print "### Database for Webinterface does not exitst, it will not be available."
- #print "### Please run: python %s syncdb" % join(self.pycore.path, "module", "web", "manage.py")
- #print "### You have to add at least one User, to gain access to webinterface: python %s createsuperuser" % join(self.pycore.path, "module", "web", "manage.py")
- #print "### Dont forget to restart pyLoad if you are done."
+ self.checkDB()
+
+ if self.https:
+ if not exists(self.cert) or not exists(self.key):
+ log.warning(_("SSL certificates not found."))
+ self.https = False
+
+ if self.server in ("lighttpd", "nginx"):
+ log.warning(_("Sorry, we dropped support for starting %s directly within pyLoad") % self.server)
+ log.warning(_("You can use the threaded server which offers good performance and ssl,"))
+ log.warning(_("of course you can still use your existing %s with pyLoads fastcgi server") % self.server)
+ log.warning(_("sample configs are located in the module/web/servers directory"))
+ self.server = "builtin"
+
+ if self.server == "fastcgi":
+ try:
+ import flup
+ except:
+ log.warning(_("Can't use %(server)s, python-flup is not installed!") % {
+ "server": self.server})
+ self.server = "builtin"
+
+ if self.server == "fastcgi":
+ self.start_fcgi()
+ elif self.server == "threaded":
+ self.start_threaded()
+ else:
+ self.start_builtin()
+
+
+ def checkDB(self):
+ conn = sqlite3.connect('web.db')
+ c = conn.cursor()
+ c.execute("SELECT * from users LIMIT 1")
+ empty = True
+ if c.fetchone():
+ empty = False
+
+ c.close()
+ conn.close()
+
+ if not empty:
+ return True
+
+ if exists("pyload.db"):
+ log.info(_("Converting old database to new web.db"))
+ conn = sqlite3.connect('pyload.db')
+ c = conn.cursor()
+ c.execute("SELECT username, password, email from auth_user WHERE is_superuser")
+ users = []
+ for r in c:
+ pw = r[1].split("$")
+ users.append((r[0], pw[1] + pw[2], r[2]))
+
+ c.close()
+ conn.close()
+
+ conn = sqlite3.connect('web.db')
+ c = conn.cursor()
+ c.executemany("INSERT INTO users(name, password, email) VALUES (?,?,?)", users)
+ conn.commit()
+ c.close()
+ conn.close()
+ return True
+
+ else:
log.warning(_("Database for Webinterface does not exitst, it will not be available."))
log.warning(_("Please run: python pyLoadCore.py -s"))
log.warning(_("Go through the setup and create a database and add an user to gain access."))
- return None
-
- try:
- import flup
- avail.append("fastcgi")
- except:
- pass
-
- try:
- call(["lighttpd", "-v"], stdout=PIPE, stderr=PIPE)
- import flup
- avail.append("lighttpd")
-
- except:
- pass
-
- try:
- call(["nginx", "-v"], stdout=PIPE, stderr=PIPE)
- import flup
- avail.append("nginx")
- except:
- pass
-
-
- try:
- if self.https:
- if exists(self.core.config["ssl"]["cert"]) and exists(self.core.config["ssl"]["key"]):
- if not exists("ssl.pem"):
- key = file(self.core.config["ssl"]["key"], "rb")
- cert = file(self.core.config["ssl"]["cert"], "rb")
-
- pem = file("ssl.pem", "wb")
- pem.writelines(key.readlines())
- pem.writelines(cert.readlines())
-
- key.close()
- cert.close()
- pem.close()
-
- else:
- log.warning(_("SSL certificates not found."))
- self.https = False
- else:
- pass
- except:
- self.https = False
-
-
- if not self.server in avail:
- log.warning(_("Can't use %(server)s, either python-flup or %(server)s is not installed!") % {"server": self.server})
- self.server = "builtin"
+ return False
+
+
+ def start_builtin(self):
+
+ if self.https:
+ log.warning(_("The simple builtin server offers no SSL, please consider using threaded instead"))
+ self.core.log.info(_("Starting builtin webserver: %(host)s:%(port)d") % {"host": self.host, "port": self.port})
+ webinterface.run_simple(host=self.host, port=self.port)
- if self.server == "nginx":
-
- if not exists(join(path, "nginx")):
- makedirs(join(path, "nginx"))
-
- config = file(join(serverpath, "servers", "nginx_default.conf"), "rb")
- content = config.read()
- config.close()
-
- content = content.replace("%(path)", join(path, "nginx"))
- content = content.replace("%(host)", host)
- content = content.replace("%(port)", str(port))
- content = content.replace("%(media)", join(serverpath, "media"))
- content = content.replace("%(version)", ".".join(map(str, version_info[0:2])))
-
- if self.https:
- content = content.replace("%(ssl)", """
- ssl on;
- ssl_certificate %s;
- ssl_certificate_key %s;
- """ % (abspath(self.core.config["ssl"]["cert"]), abspath(self.core.config["ssl"]["key"]) ))
- else:
- content = content.replace("%(ssl)", "")
-
- new_config = file(join(path, "nginx.conf"), "wb")
- new_config.write(content)
- new_config.close()
-
- command = ['nginx', '-c', join(path, "nginx.conf")]
- self.p = Popen(command, stderr=PIPE, stdin=PIPE, stdout=Output(out))
-
- log.info(_("Starting nginx Webserver: %(host)s:%(port)d") % {"host": host, "port": port})
- import run_fcgi
- run_fcgi.handle("daemonize=false", "method=threaded", "host=127.0.0.1", "port=9295")
-
-
- elif self.server == "lighttpd":
-
- if not exists(join(path, "lighttpd")):
- makedirs(join(path, "lighttpd"))
-
-
- config = file(join(serverpath, "servers", "lighttpd_default.conf"), "rb")
- content = config.readlines()
- config.close()
- content = "".join(content)
-
- content = content.replace("%(path)", join("servers", "lighttpd"))
- content = content.replace("%(host)", host)
- content = content.replace("%(port)", str(port))
- content = content.replace("%(media)", join(serverpath, "media"))
- content = content.replace("%(version)", ".".join(map(str, version_info[0:2])))
-
- if self.https:
- content = content.replace("%(ssl)", """
- ssl.engine = "enable"
- ssl.pemfile = "%s"
- ssl.ca-file = "%s"
- """ % (abspath("ssl.pem") , abspath(self.core.config["ssl"]["cert"])) )
- else:
- content = content.replace("%(ssl)", "")
- new_config = file(join("servers", "lighttpd.conf"), "wb")
- new_config.write(content)
- new_config.close()
-
- command = ['lighttpd', '-D', '-f', join(path, "lighttpd.conf")]
- self.p = Popen(command, stderr=PIPE, stdin=PIPE, stdout=Output(out))
-
- log.info(_("Starting lighttpd Webserver: %(host)s:%(port)d") % {"host": host, "port": port})
- import run_fcgi
- run_fcgi.handle("daemonize=false", "method=threaded", "host=127.0.0.1", "port=9295")
-
-
- elif self.server == "fastcgi":
- #run fastcgi on port
- import run_fcgi
- run_fcgi.handle("daemonize=false", "method=threaded", "host=127.0.0.1", "port=%s" % str(port))
+ def start_threaded(self):
+ if self.https:
+ self.core.log.info(_("Starting threaded SSL webserver: %(host)s:%(port)d") % {"host": self.host, "port": self.port})
else:
- self.core.log.info(_("Starting django builtin Webserver: %(host)s:%(port)d") % {"host": host, "port": port})
- import run_server
- run_server.handle(host, port)
+ self.cert = ""
+ self.key = ""
+ self.core.log.info(_("Starting threaded webserver: %(host)s:%(port)d") % {"host": self.host, "port": self.port})
- def quit(self):
+ webinterface.run_threaded(host=self.host, port=self.port, cert=self.cert, key=self.key)
+
+ def start_fcgi(self):
- try:
- if self.server == "lighttpd" or self.server == "nginx":
- self.p.kill()
- #self.p2.kill()
- return True
-
- else:
- #self.p.kill()
- return True
- except:
- pass
-
-
- self.running = False
-
-class Output:
- def __init__(self, stream):
- self.stream = stream
-
- def fileno(self):
- return 1
-
- def write(self, data): # Do nothing
- return None
- #self.stream.write(data)
- #self.stream.flush()
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+ self.core.log.info(_("Starting fastcgi server: %(host)s:%(port)d") % {"host": self.host, "port": self.port})
+ webinterface.run_fcgi(host=self.host, port=self.port)
+
+ def quit(self):
+ self.running = False \ No newline at end of file
diff --git a/module/web/cnl_app.py b/module/web/cnl_app.py
new file mode 100644
index 000000000..058a298d3
--- /dev/null
+++ b/module/web/cnl_app.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from os.path import join
+import re
+from urllib import unquote
+from base64 import standard_b64decode
+from binascii import unhexlify
+
+from bottle import route, request, HTTPError
+from webinterface import PYLOAD, DL_ROOT, JS
+
+try:
+ from Crypto.Cipher import AES
+except:
+ pass
+
+
+def local_check(function):
+ def _view(*args, **kwargs):
+ if request.environ.get('REMOTE_ADDR', "0") in ('127.0.0.1', 'localhost') \
+ or request.environ.get('HTTP_HOST','0') == '127.0.0.1:9666':
+ return function(*args, **kwargs)
+ else:
+ return HTTPError(403, "Forbidden")
+
+ return _view
+
+
+@route("/flash")
+@route("/flash", method="POST")
+@local_check
+def flash():
+ return "JDownloader"
+
+@route("/flash/add", method="POST")
+@local_check
+def add(request):
+ package = request.POST.get('referer', 'ClickAndLoad Package')
+ urls = filter(lambda x: x != "", request.POST['urls'].split("\n"))
+
+ PYLOAD.add_package(package, urls, False)
+
+ return ""
+
+@route("/flash/addcrypted", method="POST")
+@local_check
+def addcrypted():
+
+ package = request.forms.get('referer', 'ClickAndLoad Package')
+ dlc = request.forms['crypted'].replace(" ", "+")
+
+ dlc_path = join(DL_ROOT, package.replace("/", "").replace("\\", "").replace(":", "") + ".dlc")
+ dlc_file = file(dlc_path, "wb")
+ dlc_file.write(dlc)
+ dlc_file.close()
+
+ try:
+ PYLOAD.add_package(package, [dlc_path], False)
+ except:
+ return HTTPError()
+ else:
+ return "success"
+
+@route("/flash/addcrypted2", method="POST")
+@local_check
+def addcrypted2():
+
+ package = request.forms.get("source", "ClickAndLoad Package")
+ crypted = request.forms["crypted"]
+ jk = request.forms["jk"]
+
+ crypted = standard_b64decode(unquote(crypted.replace(" ", "+")))
+ if JS:
+ jk = "%s f()" % jk
+ jk = JS.eval(jk)
+
+ else:
+ try:
+ jk = re.findall(r"return ('|\")(.+)('|\")", jk)[0][1]
+ except:
+ ## Test for some known js functions to decode
+ if jk.find("dec") > -1 and jk.find("org") > -1:
+ org = re.findall(r"var org = ('|\")([^\"']+)", jk)[0][1]
+ jk = list(org)
+ jk.reverse()
+ jk = "".join(jk)
+ else:
+ print "Could not decrypt key, please install py-spidermonkey or ossp-js"
+
+ try:
+ Key = unhexlify(jk)
+ except:
+ print "Could not decrypt key, please install py-spidermonkey or ossp-js"
+ return "failed"
+
+ IV = Key
+
+ obj = AES.new(Key, AES.MODE_CBC, IV)
+ result = obj.decrypt(crypted).replace("\x00", "").replace("\r","").split("\n")
+
+ result = filter(lambda x: x != "", result)
+
+ try:
+ PYLOAD.add_package(package, result, False)
+ except:
+ return "failed can't add"
+ else:
+ return "success"
+
+@route("/flashgot", method="POST")
+@local_check
+def flashgot(request):
+ if request.environ['HTTP_REFERER'] != "http://localhost:9666/flashgot" and request.environ['HTTP_REFERER'] != "http://127.0.0.1:9666/flashgot":
+ return HTTPError()
+
+ autostart = int(request.forms.get('autostart', 0))
+ package = request.forms.get('package', "FlashGot")
+ urls = filter(lambda x: x != "", request.forms['urls'].split("\n"))
+ folder = request.forms.get('dir', None)
+
+ PYLOAD.add_package(package, urls, autostart)
+
+ return ""
+
+@route("/crossdomain.xml")
+@local_check
+def crossdomain():
+ rep = "<?xml version=\"1.0\"?>\n"
+ rep += "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
+ rep += "<cross-domain-policy>\n"
+ rep += "<allow-access-from domain=\"*\" />\n"
+ rep += "</cross-domain-policy>"
+ return rep
+
+
+@route("/flash/checkSupportForUrl")
+@local_check
+def checksupport():
+
+ url = request.GET.get("url")
+ res = PYLOAD.checkURLs([url])
+ supported = (not res[0][1] is None)
+
+ return str(supported).lower()
+
+@route("/jdcheck.js")
+@local_check
+def jdcheck():
+ rep = "jdownloader=true;\n"
+ rep += "var version='9.581;'"
+ return rep
diff --git a/module/web/createsuperuser.py b/module/web/createsuperuser.py
deleted file mode 100644
index 0ff1d15b8..000000000
--- a/module/web/createsuperuser.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""
-Management utility to create superusers.
-"""
-
-import os
-import sys
-
-os.environ["DJANGO_SETTINGS_MODULE"] = 'settings'
-sys.path.append(os.path.join(pypath, "module", "web"))
-
-import getpass
-import re
-from optparse import make_option
-from django.contrib.auth.models import User
-from django.core import exceptions
-from django.core.management.base import BaseCommand, CommandError
-from django.utils.translation import ugettext as _
-
-RE_VALID_USERNAME = re.compile('[\w.@+-]+$')
-
-
-def handle(username, email):
- #username = options.get('username', None)
- #email = options.get('email', None)
- interactive = False
-
- # Do quick and dirty validation if --noinput
- if not interactive:
- if not username or not email:
- raise CommandError("You must use --username and --email with --noinput.")
- if not RE_VALID_USERNAME.match(username):
- raise CommandError("Invalid username. Use only letters, digits, and underscores")
-
- password = ''
- default_username = ''
-
- User.objects.create_superuser(username, email, password)
- print "Superuser created successfully."
-
-if __name__ == "__main__":
- username = sys.argv[1]
- email = sys.argv[2]
- handle(username, email) \ No newline at end of file
diff --git a/module/web/filters.py b/module/web/filters.py
new file mode 100644
index 000000000..1b10f7cb4
--- /dev/null
+++ b/module/web/filters.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+from os.path import abspath, commonprefix, join
+from time import strftime, mktime, gmtime
+
+quotechar = "::/"
+
+try:
+ from os.path import relpath
+except:
+ from posixpath import curdir, sep, pardir
+ def relpath(path, start=curdir):
+ """Return a relative version of a path"""
+ if not path:
+ raise ValueError("no path specified")
+ start_list = abspath(start).split(sep)
+ path_list = abspath(path).split(sep)
+ # Work out how much of the filepath is shared by start and path.
+ i = len(commonprefix([start_list, path_list]))
+ rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
+ if not rel_list:
+ return curdir
+ return join(*rel_list)
+
+
+def quotepath(path):
+ try:
+ return path.replace("../", quotechar)
+ except AttributeError:
+ return path
+ except:
+ return ""
+
+def unquotepath(path):
+ try:
+ return path.replace(quotechar, "../")
+ except AttributeError:
+ return path
+ except:
+ return ""
+
+def path_make_absolute(path):
+ p = os.path.abspath(path)
+ if p[-1] == os.path.sep:
+ return p
+ else:
+ return p + os.path.sep
+
+def path_make_relative(path):
+ p = relpath(path)
+ if p[-1] == os.path.sep:
+ return p
+ else:
+ return p + os.path.sep
+
+def truncate(value, n):
+ if (n - len(value)) < 3:
+ return value[:n]+"..."
+ return value
+
+def date(date, format):
+ return date \ No newline at end of file
diff --git a/module/web/json_app.py b/module/web/json_app.py
new file mode 100644
index 000000000..2c95eaf5b
--- /dev/null
+++ b/module/web/json_app.py
@@ -0,0 +1,322 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import base64
+from os.path import join
+from traceback import print_exc
+
+from bottle import route, request, HTTPError, validate
+
+from webinterface import PYLOAD
+
+from utils import login_required
+
+
+def format_time(seconds):
+ seconds = int(seconds)
+
+ hours, seconds = divmod(seconds, 3600)
+ minutes, seconds = divmod(seconds, 60)
+ return "%.2i:%.2i:%.2i" % (hours, minutes, seconds)
+
+def get_sort_key(item):
+ return item["order"]
+
+
+@route("/json/status")
+@route("/json/status", method="POST")
+@login_required('can_see_dl')
+def status():
+ try:
+ status = PYLOAD.status_server()
+ status['captcha'] = PYLOAD.is_captcha_waiting()
+ return status
+ except:
+ return HTTPError()
+
+
+@route("/json/links")
+@route("/json/links", method="POST")
+@login_required('can_see_dl')
+def links():
+ try:
+ links = PYLOAD.status_downloads()
+ ids = []
+ for link in links:
+ ids.append(link['id'])
+
+ if link['status'] == 12:
+ link['info'] = "%s @ %s kb/s" % (link['format_eta'], round(link['speed'], 2))
+ elif link['status'] == 5:
+ link['percent'] = 0
+ link['size'] = 0
+ link['kbleft'] = 0
+ link['info'] = _("waiting %s") % link['format_wait']
+ else:
+ link['info'] = ""
+
+ data = {'links': links, 'ids': ids}
+ return data
+ except Exception, e:
+ return HTTPError()
+
+@route("/json/queue")
+@login_required('can_see_dl')
+def queue():
+ try:
+ return PYLOAD.get_queue()
+
+ except:
+ return HTTPError()
+
+
+@route("/json/pause")
+@login_required('can_change_satus')
+def pause():
+ try:
+ return PYLOAD.pause_server()
+
+ except:
+ return HTTPError()
+
+
+@route("/json/unpause")
+@login_required('can_change_status')
+def unpause():
+ try:
+ return PYLOAD.unpause_server()
+
+ except:
+ return HTTPError()
+
+
+@route("/json/cancel")
+@login_required('can_change_status')
+def cancel():
+ try:
+ return PYLOAD.stop_downloads()
+ except:
+ return HTTPError()
+
+@route("/json/packages")
+@login_required('can_see_dl')
+def packages():
+ try:
+ data = PYLOAD.get_queue()
+
+ for package in data:
+ package['links'] = []
+ for file in PYLOAD.get_package_files(package['id']):
+ package['links'].append(PYLOAD.get_file_info(file))
+
+ return data
+
+ except:
+ return HTTPError()
+
+@route("/json/package/:id")
+@validate(id=int)
+@login_required('pyload.can_see_dl')
+def package(id):
+ try:
+ data = PYLOAD.get_package_data(id)
+
+ for pyfile in data["links"].itervalues():
+ if pyfile["status"] == 0:
+ pyfile["icon"] = "status_finished.png"
+ elif pyfile["status"] in (2, 3):
+ pyfile["icon"] = "status_queue.png"
+ elif pyfile["status"] in (9, 1):
+ pyfile["icon"] = "status_offline.png"
+ elif pyfile["status"] == 5:
+ pyfile["icon"] = "status_waiting.png"
+ elif pyfile["status"] == 8:
+ pyfile["icon"] = "status_failed.png"
+ elif pyfile["status"] in (11, 13):
+ pyfile["icon"] = "status_proc.png"
+ else:
+ pyfile["icon"] = "status_downloading.png"
+
+ tmp = data["links"].values()
+ tmp.sort(key=get_sort_key)
+ data["links"] = tmp
+ return data
+
+ except:
+ return HTTPError()
+
+@route("/json/package_order/:ids")
+@login_required('can_add')
+def package_order(ids):
+ try:
+ pid, pos = ids.split("|")
+ PYLOAD.order_package(int(pid), int(pos))
+ return "success"
+ except:
+ return HTTPError()
+
+@route("/json/link/:id")
+@validate(id=int)
+@login_required('can_see_dl')
+def link(id):
+ try:
+ data = PYLOAD.get_file_info(id)
+ return data
+ except:
+ return HTTPError()
+
+@route("/json/remove_link/:id")
+@validate(id=int)
+@login_required('can_delete')
+def remove_link(id):
+ try:
+ PYLOAD.del_links([id])
+ return "success"
+ except Exception, e:
+ return HTTPError()
+
+@route("/json/restart_link/:id")
+@validate(id=int)
+@login_required('can_add')
+def restart_link(id):
+ try:
+ PYLOAD.restart_file(id)
+ return "success"
+ except Exception:
+ return HTTPError()
+
+@route("/json/abort_link/:id")
+@validate(id=int)
+@login_required('can_delete')
+def abort_link(id):
+ try:
+ PYLOAD.stop_download("link", id)
+ return "success"
+ except:
+ return HTTPError()
+
+@route("/json/link_order/:ids")
+@login_required('can_add')
+def link_order(ids):
+ try:
+ pid, pos = ids.split("|")
+ PYLOAD.order_file(int(pid), int(pos))
+ return "success"
+ except:
+ return HTTPError()
+
+@route("/json/add_package")
+@route("/json/add_package", method="POST")
+@login_required('can_add')
+def add_package():
+ name = request.forms['add_name']
+ queue = int(request.forms['add_dest'])
+ links = request.forms['add_links'].split("\n")
+ pw = request.forms.get("add_password", "").strip("\n\r")
+
+ try:
+ f = request.files['add_file']
+
+ if name is None or name == "":
+ name = f.name
+
+ fpath = join(PYLOAD.get_conf_val("general", "download_folder"), "tmp_" + f.name)
+ destination = open(fpath, 'wb')
+ for chunk in f.chunks():
+ destination.write(chunk)
+ destination.close()
+ links.insert(0, fpath)
+ except:
+ pass
+
+ if name is None or name == "":
+ return HTTPError()
+
+ links = map(lambda x: x.strip(), links)
+ links = filter(lambda x: x != "", links)
+
+ pack = PYLOAD.add_package(name, links, queue)
+ if pw:
+ data = {"password": pw}
+ PYLOAD.set_package_data(pack, data)
+
+ return "success"
+
+
+@route("/json/remove_package/:id")
+@validate(id=int)
+@login_required('can_delete')
+def remove_package(id):
+ try:
+ PYLOAD.del_packages([id])
+ return "success"
+ except Exception, e:
+ return HTTPError()
+
+@route("/json/restart_package/:id")
+@validate(id=int)
+@login_required('can_add')
+def restart_package(id):
+ try:
+ PYLOAD.restart_package(id)
+ return "success"
+ except Exception:
+ print_exc()
+ return HTTPError()
+
+@route("/json/move_package/:dest/:id")
+@validate(dest=int, id=int)
+@login_required('can_add')
+def move_package(dest, id):
+ try:
+ PYLOAD.move_package(dest, id)
+ return "success"
+ except:
+ return HTTPError()
+
+@route("/json/edit_package", method="POST")
+@login_required('can_add')
+def edit_package():
+ try:
+ id = int(request.forms.get("pack_id"))
+ data = {"name": request.forms.get("pack_name"),
+ "folder": request.forms.get("pack_folder"),
+ "priority": request.forms.get("pack_prio"),
+ "password": request.forms.get("pack_pws")}
+
+ PYLOAD.set_package_data(id, data)
+ return "success"
+
+ except:
+ return HTTPError()
+
+@route("/json/set_captcha")
+@route("/json/set_captcha", method="POST")
+@login_required('can_add')
+def set_captcha():
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ try:
+ PYLOAD.set_captcha_result(request.forms["cap_id"], request.forms["cap_text"])
+ except:
+ pass
+
+ id, binary, typ = PYLOAD.get_captcha_task()
+
+ if id:
+ binary = base64.standard_b64encode(str(binary))
+ src = "data:image/%s;base64,%s" % (typ, binary)
+
+ return {'captcha': True, 'src': src, 'id': id}
+ else:
+ return {'captcha': False}
+
+
+@route("/json/delete_finished")
+@login_required('pyload.can_delete')
+def delete_finished():
+ return {"del": PYLOAD.delete_finished()}
+
+@route("/json/restart_failed")
+@login_required('pyload.can_delete')
+def restart_failed():
+ return PYLOAD.restart_failed() \ No newline at end of file
diff --git a/module/web/locale/cs/LC_MESSAGES/django.mo b/module/web/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..1eebf6ce1
--- /dev/null
+++ b/module/web/locale/cs/LC_MESSAGES/django.mo
Binary files differ
diff --git a/module/web/manage.py b/module/web/manage.py
deleted file mode 100755
index 34b964ffc..000000000
--- a/module/web/manage.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-from django.core.management import execute_manager
-
-try:
- import settings # Assumed to be in the same directory.
-except ImportError:
- import sys
- sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
- sys.exit(1)
-
-if __name__ == "__main__":
- execute_manager(settings) \ No newline at end of file
diff --git a/module/web/middlewares.py b/module/web/middlewares.py
new file mode 100644
index 000000000..745d7e6b5
--- /dev/null
+++ b/module/web/middlewares.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import gzip
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+class StripPathMiddleware(object):
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, e, h):
+ e['PATH_INFO'] = e['PATH_INFO'].rstrip('/')
+ return self.app(e, h)
+
+
+class PrefixMiddleware(object):
+ def __init__(self, app, prefix="/pyload"):
+ self.app = app
+ self.prefix = prefix
+
+ def __call__(self, e, h):
+ path = e["PATH_INFO"]
+ if path.startswith(self.prefix):
+ e['PATH_INFO'] = path.relace(self.prefix, "", 1)
+ return self.app(e, h)
+
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+# WSGI middleware
+# Gzip-encodes the response.
+
+class GZipMiddleWare(object):
+
+ def __init__(self, application, compress_level=6):
+ self.application = application
+ self.compress_level = int(compress_level)
+
+ def __call__(self, environ, start_response):
+ if 'gzip' not in environ.get('HTTP_ACCEPT_ENCODING', ''):
+ # nothing for us to do, so this middleware will
+ # be a no-op:
+ return self.application(environ, start_response)
+ response = GzipResponse(start_response, self.compress_level)
+ app_iter = self.application(environ,
+ response.gzip_start_response)
+ if app_iter is not None:
+ response.finish_response(app_iter)
+
+ return response.write()
+
+def header_value(headers, key):
+ for header, value in headers:
+ if key.lower() == header.lower():
+ return value
+
+def update_header(headers, key, value):
+ remove_header(headers, key)
+ headers.append((key, value))
+
+def remove_header(headers, key):
+ for header, value in headers:
+ if key.lower() == header.lower():
+ headers.remove((header, value))
+ break
+
+class GzipResponse(object):
+
+ def __init__(self, start_response, compress_level):
+ self.start_response = start_response
+ self.compress_level = compress_level
+ self.buffer = StringIO()
+ self.compressible = False
+ self.content_length = None
+ self.headers = ()
+
+ def gzip_start_response(self, status, headers, exc_info=None):
+ self.headers = headers
+ ct = header_value(headers,'content-type')
+ ce = header_value(headers,'content-encoding')
+ self.compressible = False
+ if ct and (ct.startswith('text/') or ct.startswith('application/')) \
+ and 'zip' not in ct:
+ self.compressible = True
+ if ce:
+ self.compressible = False
+ if self.compressible:
+ headers.append(('content-encoding', 'gzip'))
+ remove_header(headers, 'content-length')
+ self.headers = headers
+ self.status = status
+ return self.buffer.write
+
+ def write(self):
+ out = self.buffer
+ out.seek(0)
+ s = out.getvalue()
+ out.close()
+ return [s]
+
+ def finish_response(self, app_iter):
+ if self.compressible:
+ output = gzip.GzipFile(mode='wb', compresslevel=self.compress_level,
+ fileobj=self.buffer)
+ else:
+ output = self.buffer
+ try:
+ for s in app_iter:
+ output.write(s)
+ if self.compressible:
+ output.close()
+ finally:
+ if hasattr(app_iter, 'close'):
+ app_iter.close()
+ content_length = self.buffer.tell()
+ update_header(self.headers, "Content-Length" , str(content_length))
+ self.start_response(self.status, self.headers) \ No newline at end of file
diff --git a/module/web/pyload_app.py b/module/web/pyload_app.py
new file mode 100644
index 000000000..ab0cbfb00
--- /dev/null
+++ b/module/web/pyload_app.py
@@ -0,0 +1,483 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+from copy import deepcopy
+import datetime
+from datetime import datetime
+from itertools import chain
+from operator import itemgetter
+import os
+
+import sqlite3
+import time
+from os import listdir
+from os import stat
+from os.path import isdir
+from os.path import isfile
+from os.path import join
+from sys import getfilesystemencoding
+from hashlib import sha1
+from urllib import unquote
+
+from bottle import route, static_file, request, response, redirect, HTTPError
+
+from webinterface import PYLOAD, PROJECT_DIR
+
+from utils import render_to_response, parse_permissions, parse_userdata, formatSize, login_required
+from filters import relpath, quotepath, unquotepath
+
+# Helper
+
+def pre_processor():
+ s = request.environ.get('beaker.session')
+ user = parse_userdata(s)
+ perms = parse_permissions(s)
+ return {"user": user,
+ 'status': PYLOAD.status_server(),
+ 'captcha': PYLOAD.is_captcha_waiting(),
+ 'perms': perms}
+
+
+def get_sort_key(item):
+ return item[1]["order"]
+
+
+def base(messages):
+ return render_to_response('base.html', {'messages': messages}, [pre_processor])
+
+
+## Views
+
+@route('/media/:path#.+#')
+def server_static(path):
+ response.header['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + 60 * 60 * 24 * 7))
+ response.header['Cache-control'] = "public"
+ return static_file(path, root=join(PROJECT_DIR, "media"))
+
+@route('/favicon.ico')
+def favicon():
+ return static_file("favicon.ico", root=join(PROJECT_DIR, "media", "img"))
+
+@route('/login', method="GET")
+def login():
+ return render_to_response("login.html", proc=[pre_processor])
+
+@route("/login", method="POST")
+def login_post():
+ user = request.forms.get("username")
+ password = request.forms.get("password")
+
+ conn = sqlite3.connect('web.db')
+ c = conn.cursor()
+ c.execute('SELECT name, password, role, permission,template FROM "users" WHERE name=?', (user,))
+ r = c.fetchone()
+ c.close()
+ conn.commit()
+ conn.close()
+
+ if not r:
+ return render_to_response("login.html", {"errors": True}, [pre_processor])
+
+ salt = r[1][:5]
+ pw = r[1][5:]
+
+ hash = sha1(salt + password)
+ if hash.hexdigest() == pw:
+ s = request.environ.get('beaker.session')
+ s["authenticated"] = True
+ s["name"] = r[0]
+ s["role"] = r[2]
+ s["perms"] = r[3]
+ s["template"] = r[4]
+ s.save()
+
+ return redirect("/")
+
+
+ else:
+ return render_to_response("login.html", {"errors": True}, [pre_processor])
+
+@route("/logout")
+def logout():
+ s = request.environ.get('beaker.session')
+ s.delete()
+ return render_to_response("logout.html", proc=[pre_processor])
+
+
+@route("/")
+@route("/home")
+@login_required("can_see_dl")
+def home():
+ res = PYLOAD.status_downloads()
+
+ for link in res:
+ if link["status"] == 12:
+ link["information"] = "%s kB @ %s kB/s" % (link["size"] - link["kbleft"], link["speed"])
+
+ return render_to_response("home.html", {"res": res}, [pre_processor])
+
+
+@route("/queue")
+@login_required("can_see_dl")
+def queue():
+ queue = PYLOAD.get_queue_info()
+
+ data = zip(queue.keys(), queue.values())
+ data.sort(key=get_sort_key)
+
+ return render_to_response('queue.html', {'content': data}, [pre_processor])
+
+@route("/collector")
+@login_required('can_see_dl')
+def collector():
+ queue = PYLOAD.get_collector_info()
+
+ data = zip(queue.keys(), queue.values())
+ data.sort(key=get_sort_key)
+
+ return render_to_response('collector.html', {'content': data}, [pre_processor])
+
+@route("/downloads")
+@login_required('can_download')
+def downloads():
+ root = PYLOAD.get_conf_val("general", "download_folder")
+
+ if not isdir(root):
+ return base([_('Download directory not found.')])
+ data = {
+ 'folder': [],
+ 'files': []
+ }
+
+ for item in sorted(listdir(root)):
+ if isdir(join(root, item)):
+ folder = {
+ 'name': item,
+ 'path': item,
+ 'files': []
+ }
+ for file in sorted(listdir(join(root, item))):
+ try:
+ if isfile(join(root, item, file)):
+ folder['files'].append(file)
+ except:
+ pass
+
+ data['folder'].append(folder)
+ elif isfile(join(root, item)):
+ data['files'].append(item)
+
+ return render_to_response('downloads.html', {'files': data}, [pre_processor])
+
+@route("/downloads/get/:path#.+#")
+@login_required("can_download")
+def get_download(path):
+ path = unquote(path)
+ #@TODO some files can not be downloaded
+
+ root = PYLOAD.get_conf_val("general", "download_folder")
+
+ path = path.replace("..", "")
+ try:
+ return static_file(path, root)
+
+ except Exception, e:
+ print e
+ return HTTPError(404, "File not Found.")
+
+@route("/settings")
+@route("/settings", method="POST")
+@login_required('can_change_status')
+def config():
+ conf = PYLOAD.get_config()
+ plugin = PYLOAD.get_plugin_config()
+ accs = PYLOAD.get_accounts()
+ messages = []
+
+ for section in chain(conf.itervalues(), plugin.itervalues()):
+ for key, option in section.iteritems():
+ if key == "desc": continue
+
+ if ";" in option["type"]:
+ option["list"] = option["type"].split(";")
+
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ errors = []
+
+ for key, value in request.POST.iteritems():
+ if not "|" in key: continue
+ sec, skey, okey = key.split("|")[:]
+
+ if sec == "General":
+ if conf.has_key(skey):
+ if conf[skey].has_key(okey):
+ try:
+ if str(conf[skey][okey]['value']) != value:
+ PYLOAD.set_conf_val(skey, okey, value)
+ except Exception, e:
+ errors.append("%s | %s : %s" % (skey, okey, e))
+ else:
+ continue
+ else:
+ continue
+
+ elif sec == "Plugin":
+ if plugin.has_key(skey):
+ if plugin[skey].has_key(okey):
+ try:
+ if str(plugin[skey][okey]['value']) != value:
+ PYLOAD.set_conf_val(skey, okey, value, "plugin")
+ except Exception, e:
+ errors.append("%s | %s : %s" % (skey, okey, e))
+ else:
+ continue
+ else:
+ continue
+ elif sec == "Accounts":
+ if ";" in okey:
+ action, name = okey.split(";")
+ if action == "delete":
+ PYLOAD.remove_account(skey, name)
+
+ if okey == "newacc" and value:
+ # add account
+
+ pw = request.POST.get("Accounts|%s|newpw" % skey)
+ PYLOAD.update_account(skey, value, pw)
+
+ for pluginname, accdata in accs.iteritems():
+ for data in accdata:
+ newpw = request.POST.get("Accounts|%s|password;%s" % (pluginname, data["login"]), "").strip()
+ time = request.POST.get("Accounts|%s|time;%s" % (pluginname, data["login"]), "").strip()
+
+ if newpw or (time and (not data["options"].has_key("time") or [time] != data["options"]["time"])):
+ PYLOAD.update_account(pluginname, data["login"], newpw, {"time": [time]})
+
+ if errors:
+ messages.append(_("Error occured when setting the following options:"))
+ messages.append("")
+ messages += errors
+ else:
+ messages.append(_("All options were set correctly."))
+
+ accs = deepcopy(PYLOAD.get_accounts(False, False))
+ for accounts in accs.itervalues():
+ for data in accounts:
+ if data["trafficleft"] == -1:
+ data["trafficleft"] = _("unlimited")
+ elif not data["trafficleft"]:
+ data["trafficleft"] = _("not available")
+ else:
+ data["trafficleft"] = formatSize(data["trafficleft"])
+
+ if data["validuntil"] == -1:
+ data["validuntil"] = _("unlimited")
+ elif not data["validuntil"]:
+ data["validuntil"] = _("not available")
+ else:
+ t = time.localtime(data["validuntil"])
+ data["validuntil"] = time.strftime("%d.%m.%Y", t)
+
+ if data["options"].has_key("time"):
+ try:
+ data["time"] = data["options"]["time"][0]
+ except:
+ data["time"] = "invalid"
+
+
+ return render_to_response('settings.html',
+ {'conf': {'Plugin': plugin, 'General': conf, 'Accounts': accs}, 'errors': messages},
+ [pre_processor])
+
+@route("/package_ui.js")
+@login_required('can_see_dl')
+def package_ui():
+ response.header['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + 60 * 60 * 24 * 7))
+ response.header['Cache-control'] = "public"
+ return render_to_response('package_ui.js')
+
+
+@route("/filechooser")
+@route("/pathchooser")
+@route("/filechooser/:file#.+#")
+@route("/pathchooser/:path#.+#")
+@login_required('can_change_status')
+def path(file="", path=""):
+ if file:
+ type = "file"
+ else:
+ type = "folder"
+
+ path = os.path.normpath(unquotepath(path))
+
+ if os.path.isfile(path):
+ oldfile = path
+ path = os.path.dirname(path)
+ else:
+ oldfile = ''
+
+ abs = False
+
+ if os.path.isdir(path):
+ if os.path.isabs(path):
+ cwd = os.path.abspath(path)
+ abs = True
+ else:
+ cwd = relpath(path)
+ else:
+ cwd = os.getcwd()
+
+ try:
+ cwd = cwd.encode("utf8")
+ except:
+ pass
+
+ cwd = os.path.normpath(os.path.abspath(cwd))
+ parentdir = os.path.dirname(cwd)
+ if not abs:
+ if os.path.abspath(cwd) == "/":
+ cwd = relpath(cwd)
+ else:
+ cwd = relpath(cwd) + os.path.sep
+ parentdir = relpath(parentdir) + os.path.sep
+
+ if os.path.abspath(cwd) == "/":
+ parentdir = ""
+
+ try:
+ folders = os.listdir(cwd)
+ except:
+ folders = []
+
+ files = []
+
+ for f in folders:
+ try:
+ f = f.decode(getfilesystemencoding())
+ data = {}
+ data['name'] = f
+ data['fullpath'] = join(cwd, f)
+ data['sort'] = data['fullpath'].lower()
+ data['modified'] = datetime.fromtimestamp(int(os.path.getmtime(join(cwd, f))))
+ data['ext'] = os.path.splitext(f)[1]
+ except:
+ continue
+
+ if os.path.isdir(join(cwd, f)):
+ data['type'] = 'dir'
+ else:
+ data['type'] = 'file'
+
+ if os.path.isfile(join(cwd, f)):
+ data['size'] = os.path.getsize(join(cwd, f))
+
+ power = 0
+ while (data['size']/1024) > 0.3:
+ power += 1
+ data['size'] /= 1024.
+ units = ('', 'K','M','G','T')
+ data['unit'] = units[power]+'Byte'
+ else:
+ data['size'] = ''
+
+ files.append(data)
+
+ files = sorted(files, key=itemgetter('type', 'sort'))
+
+ return render_to_response('pathchooser.html', {'cwd': cwd, 'files': files, 'parentdir': parentdir, 'type': type, 'oldfile': oldfile, 'absolute': abs}, [])
+
+@route("/logs")
+@route("/logs/:item")
+@route("/logs/:item", method="POST")
+@login_required('can_see_logs')
+def logs(item=-1):
+ s = request.environ.get('beaker.session')
+
+ perpage = s.get('perpage', 34)
+ reversed = s.get('reversed', False)
+
+ warning = ""
+ conf = PYLOAD.get_config()
+ if not conf['log']['file_log']['value']:
+ warning = "Warning: File log is disabled, see settings page."
+
+ perpage_p = ((20,20), (34, 34), (40, 40), (100, 100), (0,'all'))
+ fro = None
+
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ try:
+ fro = datetime.strptime(request.forms['from'], '%d.%m.%Y %H:%M:%S')
+ except:
+ pass
+ try:
+ perpage = int(request.forms['perpage'])
+ s['perpage'] = perpage
+
+ reversed = bool(request.forms.get('reversed', False))
+ s['reversed'] = reversed
+ except:
+ pass
+
+ s.save()
+
+ try:
+ item = int(item)
+ except:
+ pass
+
+ log = PYLOAD.get_log()
+ if not perpage:
+ item = 0
+
+ if item < 1 or type(item) is not int:
+ item = 1 if len(log) - perpage + 1 < 1 else len(log) - perpage + 1
+
+ if type(fro) is datetime: # we will search for datetime
+ item = -1
+
+ data = []
+ counter = 0
+ perpagecheck = 0
+ for l in log:
+ counter += 1
+
+ if counter >= item:
+ try:
+ date,time,level,message = l.split(" ", 3)
+ dtime = datetime.strptime(date+' '+time, '%d.%m.%Y %H:%M:%S')
+ except:
+ dtime = None
+ date = '?'
+ time = ' '
+ level = '?'
+ message = l
+ if item == -1 and dtime is not None and fro <= dtime:
+ item = counter #found our datetime
+ if item >= 0:
+ data.append({'line': counter, 'date': date+" "+time, 'level':level, 'message': message})
+ perpagecheck += 1
+ if fro is None and dtime is not None: #if fro not set set it to first showed line
+ fro = dtime
+ if perpagecheck >= perpage > 0:
+ break
+
+ if fro is None: #still not set, empty log?
+ fro = datetime.now()
+ if reversed:
+ data.reverse()
+ return render_to_response('logs.html', {'warning': warning, 'log': data, 'from': fro.strftime('%d.%m.%Y %H:%M:%S'), 'reversed': reversed, 'perpage':perpage, 'perpage_p':sorted(perpage_p), 'iprev': 1 if item - perpage < 1 else item - perpage, 'inext': (item + perpage) if item+perpage < len(log) else item}, [pre_processor]) \ No newline at end of file
diff --git a/module/web/run_fcgi.py b/module/web/run_fcgi.py
deleted file mode 100644
index 8091de5ea..000000000
--- a/module/web/run_fcgi.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-
-from flup.server.fcgi_base import BaseFCGIServer
-from flup.server.fcgi_base import FCGI_RESPONDER
-from flup.server.threadedserver import ThreadedServer
-
-
-os.environ["DJANGO_SETTINGS_MODULE"] = 'settings'
-
-def handle(*args, **options):
- from django.conf import settings
- from django.utils import translation
- # Activate the current language, because it won't get activated later.
- try:
- translation.activate(settings.LANGUAGE_CODE)
- except AttributeError:
- pass
- #from django.core.servers.fastcgi import runfastcgi
- runfastcgi(args)
-
-
-FASTCGI_OPTIONS = {
- 'protocol': 'fcgi',
- 'host': None,
- 'port': None,
- 'socket': None,
- 'method': 'fork',
- 'daemonize': None,
- 'workdir': '/',
- 'pidfile': None,
- 'maxspare': 5,
- 'minspare': 2,
- 'maxchildren': 50,
- 'maxrequests': 0,
- 'debug': None,
- 'outlog': None,
- 'errlog': None,
- 'umask': None,
-}
-
-
-def runfastcgi(argset=[], **kwargs):
- options = FASTCGI_OPTIONS.copy()
- options.update(kwargs)
- for x in argset:
- if "=" in x:
- k, v = x.split('=', 1)
- else:
- k, v = x, True
- options[k.lower()] = v
-
- try:
- import flup
- except ImportError, e:
- print >> sys.stderr, "ERROR: %s" % e
- print >> sys.stderr, " Unable to load the flup package. In order to run django"
- print >> sys.stderr, " as a FastCGI application, you will need to get flup from"
- print >> sys.stderr, " http://www.saddi.com/software/flup/ If you've already"
- print >> sys.stderr, " installed flup, then make sure you have it in your PYTHONPATH."
- return False
-
- flup_module = 'server.' + options['protocol']
-
- if options['method'] in ('prefork', 'fork'):
- wsgi_opts = {
- 'maxSpare': int(options["maxspare"]),
- 'minSpare': int(options["minspare"]),
- 'maxChildren': int(options["maxchildren"]),
- 'maxRequests': int(options["maxrequests"]),
- }
- flup_module += '_fork'
- elif options['method'] in ('thread', 'threaded'):
- wsgi_opts = {
- 'maxSpare': int(options["maxspare"]),
- 'minSpare': int(options["minspare"]),
- 'maxThreads': int(options["maxchildren"]),
- }
- else:
- print "ERROR: Implementation must be one of prefork or thread."
-
- wsgi_opts['debug'] = options['debug'] is not None
-
- #try:
- # module = importlib.import_module('.%s' % flup_module, 'flup')
- # WSGIServer = module.WSGIServer
- #except:
- # print "Can't import flup." + flup_module
- # return False
-
- # Prep up and go
- from django.core.handlers.wsgi import WSGIHandler
-
- if options["host"] and options["port"] and not options["socket"]:
- wsgi_opts['bindAddress'] = (options["host"], int(options["port"]))
- elif options["socket"] and not options["host"] and not options["port"]:
- wsgi_opts['bindAddress'] = options["socket"]
- elif not options["socket"] and not options["host"] and not options["port"]:
- wsgi_opts['bindAddress'] = None
- else:
- return fastcgi_help("Invalid combination of host, port, socket.")
-
- daemon_kwargs = {}
- if options['outlog']:
- daemon_kwargs['out_log'] = options['outlog']
- if options['errlog']:
- daemon_kwargs['err_log'] = options['errlog']
- if options['umask']:
- daemon_kwargs['umask'] = int(options['umask'])
-
- ownWSGIServer(WSGIHandler(), **wsgi_opts).run()
-
-class ownThreadedServer(ThreadedServer):
- def _installSignalHandlers(self):
- return
-
- def _restoreSignalHandlers(self):
- return
-
-
-class ownWSGIServer(BaseFCGIServer, ownThreadedServer):
-
- def __init__(self, application, environ=None,
- multithreaded=True, multiprocess=False,
- bindAddress=None, umask=None, multiplexed=False,
- debug=True, roles=(FCGI_RESPONDER,), forceCGI=False, **kw):
- BaseFCGIServer.__init__(self, application,
- environ=environ,
- multithreaded=multithreaded,
- multiprocess=multiprocess,
- bindAddress=bindAddress,
- umask=umask,
- multiplexed=multiplexed,
- debug=debug,
- roles=roles,
- forceCGI=forceCGI)
- for key in ('jobClass', 'jobArgs'):
- if kw.has_key(key):
- del kw[key]
- ownThreadedServer.__init__(self, jobClass=self._connectionClass,
- jobArgs=(self,), **kw)
-
- def _isClientAllowed(self, addr):
- return self._web_server_addrs is None or \
- (len(addr) == 2 and addr[0] in self._web_server_addrs)
-
- def run(self):
- """
- The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if
- SIGHUP was received, False otherwise.
- """
- self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS')
- if self._web_server_addrs is not None:
- self._web_server_addrs = map(lambda x: x.strip(),
- self._web_server_addrs.split(','))
-
- sock = self._setupSocket()
-
- ret = ownThreadedServer.run(self, sock)
-
- self._cleanupSocket(sock)
-
- return ret
-
-if __name__ == "__main__":
- handle(*sys.argv[1:])
-
diff --git a/module/web/run_server.py b/module/web/run_server.py
deleted file mode 100755
index 2dc97353a..000000000
--- a/module/web/run_server.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-import os
-import sys
-import django
-from django.core.servers.basehttp import AdminMediaHandler, WSGIServerException, WSGIServer, WSGIRequestHandler
-from django.core.handlers.wsgi import WSGIHandler
-
-os.environ["DJANGO_SETTINGS_MODULE"] = 'settings'
-
-class Output:
- def __init__(self, stream):
- self.stream = stream
- def write(self, data): # Do nothing
- return None
- #self.stream.write(data)
- #self.stream.flush()
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-#sys.stderr = Output(sys.stderr)
-#sys.stdout = Output(sys.stdout)
-
-def handle(* args):
- try:
- if len(args) == 1:
- try:
- addr, port = args[0].split(":")
- except:
- addr = "127.0.0.1"
- port = args[0]
- else:
- addr = args[0]
- port = args[1]
- except:
- addr = '127.0.0.1'
- port = '8000'
-
- #print addr, port
-
- admin_media_path = ''
- shutdown_message = ''
- quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C'
-
- from django.conf import settings
- from django.utils import translation
-
- #print "Django version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE)
- #print "Development server is running at http://%s:%s/" % (addr, port)
- #print "Quit the server with %s." % quit_command
-
- translation.activate(settings.LANGUAGE_CODE)
-
- try:
- handler = AdminMediaHandler(WSGIHandler(), admin_media_path)
- run(addr, int(port), handler)
- #@TODO catch unimportant Broken Pipe Errors
-
- except WSGIServerException, e:
- # Use helpful error messages instead of ugly tracebacks.
- ERRORS = {
- 13: "You don't have permission to access that port.",
- 98: "That port is already in use.",
- 99: "That IP address can't be assigned-to.",
- }
- try:
- error_text = ERRORS[e.args[0].args[0]]
- except (AttributeError, KeyError):
- error_text = str(e)
- sys.stderr.write(("Error: %s" % error_text) + '\n')
- # Need to use an OS exit because sys.exit doesn't work in a thread
- #os._exit(1)
- except KeyboardInterrupt:
- if shutdown_message:
- print shutdown_message
- sys.exit(0)
-
-class ownRequestHandler(WSGIRequestHandler):
- def log_message(self, format, *args):
- return
-
-
-def run(addr, port, wsgi_handler):
- server_address = (addr, port)
- httpd = WSGIServer(server_address, ownRequestHandler)
- httpd.set_app(wsgi_handler)
- httpd.serve_forever()
-
-if __name__ == "__main__":
- handle(*sys.argv[1:])
diff --git a/module/web/settings.py b/module/web/settings.py
deleted file mode 100644
index 5a836e11c..000000000
--- a/module/web/settings.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# -*- coding: utf-8 -*-
-# Django settings for pyload project.
-
-DEBUG = True
-TEMPLATE_DEBUG = DEBUG
-
-import os
-import sys
-import django
-
-SERVER_VERSION = "0.4.4"
-
-PROJECT_DIR = os.path.dirname(__file__)
-
-#chdir(dirname(abspath(__file__)) + sep)
-
-PYLOAD_DIR = os.path.join(PROJECT_DIR,"..","..")
-
-sys.path.append(PYLOAD_DIR)
-
-
-sys.path.append(os.path.join(PYLOAD_DIR, "module"))
-
-import InitHomeDir
-sys.path.append(pypath)
-
-config = None
-#os.chdir(PROJECT_DIR) # UNCOMMENT FOR LOCALE GENERATION
-
-
-try:
- import module.web.ServerThread
- if not module.web.ServerThread.core:
- raise Exception
- PYLOAD = module.web.ServerThread.core.server_methods
- config = module.web.ServerThread.core.config
-except:
- import xmlrpclib
- ssl = ""
-
- from module.ConfigParser import ConfigParser
- config = ConfigParser()
-
- if config.get("ssl", "activated"):
- ssl = "s"
-
- server_url = "http%s://%s:%s@%s:%s/" % (
- ssl,
- config.username,
- config.password,
- config.get("remote", "listenaddr"),
- config.get("remote", "port")
- )
-
- PYLOAD = xmlrpclib.ServerProxy(server_url, allow_none=True)
-
-DEBUG = TEMPLATE_DEBUG = config.get("general","debug_mode")
-
-from module.JsEngine import JsEngine
-JS = JsEngine()
-
-TEMPLATE = config.get('webinterface','template')
-DL_ROOT = os.path.join(PYLOAD_DIR, config.get('general','download_folder'))
-LOG_ROOT = os.path.join(PYLOAD_DIR, config.get('log','log_folder'))
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
- )
-
-MANAGERS = ADMINS
-
-DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-#DATABASE_NAME = os.path.join(PROJECT_DIR, 'pyload.db') # Or path to database file if using sqlite3.
-DATABASE_NAME = 'pyload.db' # Or path to database file if using sqlite3.
-DATABASE_USER = '' # Not used with sqlite3.
-DATABASE_PASSWORD = '' # Not used with sqlite3.
-DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
-DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-if (django.VERSION[0] > 1 or django.VERSION[1] > 1) and os.name != "nt":
- zone = None
-else:
- zone = 'Europe'
-TIME_ZONE = zone
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = config.get("general","language")
-
-SITE_ID = 1
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = True
-
-# Absolute path to the directory that holds media.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = os.path.join(PROJECT_DIR, "media/")
-
-
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a
-# trailing slash if there is a path component (optional in other cases).
-# Examples: "http://media.lawrence.com", "http://example.com/media/"
-
-#MEDIA_URL = 'http://localhost:8000/media'
-MEDIA_URL = '/media/' + config.get('webinterface','template') + '/'
-#MEDIA_URL = os.path.join(PROJECT_DIR, "media/")
-
-LOGIN_REDIRECT_URL = "/"
-
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
-# trailing slash.
-# Examples: "http://foo.com/media/", "/media/".
-ADMIN_MEDIA_PREFIX = '/admin/media/'
-
-# Make this unique, and don't share it with anybody.
-SECRET_KEY = '+u%%1t&c7!e$0$*gu%w2$@to)h0!&x-r*9e+-=wa4*zxat%x^t'
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.load_template_source',
- 'django.template.loaders.app_directories.load_template_source',
- # 'django.template.loaders.eggs.load_template_source',
- )
-
-
-MIDDLEWARE_CLASSES = (
- 'django.middleware.gzip.GZipMiddleware',
- 'django.middleware.http.ConditionalGetMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.locale.LocaleMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- #'django.contrib.csrf.middleware.CsrfViewMiddleware',
- 'django.contrib.csrf.middleware.CsrfResponseMiddleware'
- )
-
-ROOT_URLCONF = 'urls'
-
-TEMPLATE_DIRS = (
- # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
- # Always use forward slashes, even on Windows.
- # Don't forget to use absolute paths, not relative paths.
- os.path.join(PROJECT_DIR, "templates"),
- )
-
-INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- #'django.contrib.sites',
- 'django.contrib.admin',
- 'pyload',
- 'ajax',
- 'cnl',
- )
-
-
-AUTH_PROFILE_MODULE = 'pyload.UserProfile'
-LOGIN_URL = '/login/'
diff --git a/module/web/syncdb.py b/module/web/syncdb.py
deleted file mode 100644
index 669f22681..000000000
--- a/module/web/syncdb.py
+++ /dev/null
@@ -1,152 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-
-os.environ["DJANGO_SETTINGS_MODULE"] = 'settings'
-sys.path.append(os.path.join(pypath, "module", "web"))
-
-from django.conf import settings
-from django.core.management.base import NoArgsCommand
-from django.core.management.color import no_style
-from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal
-from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
-from django.utils.datastructures import SortedDict
-from django.utils.importlib import import_module
-
-
-
-def handle_noargs(**options):
-
- verbosity = int(options.get('verbosity', 1))
- interactive = False
- show_traceback = options.get('traceback', False)
-
- style = no_style()
-
- # Import the 'management' module within each installed app, to register
- # dispatcher events.
- for app_name in settings.INSTALLED_APPS:
- try:
- import_module('.management', app_name)
- except ImportError, exc:
- # This is slightly hackish. We want to ignore ImportErrors
- # if the "management" module itself is missing -- but we don't
- # want to ignore the exception if the management module exists
- # but raises an ImportError for some reason. The only way we
- # can do this is to check the text of the exception. Note that
- # we're a bit broad in how we check the text, because different
- # Python implementations may not use the same text.
- # CPython uses the text "No module named management"
- # PyPy uses "No module named myproject.myapp.management"
- msg = exc.args[0]
- if not msg.startswith('No module named') or 'management' not in msg:
- raise
-
- db = options.get('database', DEFAULT_DB_ALIAS)
- connection = connections[db]
- cursor = connection.cursor()
-
- # Get a list of already installed *models* so that references work right.
- tables = connection.introspection.table_names()
- seen_models = connection.introspection.installed_models(tables)
- created_models = set()
- pending_references = {}
-
- # Build the manifest of apps and models that are to be synchronized
- all_models = [
- (app.__name__.split('.')[-2],
- [m for m in models.get_models(app, include_auto_created=True)
- if router.allow_syncdb(db, m)])
- for app in models.get_apps()
- ]
- def model_installed(model):
- opts = model._meta
- converter = connection.introspection.table_name_converter
- return not ((converter(opts.db_table) in tables) or
- (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables))
-
- manifest = SortedDict(
- (app_name, filter(model_installed, model_list))
- for app_name, model_list in all_models
- )
-
- # Create the tables for each model
- for app_name, model_list in manifest.items():
- for model in model_list:
- # Create the model's database table, if it doesn't already exist.
- if verbosity >= 2:
- print "Processing %s.%s model" % (app_name, model._meta.object_name)
- sql, references = connection.creation.sql_create_model(model, style, seen_models)
- seen_models.add(model)
- created_models.add(model)
- for refto, refs in references.items():
- pending_references.setdefault(refto, []).extend(refs)
- if refto in seen_models:
- sql.extend(connection.creation.sql_for_pending_references(refto, style, pending_references))
- sql.extend(connection.creation.sql_for_pending_references(model, style, pending_references))
- if verbosity >= 1 and sql:
- print "Creating table %s" % model._meta.db_table
- for statement in sql:
- cursor.execute(statement)
- tables.append(connection.introspection.table_name_converter(model._meta.db_table))
-
-
- transaction.commit_unless_managed(using=db)
-
- # Send the post_syncdb signal, so individual apps can do whatever they need
- # to do at this point.
- emit_post_sync_signal(created_models, verbosity, interactive, db)
-
- # The connection may have been closed by a syncdb handler.
- cursor = connection.cursor()
-
- # Install custom SQL for the app (but only if this
- # is a model we've just created)
- for app_name, model_list in manifest.items():
- for model in model_list:
- if model in created_models:
- custom_sql = custom_sql_for_model(model, style, connection)
- if custom_sql:
- if verbosity >= 1:
- print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name)
- try:
- for sql in custom_sql:
- cursor.execute(sql)
- except Exception, e:
- sys.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
- (app_name, model._meta.object_name, e))
- if show_traceback:
- import traceback
- traceback.print_exc()
- transaction.rollback_unless_managed(using=db)
- else:
- transaction.commit_unless_managed(using=db)
- else:
- if verbosity >= 2:
- print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name)
-
- # Install SQL indicies for all newly created models
- for app_name, model_list in manifest.items():
- for model in model_list:
- if model in created_models:
- index_sql = connection.creation.sql_indexes_for_model(model, style)
- if index_sql:
- if verbosity >= 1:
- print "Installing index for %s.%s model" % (app_name, model._meta.object_name)
- try:
- for sql in index_sql:
- cursor.execute(sql)
- except Exception, e:
- sys.stderr.write("Failed to install index for %s.%s model: %s\n" % \
- (app_name, model._meta.object_name, e))
- transaction.rollback_unless_managed(using=db)
- else:
- transaction.commit_unless_managed(using=db)
-
- #from django.core.management import call_command
- #call_command('loaddata', 'initial_data', verbosity=verbosity, database=db)
-
-if __name__ == "__main__":
- handle_noargs() \ No newline at end of file
diff --git a/module/web/syncdb_django11.py b/module/web/syncdb_django11.py
deleted file mode 100644
index c579718e0..000000000
--- a/module/web/syncdb_django11.py
+++ /dev/null
@@ -1,154 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-
-os.environ["DJANGO_SETTINGS_MODULE"] = 'settings'
-sys.path.append(os.path.join(pypath, "module", "web"))
-
-from django.core.management.base import NoArgsCommand
-from django.core.management.color import no_style
-from django.utils.importlib import import_module
-from optparse import make_option
-
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
-
-def handle_noargs(**options):
- from django.db import connection, transaction, models
- from django.conf import settings
- from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal
-
- verbosity = int(options.get('verbosity', 1))
- interactive = False
- show_traceback = options.get('traceback', False)
-
- style = no_style()
-
- # Import the 'management' module within each installed app, to register
- # dispatcher events.
- for app_name in settings.INSTALLED_APPS:
- try:
- import_module('.management', app_name)
- except ImportError, exc:
- # This is slightly hackish. We want to ignore ImportErrors
- # if the "management" module itself is missing -- but we don't
- # want to ignore the exception if the management module exists
- # but raises an ImportError for some reason. The only way we
- # can do this is to check the text of the exception. Note that
- # we're a bit broad in how we check the text, because different
- # Python implementations may not use the same text.
- # CPython uses the text "No module named management"
- # PyPy uses "No module named myproject.myapp.management"
- msg = exc.args[0]
- if not msg.startswith('No module named') or 'management' not in msg:
- raise
-
- cursor = connection.cursor()
-
- # Get a list of already installed *models* so that references work right.
- tables = connection.introspection.table_names()
- seen_models = connection.introspection.installed_models(tables)
- created_models = set()
- pending_references = {}
-
- # Create the tables for each model
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- model_list = models.get_models(app)
- for model in model_list:
- # Create the model's database table, if it doesn't already exist.
- if verbosity >= 2:
- print "Processing %s.%s model" % (app_name, model._meta.object_name)
- if connection.introspection.table_name_converter(model._meta.db_table) in tables:
- continue
- sql, references = connection.creation.sql_create_model(model, style, seen_models)
- seen_models.add(model)
- created_models.add(model)
- for refto, refs in references.items():
- pending_references.setdefault(refto, []).extend(refs)
- if refto in seen_models:
- sql.extend(connection.creation.sql_for_pending_references(refto, style, pending_references))
- sql.extend(connection.creation.sql_for_pending_references(model, style, pending_references))
- if verbosity >= 1 and sql:
- print "Creating table %s" % model._meta.db_table
- for statement in sql:
- cursor.execute(statement)
- tables.append(connection.introspection.table_name_converter(model._meta.db_table))
-
- # Create the m2m tables. This must be done after all tables have been created
- # to ensure that all referred tables will exist.
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- model_list = models.get_models(app)
- for model in model_list:
- if model in created_models:
- sql = connection.creation.sql_for_many_to_many(model, style)
- if sql:
- if verbosity >= 2:
- print "Creating many-to-many tables for %s.%s model" % (app_name, model._meta.object_name)
- for statement in sql:
- cursor.execute(statement)
-
- transaction.commit_unless_managed()
-
- # Send the post_syncdb signal, so individual apps can do whatever they need
- # to do at this point.
- emit_post_sync_signal(created_models, verbosity, interactive)
-
- # The connection may have been closed by a syncdb handler.
- cursor = connection.cursor()
-
- # Install custom SQL for the app (but only if this
- # is a model we've just created)
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- for model in models.get_models(app):
- if model in created_models:
- custom_sql = custom_sql_for_model(model, style)
- if custom_sql:
- if verbosity >= 1:
- print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name)
- try:
- for sql in custom_sql:
- cursor.execute(sql)
- except Exception, e:
- sys.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
- (app_name, model._meta.object_name, e))
- if show_traceback:
- import traceback
- traceback.print_exc()
- transaction.rollback_unless_managed()
- else:
- transaction.commit_unless_managed()
- else:
- if verbosity >= 2:
- print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name)
- # Install SQL indicies for all newly created models
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- for model in models.get_models(app):
- if model in created_models:
- index_sql = connection.creation.sql_indexes_for_model(model, style)
- if index_sql:
- if verbosity >= 1:
- print "Installing index for %s.%s model" % (app_name, model._meta.object_name)
- try:
- for sql in index_sql:
- cursor.execute(sql)
- except Exception, e:
- sys.stderr.write("Failed to install index for %s.%s model: %s\n" % \
- (app_name, model._meta.object_name, e))
- transaction.rollback_unless_managed()
- else:
- transaction.commit_unless_managed()
-
- # Install the 'initial_data' fixture, using format discovery
- #from django.core.management import call_command
- #call_command('loaddata', 'initial_data', verbosity=verbosity)
-
-if __name__ == "__main__":
- handle_noargs() \ No newline at end of file
diff --git a/module/web/templates/jinja/default/base.html b/module/web/templates/jinja/default/base.html
new file mode 100644
index 000000000..04c6dfbad
--- /dev/null
+++ b/module/web/templates/jinja/default/base.html
@@ -0,0 +1,317 @@
+<?xml version="1.0" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<link rel="stylesheet" type="text/css" href="/media/default/css/default.css"/>
+
+<link rel="stylesheet" type="text/css" href="/media/default/css/window.css"/>
+
+<script type="text/javascript" src="/media/default/js/funktions.js"></script>
+<script type="text/javascript" src="/media/default/js/mootools-1.2.5-core.js"></script>
+<script type="text/javascript" src="/media/default/js/mootools-1.2.4.4-more.js"></script>
+
+<title>{% block title %}pyLoad {{_("Webinterface")}}{% endblock %}</title>
+
+<script type="text/javascript">
+var add_bg, add_box, cap_box, cap_info;
+document.addEvent("domready", function(){
+
+ add_bg = new Fx.Tween($('add_bg'));
+ add_box = new Fx.Tween($('add_box'));
+ cap_box = new Fx.Tween($('cap_box'));
+
+ add_bg.set("opacity", 0);
+ add_box.set("opacity", 0);
+ cap_box.set("opacity", 0);
+
+
+ $('add_form').onsubmit=function() {
+ $('add_form').target = 'upload_target';
+ if ($('add_name').value == "" && $('add_file').value != " "){
+ alert("{{_("Please Enter a packagename.")}}");
+ return false
+ }else{
+ out();
+ }
+ };
+
+ $('add_reset').addEvent('click', function(){
+ out();
+ });
+
+ var jsonStatus = new Request.JSON({
+ url: "/json/status",
+ onSuccess: LoadJsonToContent,
+ secure: false,
+ async: true,
+ initialDelay: 0,
+ delay: 4000,
+ limit: 30000
+ });
+
+ $('action_play').addEvent('click', function(){
+ new Request({method: 'get', url: '/json/unpause'}).send();
+ });
+
+
+ $('action_cancel').addEvent('click', function(){
+ new Request({method: 'get', url: '/json/cancel'}).send();
+ });
+
+
+ $('action_stop').addEvent('click', function(){
+ new Request({method: 'get', url: '/json/pause'}).send();
+ });
+
+ $('cap_info').addEvent('click', function(){
+ load_cap("get", "");
+ show_cap();
+ });
+
+ $('cap_reset').addEvent('click', function(){
+ hide_cap()
+ });
+
+ $('cap_form').addEvent('submit', function(e){
+ submit_cap();
+ e.stop()
+ });
+
+ jsonStatus.startTimer();
+
+});
+
+function LoadJsonToContent(data)
+{
+ $("speed").set('text', Math.round(data.speed*100)/100);
+ $("aktiv").set('text', data.activ);
+ $("aktiv_from").set('text', data.queue);
+
+ if (data.captcha){
+ $("cap_info").setStyle('display', 'inline');
+ }else{
+ $("cap_info").setStyle('display', 'none');
+ }
+
+ if (data.download) {
+ $("time").set('text', " {{_("on")}}");
+ $("time").setStyle('background-color', "#8ffc25");
+
+ }else{
+ $("time").set('text', " {{_("off")}}");
+ $("time").setStyle('background-color', "#fc6e26");
+ }
+
+ if (data.reconnect){
+ $("reconnect").set('text', " {{_("on")}}");
+ $("reconnect").setStyle('background-color', "#8ffc25");
+ }
+ else{
+ $("reconnect").set('text', " {{_("off")}}");
+ $("reconnect").setStyle('background-color', "#fc6e26");
+ }
+}
+function bg_show(){
+ $("add_bg").setStyle('display', 'block');
+ add_bg.start('opacity',0.8);
+}
+
+function bg_hide(){
+ add_bg.start('opacity',0).chain(function(){
+ $('add_bg').setStyle('display', 'none');
+ });
+}
+
+function show(){
+ bg_show();
+ $("add_form").reset();
+ $("add_box").setStyle('display', 'block');
+ add_box.start('opacity',1)
+}
+
+function out(){
+ bg_hide();
+ add_box.start('opacity',0).chain(function(){
+ $('add_box').setStyle('display', 'none');
+ });
+}
+function show_cap(){
+ bg_show();
+ $("cap_box").setStyle('display', 'block');
+ cap_box.start('opacity',1)
+}
+
+function hide_cap(){
+ bg_hide();
+ cap_box.start('opacity',0).chain(function(){
+ $('cap_box').setStyle('display', 'none');
+ });
+}
+
+function load_cap(method, post){
+ new Request.JSON({
+ url: "/json/set_captcha",
+ onSuccess: function(data){
+ if (data.captcha){
+ $('cap_img').set('src', data.src);
+ $('cap_span').setStyle('display', 'block');
+ $$('#cap_form p')[0].set('text', '{{_("Please read the text on the captcha.")}}');
+ $('cap_id').set('value', data.id);
+ } else{
+ $('cap_img').set('src', '');
+ $('cap_span').setStyle('display', 'none');
+ $$('#cap_form p')[0].set('text', '{{_("No Captchas to read.")}}');
+ }
+ },
+ secure: false,
+ async: true,
+ method: method
+ }).send(post);
+}
+
+function submit_cap(){
+ load_cap("post", "cap_id="+ $('cap_id').get('value') +"&cap_text=" + $('cap_text').get('value') );
+ $('cap_text').set('value', '');
+ return false;
+}
+
+
+function AddBox()
+{
+ if ($("add_box").getStyle("display") == "hidden" || $("add_box").getStyle("display") == "none" || $("add_box").getStyle("opacity" == 0))
+ {
+ show();
+ }
+ else
+ {
+ out();
+ }
+}
+
+</script>
+
+{% block head %}
+{% endblock %}
+</head>
+<body>
+<a class="anchor" name="top" id="top"></a>
+
+<div id="head-panel">
+
+ <div id="head-search-and-login">
+
+ {% if user.is_authenticated %}
+
+<span id="cap_info" style="display: {% if captcha %}inline{%else%}none{% endif %}">
+<img src="/media/default/img/images.png" alt="Captcha:" style="vertical-align:middle; margin:2px" />
+<span style="font-weight: bold; cursor: pointer; margin-right: 2px;">{{_("Captcha waiting")}}</span>
+</span>
+
+<img src="/media/default/img/head-login.png" alt="User:" style="vertical-align:middle; margin:2px" /><span style="padding-right: 2px;">{{user.name}}</span>
+ <ul id="user-actions">
+ <li><a href="/logout" class="action logout" rel="nofollow">{{_("Logout")}}</a></li>
+ {% if user.is_staff %}
+ <li><a href="/admin" class="action profile" rel="nofollow">{{_("Administrate")}}</a></li>
+ {% endif %}
+
+ </ul>
+{% else %}
+ <span style="padding-right: 2px;">{{_("Please Login!")}}</span>
+{% endif %}
+
+ </div>
+
+ <a href="/"><img id="head-logo" src="/media/default/img/pyload-logo-edited3.5-new-font-small.png" alt="pyLoad" /></a>
+
+ <div id="head-menu">
+ <ul>
+
+ {% block menu %}
+ <li class="selected">
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt="" /> {{_("Home")}}</a>
+ </li>
+ <li>
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt="" /> {{_("Queue")}}</a>
+ </li>
+ <li>
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png" alt="" /> {{_("Collector")}}</a>
+ </li>
+ <li>
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png" alt="" /> {{_("Downloads")}}</a>
+ </li>
+ <li class="right">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-index.png" alt="" />{{_("Logs")}}</a>
+ </li>
+ <li class="right">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-config.png" alt="" />{{_("Config")}}</a>
+ </li>
+ {% endblock %}
+
+ </ul>
+ </div>
+
+ <div style="clear:both;"></div>
+</div>
+
+{% if perms.can_change_status %}
+<ul id="page-actions2">
+ <li id="action_play"><a href="#" class="action play" accesskey="o" rel="nofollow">{{_("Start")}}</a></li>
+ <li id="action_stop"><a href="#" class="action stop" accesskey="o" rel="nofollow">{{_("Stop")}}</a></li>
+ <li id="action_cancel"><a href="#" class="action cancel" accesskey="o" rel="nofollow">{{_("Cancel")}}</a></li>
+ <li id="action_add"><a href="javascript:AddBox();" class="action add" accesskey="o" rel="nofollow" >{{_("Add")}}</a></li>
+</ul>
+{% endif %}
+
+{% if perms.can_see_dl %}
+<ul id="page-actions">
+ <li><span class="time">{{_("Download:")}}</span><a id="time" style=" background-color: {% if status.download %}#8ffc25{% else %} #fc6e26{% endif %}; padding-left: 0cm; padding-right: 0.1cm; "> {% if status.download %}{{_("on")}}{% else %}{{_("off")}}{% endif %}</a></li>
+ <li><span class="reconnect">{{_("Reconnect:")}}</span><a id="reconnect" style=" background-color: {% if status.reconnect %}#8ffc25{% else %} #fc6e26{% endif %}; padding-left: 0cm; padding-right: 0.1cm; "> {% if status.reconnect %}{{_("on")}}{% else %}{{_("off")}}{% endif %}</a></li>
+ <li><a class="action backlink">{{_("Speed:")}} <b id="speed">{{ status.speed }}</b> kb/s</a></li>
+ <li><a class="action cog">{{_("Active:")}} <b id="aktiv">{{ status.activ }}</b> / <b id="aktiv_from">{{ status.queue }}</b></a></li>
+ <li><a href="" class="action revisions" accesskey="o" rel="nofollow">{{_("Reload page")}}</a></li>
+</ul>
+{% endif %}
+
+{% block pageactions %}
+{% endblock %}
+<br/>
+
+<div id="body-wrapper" class="dokuwiki">
+
+<div id="content" lang="en" dir="ltr">
+
+<h1>{% block subtitle %}pyLoad - {{_("Webinterface")}}{% endblock %}</h1>
+
+{% block statusbar %}
+{% endblock %}
+
+
+<br/>
+
+<div class="level1" style="clear:both">
+</div>
+
+{% for message in messages %}
+ <b><p>{{message}}</p></b>
+{% endfor %}
+
+{% block content %}
+{% endblock content %}
+
+ <hr style="clear: both;" />
+
+<div id="foot">&copy; 2008-2011 pyLoad Team
+<a href="#top" class="action top" accesskey="x"><span>{{_("Back to top")}}</span></a><br />
+<!--<div class="breadcrumbs"></div>-->
+
+</div>
+</div>
+</div>
+
+{% include "default/window.html" %}
+{% include "default/captcha.html" %}
+</body>
+</html>
diff --git a/module/web/templates/jinja/default/captcha.html b/module/web/templates/jinja/default/captcha.html
new file mode 100644
index 000000000..b3be3deca
--- /dev/null
+++ b/module/web/templates/jinja/default/captcha.html
@@ -0,0 +1,35 @@
+<iframe id="upload_target" name="upload_target" src="" style="display: none; width:0;height:0"></iframe>
+<!--<div id="add_box" style="left:50%; top:200px; margin-left: -450px; width: 900px; position: absolute; background: #FFF; padding: 10px 10px 10px 10px; display:none;">-->
+
+ <!--<div style="width: 900px; text-align: right;"><b onclick="AddBox();">[Close]</b></div>-->
+<div id="cap_box" class="myform">
+ <form id="cap_form" action="/json/set_captcha" method="POST" enctype="multipart/form-data" onsubmit="return false;">
+<h1>{{_("Captcha reading")}}</h1>
+<p>{{_("Please read the text on the captcha.")}}</p>
+
+<span id="cap_span">
+
+<label>{{_("Captcha")}}
+<span class="small">{{_("The captcha.")}}</span>
+</label>
+<span class="cont">
+ <img id="cap_img" style="padding: 2px;" src="">
+</span>
+
+<label>{{_("Text")}}
+<span class="small">{{_("Input the text on the captcha.")}}</span>
+</label>
+<input id="cap_text" name="cap_text" type="text" size="20" />
+<input type="hidden" value="" name="cap_id" id="cap_id"/>
+
+</span>
+
+<button id="cap_submit" type="submit">{{_("Submit")}}</button>
+<button id="cap_reset" style="margin-left:0px;" type="reset">{{_("Close")}}</button>
+
+<div class="spacer"></div>
+
+
+</form>
+
+</div> \ No newline at end of file
diff --git a/module/web/templates/jinja/default/collector.html b/module/web/templates/jinja/default/collector.html
new file mode 100644
index 000000000..3e6b47234
--- /dev/null
+++ b/module/web/templates/jinja/default/collector.html
@@ -0,0 +1,84 @@
+{% extends 'default/base.html' %}
+{% block head %}
+
+<script type="text/javascript" src="/package_ui.js"></script>
+
+<script type="text/javascript">
+
+document.addEvent("domready", function(){
+ var pUI = new PackageUI("url", 0);
+});
+</script>
+{% endblock %}
+
+{% block title %}{{_("Collector")}} - {{super()}} {% endblock %}
+{% block subtitle %}{{_("Collector")}}{% endblock %}
+
+{% block menu %}
+<li>
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt="" /> {{_("Home")}}</a>
+</li>
+<li>
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt="" /> {{_("Queue")}}</a>
+</li>
+<li class="selected">
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png" alt="" /> {{_("Collector")}}</a>
+</li>
+<li>
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png" alt="" /> {{_("Downloads")}}</a>
+</li>
+<li class="right">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-index.png" alt="" />{{_("Logs")}}</a>
+</li>
+<li class="right">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-config.png" alt="" />{{_("Config")}}</a>
+</li>{% endblock %}
+
+{% block pageactions %}
+<ul id="page-actions-more">
+ <li id="del_finished"><a style="padding: 0; font-weight: bold;" href="#">{{_("Delete Finished")}}</a></li>
+ <li id="restart_failed"><a style="padding: 0; font-weight: bold;" href="#">{{_("Restart Failed")}}</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<div id="load-success" style="opacity: 0; float: right; color: white; background-color: #90ee90; padding: 4px; -moz-border-radius: 5px; border-radius: 5px; font-weight: bold; margin-left: -100%; margin-top: -10px;">{{_("success")}}</div>
+<div id="load-failure" style="opacity: 0; float: right; color: white; background-color: #f08080; padding: 4px; -moz-border-radius: 5px; border-radius: 5px; font-weight: bold; margin-left: -100%; margin-top: -10px;">{{_("failure")}}</div>
+<div id="load-indicator" style="opacity: 0; float: right; margin-top: -10px;">
+ <img src="/media/default/img/ajax-loader.gif" alt="" style="padding-right: 5px"/>
+ {{_("loading")}}
+</div>
+
+<ul id="package-list" style="list-style: none; padding-left: 0; margin-top: -10px;">
+{% for id, package in content %}
+ <li>
+<div id="package_{{id}}" class="package">
+ <div class="order" style="display: none;">{{ package.order }}</div>
+
+ <div class="packagename" style="cursor: pointer;">
+ <img class="package_drag" src="/media/default/img/folder.png" style="cursor: move; margin-bottom: -2px">
+ <span class="name">{{package.name}}</span>
+ &nbsp;&nbsp;
+ <span class="buttons" style="opacity:0">
+ <img title="{{_("Delete Package")}}" style="cursor: pointer" width="12px" height="12px" src="/media/default/img/delete.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Restart Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/arrow_refresh.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Edit Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/pencil.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Move Package to Queue")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/package_go.png" />
+ </span>
+ </div>
+ <div id="children_{{id}}" style="display: none;" class="children">
+ <span class="child_secrow">{{_("Folder:")}} <span class="folder">{{package.folder}}</span> | {{_("Password:")}} <span class="password">{{package.password}}</span> | {{_("Priority:")}} <span class="prio">{{package.priority}}</span></span>
+ <ul id="sort_children_{{id}}" style="list-style: none; padding-left: 0">
+ </ul>
+ </div>
+</div>
+ </li>
+{% endfor %}
+</ul>
+
+{% include "default/edit_package.html" %}
+
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/downloads.html b/module/web/templates/jinja/default/downloads.html
new file mode 100644
index 000000000..813dc8d06
--- /dev/null
+++ b/module/web/templates/jinja/default/downloads.html
@@ -0,0 +1,50 @@
+{% extends 'default/base.html' %}
+
+{% block title %}Downloads - {{super()}} {% endblock %}
+
+{% block menu %}
+<li>
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt="" /> {{_("Home")}}</a>
+</li>
+<li>
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt="" /> {{_("Queue")}}</a>
+</li>
+<li>
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png" alt="" /> {{_("Collector")}}</a>
+</li>
+<li class="selected">
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png" alt="" /> {{_("Downloads")}}</a>
+</li>
+<li class="right">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-index.png" alt="" />{{_("Logs")}}</a>
+</li>
+<li class="right">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-config.png" alt="" />{{_("Config")}}</a>
+</li>
+{% endblock %}
+
+{% block subtitle %}
+{{_("Downloads")}}
+{% endblock %}
+
+{% block content %}
+
+<ul>
+ {% for folder in files.folder %}
+ <li>
+ {{ folder.name }}
+ <ul>
+ {% for file in folder.files %}
+ <li><a href='get/{{ folder.path|escape }}/{{ file|escape }}'>{{file}}</a></li>
+ {% endfor %}
+ </ul>
+ </li>
+ {% endfor %}
+
+ {% for file in files.files %}
+ <li> <a href='get/{{ file|escape }}'>{{ file }}</a></li>
+ {% endfor %}
+
+</ul>
+
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/edit_package.html b/module/web/templates/jinja/default/edit_package.html
new file mode 100644
index 000000000..0c9dcff42
--- /dev/null
+++ b/module/web/templates/jinja/default/edit_package.html
@@ -0,0 +1,40 @@
+<div id="pack_box" class="myform" style="z-index: 2">
+<form id="pack_form" action="/json/edit_package" method="POST" enctype="multipart/form-data">
+<h1>{{_("Edit Package")}}</h1>
+<p>{{_("Edit the package detais below.")}}</p>
+<input name="pack_id" id="pack_id" type="hidden" value=""/>
+<label for="pack_name">{{_("Name")}}
+<span class="small">{{_("The name of the package.")}}</span>
+</label>
+<input id="pack_name" name="pack_name" type="text" size="20" />
+
+<label for="pack_folder">{{_("Folder")}}
+<span class="small">{{_("Name of subfolder for these downloads.")}}</span>
+</label>
+<input id="pack_folder" name="pack_folder" type="text" size="20" />
+
+<label for="pack_prio">{{_("Priority")}}
+<span class="small">{{_("Priority of the package.")}}</span>
+</label>
+ <select name="pack_prio" id="pack_prio">
+ <option value="3">{{_("highest")}}</option>
+ <option value="2">{{_("higher")}}</option>
+ <option value="1">{{_("high")}}</option>
+ <option value="0" selected="selected">{{_("normal")}}</option>
+ <option value="-1">{{_("low")}}</option>
+ <option value="-2">{{_("lower")}}</option>
+ <option value="-3">{{_("lowest")}}</option>
+ </select>
+
+<label for="pack_pws">{{_("Password")}}
+<span class="small">{{_("List of passwords used for unrar.")}}</span>
+</label>
+<textarea rows="3" name="pack_pws" id="pack_pws"></textarea>
+
+<button type="submit">{{_("Submit")}}</button>
+<button id="pack_reset" style="margin-left: 0" type="reset">{{_("Reset")}}</button>
+<div class="spacer"></div>
+
+</form>
+
+</div> \ No newline at end of file
diff --git a/module/web/templates/jinja/default/home.html b/module/web/templates/jinja/default/home.html
new file mode 100644
index 000000000..b2cef2cb7
--- /dev/null
+++ b/module/web/templates/jinja/default/home.html
@@ -0,0 +1,241 @@
+{% extends 'default/base.html' %}
+{% block head %}
+
+<script type="text/javascript">
+
+var em;
+var operafix = (navigator.userAgent.toLowerCase().search("opera") >= 0);
+
+document.addEvent("domready", function(){
+ em = new EntryManager();
+});
+
+var EntryManager = new Class({
+ initialize: function(){
+ this.json = new Request.JSON({
+ url: "json/links",
+ secure: false,
+ async: true,
+ onSuccess: this.update.bind(this),
+ initialDelay: 0,
+ delay: 2500,
+ limit: 30000
+ });
+
+ this.ids = [{% for link in content %}
+ {% if forloop.last %}
+ {{ link.id }}
+ {% else %}
+ {{ link.id }},
+ {% endif %}
+ {% endfor %}];
+
+ this.entries = [];
+ this.container = $('LinksAktiv');
+
+ this.parseFromContent();
+
+ this.json.startTimer();
+ },
+ parseFromContent: function(){
+ this.ids.each(function(id,index){
+ var entry = new LinkEntry(id);
+ entry.parse();
+ this.entries.push(entry)
+ }, this);
+ },
+ update: function(data){
+
+ try{
+ this.ids = this.entries.map(function(item){
+ return item.id
+ });
+
+ this.ids.filter(function(id){
+ return !this.ids.contains(id)
+ },data).each(function(id){
+ var index = this.ids.indexOf(id);
+ this.entries[index].remove();
+ this.entries = this.entries.filter(function(item){return item.id != this},id);
+ this.ids = this.ids.erase(id)
+ }, this);
+
+ data.links.each(function(link, i){
+ if (this.ids.contains(link.id)){
+
+ var index = this.ids.indexOf(link.id);
+ this.entries[index].update(link)
+
+ }else{
+ var entry = new LinkEntry(link.id);
+ entry.insert(link);
+ this.entries.push(entry);
+ this.ids.push(link.id);
+ this.container.adopt(entry.elements.tr,entry.elements.pgbTr);
+ entry.fade.start('opacity', 1);
+ entry.fadeBar.start('opacity', 1);
+
+ }
+ }, this)
+ }catch(e){
+ //alert(e)
+ }
+ }
+});
+
+
+var LinkEntry = new Class({
+ initialize: function(id){
+ this.id = id
+ },
+ parse: function(){
+ this.elements = {
+ tr: $("link_{id}".substitute({id: this.id})),
+ name: $("link_{id}_name".substitute({id: this.id})),
+ status: $("link_{id}_status".substitute({id: this.id})),
+ info: $("link_{id}_info".substitute({id: this.id})),
+ bleft: $("link_{id}_kbleft".substitute({id: this.id})),
+ percent: $("link_{id}_percent".substitute({id: this.id})),
+ remove: $("link_{id}_remove".substitute({id: this.id})),
+ pgbTr: $("link_{id}_pgb_tr".substitute({id: this.id})),
+ pgb: $("link_{id}_pgb".substitute({id: this.id}))
+ };
+ this.initEffects();
+ },
+ insert: function(item){
+ try{
+
+ this.elements = {
+ tr: new Element('tr', {
+ 'html': '',
+ 'styles':{
+ 'opacity': 0
+ }
+ }),
+ name: new Element('td', {
+ 'html': item.name
+ }),
+ status: new Element('td', {
+ 'html': item.statusmsg
+ }),
+ info: new Element('td', {
+ 'html': item.info
+ }),
+ bleft: new Element('td', {
+ 'html': HumanFileSize(item.size)
+ }),
+ percent: new Element('span', {
+ 'html': item.percent+ '% / '+ HumanFileSize(item.size-item.bleft)
+ }),
+ remove: new Element('img',{
+ 'src': 'media/default/img/control_cancel.png',
+ 'styles':{
+ 'vertical-align': 'middle',
+ 'margin-right': '-20px',
+ 'margin-left': '5px',
+ 'margin-top': '-2px',
+ 'cursor': 'pointer'
+ }
+ }),
+ pgbTr: new Element('tr', {
+ 'html':''
+ }),
+ pgb: new Element('div', {
+ 'html': '&nbsp;',
+ 'styles':{
+ 'height': '4px',
+ 'width': item.percent+'%',
+ 'background-color': '#ddd'
+ }
+ })
+ };
+
+ this.elements.tr.adopt(this.elements.name,this.elements.status,this.elements.info,this.elements.bleft,new Element('td').adopt(this.elements.percent,this.elements.remove));
+ this.elements.pgbTr.adopt(new Element('td',{'colspan':5}).adopt(this.elements.pgb));
+ this.initEffects();
+ }catch(e){
+ alert(e)
+ }
+ },
+ initEffects: function(){
+ if(!operafix)
+ this.bar = new Fx.Morph(this.elements.pgb, {unit: '%', duration: 5000, link: 'link', fps:30});
+ this.fade = new Fx.Tween(this.elements.tr);
+ this.fadeBar = new Fx.Tween(this.elements.pgbTr);
+
+ this.elements.remove.addEvent('click', function(){
+ new Request({method: 'get', url: '/json/abort_link/'+this.id}).send();
+ }.bind(this));
+
+ },
+ update: function(item){
+ this.elements.name.set('text', item.name);
+ this.elements.status.set('text', item.statusmsg);
+ this.elements.info.set('text', item.info);
+ this.elements.bleft.set('text', item.format_size);
+ this.elements.percent.set('text', item.percent+ '% / '+ HumanFileSize(item.size-item.bleft));
+ if(!operafix)
+ {
+ this.bar.start({
+ 'width': item.percent,
+ 'background-color': [Math.round(120/100*item.percent),100,100].hsbToRgb().rgbToHex()
+ });
+ }
+ else
+ {
+ this.elements.pgb.set(
+ 'styles', {
+ 'height': '4px',
+ 'width': item.percent+'%',
+ 'background-color': [Math.round(120/100*item.percent),100,100].hsbToRgb().rgbToHex(),
+ });
+ }
+ },
+ remove: function(){
+ this.fade.start('opacity',0).chain(function(){this.elements.tr.dispose();}.bind(this));
+ this.fadeBar.start('opacity',0).chain(function(){this.elements.pgbTr.dispose();}.bind(this));
+
+ }
+ });
+</script>
+
+{% endblock %}
+
+{% block subtitle %}
+{{_("Active Downloads")}}
+{% endblock %}
+
+{% block content %}
+<table width="100%" class="queue">
+ <thead>
+ <tr class="header">
+ <th>{{_("Name")}}</th>
+ <th>{{_("Status")}}</th>
+ <th>{{_("Information")}}</th>
+ <th>{{_("Size")}}</th>
+ <th>{{_("Progress")}}</th>
+ </tr>
+ </thead>
+ <tbody id="LinksAktiv">
+
+ {% for link in content %}
+ <tr id="link_{{ link.id }}">
+ <td id="link_{{ link.id }}_name">{{ link.name }}</td>
+ <td id="link_{{ link.id }}_status">{{ link.status }}</td>
+ <td id="link_{{ link.id }}_info">{{ link.info }}</td>
+ <td id="link_{{ link.id }}_kbleft">{{ link.format_size }}</td>
+ <td>
+ <span id="link_{{ link.id }}_percent">{{ link.percent }}% /{{ link.kbleft }}</span>
+ <img id="link_{{ link.id }}_remove" style="vertical-align: middle; margin-right: -20px; margin-left: 5px; margin-top: -2px; cursor:pointer;" src="media/default/img/control_cancel.png"/>
+ </td>
+ </tr>
+ <tr id="link_{{ link.id }}_pgb_tr">
+ <td colspan="5">
+ <div id="link_{{ link.id }}_pgb" class="progressBar" style="background-color: green; height:4px; width: {{ link.percent }}%;">&nbsp;</div>
+ </td>
+ </tr>
+ {% endfor %}
+
+ </tbody>
+</table>
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/login.html b/module/web/templates/jinja/default/login.html
new file mode 100644
index 000000000..0e9e4d568
--- /dev/null
+++ b/module/web/templates/jinja/default/login.html
@@ -0,0 +1,35 @@
+{% extends 'default/base.html' %}
+
+{% block title %}{{_("Login")}} - {{super()}} {% endblock %}
+
+{% block content %}
+
+<div class="centeralign">
+<form action="" method="post" accept-charset="utf-8" id="login">
+ <div class="no">
+ <input type="hidden" name="do" value="login" />
+ <fieldset>
+ <legend>Login</legend>
+ <label>
+ <span>{{_("Username")}}</span>
+ <input type="text" size="20" name="username"/>
+ </label>
+ <br />
+ <label>
+ <span>{{_("Password")}}</span>
+ <input type="password" size="20" name="password">
+ </label>
+ <br />
+ <input type="submit" value="Login" class="button" />
+ </fieldset>
+ </div>
+</form>
+
+{% if errors %}
+<p>{{_("Your username and password didn't match. Please try again.")}}</p>
+{% endif %}
+
+</div>
+<br>
+
+{% endblock %}
diff --git a/module/web/templates/jinja/default/logout.html b/module/web/templates/jinja/default/logout.html
new file mode 100644
index 000000000..d3f07472b
--- /dev/null
+++ b/module/web/templates/jinja/default/logout.html
@@ -0,0 +1,9 @@
+{% extends 'default/base.html' %}
+
+{% block head %}
+<meta http-equiv="refresh" content="3; url=/">
+{% endblock %}
+
+{% block content %}
+<p><b>{{_("You were successfully logged out.")}}</b></p>
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/logs.html b/module/web/templates/jinja/default/logs.html
new file mode 100644
index 000000000..7a95b4364
--- /dev/null
+++ b/module/web/templates/jinja/default/logs.html
@@ -0,0 +1,61 @@
+{% extends 'default/base.html' %}
+
+{% block title %}{{_("Logs")}} - {{super()}} {% endblock %}
+{% block subtitle %}{{_("Logs")}}{% endblock %}
+{% block head %}
+<link rel="stylesheet" type="text/css" href="/media/default/css/log.css"/>
+{% endblock %}
+{% block menu %}
+<li>
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt="" /> {{_("Home")}}</a>
+</li>
+<li>
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt="" /> {{_("Queue")}}</a>
+</li>
+<li>
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png" alt="" /> {{_("Collector")}}</a>
+</li>
+<li>
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png" alt="" /> {{_("Downloads")}}</a>
+</li>
+<li class="right selected">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-index.png" alt="" />{{_("Logs")}}</a>
+</li>
+<li class="right">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-config.png" alt="" />{{_("Config")}}</a>
+</li>
+{% endblock %}
+
+{% block content %}
+<div style="clear: both;"></div>
+
+<div class="logpaginator"><a href="{{ "/logs/1" }}">&lt;&lt; {{_("Start")}}</a> <a href="{{ "/logs/" + iprev|string }}">&lt; {{_("prev")}}</a> <a href="{{ "/logs/" + inext|string }}">{{_("next")}} &gt;</a> <a href="/logs/">{{_("End")}} &gt;&gt;</a></div>
+<div class="logperpage">
+ <form id="logform1" action="" method="POST">
+ <label for="reversed">Reversed:</label>
+ <input type="checkbox" name="reversed" onchange="this.form.submit();" {% if reversed %} checked="checked" {% endif %} />&nbsp;
+ <label for="perpage">Lines per page:</label>
+ <select name="perpage" onchange="this.form.submit();">
+ {% for value in perpage_p %}
+ <option value="{{value.0}}"{% if value.0 == perpage %} selected="selected" {% endif %}>{{value.1}}</option>
+ {% endfor %}
+ </select>
+ </form>
+</div>
+<div class="logwarn">{{warning}}</div>
+<div style="clear: both;"></div>
+<div class="logdiv">
+ <table class="logtable" cellpadding="0" cellspacing="0">
+ {% for line in log %}
+ <tr><td class="logline">{{line.line}}</td><td>{{line.date}}</td><td class="loglevel">{{line.level}}</td><td>{{line.message}}</td></tr>
+ {% endfor %}
+ </table>
+</div>
+<div class="logform">
+<form id="logform2" action="" method="POST">
+ <label for="from">Jump to time:</label><input type="text" name="from" size="15" value="{{from}}"/>
+ <input type="submit" value="ok" />
+</form>
+</div>
+<div style="clear: both; height: 10px;">&nbsp; </div>
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/package_ui.js b/module/web/templates/jinja/default/package_ui.js
new file mode 100644
index 000000000..45e284903
--- /dev/null
+++ b/module/web/templates/jinja/default/package_ui.js
@@ -0,0 +1,408 @@
+var load, success, fail, pack_box;
+
+document.addEvent("domready", function() {
+ load = new Fx.Tween($("load-indicator"), {link: "cancel"});
+ success = new Fx.Tween($("load-success"), {link: "chain"});
+ fail = new Fx.Tween($("load-failure"), {link: "chain"});
+
+ [load,success,fail].each(function(fx) {
+ fx.set("opacity", 0)
+ });
+
+ pack_box = new Fx.Tween($('pack_box'));
+ $('pack_reset').addEvent('click', function() {
+ hide_pack()
+ });
+});
+
+function indicateLoad() {
+ //$("load-indicator").reveal();
+ load.start("opacity", 1)
+}
+
+function indicateFinish() {
+ load.start("opacity", 0)
+}
+
+function indicateSuccess() {
+ indicateFinish();
+ success.start("opacity", 1).chain(function() {
+ (function() {
+ success.start("opacity", 0);
+ }).delay(250);
+ });
+
+}
+
+function indicateFail() {
+ indicateFinish();
+ fail.start("opacity", 1).chain(function() {
+ (function() {
+ fail.start("opacity", 0);
+ }).delay(250);
+ });
+}
+
+function show_pack() {
+ bg_show();
+ $("pack_box").setStyle('display', 'block');
+ pack_box.start('opacity', 1)
+}
+
+function hide_pack() {
+ bg_hide();
+ pack_box.start('opacity', 0).chain(function() {
+ $('pack_box').setStyle('display', 'none');
+ });
+}
+
+var PackageUI = new Class({
+ initialize: function(url, type) {
+ this.url = url;
+ this.type = type;
+ this.packages = [];
+ this.parsePackages();
+
+ this.sorts = new Sortables($("package-list"), {
+ constrain: false,
+ clone: true,
+ revert: true,
+ opacity: 0.4,
+ handle: ".package_drag",
+ //onStart: this.startSort,
+ onComplete: this.saveSort.bind(this)
+ });
+
+ $("del_finished").addEvent("click", this.deleteFinished.bind(this));
+ $("restart_failed").addEvent("click", this.restartFailed.bind(this));
+
+ },
+
+ parsePackages: function() {
+ $("package-list").getChildren("li").each(function(ele) {
+ var id = ele.getFirst().get("id").match(/[0-9]+/);
+ this.packages.push(new Package(this, id, ele))
+ }.bind(this))
+ },
+
+ loadPackages: function() {
+ },
+
+ deleteFinished: function() {
+ indicateLoad();
+ new Request.JSON({
+ method: 'get',
+ url: '/json/delete_finished',
+ onSuccess: function(data) {
+ if (data.del.length > 0) {
+ window.location.reload()
+ } else {
+ this.packages.each(function(pack) {
+ pack.close();
+ });
+ indicateSuccess();
+ }
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ },
+
+ restartFailed: function() {
+ indicateLoad();
+ new Request.JSON({
+ method: 'get',
+ url: '/json/restart_failed',
+ onSuccess: function(data) {
+ this.packages.each(function(pack) {
+ pack.close();
+ });
+ indicateSuccess();
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ },
+
+ startSort: function(ele, copy) {
+ },
+
+ saveSort: function(ele, copy) {
+ var order = [];
+ this.sorts.serialize(function(li, pos) {
+ if (li == ele && ele.retrieve("order") != pos) {
+ order.push(ele.retrieve("pid") + "|" + pos)
+ }
+ li.store("order", pos)
+ });
+ if (order.length > 0) {
+ indicateLoad();
+ new Request.JSON({
+ method: 'get',
+ url: '/json/package_order/' + order[0],
+ onSuccess: indicateFinish,
+ onFailure: indicateFail
+ }).send();
+ }
+ }
+
+});
+
+var Package = new Class({
+ initialize: function(ui, id, ele, data) {
+ this.ui = ui;
+ this.id = id;
+ this.linksLoaded = false;
+
+ if (!ele) {
+ this.createElement(data);
+ } else {
+ this.ele = ele;
+ this.order = ele.getElements("div.order")[0].get("html");
+ this.ele.store("order", this.order);
+ this.ele.store("pid", this.id);
+ this.parseElement();
+ }
+
+ var pname = this.ele.getElements(".packagename")[0];
+ this.buttons = new Fx.Tween(this.ele.getElements(".buttons")[0], {link: "cancel"});
+ this.buttons.set("opacity", 0);
+
+ pname.addEvent("mouseenter", function(e) {
+ this.buttons.start("opacity", 1)
+ }.bind(this));
+
+ pname.addEvent("mouseleave", function(e) {
+ this.buttons.start("opacity", 0)
+ }.bind(this));
+
+
+ },
+
+ createElement: function() {
+ alert("create")
+ },
+
+ parseElement: function() {
+ var imgs = this.ele.getElements('img');
+
+ this.name = this.ele.getElements('.name')[0];
+ this.folder = this.ele.getElements('.folder')[0];
+ this.password = this.ele.getElements('.password')[0];
+ this.prio = this.ele.getElements('.prio')[0];
+
+ imgs[1].addEvent('click', this.deletePackage.bind(this));
+
+ imgs[2].addEvent('click', this.restartPackage.bind(this));
+
+ imgs[3].addEvent('click', this.editPackage.bind(this));
+
+ imgs[4].addEvent('click', this.movePackage.bind(this));
+
+ this.ele.getElement('.packagename').addEvent('click', this.toggle.bind(this));
+
+ },
+
+ loadLinks: function() {
+ indicateLoad();
+ new Request.JSON({
+ method: 'get',
+ url: '/json/package/' + this.id,
+ onSuccess: this.createLinks.bind(this),
+ onFailure: indicateFail
+ }).send();
+ },
+
+ createLinks: function(data) {
+ var ul = $("sort_children_{id}".substitute({"id": this.id}));
+ ul.erase("html");
+ data.links.each(function(link) {
+ var li = new Element("li", {
+ "style": {
+ "margin-left": 0
+ }
+ });
+
+ var html = "<span style='cursor: move' class='child_status sorthandle'><img src='/media/default/img/{icon}' style='width: 12px; height:12px;'/></span>\n".substitute({"icon": link.icon});
+ html += "<span style='font-size: 15px'>{name}</span><br /><div class='child_secrow'>".substitute({"name": link.name});
+ html += "<span class='child_status'>{statusmsg}</span>{error}&nbsp;".substitute({"statusmsg": link.statusmsg, "error":link.error});
+ html += "<span class='child_status'>{format_size}</span>".substitute({"format_size": link.format_size});
+ html += "<span class='child_status'>{plugin}</span>&nbsp;&nbsp;".substitute({"plugin": link.plugin});
+ html += "<img title='{{_("Delete Link")}}' style='cursor: pointer;' width='10px' height='10px' src='/media/default/img/delete.png' />&nbsp;&nbsp;";
+ html += "<img title='{{_("Restart Link")}}' style='cursor: pointer;margin-left: -4px' width='10px' height='10px' src='/media/default/img/arrow_refresh.png' /></div>";
+
+ var div = new Element("div", {
+ "id": "file_" + link.id,
+ "class": "child",
+ "html": html
+ });
+
+ li.store("order", link.order);
+ li.store("lid", link.id);
+
+ li.adopt(div);
+ ul.adopt(li);
+ });
+ this.sorts = new Sortables(ul, {
+ constrain: false,
+ clone: true,
+ revert: true,
+ opacity: 0.4,
+ handle: ".sorthandle",
+ onComplete: this.saveSort.bind(this)
+ });
+ this.registerLinkEvents();
+ this.linksLoaded = true;
+ indicateFinish();
+ this.toggle();
+ },
+
+ registerLinkEvents: function() {
+ this.ele.getElements('.child').each(function(child) {
+ var lid = child.get('id').match(/[0-9]+/);
+ var imgs = child.getElements('.child_secrow img');
+ imgs[0].addEvent('click', function(e) {
+ new Request({
+ method: 'get',
+ url: '/json/remove_link/' + this,
+ onSuccess: function() {
+ $('file_' + this).nix()
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ }.bind(lid));
+
+ imgs[1].addEvent('click', function(e) {
+ new Request({
+ method: 'get',
+ url: '/json/restart_link/' + this,
+ onSuccess: function() {
+ var ele = $('file_' + this);
+ var imgs = ele.getElements("img");
+ imgs[0].set("src", "/media/default/img/status_queue.png");
+ var spans = ele.getElements(".child_status");
+ spans[1].set("html", "queued");
+ indicateSuccess();
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ }.bind(lid));
+ });
+ },
+
+ toggle: function() {
+ var child = this.ele.getElement('.children');
+ if (child.getStyle('display') == "block") {
+ child.dissolve();
+ } else {
+ if (!this.linksLoaded) {
+ this.loadLinks();
+ } else {
+ child.reveal();
+ }
+ }
+ },
+
+ deletePackage: function(event) {
+ indicateLoad();
+ new Request({
+ method: 'get',
+ url: '/json/remove_package/' + this.id,
+ onSuccess: function() {
+ this.ele.nix();
+ indicateFinish();
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ event.stop();
+ },
+
+ restartPackage: function(event) {
+ indicateLoad();
+ new Request({
+ method: 'get',
+ url: '/json/restart_package/' + this.id,
+ onSuccess: function() {
+ this.close();
+ indicateSuccess();
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ event.stop();
+ },
+
+ close: function() {
+ var child = this.ele.getElement('.children');
+ if (child.getStyle('display') == "block") {
+ child.dissolve();
+ }
+ var ul = $("sort_children_{id}".substitute({"id": this.id}));
+ ul.erase("html");
+ this.linksLoaded = false;
+ },
+
+ movePackage: function(event) {
+ indicateLoad();
+ new Request({
+ method: 'get',
+ url: '/json/move_package/' + ((this.ui.type + 1) % 2) + "/" + this.id,
+ onSuccess: function() {
+ this.ele.nix();
+ indicateFinish();
+ }.bind(this),
+ onFailure: indicateFail
+ }).send();
+ event.stop();
+ },
+
+ editPackage: function(event) {
+ $("pack_form").removeEvents("submit");
+ $("pack_form").addEvent("submit", this.savePackage.bind(this));
+
+ $("pack_id").set("value", this.id);
+ $("pack_name").set("value", this.name.get("text"));
+ $("pack_folder").set("value", this.folder.get("text"));
+ $("pack_pws").set("value", this.password.get("text"));
+
+ var prio = 3;
+ $("pack_prio").getChildren("option").each(function(item, index) {
+ item.erase("selected");
+ if (prio.toString() == this.prio.get("text")) {
+ item.set("selected", "selected");
+ }
+ prio--;
+ }.bind(this));
+
+
+ show_pack();
+ event.stop();
+ },
+
+ savePackage: function(event) {
+ $("pack_form").send();
+ this.name.set("text", $("pack_name").get("value"));
+ this.folder.set("text", $("pack_folder").get("value"));
+ this.password.set("text", $("pack_pws").get("value"));
+ this.prio.set("text", $("pack_prio").get("value"));
+ hide_pack();
+ event.stop();
+ },
+
+ saveSort: function(ele, copy) {
+ var order = [];
+ this.sorts.serialize(function(li, pos) {
+ if (li == ele && ele.retrieve("order") != pos) {
+ order.push(ele.retrieve("lid") + "|" + pos)
+ }
+ li.store("order", pos)
+ });
+ if (order.length > 0) {
+ indicateLoad();
+ new Request.JSON({
+ method: 'get',
+ url: '/json/link_order/' + order[0],
+ onSuccess: indicateFinish,
+ onFailure: indicateFail
+ }).send();
+ }
+ }
+
+}); \ No newline at end of file
diff --git a/module/web/templates/jinja/default/pathchooser.html b/module/web/templates/jinja/default/pathchooser.html
new file mode 100644
index 000000000..d00637055
--- /dev/null
+++ b/module/web/templates/jinja/default/pathchooser.html
@@ -0,0 +1,76 @@
+<html>
+<head>
+ <script class="javascript">
+ function chosen()
+ {
+ opener.ifield.value = document.forms[0].p.value;
+ close();
+ }
+ function exit()
+ {
+ close();
+ }
+ function setInvalid() {
+ document.forms[0].send.disabled = 'disabled';
+ document.forms[0].p.style.color = '#FF0000';
+ }
+ function setValid() {
+ document.forms[0].send.disabled = '';
+ document.forms[0].p.style.color = '#000000';
+ }
+ function setFile(file)
+ {
+ document.forms[0].p.value = file;
+ setValid();
+
+ }
+ </script>
+ <link rel="stylesheet" type="text/css" href="/media/default/css/pathchooser.css"/>
+</head>
+<body{% if type == 'file' %}{% if not oldfile %} onload="setInvalid();"{% endif %}{% endif %}>
+<center>
+ <div id="paths">
+ <form method="get" action="?" onSubmit="chosen();" onReset="exit();">
+ <input type="text" name="p" value="{{ oldfile|default(cwd) }}" size="60" onfocus="setValid();">
+ <input type="submit" value="Ok" name="send">
+ </form>
+
+ {% if type == 'folder' %}
+ <span class="path_abs_rel">{{_("Path")}}: <a href="{{ "/pathchooser" + cwd|path_make_absolute|quotepath }}"{% if absolute %} style="text-decoration: underline;"{% endif %}>{{_("absolute")}}</a> | <a href="{{ "/pathchooser/" + cwd|path_make_relative|quotepath }}"{% if not absolute %} style="text-decoration: underline;"{% endif %}>{{_("relative")}}</a></span>
+ {% else %}
+ <span class="path_abs_rel">{{_("Path")}}: <a href="{{ "/filechooser/" + cwd|path_make_absolute|quotepath }}"{% if absolute %} style="text-decoration: underline;"{% endif %}>{{_("absolute")}}</a> | <a href="{{ "/filechooser/" + cwd|path_make_relative|quotepath }}"{% if not absolute %} style="text-decoration: underline;"{% endif %}>{{_("relative")}}</a></span>
+ {% endif %}
+ </div>
+ <table border="0" cellspacing="0" cellpadding="3">
+ <tr>
+ <th>{{_("name")}}</th>
+ <th>{{_("size")}}</th>
+ <th>{{_("type")}}</th>
+ <th>{{_("last modified")}}</th>
+ </tr>
+ {% if parentdir %}
+ <tr>
+ <td colspan="4">
+ <a href="{% if type == 'folder' %}{{ "/pathchooser/" + parentdir|quotepath }}{% else %}{{ "/filechooser/" + parentdir|quotepath }}{% endif %}"><span class="parentdir">{{_("parent directory")}}</span></a>
+ </td>
+ </tr>
+ {% endif %}
+{% for file in files %}
+ <tr>
+ {% if type == 'folder' %}
+ <td class="name">{% if file.type == 'dir' %}<a href="{{ "/pathchooser/" + file.fullpath|quotepath }}" title="{{ file.fullpath }}"><span class="path_directory">{{ file.name|truncate(25) }}</span></a>{% else %}<span class="path_file" title="{{ file.fullpath }}">{{ file.name|truncate(25) }}{% endif %}</span></td>
+ {% else %}
+ <td class="name">{% if file.type == 'dir' %}<a href="{{ "/filechooser/" + file.fullpath|quotepath }}" title="{{ file.fullpath }}"><span class="file_directory">{{ file.name|truncate(25) }}</span></a>{% else %}<a href="#" onclick="setFile('{{ file.fullpath }}');" title="{{ file.fullpath }}"><span class="file_file">{{ file.name|truncate(25) }}{% endif %}</span></a></td>
+ {% endif %}
+ <td class="size">{{ file.size|float|filesizeformat }}</td>
+ <td class="type">{% if file.type == 'dir' %}directory{% else %}{{ file.ext|default("file") }}{% endif %}</td>
+ <td class="mtime">{{ file.modified|date("d.m.Y - H:i:s") }}</td>
+ <tr>
+<!-- <tr>
+ <td colspan="4">{{_("no content")}}</td>
+ </tr> -->
+{% endfor %}
+ </table>
+ </center>
+</body>
+</html> \ No newline at end of file
diff --git a/module/web/templates/jinja/default/queue.html b/module/web/templates/jinja/default/queue.html
new file mode 100644
index 000000000..e72871873
--- /dev/null
+++ b/module/web/templates/jinja/default/queue.html
@@ -0,0 +1,85 @@
+{% extends 'default/base.html' %}
+{% block head %}
+
+<script type="text/javascript" src="/package_ui.js"></script>
+
+<script type="text/javascript">
+
+document.addEvent("domready", function(){
+ var pUI = new PackageUI("url",1);
+});
+</script>
+{% endblock %}
+
+{% block title %}{{_("Queue")}} - {{super()}} {% endblock %}
+{% block subtitle %}{{_("Queue")}}{% endblock %}
+
+{% block menu %}
+<li>
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt="" /> {{_("Home")}}</a>
+</li>
+<li class="selected">
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt="" /> {{_("Queue")}}</a>
+</li>
+<li>
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png" alt="" /> {{_("Collector")}}</a>
+</li>
+<li>
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png" alt="" /> {{_("Downloads")}}</a>
+</li>
+<li class="right">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-index.png" alt="" />{{_("Logs")}}</a>
+</li>
+<li class="right">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img src="/media/default/img/head-menu-config.png" alt="" />{{_("Config")}}</a>
+</li>
+{% endblock %}
+
+{% block pageactions %}
+<ul id="page-actions-more">
+ <li id="del_finished"><a style="padding: 0; font-weight: bold;" href="#">{{_("Delete Finished")}}</a></li>
+ <li id="restart_failed"><a style="padding: 0; font-weight: bold;" href="#">{{_("Restart Failed")}}</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<div id="load-success" style="opacity: 0; float: right; color: white; background-color: #90ee90; padding: 4px; -moz-border-radius: 5px; border-radius: 5px; font-weight: bold; margin-left: -100%; margin-top: -10px;">{{_("success")}}</div>
+<div id="load-failure" style="opacity: 0; float: right; color: white; background-color: #f08080; padding: 4px; -moz-border-radius: 5px; border-radius: 5px; font-weight: bold; margin-left: -100%; margin-top: -10px;">{{_("failure")}}</div>
+<div id="load-indicator" style="opacity: 0; float: right; margin-top: -10px;">
+ <img src="/media/default/img/ajax-loader.gif" alt="" style="padding-right: 5px"/>
+ {{_("loading")}}
+</div>
+
+<ul id="package-list" style="list-style: none; padding-left: 0; margin-top: -10px;">
+{% for id, package in content %}
+ <li>
+<div id="package_{{id}}" class="package">
+ <div class="order" style="display: none;">{{ package.order }}</div>
+
+ <div class="packagename" style="cursor: pointer;">
+ <img class="package_drag" src="/media/default/img/folder.png" style="cursor: move; margin-bottom: -2px">
+ <span class="name">{{package.name}}</span>
+ &nbsp;&nbsp;
+ <span class="buttons" style="opacity:0">
+ <img title="{{_("Delete Package")}}" style="cursor: pointer" width="12px" height="12px" src="/media/default/img/delete.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Restart Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/arrow_refresh.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Edit Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/pencil.png" />
+ &nbsp;&nbsp;
+ <img title="{{_("Move Package to Collector")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/package_go.png" />
+ </span>
+ </div>
+ <div id="children_{{id}}" style="display: none;" class="children">
+ <span class="child_secrow">{{_("Folder:")}} <span class="folder">{{package.folder}}</span> | {{_("Password:")}} <span class="password">{{package.password}}</span> | {{_("Priority:")}} <span class="prio">{{package.priority}}</span></span>
+ <ul id="sort_children_{{id}}" style="list-style: none; padding-left: 0">
+ </ul>
+ </div>
+</div>
+ </li>
+{% endfor %}
+</ul>
+
+{% include "default/edit_package.html" %}
+
+{% endblock %} \ No newline at end of file
diff --git a/module/web/templates/jinja/default/settings.html b/module/web/templates/jinja/default/settings.html
new file mode 100644
index 000000000..18bc78e30
--- /dev/null
+++ b/module/web/templates/jinja/default/settings.html
@@ -0,0 +1,232 @@
+{% extends 'default/base.html' %}
+
+{% block title %}{{ _("Config") }} - {{ super() }} {% endblock %}
+{% block subtitle %}{{ _("Config") }}{% endblock %}
+
+{% block head %}
+ <script type="text/javascript">
+ window.addEvent('domready', function() {
+ $$('#toptabs a').addEvent('click', function(e) {
+ $$('#toptabs a').removeProperty('class');
+ e.target.set('class', 'selected');
+
+ $$('#tabs span').removeProperty('class');
+ $('g_' + e.target.get('href').substring(1)).set('class', 'selected');
+
+ var firstsel = $$('#tabs span.selected a')[0];
+ firstsel.fireEvent('click', {target: firstsel});
+ return false;
+ });
+
+ $$('#tabs a').addEvent('click', function(e) {
+ $$('#tabs a').removeProperty('class');
+ e.target.set('class', 'selected');
+
+ $$('div.tabContent').set('class', 'tabContent hide');
+ $(e.target.get('href').substring(1)).set('class', 'tabContent');
+ return false;
+ });
+
+ $$('#toptabs a')[0].set('class', 'selected');
+ $$('#tabs span')[0].set('class', 'selected');
+
+ var firstsel = $$('#tabs span.selected a')[0];
+ firstsel.fireEvent('click', {target: firstsel});
+ });
+
+
+ </script>
+
+{% endblock %}
+
+{% block menu %}
+ <li>
+ <a href="/" title=""><img src="/media/default/img/head-menu-home.png" alt=""/> {{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/queue/" title=""><img src="/media/default/img/head-menu-queue.png" alt=""/> {{ _("Queue") }}</a>
+ </li>
+ <li>
+ <a href="/collector/" title=""><img src="/media/default/img/head-menu-collector.png"
+ alt=""/> {{ _("Collector") }}</a>
+ </li>
+ <li>
+ <a href="/downloads/" title=""><img src="/media/default/img/head-menu-development.png"
+ alt=""/> {{ _("Downloads") }}</a>
+ </li>
+ <li class="right">
+ <a href="/logs/" class="action index" accesskey="x" rel="nofollow"><img
+ src="/media/default/img/head-menu-index.png" alt=""/>{{ _("Logs") }}</a>
+ </li>
+ <li class="right selected">
+ <a href="/settings/" class="action index" accesskey="x" rel="nofollow"><img
+ src="/media/default/img/head-menu-config.png" alt=""/>{{ _("Config") }}</a>
+ </li>
+{% endblock %}
+
+{% block content %}
+
+ <ul id="toptabs" class="tabs">
+ {% for configname, config in conf.iteritems() %}
+ <li><a href="#{{configname}}">{{ configname }}</a></li>
+ {% endfor %}
+ </ul>
+
+ <div id="tabsback">
+ <ul id="tabs" class="tabs">
+ {% for configname, config in conf.iteritems() %}
+ <span id="g_{{configname}}">
+ {% if configname != "Accounts" %}
+ {% for skey, section in config.iteritems() %}
+ <li><a href="#{{configname}}{{skey}}">{{ section.desc }}</a></li>
+ {% endfor %}
+ {% else %}
+ {% for skey, section in config.iteritems() %}
+ <li><a href="#{{configname}}{{skey}}">{{ skey }}</a></li>
+ {% endfor %}
+ {% endif %}
+ </span>
+ {% endfor %}
+ </ul>
+ </div>
+ <form id="horizontalForm" action="" method="POST" autocomplete="off">
+ {% for configname, config in conf.iteritems() %}
+ {% if configname != "Accounts" %}
+ {% for skey, section in config.iteritems() %}
+ <div class="tabContent" id="{{configname}}{{skey}}">
+ <table class="settable">
+ {% for okey, option in section.iteritems() %}
+ {% if okey != "desc" %}
+ <tr>
+ <td><label for="{{configname}}|{{skey}}|{{okey}}"
+ style="color:#424242;">{{ option.desc }}:</label></td>
+ <td>
+ {% if option.type == "bool" %}
+ <select id="{{skey}}|{{okey}}" name="{{configname}}|{{skey}}|{{okey}}">
+ <option {% if option.value %} selected="selected"
+ {% endif %}value="True">{{ _("on") }}</option>
+ <option {% if not option.value %} selected="selected"
+ {% endif %}value="False">{{ _("off") }}</option>
+ </select>
+ {% elif ";" in option.type %}
+ <select id="{{skey}}|{{okey}}" name="{{configname}}|{{skey}}|{{okey}}">
+ {% for entry in option.list %}
+ <option {% if option.value == entry %}
+ selected="selected" {% endif %}>{{ entry }}</option>
+ {% endfor %}
+ </select>
+ {% elif option.type == "folder" %}
+ <input name="{{configname}}|{{skey}}|{{okey}}" type="text"
+ id="{{skey}}|{{okey}}" value="{{option.value}}"/>
+ <input name="browsebutton" type="button"
+ onclick="ifield = document.getElementById('{{skey}}|{{okey}}'); pathchooser = window.open('{% if option.value %}{{ "/pathchooser/" + option.value|quotepath }}{% else %}{{ pathroot }}{% endif %}', 'pathchooser', 'scrollbars=yes,toolbar=no,menubar=no,statusbar=no,width=650,height=300'); pathchooser.ifield = ifield; window.ifield = ifield;"
+ value="{{_("Browse")}}"/>
+ {% elif option.type == "file" %}
+ <input name="{{configname}}|{{skey}}|{{okey}}" type="text"
+ id="{{skey}}|{{okey}}" value="{{option.value}}"/>
+ <input name="browsebutton" type="button"
+ onclick="ifield = document.getElementById('{{skey}}|{{okey}}'); filechooser = window.open('{% if option.value %}{{ "/filechooser/" + option.value|quotepath }}{% else %}{{ fileroot }}{% endif %}', 'filechooser', 'scrollbars=yes,toolbar=no,menubar=no,statusbar=no,width=650,height=300'); filechooser.ifield = ifield; window.ifield = ifield;"
+ value="{{_("Browse")}}"/>
+ {% else %}
+ <input id="{{skey}}|{{okey}}" name="{{configname}}|{{skey}}|{{okey}}"
+ type="text" value="{{option.value}}"/>
+ {% endif %}
+ </td>
+ </tr>
+ {% endif %}
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
+ {% else %}
+ <!-- Accounts -->
+ {% for plugin, accounts in config.iteritems() %}
+ <div class="tabContent" id="{{configname}}{{plugin}}">
+ <table class="settable">
+ {% for account in accounts %}
+ <tr>
+ <td><label for="{{configname}}|{{plugin}}|password;{{account.login}}"
+ style="color:#424242;">{{ account.login }}:</label></td>
+ <td>
+ <input id="{{plugin}}|password;{{account.login}}"
+ name="{{configname}}|{{plugin}}|password;{{account.login}}"
+ type="password" value="{{account.password}}" size="14"/>
+ </td>
+ <td>
+ {{ _("Status:") }}
+ {% if account.valid %}
+ <span style="font-weight: bold; color: #006400;">
+ {{ _("valid") }}
+ {% else %}
+ <span style="font-weight: bold; color: #8b0000;">
+ {{ _("not valid") }}
+ {% endif %}
+ </span>
+ </td>
+ <td>
+ {{ _("Valid until:") }}
+ <span style="font-weight: bold;">
+ {{ account.validuntil }}
+ </span>
+ </td>
+ <td>
+ {{ _("Traffic left:") }}
+ <span style="font-weight: bold;">
+ {{ account.trafficleft }}
+ </span>
+ </td>
+ <td>
+ {{ _("Time:") }}
+ <input id="{{configname}}|{{plugin}}|time;{{account.login}}"
+ name="{{configname}}|{{plugin}}|time;{{account.login}}" type="text"
+ size="7" value="{{account.time}}"/>
+ </td>
+ <td>
+ {{ _("Delete? ") }}
+ <input id="{{configname}}|{{plugin}}|delete;{{account.login}}"
+ name="{{configname}}|{{plugin}}|delete;{{account.login}}" type="checkbox"
+ value="True"/>
+ </td>
+ </tr>
+
+ {% endfor %}
+ <tr>
+ <td>&nbsp;</td>
+ </tr>
+
+ <tr>
+ <td><label for="{{configname}}|{{plugin}}"
+ style="color:#424242;">{{ _("New account:") }}</label></td>
+
+ <td>
+ <input id="{{plugin}}|newacc" name="{{configname}}|{{plugin}}|newacc" type="text"
+ size="14"/>
+ </td>
+ </tr>
+ <tr>
+ <td><label for="{{configname}}|{{plugin}}"
+ style="color:#424242;">{{ _("New password:") }}</label></td>
+
+ <td>
+ <input id="{{configname}}|{{plugin}}" name="{{configname}}|{{plugin}}|newpw"
+ type="password" size="14"/>
+ </td>
+ </tr>
+
+ </table>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% if conf %}
+ <input class="submit" type="submit" value="{{_("Submit")}}"/>
+ </form>
+
+ <br>
+ {% for message in errors %}
+ <b>{{ message }}</b><br>
+ {% endfor %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/module/web/templates/jinja/default/test.html b/module/web/templates/jinja/default/test.html
new file mode 100644
index 000000000..b4f17f134
--- /dev/null
+++ b/module/web/templates/jinja/default/test.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Test</title>
+</head>
+<body>
+<h1>Template Test</h1>
+{{ user }}
+{{ status }}
+</body>
+</html> \ No newline at end of file
diff --git a/module/web/templates/jinja/default/window.html b/module/web/templates/jinja/default/window.html
new file mode 100644
index 000000000..734745887
--- /dev/null
+++ b/module/web/templates/jinja/default/window.html
@@ -0,0 +1,45 @@
+<iframe id="upload_target" name="upload_target" src="" style="display: none; width:0;height:0"></iframe>
+<div id="add_bg" style="filter:alpha(opacity:80);KHTMLOpacity:0.80;MozOpacity:0.80;opacity:0.80; background:#000; width:100%; height: 100%; position:fixed; top:0; left:0; display:none;">&nbsp;</div>
+<!--<div id="add_box" style="left:50%; top:200px; margin-left: -450px; width: 900px; position: absolute; background: #FFF; padding: 10px 10px 10px 10px; display:none;">-->
+
+ <!--<div style="width: 900px; text-align: right;"><b onclick="AddBox();">[Close]</b></div>-->
+<div id="add_box" class="myform">
+<form id="add_form" action="/json/add_package" method="POST" enctype="multipart/form-data">
+<h1>{{_("Add Package")}}</h1>
+<p>{{_("Paste your links or upload a container.")}}</p>
+<label for="add_name">{{_("Name")}}
+<span class="small">{{_("The name of the new package.")}}</span>
+</label>
+<input id="add_name" name="add_name" type="text" size="20" />
+
+<label for="add_links">{{_("Links")}}
+<span class="small">{{_("Paste your links here")}}</span>
+</label>
+<textarea rows="5" name="add_links" id="add_links"></textarea>
+
+<label for="add_password">{{_("Password")}}
+ <span class="small">{{_("Password for RAR-Archive")}}</span>
+</label>
+<input id="add_password" name="add_password" type="text" size="20">
+
+<label>{{_("File")}}
+<span class="small">{{_("Upload a container.")}}</span>
+</label>
+<input type="file" name="add_file" id="add_file"/>
+
+<label for="add_dest">{{_("Destination")}}
+</label>
+<span class="cont">
+ {{_("Queue")}}
+ <input type="radio" name="add_dest" id="add_dest" value="1" checked="checked"/>
+ {{_("Collector")}}
+ <input type="radio" name="add_dest" id="add_dest2" value="0"/>
+</span>
+
+<button type="submit">{{_("Add Package")}}</button>
+<button id="add_reset" style="margin-left:0;" type="reset">{{_("Reset")}}</button>
+<div class="spacer"></div>
+
+</form>
+
+</div> \ No newline at end of file
diff --git a/module/web/urls.py b/module/web/urls.py
deleted file mode 100644
index 9fe11f925..000000000
--- a/module/web/urls.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# -*- coding: utf-8 -*-
-from django.conf.urls.defaults import *
-from django.contrib import admin
-from django.conf import settings
-
-
-admin.autodiscover()
-
-urlpatterns = patterns('',
- # Example:
-
- # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
- # to INSTALLED_APPS to enable admin documentation:
- # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
-
- (r'^admin/', include(admin.site.urls)), # django 1.0 not working
- (r'^json/', include('ajax.urls')),
- (r'^flashgot$', 'cnl.views.flashgot'),
- (r'^flash(got)?/?', include('cnl.urls')),
- (r'^crossdomain.xml$', 'cnl.views.crossdomain'),
- (r'^jdcheck.js', 'cnl.views.jdcheck'),
- (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/media/img/favicon.ico'}),
- (r'^media/(?P<path>.*)$', 'django.views.static.serve',
- {'document_root': settings.MEDIA_ROOT}),
- (r'^', include('pyload.urls')),
- )
diff --git a/module/web/utils.py b/module/web/utils.py
new file mode 100644
index 000000000..cf3f2d5f3
--- /dev/null
+++ b/module/web/utils.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this plrogram; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+from os.path import join, abspath, commonprefix
+
+from bottle import request, HTTPError, redirect, ServerAdapter
+
+from webinterface import env, TEMPLATE
+
+def render_to_response(name, args={}, proc=[]):
+ for p in proc:
+ args.update(p())
+
+ t = env.get_template(join(TEMPLATE, name))
+ return t.render(**args)
+
+def parse_permissions(session):
+ perms = {"can_change_status": False,
+ "can_see_dl": False}
+
+ if not session.get("authenticated", False):
+ return perms
+
+ perms["can_change_status"] = True
+ perms["can_see_dl"] = True
+
+ return perms
+
+def parse_userdata(session):
+ return {"name": session.get("name", "Anonymous"),
+ "is_staff": True,
+ "is_authenticated": session.get("authenticated", False)}
+
+def formatSize(size):
+ """formats size of bytes"""
+ size = int(size)
+ steps = 0
+ sizes = ["KB", "MB", "GB", "TB"]
+
+ while size > 1000:
+ size /= 1024.0
+ steps += 1
+
+ return "%.2f %s" % (size, sizes[steps])
+
+def login_required(perm=None):
+ def _dec(func):
+ def _view(*args, **kwargs):
+ s = request.environ.get('beaker.session')
+ if s.get("name", None) and s.get("authenticated", False):
+ if perm:
+ pass
+ #print perm
+ return func(*args, **kwargs)
+ else:
+ if request.header.get('X-Requested-With') == 'XMLHttpRequest':
+ return HTTPError(403, "Forbidden")
+ else:
+ return redirect("/login")
+
+ return _view
+
+ return _dec
+
+class CherryPyWSGI(ServerAdapter):
+
+ def run(self, handler):
+ from wsgiserver import CherryPyWSGIServer
+
+ server = CherryPyWSGIServer((self.host, self.port), handler)
+ server.start()
diff --git a/module/web/webinterface.py b/module/web/webinterface.py
new file mode 100644
index 000000000..fe59c57b1
--- /dev/null
+++ b/module/web/webinterface.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License,
+ or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+
+import sys
+import gettext
+import sqlite3
+
+from os.path import join, abspath,dirname, exists
+from os import makedirs
+
+PROJECT_DIR = abspath(dirname(__file__))
+PYLOAD_DIR = abspath(join(PROJECT_DIR, "..", ".."))
+
+sys.path.append(PYLOAD_DIR)
+sys.path.append(join(PYLOAD_DIR, "module", "lib"))
+
+from module import InitHomeDir
+
+import bottle
+from bottle import run, app
+
+from jinja2 import Environment, FileSystemLoader, FileSystemBytecodeCache
+from middlewares import StripPathMiddleware, GZipMiddleWare, PrefixMiddleware
+
+try:
+ import module.web.ServerThread
+
+ if not module.web.ServerThread.core:
+ raise Exception
+ PYLOAD = module.web.ServerThread.core.server_methods
+ config = module.web.ServerThread.core.config
+except:
+ import xmlrpclib
+
+ ssl = ""
+
+ from module.ConfigParser import ConfigParser
+
+ config = ConfigParser()
+
+ if config.get("ssl", "activated"):
+ ssl = "s"
+
+ server_url = "http%s://%s:%s@%s:%s/" % (
+ ssl,
+ config.username,
+ config.password,
+ config.get("remote", "listenaddr"),
+ config.get("remote", "port")
+ )
+
+ PYLOAD = xmlrpclib.ServerProxy(server_url, allow_none=True)
+
+from module.JsEngine import JsEngine
+
+JS = JsEngine()
+
+TEMPLATE = config.get('webinterface', 'template')
+DL_ROOT = join(PYLOAD_DIR, config.get('general', 'download_folder'))
+LOG_ROOT = join(PYLOAD_DIR, config.get('log', 'log_folder'))
+DEBUG = config.get("general","debug_mode")
+bottle.debug(DEBUG)
+
+def setup_database():
+ conn = sqlite3.connect('web.db')
+ c = conn.cursor()
+ c.execute(
+ 'CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "email" TEXT DEFAULT "" NOT NULL, "password" TEXT NOT NULL, "role" INTEGER DEFAULT 0 NOT NULL, "permission" INTEGER DEFAULT 0 NOT NULL, "template" TEXT DEFAULT "default" NOT NULL)')
+ c.close()
+ conn.commit()
+ conn.close()
+
+setup_database()
+
+
+if not exists(join("tmp", "jinja_cache")):
+ makedirs(join("tmp", "jinja_cache"))
+
+bcc = FileSystemBytecodeCache(join("tmp","jinja_cache"))
+env = Environment(loader=FileSystemLoader(join(PROJECT_DIR, "templates", "jinja")), extensions=['jinja2.ext.i18n'], trim_blocks=True, auto_reload=False, bytecode_cache=bcc)
+
+from filters import quotepath, path_make_relative, path_make_absolute, truncate,date
+
+env.filters["quotepath"] = quotepath
+env.filters["truncate"] = truncate
+env.filters["date"] = date
+env.filters["path_make_relative"] = path_make_relative
+env.filters["path_make_absolute"] = path_make_absolute
+
+
+translation = gettext.translation("django", join(PROJECT_DIR, "locale"),
+ languages=["en", config.get("general","language")])
+translation.install(True)
+env.install_gettext_translations(translation)
+
+from beaker.middleware import SessionMiddleware
+
+session_opts = {
+ 'session.type': 'file',
+ 'session.cookie_expires': -1,
+ 'session.data_dir': './tmp',
+ 'session.auto': False
+}
+
+web = StripPathMiddleware(SessionMiddleware(app(), session_opts))
+web = PrefixMiddleware(web)
+web = GZipMiddleWare(web)
+
+import pyload_app
+import json_app
+import cnl_app
+
+
+def run_simple(host="0.0.0.0", port="8000"):
+ run(app=web, host=host, port=port, quiet=True)
+
+def run_threaded(host="0.0.0.0", port="8000", theads=3, cert="", key=""):
+ from wsgiserver import CherryPyWSGIServer
+ if cert and key:
+ CherryPyWSGIServer.ssl_certificate = cert
+ CherryPyWSGIServer.ssl_private_key = key
+
+ CherryPyWSGIServer.numthreads = theads
+
+ from utils import CherryPyWSGI
+ run(app=web, host=host, port=port, server=CherryPyWSGI, quiet=True)
+
+def run_fcgi(host="0.0.0.0", port="8000"):
+ from bottle import FlupFCGIServer
+ run(app=web, host=host, port=port, server=FlupFCGIServer, quiet=True)
+
+
+if __name__ == "__main__":
+
+ run(app=web, port=8001) \ No newline at end of file
diff --git a/systemCheck.py b/systemCheck.py
index 85c37873d..667a1a53c 100644
--- a/systemCheck.py
+++ b/systemCheck.py
@@ -1,62 +1,50 @@
import os
-from os.path import dirname
-from os.path import exists
-from os.path import join
import subprocess
import sys
-from module import InitHomeDir
+#from module import InitHomeDir
+
+#very ugly prints, but at least it works with python 3
def main():
- print "##### System Information #####"
- print ""
- print "Platform:", sys.platform
- print "Operating System:", os.name
- print "Python:", sys.version.replace("\n", "")
- print ""
+ print("##### System Information #####\n")
+ print("Platform:", sys.platform)
+ print("Operating System:", os.name)
+ print("Python:", sys.version.replace("\n", "")+ "\n")
try:
import pycurl
- print "pycurl:", pycurl.version
+ print("pycurl:", pycurl.version)
except:
- print "pycurl:", "missing"
+ print("pycurl:", "missing")
try:
import Crypto
- print "py-crypto:", Crypto.__version__
+ print("py-crypto:", Crypto.__version__)
except:
- print "py-crypto:", "missing"
+ print("py-crypto:", "missing")
try:
import OpenSSL
- print "OpenSSL:", OpenSSL.version.__version__
+ print("OpenSSL:", OpenSSL.version.__version__)
except:
- print "OpenSSL:", "missing"
+ print("OpenSSL:", "missing")
try:
import Image
- print "image libary:", Image.VERSION
- except:
- print "image libary:", "missing"
-
- try:
- import django
- print "django:", django.get_version()
+ print("image libary:", Image.VERSION)
except:
- print "django:", "missing"
+ print("image libary:", "missing")
try:
import PyQt4.QtCore
- print "pyqt:", PyQt4.QtCore.PYQT_VERSION_STR
+ print("pyqt:", PyQt4.QtCore.PYQT_VERSION_STR)
except:
- print "pyqt:", "missing"
+ print("pyqt:", "missing")
- print ""
- print ""
- print "##### System Status #####"
- print ""
- print "## pyLoadCore ##"
+ print("\n\n##### System Status #####")
+ print("\n## pyLoadCore ##")
core_err = []
core_info = []
@@ -95,23 +83,19 @@ def main():
core_info.append("Install OpenSSL if you want to create a secure connection to the core.")
if core_err:
- print "The system check has detected some errors:"
- print ""
+ print("The system check has detected some errors:\n")
for err in core_err:
- print err
+ print(err)
else:
- print "No Problems detected, pyLoadCore should work fine."
+ print("No Problems detected, pyLoadCore should work fine.")
if core_info:
- print ""
- print "Possible improvements for pyload:"
- print ""
+ print("\nPossible improvements for pyload:\n")
for line in core_info:
- print line
+ print(line)
- print ""
- print "## pyLoadGui ##"
+ print("\n## pyLoadGui ##")
gui_err = []
@@ -121,66 +105,38 @@ def main():
gui_err.append("GUI won't work without pyqt4 !!")
if gui_err:
- print "The system check has detected some errors:"
- print ""
+ print("The system check has detected some errors:\n")
for err in gui_err:
- print err
+ print(err)
else:
- print "No Problems detected, pyLoadGui should work fine."
+ print("No Problems detected, pyLoadGui should work fine.")
- print ""
- print "## Webinterface ##"
+
+ print("\n## Webinterface ##")
web_err = []
web_info = []
try:
- import django
-
- if django.VERSION < (1, 1):
- web_err.append("Your django version is to old, please upgrade to django 1.1")
- elif django.VERSION > (1, 3):
- web_err.append("Your django version is to new, please use django 1.2")
-
- except:
- web_err.append("Webinterface won't work without django !!")
-
- if not exists("pyload.db"):
- web_err.append("You dont have created database yet.")
- web_err.append("Please run: python %s syncdb" % join(dirname(__file__), "module", "web", "manage.py"))
-
- try:
import flup
except:
web_info.append("Install Flup to use FastCGI or optional webservers.")
- try:
- p = subprocess.call(["lighttpd", "-v"], stdout=pipe, stderr=pipe)
- except:
- web_info.append("Install lighttpd as optional webserver.")
-
- try:
- p = subprocess.call(["nginx", "-v"], stdout=pipe, stderr=pipe)
- except:
- web_info.append("Install nginx as optional webserver.")
if web_err:
- print "The system check has detected some errors:"
- print ""
+ print("The system check has detected some errors:\n")
for err in web_err:
- print err
+ print(err)
else:
- print "No Problems detected, Webinterface should work fine."
+ print("No Problems detected, Webinterface should work fine.")
if web_info:
- print ""
- print "Possible improvements for webinterface:"
- print ""
+ print("\nPossible improvements for webinterface:\n")
for line in web_info:
- print line
+ print(line)
if __name__ == "__main__":
main()
- raw_input("Press Enter to Exit.")
+ input("Press Enter to Exit.")