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