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