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 return self.category, self.subcategory 183 184 def get_specificity(self): 185 if "*" in self.get_parts(): 186 return -list(self.get_parts()).count("*") 187 else: 188 return len(self.parameters) 189 190 def permits(self, other): 191 if not isinstance(other, MediaRange): 192 other = MediaRange(other) 193 194 category = categoryPermits(self.category, other.category) 195 subcategory = categoryPermits(self.subcategory, other.subcategory) 196 197 if category and subcategory: 198 if "*" not in (category, subcategory): 199 return not self.parameters or self.parameters == other.parameters 200 else: 201 return True 202 else: 203 return False 204 205 def __eq__(self, other): 206 if not isinstance(other, MediaRange): 207 other = MediaRange(other) 208 209 category = categoryMatches(self.category, other.category) 210 subcategory = categoryMatches(self.subcategory, other.subcategory) 211 212 if category and subcategory: 213 if "*" not in (category, subcategory): 214 return self.parameters == other.parameters or \ 215 not self.parameters or not other.parameters 216 else: 217 return True 218 else: 219 return False 220 221 def __ne__(self, other): 222 return not self.__eq__(other) 223 224 def __hash__(self): 225 return hash(self.media_range) 226 227 def __repr__(self): 228 return "MediaRange(%r)" % self.media_range 229 230 def categoryMatches(this, that): 231 232 """ 233 Return the basis of a match between 'this' and 'that' or False if the given 234 categories do not match. 235 """ 236 237 return (this == "*" or this == that) and this or \ 238 that == "*" and that or False 239 240 def categoryPermits(this, that): 241 242 """ 243 Return whether 'this' category permits 'that' category. Where 'this' is a 244 wildcard ("*"), 'that' should always match. A value of False is returned if 245 the categories do not otherwise match. 246 """ 247 248 return (this == "*" or this == that) and this or False 249 250 def getMappingFromParameterStrings(l): 251 252 """ 253 Return a mapping representing the list of "name=value" strings given by 'l'. 254 """ 255 256 parameters = {} 257 258 for parameter in l: 259 parts = parameter.split("=") 260 name = parts[0].strip() 261 value = "=".join(parts[1:]).strip() 262 parameters[name] = value 263 264 return parameters 265 266 def getContentPreferences(accept): 267 268 """ 269 Return a mapping from media types to parameters for content/media types 270 extracted from the given 'accept' header value. The mapping is returned in 271 the form of a list of (media type, parameters) tuples. 272 273 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 274 """ 275 276 preferences = [] 277 278 for field in accept.split(","): 279 280 # The media type with parameters (defined by the "media-range") is 281 # separated from any other parameters (defined as "accept-extension" 282 # parameters) by a quality parameter. 283 284 fparts = accept_regexp.split(field) 285 286 # The first part is always the media type. 287 288 media_type = fparts[0].strip() 289 290 # Any other parts can be interpreted as extension parameters. 291 292 if len(fparts) > 1: 293 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 294 else: 295 fparts = [] 296 297 # Each field in the preferences can incorporate parameters separated by 298 # semicolon characters. 299 300 parameters = getMappingFromParameterStrings(fparts) 301 media_range = MediaRange(media_type, parameters) 302 preferences.append(media_range) 303 304 return ContentPreferences(preferences) 305 306 class ContentPreferences: 307 308 "A wrapper around content preference information." 309 310 def __init__(self, preferences): 311 self.preferences = preferences 312 313 def __iter__(self): 314 return iter(self.preferences) 315 316 def get_ordered(self, by_quality=0): 317 318 """ 319 Return a list of content/media types in descending order of preference. 320 If 'by_quality' is set to a true value, the "q" value will be used as 321 the primary measure of preference; otherwise, only the specificity will 322 be considered. 323 """ 324 325 ordered = {} 326 327 for media_range in self.preferences: 328 specificity = media_range.get_specificity() 329 330 if by_quality: 331 q = float(media_range.accept_parameters.get("q", "1")) 332 key = q, specificity 333 else: 334 key = specificity 335 336 if not ordered.has_key(key): 337 ordered[key] = [] 338 339 ordered[key].append(media_range) 340 341 # Return the preferences in descending order of quality and specificity. 342 343 keys = ordered.keys() 344 keys.sort(reverse=True) 345 return [ordered[key] for key in keys] 346 347 def get_preferred_type(self, available): 348 349 """ 350 Return the preferred content/media type from those in the 'available' 351 list, given the known preferences. 352 """ 353 354 matches = {} 355 available = set(available[:]) 356 357 for level in self.get_ordered(): 358 for media_range in level: 359 360 # Attempt to match available types. 361 362 found = set() 363 for available_type in available: 364 if media_range.permits(available_type): 365 q = float(media_range.accept_parameters.get("q", "1")) 366 if not matches.has_key(q): 367 matches[q] = [] 368 matches[q].append(available_type) 369 found.add(available_type) 370 371 # Stop looking for matches for matched available types. 372 373 if found: 374 available.difference_update(found) 375 376 # Sort the matches in descending order of quality. 377 378 all_q = matches.keys() 379 380 if all_q: 381 all_q.sort(reverse=True) 382 return matches[all_q[0]] 383 else: 384 return None 385 386 # Page access functions. 387 388 def getPageURL(page): 389 390 "Return the URL of the given 'page'." 391 392 request = page.request 393 return request.getQualifiedURL(page.url(request, relative=0)) 394 395 def getFormat(page): 396 397 "Get the format used on the given 'page'." 398 399 return page.pi["format"] 400 401 def getMetadata(page): 402 403 """ 404 Return a dictionary containing items describing for the given 'page' the 405 page's "created" time, "last-modified" time, "sequence" (or revision number) 406 and the "last-comment" made about the last edit. 407 """ 408 409 request = page.request 410 411 # Get the initial revision of the page. 412 413 revisions = page.getRevList() 414 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 415 416 # Get the created and last modified times. 417 418 initial_revision = getPageRevision(event_page_initial) 419 420 metadata = {} 421 metadata["created"] = initial_revision["timestamp"] 422 latest_revision = getPageRevision(page) 423 metadata["last-modified"] = latest_revision["timestamp"] 424 metadata["sequence"] = len(revisions) - 1 425 metadata["last-comment"] = latest_revision["comment"] 426 427 return metadata 428 429 def getPageRevision(page): 430 431 "Return the revision details dictionary for the given 'page'." 432 433 # From Page.edit_info... 434 435 if hasattr(page, "editlog_entry"): 436 line = page.editlog_entry() 437 else: 438 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 439 440 # Similar to Page.mtime_usecs behaviour... 441 442 if line: 443 timestamp = line.ed_time_usecs 444 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 445 comment = line.comment 446 else: 447 mtime = 0 448 comment = "" 449 450 # Leave the time zone empty. 451 452 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 453 454 # User interface functions. 455 456 def getParameter(request, name, default=None): 457 458 """ 459 Using the given 'request', return the value of the parameter with the given 460 'name', returning the optional 'default' (or None) if no value was supplied 461 in the 'request'. 462 """ 463 464 return get_form(request).get(name, [default])[0] 465 466 def getQualifiedParameter(request, prefix, argname, default=None): 467 468 """ 469 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 470 qualified parameter, returning the optional 'default' (or None) if no value 471 was supplied in the 'request'. 472 """ 473 474 argname = getQualifiedParameterName(prefix, argname) 475 return getParameter(request, argname, default) 476 477 def getQualifiedParameterName(prefix, argname): 478 479 """ 480 Return the qualified parameter name using the given 'prefix' and 'argname'. 481 """ 482 483 if prefix is None: 484 return argname 485 else: 486 return "%s-%s" % (prefix, argname) 487 488 # Page-related functions. 489 490 def getPrettyPageName(page): 491 492 "Return a nicely formatted title/name for the given 'page'." 493 494 title = page.split_title(force=1) 495 return getPrettyTitle(title) 496 497 def linkToPage(request, page, text, query_string=None): 498 499 """ 500 Using 'request', return a link to 'page' with the given link 'text' and 501 optional 'query_string'. 502 """ 503 504 text = wikiutil.escape(text) 505 return page.link_to_raw(request, text, query_string) 506 507 def linkToResource(url, request, text, query_string=None): 508 509 """ 510 Using 'request', return a link to 'url' with the given link 'text' and 511 optional 'query_string'. 512 """ 513 514 if query_string: 515 query_string = wikiutil.makeQueryString(query_string) 516 url = "%s?%s" % (url, query_string) 517 518 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 519 520 output = [] 521 output.append(formatter.url(1, url)) 522 output.append(formatter.text(text)) 523 output.append(formatter.url(0)) 524 return "".join(output) 525 526 def getFullPageName(parent, title): 527 528 """ 529 Return a full page name from the given 'parent' page (can be empty or None) 530 and 'title' (a simple page name). 531 """ 532 533 if parent: 534 return "%s/%s" % (parent.rstrip("/"), title) 535 else: 536 return title 537 538 # vim: tabstop=4 expandtab shiftwidth=4