1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3
4"""
5A module of many disparate routines.
6"""
7
8from __future__ import print_function
9
10# functions which moved to paste.request and paste.response
11# Deprecated around 15 Dec 2005
12from paste.request import get_cookies, parse_querystring, parse_formvars
13from paste.request import construct_url, path_info_split, path_info_pop
14from paste.response import HeaderDict, has_header, header_value, remove_header
15from paste.response import error_body_response, error_response, error_response_app
16
17from traceback import print_exception
18import six
19import sys
20from six.moves import cStringIO as StringIO
21from six.moves.urllib.parse import unquote, urlsplit
22import warnings
23
24__all__ = ['add_close', 'add_start_close', 'capture_output', 'catch_errors',
25           'catch_errors_app', 'chained_app_iters', 'construct_url',
26           'dump_environ', 'encode_unicode_app_iter', 'error_body_response',
27           'error_response', 'get_cookies', 'has_header', 'header_value',
28           'interactive', 'intercept_output', 'path_info_pop',
29           'path_info_split', 'raw_interactive', 'send_file']
30
31class add_close(object):
32    """
33    An an iterable that iterates over app_iter, then calls
34    close_func.
35    """
36
37    def __init__(self, app_iterable, close_func):
38        self.app_iterable = app_iterable
39        self.app_iter = iter(app_iterable)
40        self.close_func = close_func
41        self._closed = False
42
43    def __iter__(self):
44        return self
45
46    def next(self):
47        return self.app_iter.next()
48
49    def close(self):
50        self._closed = True
51        if hasattr(self.app_iterable, 'close'):
52            self.app_iterable.close()
53        self.close_func()
54
55    def __del__(self):
56        if not self._closed:
57            # We can't raise an error or anything at this stage
58            print("Error: app_iter.close() was not called when finishing "
59                "WSGI request. finalization function %s not called"
60                  % self.close_func, file=sys.stderr)
61
62class add_start_close(object):
63    """
64    An an iterable that iterates over app_iter, calls start_func
65    before the first item is returned, then calls close_func at the
66    end.
67    """
68
69    def __init__(self, app_iterable, start_func, close_func=None):
70        self.app_iterable = app_iterable
71        self.app_iter = iter(app_iterable)
72        self.first = True
73        self.start_func = start_func
74        self.close_func = close_func
75        self._closed = False
76
77    def __iter__(self):
78        return self
79
80    def next(self):
81        if self.first:
82            self.start_func()
83            self.first = False
84        return next(self.app_iter)
85    __next__ = next
86
87    def close(self):
88        self._closed = True
89        if hasattr(self.app_iterable, 'close'):
90            self.app_iterable.close()
91        if self.close_func is not None:
92            self.close_func()
93
94    def __del__(self):
95        if not self._closed:
96            # We can't raise an error or anything at this stage
97            print("Error: app_iter.close() was not called when finishing "
98                "WSGI request. finalization function %s not called"
99                  % self.close_func, file=sys.stderr)
100
101class chained_app_iters(object):
102
103    """
104    Chains several app_iters together, also delegating .close() to each
105    of them.
106    """
107
108    def __init__(self, *chained):
109        self.app_iters = chained
110        self.chained = [iter(item) for item in chained]
111        self._closed = False
112
113    def __iter__(self):
114        return self
115
116    def next(self):
117        if len(self.chained) == 1:
118            return self.chained[0].next()
119        else:
120            try:
121                return self.chained[0].next()
122            except StopIteration:
123                self.chained.pop(0)
124                return self.next()
125
126    def close(self):
127        self._closed = True
128        got_exc = None
129        for app_iter in self.app_iters:
130            try:
131                if hasattr(app_iter, 'close'):
132                    app_iter.close()
133            except:
134                got_exc = sys.exc_info()
135        if got_exc:
136            six.reraise(got_exc[0], got_exc[1], got_exc[2])
137
138    def __del__(self):
139        if not self._closed:
140            # We can't raise an error or anything at this stage
141            print("Error: app_iter.close() was not called when finishing "
142                "WSGI request. finalization function %s not called"
143                  % self.close_func, file=sys.stderr)
144
145class encode_unicode_app_iter(object):
146    """
147    Encodes an app_iterable's unicode responses as strings
148    """
149
150    def __init__(self, app_iterable, encoding=sys.getdefaultencoding(),
151                 errors='strict'):
152        self.app_iterable = app_iterable
153        self.app_iter = iter(app_iterable)
154        self.encoding = encoding
155        self.errors = errors
156
157    def __iter__(self):
158        return self
159
160    def next(self):
161        content = next(self.app_iter)
162        if isinstance(content, six.text_type):
163            content = content.encode(self.encoding, self.errors)
164        return content
165    __next__ = next
166
167    def close(self):
168        if hasattr(self.app_iterable, 'close'):
169            self.app_iterable.close()
170
171def catch_errors(application, environ, start_response, error_callback,
172                 ok_callback=None):
173    """
174    Runs the application, and returns the application iterator (which should be
175    passed upstream).  If an error occurs then error_callback will be called with
176    exc_info as its sole argument.  If no errors occur and ok_callback is given,
177    then it will be called with no arguments.
178    """
179    try:
180        app_iter = application(environ, start_response)
181    except:
182        error_callback(sys.exc_info())
183        raise
184    if type(app_iter) in (list, tuple):
185        # These won't produce exceptions
186        if ok_callback:
187            ok_callback()
188        return app_iter
189    else:
190        return _wrap_app_iter(app_iter, error_callback, ok_callback)
191
192class _wrap_app_iter(object):
193
194    def __init__(self, app_iterable, error_callback, ok_callback):
195        self.app_iterable = app_iterable
196        self.app_iter = iter(app_iterable)
197        self.error_callback = error_callback
198        self.ok_callback = ok_callback
199        if hasattr(self.app_iterable, 'close'):
200            self.close = self.app_iterable.close
201
202    def __iter__(self):
203        return self
204
205    def next(self):
206        try:
207            return self.app_iter.next()
208        except StopIteration:
209            if self.ok_callback:
210                self.ok_callback()
211            raise
212        except:
213            self.error_callback(sys.exc_info())
214            raise
215
216def catch_errors_app(application, environ, start_response, error_callback_app,
217                     ok_callback=None, catch=Exception):
218    """
219    Like ``catch_errors``, except error_callback_app should be a
220    callable that will receive *three* arguments -- ``environ``,
221    ``start_response``, and ``exc_info``.  It should call
222    ``start_response`` (*with* the exc_info argument!) and return an
223    iterator.
224    """
225    try:
226        app_iter = application(environ, start_response)
227    except catch:
228        return error_callback_app(environ, start_response, sys.exc_info())
229    if type(app_iter) in (list, tuple):
230        # These won't produce exceptions
231        if ok_callback is not None:
232            ok_callback()
233        return app_iter
234    else:
235        return _wrap_app_iter_app(
236            environ, start_response, app_iter,
237            error_callback_app, ok_callback, catch=catch)
238
239class _wrap_app_iter_app(object):
240
241    def __init__(self, environ, start_response, app_iterable,
242                 error_callback_app, ok_callback, catch=Exception):
243        self.environ = environ
244        self.start_response = start_response
245        self.app_iterable = app_iterable
246        self.app_iter = iter(app_iterable)
247        self.error_callback_app = error_callback_app
248        self.ok_callback = ok_callback
249        self.catch = catch
250        if hasattr(self.app_iterable, 'close'):
251            self.close = self.app_iterable.close
252
253    def __iter__(self):
254        return self
255
256    def next(self):
257        try:
258            return self.app_iter.next()
259        except StopIteration:
260            if self.ok_callback:
261                self.ok_callback()
262            raise
263        except self.catch:
264            if hasattr(self.app_iterable, 'close'):
265                try:
266                    self.app_iterable.close()
267                except:
268                    # @@: Print to wsgi.errors?
269                    pass
270            new_app_iterable = self.error_callback_app(
271                self.environ, self.start_response, sys.exc_info())
272            app_iter = iter(new_app_iterable)
273            if hasattr(new_app_iterable, 'close'):
274                self.close = new_app_iterable.close
275            self.next = app_iter.next
276            return self.next()
277
278def raw_interactive(application, path='', raise_on_wsgi_error=False,
279                    **environ):
280    """
281    Runs the application in a fake environment.
282    """
283    assert "path_info" not in environ, "argument list changed"
284    if raise_on_wsgi_error:
285        errors = ErrorRaiser()
286    else:
287        errors = six.BytesIO()
288    basic_environ = {
289        # mandatory CGI variables
290        'REQUEST_METHOD': 'GET',     # always mandatory
291        'SCRIPT_NAME': '',           # may be empty if app is at the root
292        'PATH_INFO': '',             # may be empty if at root of app
293        'SERVER_NAME': 'localhost',  # always mandatory
294        'SERVER_PORT': '80',         # always mandatory
295        'SERVER_PROTOCOL': 'HTTP/1.0',
296        # mandatory wsgi variables
297        'wsgi.version': (1, 0),
298        'wsgi.url_scheme': 'http',
299        'wsgi.input': six.BytesIO(),
300        'wsgi.errors': errors,
301        'wsgi.multithread': False,
302        'wsgi.multiprocess': False,
303        'wsgi.run_once': False,
304        }
305    if path:
306        (_, _, path_info, query, fragment) = urlsplit(str(path))
307        path_info = unquote(path_info)
308        # urlsplit returns unicode so coerce it back to str
309        path_info, query = str(path_info), str(query)
310        basic_environ['PATH_INFO'] = path_info
311        if query:
312            basic_environ['QUERY_STRING'] = query
313    for name, value in environ.items():
314        name = name.replace('__', '.')
315        basic_environ[name] = value
316    if ('SERVER_NAME' in basic_environ
317        and 'HTTP_HOST' not in basic_environ):
318        basic_environ['HTTP_HOST'] = basic_environ['SERVER_NAME']
319    istream = basic_environ['wsgi.input']
320    if isinstance(istream, bytes):
321        basic_environ['wsgi.input'] = six.BytesIO(istream)
322        basic_environ['CONTENT_LENGTH'] = len(istream)
323    data = {}
324    output = []
325    headers_set = []
326    headers_sent = []
327    def start_response(status, headers, exc_info=None):
328        if exc_info:
329            try:
330                if headers_sent:
331                    # Re-raise original exception only if headers sent
332                    six.reraise(exc_info[0], exc_info[1], exc_info[2])
333            finally:
334                # avoid dangling circular reference
335                exc_info = None
336        elif headers_set:
337            # You cannot set the headers more than once, unless the
338            # exc_info is provided.
339            raise AssertionError("Headers already set and no exc_info!")
340        headers_set.append(True)
341        data['status'] = status
342        data['headers'] = headers
343        return output.append
344    app_iter = application(basic_environ, start_response)
345    try:
346        try:
347            for s in app_iter:
348                if not isinstance(s, six.binary_type):
349                    raise ValueError(
350                        "The app_iter response can only contain bytes (not "
351                        "unicode); got: %r" % s)
352                headers_sent.append(True)
353                if not headers_set:
354                    raise AssertionError("Content sent w/o headers!")
355                output.append(s)
356        except TypeError as e:
357            # Typically "iteration over non-sequence", so we want
358            # to give better debugging information...
359            e.args = ((e.args[0] + ' iterable: %r' % app_iter),) + e.args[1:]
360            raise
361    finally:
362        if hasattr(app_iter, 'close'):
363            app_iter.close()
364    return (data['status'], data['headers'], b''.join(output),
365            errors.getvalue())
366
367class ErrorRaiser(object):
368
369    def flush(self):
370        pass
371
372    def write(self, value):
373        if not value:
374            return
375        raise AssertionError(
376            "No errors should be written (got: %r)" % value)
377
378    def writelines(self, seq):
379        raise AssertionError(
380            "No errors should be written (got lines: %s)" % list(seq))
381
382    def getvalue(self):
383        return ''
384
385def interactive(*args, **kw):
386    """
387    Runs the application interatively, wrapping `raw_interactive` but
388    returning the output in a formatted way.
389    """
390    status, headers, content, errors = raw_interactive(*args, **kw)
391    full = StringIO()
392    if errors:
393        full.write('Errors:\n')
394        full.write(errors.strip())
395        full.write('\n----------end errors\n')
396    full.write(status + '\n')
397    for name, value in headers:
398        full.write('%s: %s\n' % (name, value))
399    full.write('\n')
400    full.write(content)
401    return full.getvalue()
402interactive.proxy = 'raw_interactive'
403
404def dump_environ(environ, start_response):
405    """
406    Application which simply dumps the current environment
407    variables out as a plain text response.
408    """
409    output = []
410    keys = list(environ.keys())
411    keys.sort()
412    for k in keys:
413        v = str(environ[k]).replace("\n","\n    ")
414        output.append("%s: %s\n" % (k, v))
415    output.append("\n")
416    content_length = environ.get("CONTENT_LENGTH", '')
417    if content_length:
418        output.append(environ['wsgi.input'].read(int(content_length)))
419        output.append("\n")
420    output = "".join(output)
421    if six.PY3:
422        output = output.encode('utf8')
423    headers = [('Content-Type', 'text/plain'),
424               ('Content-Length', str(len(output)))]
425    start_response("200 OK", headers)
426    return [output]
427
428def send_file(filename):
429    warnings.warn(
430        "wsgilib.send_file has been moved to paste.fileapp.FileApp",
431        DeprecationWarning, 2)
432    from paste import fileapp
433    return fileapp.FileApp(filename)
434
435def capture_output(environ, start_response, application):
436    """
437    Runs application with environ and start_response, and captures
438    status, headers, and body.
439
440    Sends status and header, but *not* body.  Returns (status,
441    headers, body).  Typically this is used like:
442
443    .. code-block:: python
444
445        def dehtmlifying_middleware(application):
446            def replacement_app(environ, start_response):
447                status, headers, body = capture_output(
448                    environ, start_response, application)
449                content_type = header_value(headers, 'content-type')
450                if (not content_type
451                    or not content_type.startswith('text/html')):
452                    return [body]
453                body = re.sub(r'<.*?>', '', body)
454                return [body]
455            return replacement_app
456
457    """
458    warnings.warn(
459        'wsgilib.capture_output has been deprecated in favor '
460        'of wsgilib.intercept_output',
461        DeprecationWarning, 2)
462    data = []
463    output = StringIO()
464    def replacement_start_response(status, headers, exc_info=None):
465        if data:
466            data[:] = []
467        data.append(status)
468        data.append(headers)
469        start_response(status, headers, exc_info)
470        return output.write
471    app_iter = application(environ, replacement_start_response)
472    try:
473        for item in app_iter:
474            output.write(item)
475    finally:
476        if hasattr(app_iter, 'close'):
477            app_iter.close()
478    if not data:
479        data.append(None)
480    if len(data) < 2:
481        data.append(None)
482    data.append(output.getvalue())
483    return data
484
485def intercept_output(environ, application, conditional=None,
486                     start_response=None):
487    """
488    Runs application with environ and captures status, headers, and
489    body.  None are sent on; you must send them on yourself (unlike
490    ``capture_output``)
491
492    Typically this is used like:
493
494    .. code-block:: python
495
496        def dehtmlifying_middleware(application):
497            def replacement_app(environ, start_response):
498                status, headers, body = intercept_output(
499                    environ, application)
500                start_response(status, headers)
501                content_type = header_value(headers, 'content-type')
502                if (not content_type
503                    or not content_type.startswith('text/html')):
504                    return [body]
505                body = re.sub(r'<.*?>', '', body)
506                return [body]
507            return replacement_app
508
509    A third optional argument ``conditional`` should be a function
510    that takes ``conditional(status, headers)`` and returns False if
511    the request should not be intercepted.  In that case
512    ``start_response`` will be called and ``(None, None, app_iter)``
513    will be returned.  You must detect that in your code and return
514    the app_iter, like:
515
516    .. code-block:: python
517
518        def dehtmlifying_middleware(application):
519            def replacement_app(environ, start_response):
520                status, headers, body = intercept_output(
521                    environ, application,
522                    lambda s, h: header_value(headers, 'content-type').startswith('text/html'),
523                    start_response)
524                if status is None:
525                    return body
526                start_response(status, headers)
527                body = re.sub(r'<.*?>', '', body)
528                return [body]
529            return replacement_app
530    """
531    if conditional is not None and start_response is None:
532        raise TypeError(
533            "If you provide conditional you must also provide "
534            "start_response")
535    data = []
536    output = StringIO()
537    def replacement_start_response(status, headers, exc_info=None):
538        if conditional is not None and not conditional(status, headers):
539            data.append(None)
540            return start_response(status, headers, exc_info)
541        if data:
542            data[:] = []
543        data.append(status)
544        data.append(headers)
545        return output.write
546    app_iter = application(environ, replacement_start_response)
547    if data[0] is None:
548        return (None, None, app_iter)
549    try:
550        for item in app_iter:
551            output.write(item)
552    finally:
553        if hasattr(app_iter, 'close'):
554            app_iter.close()
555    if not data:
556        data.append(None)
557    if len(data) < 2:
558        data.append(None)
559    data.append(output.getvalue())
560    return data
561
562## Deprecation warning wrapper:
563
564class ResponseHeaderDict(HeaderDict):
565
566    def __init__(self, *args, **kw):
567        warnings.warn(
568            "The class wsgilib.ResponseHeaderDict has been moved "
569            "to paste.response.HeaderDict",
570            DeprecationWarning, 2)
571        HeaderDict.__init__(self, *args, **kw)
572
573def _warn_deprecated(new_func):
574    new_name = new_func.func_name
575    new_path = new_func.func_globals['__name__'] + '.' + new_name
576    def replacement(*args, **kw):
577        warnings.warn(
578            "The function wsgilib.%s has been moved to %s"
579            % (new_name, new_path),
580            DeprecationWarning, 2)
581        return new_func(*args, **kw)
582    try:
583        replacement.func_name = new_func.func_name
584    except:
585        pass
586    return replacement
587
588# Put warnings wrapper in place for all public functions that
589# were imported from elsewhere:
590
591for _name in __all__:
592    _func = globals()[_name]
593    if (hasattr(_func, 'func_globals')
594        and _func.func_globals['__name__'] != __name__):
595        globals()[_name] = _warn_deprecated(_func)
596
597if __name__ == '__main__':
598    import doctest
599    doctest.testmod()
600
601