1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - ContentTypeSupport library 4 5 @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 import re 10 11 # Content type parsing. 12 13 encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?' 14 encoding_regexp = re.compile(encoding_regexp_str) 15 16 # Accept header parsing. 17 18 accept_regexp_str = ur';\s*q=' 19 accept_regexp = re.compile(accept_regexp_str) 20 21 # Content/media type and preferences support. 22 23 class MediaRange: 24 25 "A content/media type value which supports whole categories of data." 26 27 def __init__(self, media_range, accept_parameters=None): 28 self.media_range = media_range 29 self.accept_parameters = accept_parameters or {} 30 31 parts = media_range.split(";") 32 self.media_type = parts[0] 33 self.parameters = getMappingFromParameterStrings(parts[1:]) 34 35 # The media type is divided into category and subcategory. 36 37 parts = self.media_type.split("/") 38 self.category = parts[0] 39 self.subcategory = "/".join(parts[1:]) 40 41 def get_parts(self): 42 43 "Return the category, subcategory parts." 44 45 return self.category, self.subcategory 46 47 def get_specificity(self): 48 49 """ 50 Return the specificity of the media type in terms of the scope of the 51 category and subcategory, and also in terms of any qualifying 52 parameters. 53 """ 54 55 if "*" in self.get_parts(): 56 return -list(self.get_parts()).count("*") 57 else: 58 return len(self.parameters) 59 60 def permits(self, other): 61 62 """ 63 Return whether this media type permits the use of the 'other' media type 64 if suggested as suitable content. 65 """ 66 67 if not isinstance(other, MediaRange): 68 other = MediaRange(other) 69 70 category = categoryPermits(self.category, other.category) 71 subcategory = categoryPermits(self.subcategory, other.subcategory) 72 73 if category and subcategory: 74 if "*" not in (category, subcategory): 75 return not self.parameters or self.parameters == other.parameters 76 else: 77 return True 78 else: 79 return False 80 81 def __eq__(self, other): 82 83 """ 84 Return whether this media type is effectively the same as the 'other' 85 media type. 86 """ 87 88 if not isinstance(other, MediaRange): 89 other = MediaRange(other) 90 91 category = categoryMatches(self.category, other.category) 92 subcategory = categoryMatches(self.subcategory, other.subcategory) 93 94 if category and subcategory: 95 if "*" not in (category, subcategory): 96 return self.parameters == other.parameters or \ 97 not self.parameters or not other.parameters 98 else: 99 return True 100 else: 101 return False 102 103 def __ne__(self, other): 104 return not self.__eq__(other) 105 106 def __hash__(self): 107 return hash(self.media_range) 108 109 def __repr__(self): 110 return "MediaRange(%r)" % self.media_range 111 112 def categoryMatches(this, that): 113 114 """ 115 Return the basis of a match between 'this' and 'that' or False if the given 116 categories do not match. 117 """ 118 119 return (this == "*" or this == that) and this or \ 120 that == "*" and that or False 121 122 def categoryPermits(this, that): 123 124 """ 125 Return whether 'this' category permits 'that' category. Where 'this' is a 126 wildcard ("*"), 'that' should always match. A value of False is returned if 127 the categories do not otherwise match. 128 """ 129 130 return (this == "*" or this == that) and this or False 131 132 def getMappingFromParameterStrings(l): 133 134 """ 135 Return a mapping representing the list of "name=value" strings given by 'l'. 136 """ 137 138 parameters = {} 139 140 for parameter in l: 141 parts = parameter.split("=") 142 name = parts[0].strip() 143 value = "=".join(parts[1:]).strip() 144 parameters[name] = value 145 146 return parameters 147 148 def getContentPreferences(accept): 149 150 """ 151 Return a mapping from media types to parameters for content/media types 152 extracted from the given 'accept' header value. The mapping is returned in 153 the form of a list of (media type, parameters) tuples. 154 155 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 156 """ 157 158 preferences = [] 159 160 for field in accept.split(","): 161 162 # The media type with parameters (defined by the "media-range") is 163 # separated from any other parameters (defined as "accept-extension" 164 # parameters) by a quality parameter. 165 166 fparts = accept_regexp.split(field) 167 168 # The first part is always the media type. 169 170 media_type = fparts[0].strip() 171 172 # Any other parts can be interpreted as extension parameters. 173 174 if len(fparts) > 1: 175 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 176 else: 177 fparts = [] 178 179 # Each field in the preferences can incorporate parameters separated by 180 # semicolon characters. 181 182 parameters = getMappingFromParameterStrings(fparts) 183 media_range = MediaRange(media_type, parameters) 184 preferences.append(media_range) 185 186 return ContentPreferences(preferences) 187 188 class ContentPreferences: 189 190 "A wrapper around content preference information." 191 192 def __init__(self, preferences): 193 self.preferences = preferences 194 195 def __iter__(self): 196 return iter(self.preferences) 197 198 def get_ordered(self, by_quality=0): 199 200 """ 201 Return a list of content/media types in descending order of preference. 202 If 'by_quality' is set to a true value, the "q" value will be used as 203 the primary measure of preference; otherwise, only the specificity will 204 be considered. 205 """ 206 207 ordered = {} 208 209 for media_range in self.preferences: 210 specificity = media_range.get_specificity() 211 212 if by_quality: 213 q = float(media_range.accept_parameters.get("q", "1")) 214 key = q, specificity 215 else: 216 key = specificity 217 218 if not ordered.has_key(key): 219 ordered[key] = [] 220 221 ordered[key].append(media_range) 222 223 # Return the preferences in descending order of quality and specificity. 224 225 keys = ordered.keys() 226 keys.sort(reverse=True) 227 return [ordered[key] for key in keys] 228 229 def get_acceptable_types(self, available): 230 231 """ 232 Return content/media types from those in the 'available' list supported 233 by the known preferences grouped by preference level in descending order 234 of preference. 235 """ 236 237 matches = {} 238 available = set(available[:]) 239 240 for level in self.get_ordered(): 241 for media_range in level: 242 243 # Attempt to match available types. 244 245 found = set() 246 for available_type in available: 247 if media_range.permits(available_type): 248 q = float(media_range.accept_parameters.get("q", "1")) 249 if not matches.has_key(q): 250 matches[q] = [] 251 matches[q].append(available_type) 252 found.add(available_type) 253 254 # Stop looking for matches for matched available types. 255 256 if found: 257 available.difference_update(found) 258 259 # Sort the matches in descending order of quality. 260 261 all_q = matches.keys() 262 263 if all_q: 264 all_q.sort(reverse=True) 265 return [matches[q] for q in all_q] 266 else: 267 return [] 268 269 def get_preferred_types(self, available): 270 271 """ 272 Return the preferred content/media types from those in the 'available' 273 list, given the known preferences. 274 """ 275 276 preferred = self.get_acceptable_types(available) 277 if preferred: 278 return preferred[0] 279 else: 280 return [] 281 282 # Content type parsing. 283 284 def getContentTypeAndEncoding(content_type): 285 286 """ 287 Return a tuple with the content/media type and encoding, extracted from the 288 given 'content_type' header value. 289 """ 290 291 m = encoding_regexp.search(content_type) 292 if m: 293 return m.group("content_type"), m.group("encoding") 294 else: 295 return None, None 296 297 # vim: tabstop=4 expandtab shiftwidth=4