Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/utils.py: 23%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import io
4import mimetypes
5import os
6import pkgutil
7import re
8import sys
9import typing as t
10import unicodedata
11import wsgiref.types as wt
12from datetime import datetime
13from functools import update_wrapper
14from time import time
15from urllib.parse import quote
17from markupsafe import escape
19from ._internal import _missing
20from .datastructures.cache_control import ResponseCacheControl
21from .datastructures.etag import ETag
22from .datastructures.headers import Headers
23from .exceptions import HTTPException
24from .exceptions import NotFound
25from .security import _windows_device_files
26from .security import safe_join
28if t.TYPE_CHECKING:
29 from .wrappers.response import Response
31T = t.TypeVar("T")
33_entity_re = re.compile(r"&([^;]+);")
34_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]")
37class cached_property(property, t.Generic[T]):
38 """A :func:`property` that is only evaluated once. Subsequent access
39 returns the cached value. Setting the property sets the cached
40 value. Deleting the property clears the cached value, accessing it
41 again will evaluate it again.
43 .. code-block:: python
45 class Example:
46 @cached_property
47 def value(self):
48 # calculate something important here
49 return 42
51 e = Example()
52 e.value # evaluates
53 e.value # uses cache
54 e.value = 16 # sets cache
55 del e.value # clears cache
57 If the class defines ``__slots__``, it must add ``_cache_{name}`` as
58 a slot. Alternatively, it can add ``__dict__``, but that's usually
59 not desirable.
61 .. versionchanged:: 2.1
62 Works with ``__slots__``.
64 .. versionchanged:: 2.0
65 ``del obj.name`` clears the cached value.
66 """
68 def __init__(
69 self,
70 fget: t.Callable[[t.Any], T],
71 name: str | None = None,
72 doc: str | None = None,
73 ) -> None:
74 update_wrapper(self, fget) # type: ignore[arg-type]
75 super().__init__(fget, doc=doc)
77 if name is not None:
78 self.__name__ = name
80 self.slot_name = f"_cache_{self.__name__}"
82 @t.overload
83 def __get__(self, obj: None, cls: type[t.Any], /) -> t.Self: ...
84 @t.overload
85 def __get__(self, obj: t.Any, cls: type[t.Any] | None = ..., /) -> T: ...
86 def __get__(
87 self, obj: t.Any | None = None, cls: type[t.Any] | None = None, /
88 ) -> T | t.Self:
89 if obj is None:
90 return self
92 obj_dict = getattr(obj, "__dict__", None)
94 if obj_dict is not None:
95 value: T = obj_dict.get(self.__name__, _missing)
96 else:
97 value = getattr(obj, self.slot_name, _missing) # type: ignore[arg-type]
99 if value is _missing:
100 value = self.fget(obj) # type: ignore[misc]
102 if obj_dict is not None:
103 obj.__dict__[self.__name__] = value
104 else:
105 setattr(obj, self.slot_name, value)
107 return value
109 def __set__(self, obj: object, value: T) -> None:
110 if hasattr(obj, "__dict__"):
111 obj.__dict__[self.__name__] = value
112 else:
113 setattr(obj, self.slot_name, value)
115 def __delete__(self, obj: t.Any) -> None:
116 if hasattr(obj, "__dict__"):
117 obj.__dict__.pop(self.__name__, None)
118 else:
119 setattr(obj, self.slot_name, _missing)
122# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in
123# https://www.iana.org/assignments/media-types/media-types.xhtml
124# Types listed in the XDG mime info that have a charset in the IANA registration.
125_charset_mimetypes = {
126 "application/ecmascript",
127 "application/javascript",
128 "application/sql",
129 "application/xml",
130 "application/xml-dtd",
131 "application/xml-external-parsed-entity",
132}
135def _get_content_type(mimetype: str, charset: str) -> str:
136 """Returns the full content type string with charset for a mimetype.
138 If the mimetype represents text, the charset parameter will be
139 appended, otherwise the mimetype is returned unchanged.
141 :param mimetype: The mimetype to be used as content type.
142 :param charset: The charset to be appended for text mimetypes.
143 :return: The content type.
145 .. deprecated:: 3.2
146 Will be removed in Werkzeug 4.0. Set ``Response.mimetype`` instead.
148 .. versionchanged:: 0.15
149 Any type that ends with ``+xml`` gets a charset, not just those
150 that start with ``application/``. Known text types such as
151 ``application/javascript`` are also given charsets.
152 """
153 # TODO remove charset param after deprecation
154 if (
155 mimetype.startswith("text/")
156 or mimetype in _charset_mimetypes
157 or mimetype.endswith("+xml")
158 ):
159 mimetype += f"; charset={charset}"
161 return mimetype
164def secure_filename(filename: str) -> str:
165 """Validate and modify a filename so that it is safe to use on a regular
166 filesystem and in :func:`os.path.join`. This may produce an empty string if
167 nothing in the filename could be safely preserved.
169 Only the ASCII letters, digits, and ``._-`` characters are allowed, all
170 others are removed or replaced. Unicode characters are decomposed and
171 replaced with ASCII equivalents according to NFKD. ``/`` (and ``\\`` on
172 Windows) and whitespace, are replaced with ``_``. Surrounding ``._``
173 characters are removed.
175 On Windows, special device names such as ``CON`` and ``NUL`` will be
176 prefixed with ``_`` to avoid access.
178 This only considers the filename, it does not validate if the file does or
179 does not exist, if it is a symlink, or other properties of the file.
181 :param filename: The filename to validate and modify.
183 .. versionadded:: 0.5
184 """
185 # decompose combined chars, replace compat chars
186 filename = unicodedata.normalize("NFKD", filename)
187 # only keep ASCII chars
188 filename = filename.encode("ascii", "ignore").decode("ascii")
190 for sep in os.sep, os.path.altsep:
191 if sep:
192 filename = filename.replace(sep, " ")
194 # replace any whitespace with _
195 filename = "_".join(filename.split())
196 # only allow ASCII letters, numbers, and _.-
197 filename = _filename_ascii_strip_re.sub("", filename)
198 # remove surrounding ._, left behind after replacing slash in ../
199 filename = filename.strip("._")
201 # prefix special device names on Windows
202 if (
203 os.name == "nt"
204 and filename.partition(".")[0].strip().upper() in _windows_device_files
205 ):
206 filename = f"_{filename}"
208 return filename
211def redirect(
212 location: str, code: int = 303, Response: type[Response] | None = None
213) -> Response:
214 """Create a response that redirects the client to the target location.
216 The default ``303 See Other`` status code instructs the client to make a
217 ``GET`` request to the target, regardless of what method the current request
218 is. This produces the correct result for the common use cases: page redirects
219 and form success. The status codes you're likely to use are:
221 - ``303 See Other`` always uses a ``GET`` request.
222 - ``307 Temporary Redirect`` preserves the current method.
223 - ``308 Permanent Redirect`` preserves the current method, and instructs
224 the client to permanently apply the result. This is hard to undo once
225 you've sent it, so be sure the permanence is what you want.
227 Two older codes, ``302 Found`` and ``301 Moved Permanently``, are
228 superseded by ``307`` and ``308`` respectively. They were not consistently
229 implemented by clients, which tend to switch ``POST`` to ``GET`` but
230 preserve other methods. Prefer using ``303``, ``307``, and ``308`` to get
231 the exact behavior you intend. Other ``3xx`` codes are either not defined or
232 have specific use cases.
234 :param location: The URL to redirect to. The client will interpret a
235 relative URL (without the host) as relative to the host it's accessing.
236 :param code: The redirect status code. This affects how the client issues
237 the next request. Defaults to ``303``.
238 :param Response: The response class. Defaults to
239 :class:`werkzeug.wrappers.Response`.
241 .. versionchanged:: 3.2
242 ``code`` defaults to 303 instead of 302.
244 .. versionchanged:: 0.10
245 Added the ``response`` parameter.
247 .. versionchanged:: 0.6
248 ``location`` can contain Unicode characters.
249 """
250 if Response is None:
251 from .wrappers import Response
253 html_location = escape(location)
254 response = Response( # type: ignore[misc]
255 "<!doctype html>\n"
256 "<html lang=en>\n"
257 "<title>Redirecting...</title>\n"
258 "<h1>Redirecting...</h1>\n"
259 "<p>You should be redirected automatically to the target URL: "
260 f'<a href="{html_location}">{html_location}</a>. If not, click the link.\n',
261 code,
262 mimetype="text/html",
263 )
264 response.location = location
265 return response
268def _append_slash_redirect(environ: wt.WSGIEnvironment, code: int = 308) -> Response:
269 """Redirect to the current URL with a slash appended.
271 If the current URL is ``/user/42``, the redirect URL will be
272 ``42/``. When joined to the current URL during response
273 processing or by the browser, this will produce ``/user/42/``.
275 The behavior is undefined if the path ends with a slash already. If
276 called unconditionally on a URL, it may produce a redirect loop.
278 :param environ: Use the path and query from this WSGI environment
279 to produce the redirect URL.
280 :param code: the status code for the redirect.
282 .. deprecated:: 3.2
283 Will be removed in Werkzeug 4.0. Use ``routing.Map`` with
284 ``strict_slashes`` instead.
286 .. versionchanged:: 2.1
287 Produce a relative URL that only modifies the last segment.
288 Relevant when the current path has multiple segments.
290 .. versionchanged:: 2.1
291 The default status code is 308 instead of 301. This preserves
292 the request method and body.
293 """
294 tail = environ["PATH_INFO"].rpartition("/")[2]
296 if not tail:
297 new_path = "./"
298 else:
299 new_path = f"{tail}/"
301 query_string = environ.get("QUERY_STRING")
303 if query_string:
304 new_path = f"{new_path}?{query_string}"
306 return redirect(new_path, code)
309def send_file(
310 path_or_file: os.PathLike[str] | str | t.IO[bytes],
311 environ: wt.WSGIEnvironment,
312 mimetype: str | None = None,
313 as_attachment: bool = False,
314 download_name: str | None = None,
315 conditional: bool = True,
316 etag: bool | ETag | str = True,
317 last_modified: datetime | int | float | None = None,
318 max_age: int
319 | t.Callable[[os.PathLike[str] | str | None], int | None]
320 | None = None,
321 use_x_sendfile: bool = False,
322 response_class: type[Response] | None = None,
323 _root_path: os.PathLike[str] | str | None = None,
324) -> Response:
325 """Send the contents of a file to the client.
327 The first argument can be a file path or a file-like object in ``rb`` mode.
328 Paths are preferred in most cases because Werkzeug can manage the file.
329 Passing a file-like object is mostly useful when building a file in memory
330 with :class:`io.BytesIO`. If the file is seekable, its position will be set
331 to the beginning.
333 Never pass a path provided by a user. The path is assumed to be trusted, so
334 a user could craft a path to access a file you didn't intend. Use
335 :func:`send_from_directory` to safely serve user-provided paths.
337 If the WSGI server provides ``environ["wsgi.file_wrapper"]``, it is
338 used. Alternatively, if the HTTP server supports ``X-Sendfile``,
339 ``use_x_sendfile=True`` will tell the server to send the given path, which
340 is much more efficient than reading it in Python.
342 :param path_or_file: The path to the file to send, relative to the current
343 directory if a relative path is given. Alternatively, a file-like object
344 in ``rb`` mode. The path will be ``file.name`` if available.
345 :param environ: The WSGI environ for the current request.
346 :param mimetype: The MIME type to send for the file. If not provided, it is
347 detected from the file, and falls back to ``application/octet-stream``.
348 :param as_attachment: Indicate to a browser that it should offer to save the
349 file instead of displaying it.
350 :param download_name: The default name browsers will use when saving the
351 file. Defaults to the passed file name.
352 :param conditional: Enable conditional and range responses based on request
353 headers.
354 :param etag: Generate a strong ETag based on the file's last modified time
355 and size if available. Or an already generated ETag, assumed strong if a
356 ``str`` is given.
357 :param last_modified: The last modified time as a ``datetime`` or seconds
358 seconds. If not provided, it is detected from the file. Typically only
359 needs to be provided for ``BytesIO``.
360 :param max_age: How long the client should cache the file, in seconds. If
361 set, ``Cache-Control`` will be ``public``, otherwise it will be
362 ``no-cache`` to prefer conditional caching.
363 :param use_x_sendfile: Set the ``X-Sendfile`` header to let the server
364 efficiently send the file. Requires support from the HTTP server..
365 :param response_class: Build the response using this class. Defaults to
366 :class:`.Response`.
367 :param _root_path: Do not use. For internal use only. Use
368 :func:`send_from_directory` to safely send files under a path.
370 .. versionchanged:: 3.2
371 Improve handling of conditional and range requests.
373 Path, size, and modification time are detected from file-like objects.
374 The position is set to the beginning.
376 If ``mimetype`` nor ``download_name`` is given, default to
377 ``application/octet-stream``.
379 .. versionchanged:: 2.0.2
380 ``send_file`` only sets a detected ``Content-Encoding`` if
381 ``as_attachment`` is disabled.
383 .. versionadded:: 2.0
384 Adapted from Flask's implementation.
386 ``download_name`` replaces Flask's ``attachment_filename`` parameter. If
387 ``as_attachment=False``, it is passed with
388 ``Content-Disposition: inline`` instead.
390 ``max_age`` replaces Flask's ``cache_timeout`` parameter.
391 ``conditional`` is enabled and ``max_age`` is not set by default.
393 ``etag`` replaces Flask's ``add_etags`` parameter. It can be a string to
394 use instead of generating one.
396 If an encoding is returned when guessing ``mimetype``, set the
397 ``Content-Encoding`` header.
398 """
399 if response_class is None:
400 from .wrappers import Response
402 response_class = Response
404 path: os.PathLike[str] | str | None = None
405 file: t.IO[bytes]
406 size: int | None = None
407 headers = Headers()
409 if isinstance(path_or_file, (os.PathLike, str)):
410 path = path_or_file
412 # Flask will pass app.root_path, allowing its send_file wrapper
413 # to not have to deal with paths.
414 if _root_path is not None:
415 path = os.path.join(_root_path, path)
416 else:
417 path = os.path.abspath(path)
419 stat = os.stat(path)
420 size = stat.st_size
422 if last_modified is None:
423 last_modified = stat.st_mtime
425 file = open(path, "rb")
426 else:
427 file = path_or_file
429 if isinstance(file, io.TextIOBase):
430 raise ValueError("File must be in binary mode, or 'BytesIO'.")
432 if hasattr(file, "name"):
433 path = file.name
435 if file.seekable():
436 size = file.seek(0, os.SEEK_END)
437 file.seek(0)
439 if size is None or last_modified is None:
440 try:
441 fileno = file.fileno()
442 except OSError:
443 pass
444 else:
445 stat = os.stat(fileno)
447 if size is None:
448 size = stat.st_size
450 if last_modified is None:
451 last_modified = stat.st_mtime
453 if download_name is None and path is not None:
454 download_name = os.path.basename(path)
456 if mimetype is None:
457 if download_name is None:
458 mimetype = "application/octet-stream"
459 else:
460 mimetype, encoding = mimetypes.guess_type(download_name)
462 if mimetype is None:
463 mimetype = "application/octet-stream"
465 # Don't send encoding for attachments, it causes browsers to
466 # decompress tar.gz files when saving.
467 if encoding is not None and not as_attachment:
468 headers["Content-Encoding"] = encoding
470 if download_name is not None:
471 if download_name.isascii():
472 names = {"filename": download_name}
473 else:
474 simple = unicodedata.normalize("NFKD", download_name)
475 simple = simple.encode("ascii", "ignore").decode("ascii")
476 # safe = RFC 5987 attr-char
477 quoted = quote(download_name, safe="!#$&+-.^_`|~")
478 names = {"filename": simple, "filename*": f"UTF-8''{quoted}"}
480 value = "attachment" if as_attachment else "inline"
481 headers.set("Content-Disposition", value, **names)
482 elif as_attachment:
483 raise TypeError(
484 "No name provided for attachment. Either pass 'download_name', or"
485 " pass a path instead of a file."
486 )
488 from .wrappers.request import Request
490 request = Request(environ)
491 rv = response_class(
492 request.wrap_file(file),
493 mimetype=mimetype,
494 headers=headers,
495 direct_passthrough=True,
496 )
498 if use_x_sendfile and path is not None:
499 file.close()
500 rv.headers["X-Sendfile"] = path
501 rv.response = []
503 if size is not None:
504 rv.content_length = size
506 if last_modified is not None:
507 rv.last_modified = last_modified
509 cache_control = ResponseCacheControl()
510 cache_control.no_cache = True
512 # Flask will pass app.get_send_file_max_age, allowing its send_file
513 # wrapper to not have to deal with paths.
514 if callable(max_age):
515 max_age = max_age(path)
517 if max_age is not None:
518 if max_age > 0:
519 cache_control.no_cache = None
520 cache_control.public = True
522 cache_control.max_age = max_age
523 rv.expires = int(time() + max_age)
525 rv.cache_control = cache_control
527 if isinstance(etag, str):
528 rv.etag = etag
529 elif etag:
530 parts = []
532 if last_modified is not None:
533 if isinstance(last_modified, datetime):
534 parts.append(str(last_modified.timestamp()))
535 else:
536 parts.append(str(last_modified))
538 if size is not None:
539 parts.append(str(size))
541 rv.etag = "-".join(parts)
543 if conditional:
544 try:
545 rv.apply_conditions(request)
546 rv.apply_range(request)
547 except HTTPException:
548 rv.close()
549 raise
551 if rv.status_code == 304:
552 rv.headers.pop("X-Sendfile", None)
554 return rv
557def send_from_directory(
558 directory: os.PathLike[str] | str,
559 path: os.PathLike[str] | str,
560 environ: wt.WSGIEnvironment,
561 **kwargs: t.Any,
562) -> Response:
563 """Send a file from within a directory using :func:`send_file`.
565 This is a secure way to serve files from a folder, such as static
566 files or uploads. Uses :func:`~werkzeug.security.safe_join` to
567 ensure the path coming from the client is not maliciously crafted to
568 point outside the specified directory.
570 If the final path does not point to an existing regular file,
571 returns a 404 :exc:`~werkzeug.exceptions.NotFound` error.
573 :param directory: The directory that ``path`` must be located under. This *must not*
574 be a value provided by the client, otherwise it becomes insecure.
575 :param path: The path to the file to send, relative to ``directory``. This is the
576 part of the path provided by the client, which is checked for security.
577 :param environ: The WSGI environ for the current request.
578 :param kwargs: Arguments to pass to :func:`send_file`.
580 .. versionadded:: 2.0
581 Adapted from Flask's implementation.
582 """
583 path_str = safe_join(os.fspath(directory), os.fspath(path))
585 if path_str is None:
586 raise NotFound()
588 # Flask will pass app.root_path, allowing its send_from_directory
589 # wrapper to not have to deal with paths.
590 if "_root_path" in kwargs:
591 path_str = os.path.join(kwargs["_root_path"], path_str)
593 if not os.path.isfile(path_str):
594 raise NotFound()
596 return send_file(path_str, environ, **kwargs)
599def import_string(import_name: str, silent: bool = False) -> t.Any:
600 """Imports an object based on a string. This is useful if you want to
601 use import paths as endpoints or something similar. An import path can
602 be specified either in dotted notation (``xml.sax.saxutils.escape``)
603 or with a colon as object delimiter (``xml.sax.saxutils:escape``).
605 If `silent` is True the return value will be `None` if the import fails.
607 :param import_name: the dotted name for the object to import.
608 :param silent: if set to `True` import errors are ignored and
609 `None` is returned instead.
610 :return: imported object
611 """
612 import_name = import_name.replace(":", ".")
613 try:
614 try:
615 __import__(import_name)
616 except ImportError:
617 if "." not in import_name:
618 raise
619 else:
620 return sys.modules[import_name]
622 module_name, obj_name = import_name.rsplit(".", 1)
623 module = __import__(module_name, globals(), locals(), [obj_name])
624 try:
625 return getattr(module, obj_name)
626 except AttributeError as e:
627 raise ImportError(e) from None
629 except ImportError as e:
630 if not silent:
631 raise ImportStringError(import_name, e).with_traceback(
632 sys.exc_info()[2]
633 ) from None
635 return None
638def find_modules(
639 import_path: str, include_packages: bool = False, recursive: bool = False
640) -> t.Iterator[str]:
641 """Finds all the modules below a package. This can be useful to
642 automatically import all views / controllers so that their metaclasses /
643 function decorators have a chance to register themselves on the
644 application.
646 Packages are not returned unless `include_packages` is `True`. This can
647 also recursively list modules but in that case it will import all the
648 packages to get the correct load path of that module.
650 :param import_path: the dotted name for the package to find child modules.
651 :param include_packages: set to `True` if packages should be returned, too.
652 :param recursive: set to `True` if recursion should happen.
653 :return: generator
654 """
655 module = import_string(import_path)
656 path = getattr(module, "__path__", None)
657 if path is None:
658 raise ValueError(f"{import_path!r} is not a package")
659 basename = f"{module.__name__}."
660 for _importer, modname, ispkg in pkgutil.iter_modules(path):
661 modname = basename + modname
662 if ispkg:
663 if include_packages:
664 yield modname
665 if recursive:
666 yield from find_modules(modname, include_packages, True)
667 else:
668 yield modname
671class ImportStringError(ImportError):
672 """Provides information about a failed :func:`import_string` attempt."""
674 #: String in dotted notation that failed to be imported.
675 import_name: str
676 #: Wrapped exception.
677 exception: BaseException
679 def __init__(self, import_name: str, exception: BaseException) -> None:
680 self.import_name = import_name
681 self.exception = exception
682 msg = import_name
683 name = ""
684 tracked = []
685 for part in import_name.replace(":", ".").split("."):
686 name = f"{name}.{part}" if name else part
687 imported = import_string(name, silent=True)
688 if imported:
689 tracked.append((name, getattr(imported, "__file__", None)))
690 else:
691 track = [f"- {n!r} found in {i!r}." for n, i in tracked]
692 track.append(f"- {name!r} not found.")
693 track_str = "\n".join(track)
694 msg = (
695 f"import_string() failed for {import_name!r}. Possible reasons"
696 f" are:\n\n"
697 "- missing __init__.py in a package;\n"
698 "- package or module path not included in sys.path;\n"
699 "- duplicated package or module name taking precedence in"
700 " sys.path;\n"
701 "- missing module, class, function or variable;\n\n"
702 f"Debugged import:\n\n{track_str}\n\n"
703 f"Original exception:\n\n{type(exception).__name__}: {exception}"
704 )
705 break
707 super().__init__(msg)
709 def __repr__(self) -> str:
710 return f"<{type(self).__name__}({self.import_name!r}, {self.exception!r})>"
713if not t.TYPE_CHECKING:
715 def __getattr__(name: str) -> t.Any:
716 import warnings
718 if name == "environ_property":
719 from ._header_property import environ_property
721 warnings.warn(
722 "'environ_property' is deprecated and will be removed in"
723 " Werkzeug 4.0. Access 'environ' directly instead.",
724 DeprecationWarning,
725 stacklevel=2,
726 )
727 return environ_property
729 if name == "header_property":
730 from ._header_property import header_property
732 warnings.warn(
733 "'header_property' is deprecated and will be removed in"
734 " Werkzeug 4.0. Access 'headers' directly instead.",
735 DeprecationWarning,
736 stacklevel=2,
737 )
738 return header_property
740 if name == "get_content_type":
741 warnings.warn(
742 "'get_content_type' is deprecated and will be removed in"
743 " Werkzeug 4.0. Set 'Response.mimetype' instead.",
744 DeprecationWarning,
745 stacklevel=2,
746 )
747 return _get_content_type
749 if name == "append_slash_redirect":
750 warnings.warn(
751 "'append_slash_redirect' is deprecated and will be removed in Werkzeug"
752 " 4.0. Use 'routing.Map' with 'strict_slashes' instead.",
753 DeprecationWarning,
754 stacklevel=2,
755 )
756 return _append_slash_redirect
758 raise AttributeError(name)