1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinSupport library (derived from EventAggregatorSupport) 4 5 @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from DateSupport import * 12 from MoinMoin.Page import Page 13 from MoinMoin import wikiutil 14 import re 15 import time 16 17 __version__ = "0.1" 18 19 # Content type parsing. 20 21 encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?' 22 encoding_regexp = re.compile(encoding_regexp_str) 23 24 # Accept header parsing. 25 26 accept_regexp_str = ur';\s*q=' 27 accept_regexp = re.compile(accept_regexp_str) 28 29 # Utility functions. 30 31 def getContentTypeAndEncoding(content_type): 32 33 """ 34 Return a tuple with the content/media type and encoding, extracted from the 35 given 'content_type' header value. 36 """ 37 38 m = encoding_regexp.search(content_type) 39 if m: 40 return m.group("content_type"), m.group("encoding") 41 else: 42 return None, None 43 44 def int_or_none(x): 45 if x is None: 46 return x 47 else: 48 return int(x) 49 50 # Utility classes and associated functions. 51 52 class Form: 53 54 """ 55 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 56 environment. 57 """ 58 59 def __init__(self, form): 60 self.form = form 61 62 def has_key(self, name): 63 return not not self.form.getlist(name) 64 65 def get(self, name, default=None): 66 values = self.form.getlist(name) 67 if not values: 68 return default 69 else: 70 return values 71 72 def __getitem__(self, name): 73 return self.form.getlist(name) 74 75 class ActionSupport: 76 77 """ 78 Work around disruptive MoinMoin changes in 1.9, and also provide useful 79 convenience methods. 80 """ 81 82 def get_form(self): 83 return get_form(self.request) 84 85 def _get_selected(self, value, input_value): 86 87 """ 88 Return the HTML attribute text indicating selection of an option (or 89 otherwise) if 'value' matches 'input_value'. 90 """ 91 92 return input_value is not None and value == input_value and 'selected="selected"' or '' 93 94 def _get_selected_for_list(self, value, input_values): 95 96 """ 97 Return the HTML attribute text indicating selection of an option (or 98 otherwise) if 'value' matches one of the 'input_values'. 99 """ 100 101 return value in input_values and 'selected="selected"' or '' 102 103 def _get_input(self, form, name, default=None): 104 105 """ 106 Return the input from 'form' having the given 'name', returning either 107 the input converted to an integer or the given 'default' (optional, None 108 if not specified). 109 """ 110 111 value = form.get(name, [None])[0] 112 if not value: # true if 0 obtained 113 return default 114 else: 115 return int(value) 116 117 def get_form(request): 118 119 "Work around disruptive MoinMoin changes in 1.9." 120 121 if hasattr(request, "values"): 122 return Form(request.values) 123 else: 124 return request.form 125 126 class send_headers_cls: 127 128 """ 129 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 130 1.9.x environment. 131 """ 132 133 def __init__(self, request): 134 self.request = request 135 136 def __call__(self, headers): 137 for header in headers: 138 parts = header.split(":") 139 self.request.headers.add(parts[0], ":".join(parts[1:])) 140 141 def get_send_headers(request): 142 143 "Return a function that can send response headers." 144 145 if hasattr(request, "http_headers"): 146 return request.http_headers 147 elif hasattr(request, "emit_http_headers"): 148 return request.emit_http_headers 149 else: 150 return send_headers_cls(request) 151 152 def escattr(s): 153 return wikiutil.escape(s, 1) 154 155 def getPathInfo(request): 156 if hasattr(request, "getPathinfo"): 157 return request.getPathinfo() 158 else: 159 return request.path 160 161 # Content/media type and preferences support. 162 163 class MediaRange: 164 165 "A content/media type value which supports whole categories of data." 166 167 def __init__(self, media_range, accept_parameters=None): 168 self.media_range = media_range 169 self.accept_parameters = accept_parameters or {} 170 171 parts = media_range.split(";") 172 self.media_type = parts[0] 173 self.parameters = getMappingFromParameterStrings(parts[1:]) 174 175 # The media type is divided into category and subcategory. 176 177 parts = self.media_type.split("/") 178 self.category = parts[0] 179 self.subcategory = "/".join(parts[1:]) 180 181 def get_parts(self): 182 183 "Return the category, subcategory parts." 184 185 return self.category, self.subcategory 186 187 def get_specificity(self): 188 189 """ 190 Return the specificity of the media type in terms of the scope of the 191 category and subcategory, and also in terms of any qualifying 192 parameters. 193 """ 194 195 if "*" in self.get_parts(): 196 return -list(self.get_parts()).count("*") 197 else: 198 return len(self.parameters) 199 200 def permits(self, other): 201 202 """ 203 Return whether this media type permits the use of the 'other' media type 204 if suggested as suitable content. 205 """ 206 207 if not isinstance(other, MediaRange): 208 other = MediaRange(other) 209 210 category = categoryPermits(self.category, other.category) 211 subcategory = categoryPermits(self.subcategory, other.subcategory) 212 213 if category and subcategory: 214 if "*" not in (category, subcategory): 215 return not self.parameters or self.parameters == other.parameters 216 else: 217 return True 218 else: 219 return False 220 221 def __eq__(self, other): 222 223 """ 224 Return whether this media type is effectively the same as the 'other' 225 media type. 226 """ 227 228 if not isinstance(other, MediaRange): 229 other = MediaRange(other) 230 231 category = categoryMatches(self.category, other.category) 232 subcategory = categoryMatches(self.subcategory, other.subcategory) 233 234 if category and subcategory: 235 if "*" not in (category, subcategory): 236 return self.parameters == other.parameters or \ 237 not self.parameters or not other.parameters 238 else: 239 return True 240 else: 241 return False 242 243 def __ne__(self, other): 244 return not self.__eq__(other) 245 246 def __hash__(self): 247 return hash(self.media_range) 248 249 def __repr__(self): 250 return "MediaRange(%r)" % self.media_range 251 252 def categoryMatches(this, that): 253 254 """ 255 Return the basis of a match between 'this' and 'that' or False if the given 256 categories do not match. 257 """ 258 259 return (this == "*" or this == that) and this or \ 260 that == "*" and that or False 261 262 def categoryPermits(this, that): 263 264 """ 265 Return whether 'this' category permits 'that' category. Where 'this' is a 266 wildcard ("*"), 'that' should always match. A value of False is returned if 267 the categories do not otherwise match. 268 """ 269 270 return (this == "*" or this == that) and this or False 271 272 def getMappingFromParameterStrings(l): 273 274 """ 275 Return a mapping representing the list of "name=value" strings given by 'l'. 276 """ 277 278 parameters = {} 279 280 for parameter in l: 281 parts = parameter.split("=") 282 name = parts[0].strip() 283 value = "=".join(parts[1:]).strip() 284 parameters[name] = value 285 286 return parameters 287 288 def getContentPreferences(accept): 289 290 """ 291 Return a mapping from media types to parameters for content/media types 292 extracted from the given 'accept' header value. The mapping is returned in 293 the form of a list of (media type, parameters) tuples. 294 295 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 296 """ 297 298 preferences = [] 299 300 for field in accept.split(","): 301 302 # The media type with parameters (defined by the "media-range") is 303 # separated from any other parameters (defined as "accept-extension" 304 # parameters) by a quality parameter. 305 306 fparts = accept_regexp.split(field) 307 308 # The first part is always the media type. 309 310 media_type = fparts[0].strip() 311 312 # Any other parts can be interpreted as extension parameters. 313 314 if len(fparts) > 1: 315 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 316 else: 317 fparts = [] 318 319 # Each field in the preferences can incorporate parameters separated by 320 # semicolon characters. 321 322 parameters = getMappingFromParameterStrings(fparts) 323 media_range = MediaRange(media_type, parameters) 324 preferences.append(media_range) 325 326 return ContentPreferences(preferences) 327 328 class ContentPreferences: 329 330 "A wrapper around content preference information." 331 332 def __init__(self, preferences): 333 self.preferences = preferences 334 335 def __iter__(self): 336 return iter(self.preferences) 337 338 def get_ordered(self, by_quality=0): 339 340 """ 341 Return a list of content/media types in descending order of preference. 342 If 'by_quality' is set to a true value, the "q" value will be used as 343 the primary measure of preference; otherwise, only the specificity will 344 be considered. 345 """ 346 347 ordered = {} 348 349 for media_range in self.preferences: 350 specificity = media_range.get_specificity() 351 352 if by_quality: 353 q = float(media_range.accept_parameters.get("q", "1")) 354 key = q, specificity 355 else: 356 key = specificity 357 358 if not ordered.has_key(key): 359 ordered[key] = [] 360 361 ordered[key].append(media_range) 362 363 # Return the preferences in descending order of quality and specificity. 364 365 keys = ordered.keys() 366 keys.sort(reverse=True) 367 return [ordered[key] for key in keys] 368 369 def get_preferred_type(self, available): 370 371 """ 372 Return the preferred content/media type from those in the 'available' 373 list, given the known preferences. 374 """ 375 376 matches = {} 377 available = set(available[:]) 378 379 for level in self.get_ordered(): 380 for media_range in level: 381 382 # Attempt to match available types. 383 384 found = set() 385 for available_type in available: 386 if media_range.permits(available_type): 387 q = float(media_range.accept_parameters.get("q", "1")) 388 if not matches.has_key(q): 389 matches[q] = [] 390 matches[q].append(available_type) 391 found.add(available_type) 392 393 # Stop looking for matches for matched available types. 394 395 if found: 396 available.difference_update(found) 397 398 # Sort the matches in descending order of quality. 399 400 all_q = matches.keys() 401 402 if all_q: 403 all_q.sort(reverse=True) 404 return matches[all_q[0]] 405 else: 406 return None 407 408 # Page access functions. 409 410 def getPageURL(page): 411 412 "Return the URL of the given 'page'." 413 414 request = page.request 415 return request.getQualifiedURL(page.url(request, relative=0)) 416 417 def getFormat(page): 418 419 "Get the format used on the given 'page'." 420 421 return page.pi["format"] 422 423 def getMetadata(page): 424 425 """ 426 Return a dictionary containing items describing for the given 'page' the 427 page's "created" time, "last-modified" time, "sequence" (or revision number) 428 and the "last-comment" made about the last edit. 429 """ 430 431 request = page.request 432 433 # Get the initial revision of the page. 434 435 revisions = page.getRevList() 436 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 437 438 # Get the created and last modified times. 439 440 initial_revision = getPageRevision(event_page_initial) 441 442 metadata = {} 443 metadata["created"] = initial_revision["timestamp"] 444 latest_revision = getPageRevision(page) 445 metadata["last-modified"] = latest_revision["timestamp"] 446 metadata["sequence"] = len(revisions) - 1 447 metadata["last-comment"] = latest_revision["comment"] 448 449 return metadata 450 451 def getPageRevision(page): 452 453 "Return the revision details dictionary for the given 'page'." 454 455 # From Page.edit_info... 456 457 if hasattr(page, "editlog_entry"): 458 line = page.editlog_entry() 459 else: 460 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 461 462 # Similar to Page.mtime_usecs behaviour... 463 464 if line: 465 timestamp = line.ed_time_usecs 466 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 467 comment = line.comment 468 else: 469 mtime = 0 470 comment = "" 471 472 # Leave the time zone empty. 473 474 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 475 476 # User interface functions. 477 478 def getParameter(request, name, default=None): 479 480 """ 481 Using the given 'request', return the value of the parameter with the given 482 'name', returning the optional 'default' (or None) if no value was supplied 483 in the 'request'. 484 """ 485 486 return get_form(request).get(name, [default])[0] 487 488 def getQualifiedParameter(request, prefix, argname, default=None): 489 490 """ 491 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 492 qualified parameter, returning the optional 'default' (or None) if no value 493 was supplied in the 'request'. 494 """ 495 496 argname = getQualifiedParameterName(prefix, argname) 497 return getParameter(request, argname, default) 498 499 def getQualifiedParameterName(prefix, argname): 500 501 """ 502 Return the qualified parameter name using the given 'prefix' and 'argname'. 503 """ 504 505 if prefix is None: 506 return argname 507 else: 508 return "%s-%s" % (prefix, argname) 509 510 # Page-related functions. 511 512 def getPrettyPageName(page): 513 514 "Return a nicely formatted title/name for the given 'page'." 515 516 title = page.split_title(force=1) 517 return getPrettyTitle(title) 518 519 def linkToPage(request, page, text, query_string=None, **kw): 520 521 """ 522 Using 'request', return a link to 'page' with the given link 'text' and 523 optional 'query_string'. 524 """ 525 526 text = wikiutil.escape(text) 527 return page.link_to_raw(request, text, query_string, **kw) 528 529 def linkToResource(url, request, text, query_string=None): 530 531 """ 532 Using 'request', return a link to 'url' with the given link 'text' and 533 optional 'query_string'. 534 """ 535 536 if query_string: 537 query_string = wikiutil.makeQueryString(query_string) 538 url = "%s?%s" % (url, query_string) 539 540 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 541 542 output = [] 543 output.append(formatter.url(1, url)) 544 output.append(formatter.text(text)) 545 output.append(formatter.url(0)) 546 return "".join(output) 547 548 def getFullPageName(parent, title): 549 550 """ 551 Return a full page name from the given 'parent' page (can be empty or None) 552 and 'title' (a simple page name). 553 """ 554 555 if parent: 556 return "%s/%s" % (parent.rstrip("/"), title) 557 else: 558 return title 559 560 # vim: tabstop=4 expandtab shiftwidth=4