1""" 2Parses a variety of ``Accept-*`` headers. 3 4These headers generally take the form of:: 5 6 value1; q=0.5, value2; q=0 7 8Where the ``q`` parameter is optional. In theory other parameters 9exists, but this ignores them. 10""" 11 12import re 13 14from webob.headers import _trans_name as header_to_key 15from webob.util import ( 16 header_docstring, 17 warn_deprecation, 18 ) 19 20part_re = re.compile( 21 r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?') 22 23 24 25 26def _warn_first_match(): 27 # TODO: remove .first_match in version 1.3 28 warn_deprecation("Use best_match instead", '1.2', 3) 29 30class Accept(object): 31 """ 32 Represents a generic ``Accept-*`` style header. 33 34 This object should not be modified. To add items you can use 35 ``accept_obj + 'accept_thing'`` to get a new object 36 """ 37 38 def __init__(self, header_value): 39 self.header_value = header_value 40 self._parsed = list(self.parse(header_value)) 41 self._parsed_nonzero = [(m,q) for (m,q) in self._parsed if q] 42 43 @staticmethod 44 def parse(value): 45 """ 46 Parse ``Accept-*`` style header. 47 48 Return iterator of ``(value, quality)`` pairs. 49 ``quality`` defaults to 1. 50 """ 51 for match in part_re.finditer(','+value): 52 name = match.group(1) 53 if name == 'q': 54 continue 55 quality = match.group(2) or '' 56 if quality: 57 try: 58 quality = max(min(float(quality), 1), 0) 59 yield (name, quality) 60 continue 61 except ValueError: 62 pass 63 yield (name, 1) 64 65 def __repr__(self): 66 return '<%s(%r)>' % (self.__class__.__name__, str(self)) 67 68 def __iter__(self): 69 for m,q in sorted( 70 self._parsed_nonzero, 71 key=lambda i: i[1], 72 reverse=True 73 ): 74 yield m 75 76 def __str__(self): 77 result = [] 78 for mask, quality in self._parsed: 79 if quality != 1: 80 mask = '%s;q=%0.*f' % ( 81 mask, min(len(str(quality).split('.')[1]), 3), quality) 82 result.append(mask) 83 return ', '.join(result) 84 85 def __add__(self, other, reversed=False): 86 if isinstance(other, Accept): 87 other = other.header_value 88 if hasattr(other, 'items'): 89 other = sorted(other.items(), key=lambda item: -item[1]) 90 if isinstance(other, (list, tuple)): 91 result = [] 92 for item in other: 93 if isinstance(item, (list, tuple)): 94 name, quality = item 95 result.append('%s; q=%s' % (name, quality)) 96 else: 97 result.append(item) 98 other = ', '.join(result) 99 other = str(other) 100 my_value = self.header_value 101 if reversed: 102 other, my_value = my_value, other 103 if not other: 104 new_value = my_value 105 elif not my_value: 106 new_value = other 107 else: 108 new_value = my_value + ', ' + other 109 return self.__class__(new_value) 110 111 def __radd__(self, other): 112 return self.__add__(other, True) 113 114 def __contains__(self, offer): 115 """ 116 Returns true if the given object is listed in the accepted 117 types. 118 """ 119 for mask, quality in self._parsed_nonzero: 120 if self._match(mask, offer): 121 return True 122 123 def quality(self, offer, modifier=1): 124 """ 125 Return the quality of the given offer. Returns None if there 126 is no match (not 0). 127 """ 128 bestq = 0 129 for mask, q in self._parsed: 130 if self._match(mask, offer): 131 bestq = max(bestq, q * modifier) 132 return bestq or None 133 134 def first_match(self, offers): 135 """ 136 DEPRECATED 137 Returns the first allowed offered type. Ignores quality. 138 Returns the first offered type if nothing else matches; or if you include None 139 at the end of the match list then that will be returned. 140 """ 141 _warn_first_match() 142 143 def best_match(self, offers, default_match=None): 144 """ 145 Returns the best match in the sequence of offered types. 146 147 The sequence can be a simple sequence, or you can have 148 ``(match, server_quality)`` items in the sequence. If you 149 have these tuples then the client quality is multiplied by the 150 server_quality to get a total. If two matches have equal 151 weight, then the one that shows up first in the `offers` list 152 will be returned. 153 154 But among matches with the same quality the match to a more specific 155 requested type will be chosen. For example a match to text/* trumps */*. 156 157 default_match (default None) is returned if there is no intersection. 158 """ 159 best_quality = -1 160 best_offer = default_match 161 matched_by = '*/*' 162 for offer in offers: 163 if isinstance(offer, (tuple, list)): 164 offer, server_quality = offer 165 else: 166 server_quality = 1 167 for mask, quality in self._parsed_nonzero: 168 possible_quality = server_quality * quality 169 if possible_quality < best_quality: 170 continue 171 elif possible_quality == best_quality: 172 # 'text/plain' overrides 'message/*' overrides '*/*' 173 # (if all match w/ the same q=) 174 if matched_by.count('*') <= mask.count('*'): 175 continue 176 if self._match(mask, offer): 177 best_quality = possible_quality 178 best_offer = offer 179 matched_by = mask 180 return best_offer 181 182 def _match(self, mask, offer): 183 _check_offer(offer) 184 return mask == '*' or offer.lower() == mask.lower() 185 186 187 188class NilAccept(object): 189 MasterClass = Accept 190 191 def __repr__(self): 192 return '<%s: %s>' % (self.__class__.__name__, self.MasterClass) 193 194 def __str__(self): 195 return '' 196 197 def __nonzero__(self): 198 return False 199 __bool__ = __nonzero__ # python 3 200 201 def __iter__(self): 202 return iter(()) 203 204 def __add__(self, item): 205 if isinstance(item, self.MasterClass): 206 return item 207 else: 208 return self.MasterClass('') + item 209 210 def __radd__(self, item): 211 if isinstance(item, self.MasterClass): 212 return item 213 else: 214 return item + self.MasterClass('') 215 216 def __contains__(self, item): 217 _check_offer(item) 218 return True 219 220 def quality(self, offer, default_quality=1): 221 return 0 222 223 def first_match(self, offers): # pragma: no cover 224 _warn_first_match() 225 226 def best_match(self, offers, default_match=None): 227 best_quality = -1 228 best_offer = default_match 229 for offer in offers: 230 _check_offer(offer) 231 if isinstance(offer, (list, tuple)): 232 offer, quality = offer 233 else: 234 quality = 1 235 if quality > best_quality: 236 best_offer = offer 237 best_quality = quality 238 return best_offer 239 240class NoAccept(NilAccept): 241 def __contains__(self, item): 242 return False 243 244class AcceptCharset(Accept): 245 @staticmethod 246 def parse(value): 247 latin1_found = False 248 for m, q in Accept.parse(value): 249 _m = m.lower() 250 if _m == '*' or _m == 'iso-8859-1': 251 latin1_found = True 252 yield _m, q 253 if not latin1_found: 254 yield ('iso-8859-1', 1) 255 256class AcceptLanguage(Accept): 257 def _match(self, mask, item): 258 item = item.replace('_', '-').lower() 259 mask = mask.lower() 260 return (mask == '*' 261 or item == mask 262 or item.split('-')[0] == mask 263 or item == mask.split('-')[0] 264 ) 265 266 267class MIMEAccept(Accept): 268 """ 269 Represents the ``Accept`` header, which is a list of mimetypes. 270 271 This class knows about mime wildcards, like ``image/*`` 272 """ 273 @staticmethod 274 def parse(value): 275 for mask, q in Accept.parse(value): 276 try: 277 mask_major, mask_minor = map(lambda x: x.lower(), mask.split('/')) 278 except ValueError: 279 continue 280 if mask_major == '*' and mask_minor != '*': 281 continue 282 if mask_major != "*" and "*" in mask_major: 283 continue 284 if mask_minor != "*" and "*" in mask_minor: 285 continue 286 yield ("%s/%s" % (mask_major, mask_minor), q) 287 288 def accept_html(self): 289 """ 290 Returns true if any HTML-like type is accepted 291 """ 292 return ('text/html' in self 293 or 'application/xhtml+xml' in self 294 or 'application/xml' in self 295 or 'text/xml' in self) 296 297 accepts_html = property(accept_html) # note the plural 298 299 def _match(self, mask, offer): 300 """ 301 Check if the offer is covered by the mask 302 """ 303 _check_offer(offer) 304 if '*' not in mask: 305 return offer.lower() == mask.lower() 306 elif mask == '*/*': 307 return True 308 else: 309 assert mask.endswith('/*') 310 mask_major = mask[:-2].lower() 311 offer_major = offer.split('/', 1)[0].lower() 312 return offer_major == mask_major 313 314 315class MIMENilAccept(NilAccept): 316 MasterClass = MIMEAccept 317 318def _check_offer(offer): 319 if '*' in offer: 320 raise ValueError("The application should offer specific types, got %r" % offer) 321 322 323 324def accept_property(header, rfc_section, 325 AcceptClass=Accept, NilClass=NilAccept 326): 327 key = header_to_key(header) 328 doc = header_docstring(header, rfc_section) 329 #doc += " Converts it as a %s." % convert_name 330 def fget(req): 331 value = req.environ.get(key) 332 if not value: 333 return NilClass() 334 return AcceptClass(value) 335 def fset(req, val): 336 if val: 337 if isinstance(val, (list, tuple, dict)): 338 val = AcceptClass('') + val 339 val = str(val) 340 req.environ[key] = val or None 341 def fdel(req): 342 del req.environ[key] 343 return property(fget, fset, fdel, doc) 344