Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/sansio/http.py: 27%

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

64 statements  

1from __future__ import annotations 

2 

3import re 

4import typing as t 

5from datetime import datetime 

6 

7from .._internal import _dt_as_utc 

8from ..http import generate_etag 

9from ..http import parse_date 

10from ..http import parse_etags 

11from ..http import parse_if_range_header 

12from ..http import unquote_etag 

13 

14_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') 

15 

16 

17def is_resource_modified( 

18 http_range: str | None = None, 

19 http_if_range: str | None = None, 

20 http_if_modified_since: str | None = None, 

21 http_if_none_match: str | None = None, 

22 http_if_match: str | None = None, 

23 etag: str | None = None, 

24 data: bytes | None = None, 

25 last_modified: datetime | str | None = None, 

26 ignore_if_range: bool = True, 

27) -> bool: 

28 """Convenience method for conditional requests. 

29 :param http_range: Range HTTP header 

30 :param http_if_range: If-Range HTTP header 

31 :param http_if_modified_since: If-Modified-Since HTTP header 

32 :param http_if_none_match: If-None-Match HTTP header 

33 :param http_if_match: If-Match HTTP header 

34 :param etag: the etag for the response for comparison. 

35 :param data: or alternatively the data of the response to automatically 

36 generate an etag using :func:`generate_etag`. 

37 :param last_modified: an optional date of the last modification. 

38 :param ignore_if_range: If `False`, `If-Range` header will be taken into 

39 account. 

40 :return: `True` if the resource was modified, otherwise `False`. 

41 

42 .. versionadded:: 2.2 

43 """ 

44 if etag is None and data is not None: 

45 etag = generate_etag(data) 

46 elif data is not None: 

47 raise TypeError("both data and etag given") 

48 

49 unmodified = False 

50 if isinstance(last_modified, str): 

51 last_modified = parse_date(last_modified) 

52 

53 # HTTP doesn't use microsecond, remove it to avoid false positive 

54 # comparisons. Mark naive datetimes as UTC. 

55 if last_modified is not None: 

56 last_modified = _dt_as_utc(last_modified.replace(microsecond=0)) 

57 

58 if_range = None 

59 if not ignore_if_range and http_range is not None: 

60 # https://tools.ietf.org/html/rfc7233#section-3.2 

61 # A server MUST ignore an If-Range header field received in a request 

62 # that does not contain a Range header field. 

63 if_range = parse_if_range_header(http_if_range) 

64 

65 if if_range is not None and if_range.date is not None: 

66 modified_since: datetime | None = if_range.date 

67 else: 

68 modified_since = parse_date(http_if_modified_since) 

69 

70 if modified_since and last_modified and last_modified <= modified_since: 

71 unmodified = True 

72 

73 if etag: 

74 etag, _ = unquote_etag(etag) 

75 

76 if if_range is not None and if_range.etag is not None: 

77 unmodified = parse_etags(if_range.etag).contains(etag) 

78 else: 

79 if_none_match = parse_etags(http_if_none_match) 

80 if if_none_match: 

81 # https://tools.ietf.org/html/rfc7232#section-3.2 

82 # "A recipient MUST use the weak comparison function when comparing 

83 # entity-tags for If-None-Match" 

84 unmodified = if_none_match.contains_weak(etag) 

85 

86 # https://tools.ietf.org/html/rfc7232#section-3.1 

87 # "Origin server MUST use the strong comparison function when 

88 # comparing entity-tags for If-Match" 

89 if_match = parse_etags(http_if_match) 

90 if if_match: 

91 unmodified = not if_match.is_strong(etag) 

92 

93 return not unmodified 

94 

95 

96_cookie_re = re.compile( 

97 r""" 

98 ([^=;]*) 

99 (?:\s*=\s* 

100 ( 

101 "(?:[^\\"]|\\.)*" 

102 | 

103 .*? 

104 ) 

105 )? 

106 \s*;\s* 

107 """, 

108 flags=re.ASCII | re.VERBOSE, 

109) 

110_cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)") 

111 

112 

113def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes: 

114 v = m.group(1) 

115 

116 if len(v) == 1: 

117 return v 

118 

119 return int(v, 8).to_bytes(1, "big") 

120 

121 

122def parse_cookie( 

123 cookie: str | None = None, 

124 cls: type[ds.MultiDict[str, str]] | None = None, 

125) -> ds.MultiDict[str, str]: 

126 """Parse a cookie from a string. 

127 

128 The same key can be provided multiple times, the values are stored 

129 in-order. The default :class:`MultiDict` will have the first value 

130 first, and all values can be retrieved with 

131 :meth:`MultiDict.getlist`. 

132 

133 :param cookie: The cookie header as a string. 

134 :param cls: A dict-like class to store the parsed cookies in. 

135 Defaults to :class:`MultiDict`. 

136 

137 .. versionchanged:: 3.0 

138 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed. 

139 

140 .. versionadded:: 2.2 

141 """ 

142 if cls is None: 

143 cls = t.cast("type[ds.MultiDict[str, str]]", ds.MultiDict) 

144 

145 if not cookie: 

146 return cls() 

147 

148 cookie = f"{cookie};" 

149 out = [] 

150 

151 for ck, cv in _cookie_re.findall(cookie): 

152 ck = ck.strip() 

153 cv = cv.strip() 

154 

155 if not ck: 

156 continue 

157 

158 if len(cv) >= 2 and cv[0] == cv[-1] == '"': 

159 # Work with bytes here, since a UTF-8 character could be multiple bytes. 

160 cv = _cookie_unslash_re.sub( 

161 _cookie_unslash_replace, cv[1:-1].encode() 

162 ).decode(errors="replace") 

163 

164 out.append((ck, cv)) 

165 

166 return cls(out) 

167 

168 

169# circular dependencies 

170from .. import datastructures as ds