diff options
Diffstat (limited to 'module')
37 files changed, 6970 insertions, 1036 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.moBinary files differ new 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 | 
