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

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

67 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 

15def is_resource_modified( 

16 http_range: str | None = None, 

17 http_if_range: str | None = None, 

18 http_if_modified_since: str | None = None, 

19 http_if_none_match: str | None = None, 

20 http_if_match: str | None = None, 

21 etag: str | None = None, 

22 data: bytes | None = None, 

23 last_modified: datetime | str | None = None, 

24 ignore_if_range: bool = True, 

25) -> bool: 

26 """Convenience method for conditional requests. 

27 :param http_range: Range HTTP header 

28 :param http_if_range: If-Range HTTP header 

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

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

31 :param http_if_match: If-Match HTTP header 

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

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

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

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

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

37 account. 

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

39 

40 .. versionadded:: 2.2 

41 """ 

42 if etag is None and data is not None: 

43 etag = f'"{generate_etag(data)}"' 

44 elif data is not None: 

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

46 

47 unmodified = False 

48 if isinstance(last_modified, str): 

49 last_modified = parse_date(last_modified) 

50 

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

52 # comparisons. Mark naive datetimes as UTC. 

53 if last_modified is not None: 

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

55 

56 if_range = None 

57 if not ignore_if_range and http_range is not None: 

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

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

60 # that does not contain a Range header field. 

61 if_range = parse_if_range_header(http_if_range) 

62 

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

64 modified_since: datetime | None = if_range.date 

65 else: 

66 modified_since = parse_date(http_if_modified_since) 

67 

68 if modified_since and last_modified and last_modified <= modified_since: 

69 unmodified = True 

70 

71 if etag: 

72 etag, _ = unquote_etag(etag) 

73 

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

75 unmodified = if_range.etag == etag 

76 else: 

77 if_none_match = parse_etags(http_if_none_match) 

78 if if_none_match: 

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

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

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

82 unmodified = if_none_match.contains_weak(etag) 

83 

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

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

86 # comparing entity-tags for If-Match" 

87 if_match = parse_etags(http_if_match) 

88 if if_match: 

89 unmodified = not if_match.is_strong(etag) 

90 

91 return not unmodified 

92 

93 

94_cookie_re = re.compile( 

95 r""" 

96 [ \t]* # ignore leading space 

97 ([^ \t=";]+) # key 

98 (?:[ \t]*=[ \t]* # optional =value, ignoring invalid space 

99 ( 

100 "(?:[^\\"]|\\.)*" # quoted value with backslash escapes 

101 | 

102 [^ \t";]* # token value 

103 ) 

104 )? 

105 [ \t]* # ignore trailing space 

106 (?:;|\Z) # only if followed by semicolon or end 

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 out = [] 

149 pos = 0 

150 

151 while True: 

152 if (m := _cookie_re.match(cookie, pos)) is None: 

153 # Skip invalid chars until the next semicolon. 

154 if (pos := cookie.find(";", pos) + 1) == 0: 

155 break 

156 

157 continue 

158 

159 ck, cv = m.groups() 

160 pos = m.end() 

161 

162 if cv is None: 

163 cv = "" 

164 

165 if cv.startswith('"') and cv.endswith('"'): 

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

167 cv = _cookie_unslash_re.sub( 

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

169 ).decode(errors="replace") 

170 

171 out.append((ck, cv)) 

172 

173 return cls(out) 

174 

175 

176# circular dependencies 

177from .. import datastructures as ds # noqa: E402