diff options
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 Binary files differnew file mode 100644 index 000000000..1eebf6ce1 --- /dev/null +++ b/module/web/locale/cs/LC_MESSAGES/django.mo 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">© 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>
+
+ <span class="buttons" style="opacity:0">
+ <img title="{{_("Delete Package")}}" style="cursor: pointer" width="12px" height="12px" src="/media/default/img/delete.png" />
+
+ <img title="{{_("Restart Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/arrow_refresh.png" />
+
+ <img title="{{_("Edit Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/pencil.png" />
+
+ <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': ' ',
+ '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 }}%;"> </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" }}"><< {{_("Start")}}</a> <a href="{{ "/logs/" + iprev|string }}">< {{_("prev")}}</a> <a href="{{ "/logs/" + inext|string }}">{{_("next")}} ></a> <a href="/logs/">{{_("End")}} >></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 %} /> + <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;"> </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} ".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> ".substitute({"plugin": link.plugin}); + html += "<img title='{{_("Delete Link")}}' style='cursor: pointer;' width='10px' height='10px' src='/media/default/img/delete.png' /> "; + 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>
+
+ <span class="buttons" style="opacity:0">
+ <img title="{{_("Delete Package")}}" style="cursor: pointer" width="12px" height="12px" src="/media/default/img/delete.png" />
+
+ <img title="{{_("Restart Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/arrow_refresh.png" />
+
+ <img title="{{_("Edit Package")}}" style="margin-left: -10px; cursor: pointer" height="12px" src="/media/default/img/pencil.png" />
+
+ <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> </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;"> </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.") |