1# -*- coding: iso-8859-1 -*- 2""" 3 MoinMoin - Wiki Utility Functions 4 5 @copyright: 2000 - 2004 by J�rgen Hermann <jh@web.de> 6 2007 by Reimar Bauer 7 @license: GNU GPL, see COPYING for details. 8""" 9 10import cgi 11import codecs 12import hashlib 13import os 14import re 15import time 16import urllib 17 18from MoinMoin import config 19from MoinMoin.util import pysupport, lock 20 21# Exceptions 22class InvalidFileNameError(Exception): 23 """ Called when we find an invalid file name """ 24 pass 25 26# constants for page names 27PARENT_PREFIX = "../" 28PARENT_PREFIX_LEN = len(PARENT_PREFIX) 29CHILD_PREFIX = "/" 30CHILD_PREFIX_LEN = len(CHILD_PREFIX) 31 32############################################################################# 33### Getting data from user/Sending data to user 34############################################################################# 35 36def decodeUnknownInput(text): 37 """ Decode unknown input, like text attachments 38 39 First we try utf-8 because it has special format, and it will decode 40 only utf-8 files. Then we try config.charset, then iso-8859-1 using 41 'replace'. We will never raise an exception, but may return junk 42 data. 43 44 WARNING: Use this function only for data that you view, not for data 45 that you save in the wiki. 46 47 @param text: the text to decode, string 48 @rtype: unicode 49 @return: decoded text (maybe wrong) 50 """ 51 # Shortcut for unicode input 52 if isinstance(text, unicode): 53 return text 54 55 try: 56 return unicode(text, 'utf-8') 57 except UnicodeError: 58 if config.charset not in ['utf-8', 'iso-8859-1']: 59 try: 60 return unicode(text, config.charset) 61 except UnicodeError: 62 pass 63 return unicode(text, 'iso-8859-1', 'replace') 64 65 66def decodeUserInput(s, charsets=[config.charset]): 67 """ 68 Decodes input from the user. 69 70 @param s: the string to unquote 71 @param charsets: list of charsets to assume the string is in 72 @rtype: unicode 73 @return: the unquoted string as unicode 74 """ 75 for charset in charsets: 76 try: 77 return s.decode(charset) 78 except UnicodeError: 79 pass 80 raise UnicodeError('The string %r cannot be decoded.' % s) 81 82 83# this is a thin wrapper around urllib (urllib only handles str, not unicode) 84# with py <= 2.4.1, it would give incorrect results with unicode 85# with py == 2.4.2, it crashes with unicode, if it contains non-ASCII chars 86def url_quote(s, safe='/', want_unicode=False): 87 """ 88 Wrapper around urllib.quote doing the encoding/decoding as usually wanted: 89 90 @param s: the string to quote (can be str or unicode, if it is unicode, 91 config.charset is used to encode it before calling urllib) 92 @param safe: just passed through to urllib 93 @param want_unicode: for the less usual case that you want to get back 94 unicode and not str, set this to True 95 Default is False. 96 """ 97 if isinstance(s, unicode): 98 s = s.encode(config.charset) 99 elif not isinstance(s, str): 100 s = str(s) 101 s = urllib.quote(s, safe) 102 if want_unicode: 103 s = s.decode(config.charset) # ascii would also work 104 return s 105 106def url_quote_plus(s, safe='/', want_unicode=False): 107 """ 108 Wrapper around urllib.quote_plus doing the encoding/decoding as usually wanted: 109 110 @param s: the string to quote (can be str or unicode, if it is unicode, 111 config.charset is used to encode it before calling urllib) 112 @param safe: just passed through to urllib 113 @param want_unicode: for the less usual case that you want to get back 114 unicode and not str, set this to True 115 Default is False. 116 """ 117 if isinstance(s, unicode): 118 s = s.encode(config.charset) 119 elif not isinstance(s, str): 120 s = str(s) 121 s = urllib.quote_plus(s, safe) 122 if want_unicode: 123 s = s.decode(config.charset) # ascii would also work 124 return s 125 126def url_unquote(s, want_unicode=True): 127 """ 128 Wrapper around urllib.unquote doing the encoding/decoding as usually wanted: 129 130 @param s: the string to unquote (can be str or unicode, if it is unicode, 131 config.charset is used to encode it before calling urllib) 132 @param want_unicode: for the less usual case that you want to get back 133 str and not unicode, set this to False. 134 Default is True. 135 """ 136 if isinstance(s, unicode): 137 s = s.encode(config.charset) # ascii would also work 138 s = urllib.unquote(s) 139 if want_unicode: 140 s = s.decode(config.charset) 141 return s 142 143def parseQueryString(qstr, want_unicode=True): 144 """ Parse a querystring "key=value&..." into a dict. 145 """ 146 is_unicode = isinstance(qstr, unicode) 147 if is_unicode: 148 qstr = qstr.encode(config.charset) 149 values = {} 150 for key, value in cgi.parse_qs(qstr).items(): 151 if len(value) < 2: 152 v = ''.join(value) 153 if want_unicode: 154 try: 155 v = unicode(v, config.charset) 156 except UnicodeDecodeError: 157 v = unicode(v, 'iso-8859-1', 'replace') 158 values[key] = v 159 return values 160 161def makeQueryString(qstr=None, want_unicode=False, **kw): 162 """ Make a querystring from arguments. 163 164 kw arguments overide values in qstr. 165 166 If a string is passed in, it's returned verbatim and 167 keyword parameters are ignored. 168 169 @param qstr: dict to format as query string, using either ascii or unicode 170 @param kw: same as dict when using keywords, using ascii or unicode 171 @rtype: string 172 @return: query string ready to use in a url 173 """ 174 if qstr is None: 175 qstr = {} 176 if isinstance(qstr, dict): 177 qstr.update(kw) 178 items = ['%s=%s' % (url_quote_plus(key, want_unicode=want_unicode), url_quote_plus(value, want_unicode=want_unicode)) for key, value in qstr.items()] 179 qstr = '&'.join(items) 180 return qstr 181 182 183def quoteWikinameURL(pagename, charset=config.charset): 184 """ Return a url encoding of filename in plain ascii 185 186 Use urllib.quote to quote any character that is not always safe. 187 188 @param pagename: the original pagename (unicode) 189 @param charset: url text encoding, 'utf-8' recommended. Other charset 190 might not be able to encode the page name and raise 191 UnicodeError. (default config.charset ('utf-8')). 192 @rtype: string 193 @return: the quoted filename, all unsafe characters encoded 194 """ 195 pagename = pagename.encode(charset) 196 return urllib.quote(pagename) 197 198 199def escape(s, quote=0): 200 """ Escape possible html tags 201 202 Replace special characters '&', '<' and '>' by SGML entities. 203 (taken from cgi.escape so we don't have to include that, even if we 204 don't use cgi at all) 205 206 @param s: (unicode) string to escape 207 @param quote: bool, should transform '\"' to '"' 208 @rtype: when called with a unicode object, return unicode object - otherwise return string object 209 @return: escaped version of s 210 """ 211 if not isinstance(s, (str, unicode)): 212 s = str(s) 213 214 # Must first replace & 215 s = s.replace("&", "&") 216 217 # Then other... 218 s = s.replace("<", "<") 219 s = s.replace(">", ">") 220 if quote: 221 s = s.replace('"', """) 222 return s 223 224def clean_comment(comment): 225 """ Clean comment - replace CR, LF, TAB by whitespace, delete control chars 226 TODO: move this to config, create on first call then return cached. 227 """ 228 # we only have input fields with max 200 chars, but spammers send us more 229 if len(comment) > 201: 230 comment = u'' 231 remap_chars = { 232 ord(u'\t'): u' ', 233 ord(u'\r'): u' ', 234 ord(u'\n'): u' ', 235 } 236 control_chars = u'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f' \ 237 '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f' 238 for c in control_chars: 239 remap_chars[c] = None 240 comment = comment.translate(remap_chars) 241 return comment 242 243def make_breakable(text, maxlen): 244 """ make a text breakable by inserting spaces into nonbreakable parts 245 """ 246 text = text.split(" ") 247 newtext = [] 248 for part in text: 249 if len(part) > maxlen: 250 while part: 251 newtext.append(part[:maxlen]) 252 part = part[maxlen:] 253 else: 254 newtext.append(part) 255 return " ".join(newtext) 256 257######################################################################## 258### Storage 259######################################################################## 260 261# Precompiled patterns for file name [un]quoting 262UNSAFE = re.compile(r'[^a-zA-Z0-9_]+') 263QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)') 264 265 266def quoteWikinameFS(wikiname, charset=config.charset): 267 """ Return file system representation of a Unicode WikiName. 268 269 Warning: will raise UnicodeError if wikiname can not be encoded using 270 charset. The default value of config.charset, 'utf-8' can encode any 271 character. 272 273 @param wikiname: Unicode string possibly containing non-ascii characters 274 @param charset: charset to encode string 275 @rtype: string 276 @return: quoted name, safe for any file system 277 """ 278 filename = wikiname.encode(charset) 279 280 quoted = [] 281 location = 0 282 for needle in UNSAFE.finditer(filename): 283 # append leading safe stuff 284 quoted.append(filename[location:needle.start()]) 285 location = needle.end() 286 # Quote and append unsafe stuff 287 quoted.append('(') 288 for character in needle.group(): 289 quoted.append('%02x' % ord(character)) 290 quoted.append(')') 291 292 # append rest of string 293 quoted.append(filename[location:]) 294 return ''.join(quoted) 295 296 297def unquoteWikiname(filename, charsets=[config.charset]): 298 """ Return Unicode WikiName from quoted file name. 299 300 We raise an InvalidFileNameError if we find an invalid name, so the 301 wiki could alarm the admin or suggest the user to rename a page. 302 Invalid file names should never happen in normal use, but are rather 303 cheap to find. 304 305 This function should be used only to unquote file names, not page 306 names we receive from the user. These are handled in request by 307 urllib.unquote, decodePagename and normalizePagename. 308 309 Todo: search clients of unquoteWikiname and check for exceptions. 310 311 @param filename: string using charset and possibly quoted parts 312 @param charsets: list of charsets used by string 313 @rtype: Unicode String 314 @return: WikiName 315 """ 316 ### Temporary fix start ### 317 # From some places we get called with Unicode strings 318 if isinstance(filename, type(u'')): 319 filename = filename.encode(config.charset) 320 ### Temporary fix end ### 321 322 parts = [] 323 start = 0 324 for needle in QUOTED.finditer(filename): 325 # append leading unquoted stuff 326 parts.append(filename[start:needle.start()]) 327 start = needle.end() 328 # Append quoted stuff 329 group = needle.group(1) 330 # Filter invalid filenames 331 if (len(group) % 2 != 0): 332 raise InvalidFileNameError(filename) 333 try: 334 for i in range(0, len(group), 2): 335 byte = group[i:i+2] 336 character = chr(int(byte, 16)) 337 parts.append(character) 338 except ValueError: 339 # byte not in hex, e.g 'xy' 340 raise InvalidFileNameError(filename) 341 342 # append rest of string 343 if start == 0: 344 wikiname = filename 345 else: 346 parts.append(filename[start:len(filename)]) 347 wikiname = ''.join(parts) 348 349 # FIXME: This looks wrong, because at this stage "()" can be both errors 350 # like open "(" without close ")", or unquoted valid characters in the file name. 351 # Filter invalid filenames. Any left (xx) must be invalid 352 #if '(' in wikiname or ')' in wikiname: 353 # raise InvalidFileNameError(filename) 354 355 wikiname = decodeUserInput(wikiname, charsets) 356 return wikiname 357 358# time scaling 359def timestamp2version(ts): 360 """ Convert UNIX timestamp (may be float or int) to our version 361 (long) int. 362 We don't want to use floats, so we just scale by 1e6 to get 363 an integer in usecs. 364 """ 365 return long(ts*1000000L) # has to be long for py 2.2.x 366 367def version2timestamp(v): 368 """ Convert version number to UNIX timestamp (float). 369 This must ONLY be used for display purposes. 370 """ 371 return v / 1000000.0 372 373 374# This is the list of meta attribute names to be treated as integers. 375# IMPORTANT: do not use any meta attribute names with "-" (or any other chars 376# invalid in python attribute names), use e.g. _ instead. 377INTEGER_METAS = ['current', 'revision', # for page storage (moin 2.0) 378 'data_format_revision', # for data_dir format spec (use by mig scripts) 379 ] 380 381class MetaDict(dict): 382 """ store meta informations as a dict. 383 """ 384 def __init__(self, metafilename, cache_directory): 385 """ create a MetaDict from metafilename """ 386 dict.__init__(self) 387 self.metafilename = metafilename 388 self.dirty = False 389 lock_dir = os.path.join(cache_directory, '__metalock__') 390 self.rlock = lock.ReadLock(lock_dir, 60.0) 391 self.wlock = lock.WriteLock(lock_dir, 60.0) 392 393 if not self.rlock.acquire(3.0): 394 raise EnvironmentError("Could not lock in MetaDict") 395 try: 396 self._get_meta() 397 finally: 398 self.rlock.release() 399 400 def _get_meta(self): 401 """ get the meta dict from an arbitrary filename. 402 does not keep state, does uncached, direct disk access. 403 @param metafilename: the name of the file to read 404 @return: dict with all values or {} if empty or error 405 """ 406 407 try: 408 metafile = codecs.open(self.metafilename, "r", "utf-8") 409 meta = metafile.read() # this is much faster than the file's line-by-line iterator 410 metafile.close() 411 except IOError: 412 meta = u'' 413 for line in meta.splitlines(): 414 key, value = line.split(':', 1) 415 value = value.strip() 416 if key in INTEGER_METAS: 417 value = int(value) 418 dict.__setitem__(self, key, value) 419 420 def _put_meta(self): 421 """ put the meta dict into an arbitrary filename. 422 does not keep or modify state, does uncached, direct disk access. 423 @param metafilename: the name of the file to write 424 @param metadata: dict of the data to write to the file 425 """ 426 meta = [] 427 for key, value in self.items(): 428 if key in INTEGER_METAS: 429 value = str(value) 430 meta.append("%s: %s" % (key, value)) 431 meta = '\r\n'.join(meta) 432 433 metafile = codecs.open(self.metafilename, "w", "utf-8") 434 metafile.write(meta) 435 metafile.close() 436 self.dirty = False 437 438 def sync(self, mtime_usecs=None): 439 """ No-Op except for that parameter """ 440 if not mtime_usecs is None: 441 self.__setitem__('mtime', str(mtime_usecs)) 442 # otherwise no-op 443 444 def __getitem__(self, key): 445 """ We don't care for cache coherency here. """ 446 return dict.__getitem__(self, key) 447 448 def __setitem__(self, key, value): 449 """ Sets a dictionary entry. """ 450 if not self.wlock.acquire(5.0): 451 raise EnvironmentError("Could not lock in MetaDict") 452 try: 453 self._get_meta() # refresh cache 454 try: 455 oldvalue = dict.__getitem__(self, key) 456 except KeyError: 457 oldvalue = None 458 if value != oldvalue: 459 dict.__setitem__(self, key, value) 460 self._put_meta() # sync cache 461 finally: 462 self.wlock.release() 463 464 465# Quoting of wiki names, file names, etc. (in the wiki markup) ----------------------------------- 466 467QUOTE_CHARS = u"'\"" 468 469def quoteName(name): 470 """ put quotes around a given name """ 471 for quote_char in QUOTE_CHARS: 472 if quote_char not in name: 473 return u"%s%s%s" % (quote_char, name, quote_char) 474 else: 475 return name # XXX we need to be able to escape the quote char for worst case 476 477def unquoteName(name): 478 """ if there are quotes around the name, strip them """ 479 for quote_char in QUOTE_CHARS: 480 if quote_char == name[0] == name[-1]: 481 return name[1:-1] 482 else: 483 return name 484 485############################################################################# 486### InterWiki 487############################################################################# 488INTERWIKI_PAGE = "InterWikiMap" 489 490def generate_file_list(request): 491 """ generates a list of all files. for internal use. """ 492 493 # order is important here, the local intermap file takes 494 # precedence over the shared one, and is thus read AFTER 495 # the shared one 496 intermap_files = request.cfg.shared_intermap 497 if not isinstance(intermap_files, list): 498 intermap_files = [intermap_files] 499 else: 500 intermap_files = intermap_files[:] 501 intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt")) 502 request.cfg.shared_intermap_files = [filename for filename in intermap_files 503 if filename and os.path.isfile(filename)] 504 505 506def get_max_mtime(file_list, page): 507 """ Returns the highest modification time of the files in file_list and the 508 page page. """ 509 timestamps = [os.stat(filename).st_mtime for filename in file_list] 510 if page.exists(): 511 # exists() is cached and thus cheaper than mtime_usecs() 512 timestamps.append(version2timestamp(page.mtime_usecs())) 513 return max(timestamps) 514 515 516def load_wikimap(request): 517 """ load interwiki map (once, and only on demand) """ 518 from MoinMoin.Page import Page 519 520 now = int(time.time()) 521 if getattr(request.cfg, "shared_intermap_files", None) is None: 522 generate_file_list(request) 523 524 try: 525 _interwiki_list = request.cfg.cache.interwiki_list 526 old_mtime = request.cfg.cache.interwiki_mtime 527 if request.cfg.cache.interwiki_ts + (1*60) < now: # 1 minutes caching time 528 max_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE)) 529 if max_mtime > old_mtime: 530 raise AttributeError # refresh cache 531 else: 532 request.cfg.cache.interwiki_ts = now 533 except AttributeError: 534 _interwiki_list = {} 535 lines = [] 536 537 for filename in request.cfg.shared_intermap_files: 538 f = open(filename, "r") 539 lines.extend(f.readlines()) 540 f.close() 541 542 # add the contents of the InterWikiMap page 543 lines += Page(request, INTERWIKI_PAGE).get_raw_body().splitlines() 544 545 for line in lines: 546 if not line or line[0] == '#': continue 547 try: 548 line = "%s %s/InterWiki" % (line, request.script_root) 549 wikitag, urlprefix, dummy = line.split(None, 2) 550 except ValueError: 551 pass 552 else: 553 _interwiki_list[wikitag] = urlprefix 554 555 del lines 556 557 # add own wiki as "Self" and by its configured name 558 _interwiki_list['Self'] = request.script_root + '/' 559 if request.cfg.interwikiname: 560 _interwiki_list[request.cfg.interwikiname] = request.script_root + '/' 561 562 # save for later 563 request.cfg.cache.interwiki_list = _interwiki_list 564 request.cfg.cache.interwiki_ts = now 565 request.cfg.cache.interwiki_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE)) 566 567 return _interwiki_list 568 569def split_wiki(wikiurl): 570 """ Split a wiki url, e.g: 571 572 'MoinMoin:FrontPage' -> "MoinMoin", "FrontPage", "" 573 'FrontPage' -> "Self", "FrontPage", "" 574 'MoinMoin:"Page with blanks" link title' -> "MoinMoin", "Page with blanks", "link title" 575 576 can also be used for: 577 578 'attachment:"filename with blanks.txt" other title' -> "attachment", "filename with blanks.txt", "other title" 579 580 @param wikiurl: the url to split 581 @rtype: tuple 582 @return: (wikiname, pagename, linktext) 583 """ 584 try: 585 wikiname, rest = wikiurl.split(":", 1) # e.g. MoinMoin:FrontPage 586 except ValueError: 587 try: 588 wikiname, rest = wikiurl.split("/", 1) # for what is this used? 589 except ValueError: 590 wikiname, rest = 'Self', wikiurl 591 if rest: 592 first_char = rest[0] 593 if first_char in QUOTE_CHARS: # quoted pagename 594 pagename_linktext = rest[1:].split(first_char, 1) 595 else: # not quoted, split on whitespace 596 pagename_linktext = rest.split(None, 1) 597 else: 598 pagename_linktext = "", "" 599 if len(pagename_linktext) == 1: 600 pagename, linktext = pagename_linktext[0], "" 601 else: 602 pagename, linktext = pagename_linktext 603 linktext = linktext.strip() 604 return wikiname, pagename, linktext 605 606def resolve_wiki(request, wikiurl): 607 """ Resolve an interwiki link. 608 609 @param request: the request object 610 @param wikiurl: the InterWiki:PageName link 611 @rtype: tuple 612 @return: (wikitag, wikiurl, wikitail, err) 613 """ 614 _interwiki_list = load_wikimap(request) 615 wikiname, pagename, linktext = split_wiki(wikiurl) 616 if _interwiki_list.has_key(wikiname): 617 return (wikiname, _interwiki_list[wikiname], pagename, False) 618 else: 619 return (wikiname, request.script_root, "/InterWiki", True) 620 621def join_wiki(wikiurl, wikitail): 622 """ 623 Add a (url_quoted) page name to an interwiki url. 624 625 Note: We can't know what kind of URL quoting a remote wiki expects. 626 We just use a utf-8 encoded string with standard URL quoting. 627 628 @param wikiurl: wiki url, maybe including a $PAGE placeholder 629 @param wikitail: page name 630 @rtype: string 631 @return: generated URL of the page in the other wiki 632 """ 633 wikitail = url_quote(wikitail) 634 if '$PAGE' in wikiurl: 635 return wikiurl.replace('$PAGE', wikitail) 636 else: 637 return wikiurl + wikitail 638 639 640############################################################################# 641### Page types (based on page names) 642############################################################################# 643 644def isSystemPage(request, pagename): 645 """ Is this a system page? Uses AllSystemPagesGroup internally. 646 647 @param request: the request object 648 @param pagename: the page name 649 @rtype: bool 650 @return: true if page is a system page 651 """ 652 return (pagename in request.groups.get(u'SystemPagesGroup', []) or 653 isTemplatePage(request, pagename)) 654 655 656def isTemplatePage(request, pagename): 657 """ Is this a template page? 658 659 @param pagename: the page name 660 @rtype: bool 661 @return: true if page is a template page 662 """ 663 return request.cfg.cache.page_template_regex.search(pagename) is not None 664 665 666def isGroupPage(request, pagename): 667 """ Is this a name of group page? 668 669 @param pagename: the page name 670 @rtype: bool 671 @return: true if page is a form page 672 """ 673 return request.cfg.cache.page_group_regex.search(pagename) is not None 674 675 676def filterCategoryPages(request, pagelist): 677 """ Return category pages in pagelist 678 679 WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use 680 getPageList with a filter function. 681 682 If you pass a list with a single pagename, either that is returned 683 or an empty list, thus you can use this function like a `isCategoryPage` 684 one. 685 686 @param pagelist: a list of pages 687 @rtype: list 688 @return: only the category pages of pagelist 689 """ 690 func = request.cfg.cache.page_category_regex.search 691 return filter(func, pagelist) 692 693 694def getLocalizedPage(request, pagename): # was: getSysPage 695 """ Get a system page according to user settings and available translations. 696 697 We include some special treatment for the case that <pagename> is the 698 currently rendered page, as this is the case for some pages used very 699 often, like FrontPage, RecentChanges etc. - in that case we reuse the 700 already existing page object instead creating a new one. 701 702 @param request: the request object 703 @param pagename: the name of the page 704 @rtype: Page object 705 @return: the page object of that system page, using a translated page, 706 if it exists 707 """ 708 from MoinMoin.Page import Page 709 i18n_name = request.getText(pagename, formatted=False) 710 pageobj = None 711 if i18n_name != pagename: 712 if request.page and i18n_name == request.page.page_name: 713 # do not create new object for current page 714 i18n_page = request.page 715 if i18n_page.exists(): 716 pageobj = i18n_page 717 else: 718 i18n_page = Page(request, i18n_name) 719 if i18n_page.exists(): 720 pageobj = i18n_page 721 722 # if we failed getting a translated version of <pagename>, 723 # we fall back to english 724 if not pageobj: 725 if request.page and pagename == request.page.page_name: 726 # do not create new object for current page 727 pageobj = request.page 728 else: 729 pageobj = Page(request, pagename) 730 return pageobj 731 732 733def getFrontPage(request): 734 """ Convenience function to get localized front page 735 736 @param request: current request 737 @rtype: Page object 738 @return localized page_front_page, if there is a translation 739 """ 740 return getLocalizedPage(request, request.cfg.page_front_page) 741 742 743def getHomePage(request, username=None): 744 """ 745 Get a user's homepage, or return None for anon users and 746 those who have not created a homepage. 747 748 DEPRECATED - try to use getInterwikiHomePage (see below) 749 750 @param request: the request object 751 @param username: the user's name 752 @rtype: Page 753 @return: user's homepage object - or None 754 """ 755 from MoinMoin.Page import Page 756 # default to current user 757 if username is None and request.user.valid: 758 username = request.user.name 759 760 # known user? 761 if username: 762 # Return home page 763 page = Page(request, username) 764 if page.exists(): 765 return page 766 767 return None 768 769 770def getInterwikiHomePage(request, username=None): 771 """ 772 Get a user's homepage. 773 774 cfg.user_homewiki influences behaviour of this: 775 'Self' does mean we store user homepage in THIS wiki. 776 When set to our own interwikiname, it behaves like with 'Self'. 777 778 'SomeOtherWiki' means we store user homepages in another wiki. 779 780 @param request: the request object 781 @param username: the user's name 782 @rtype: tuple (or None for anon users) 783 @return: (wikiname, pagename) 784 """ 785 # default to current user 786 if username is None and request.user.valid: 787 username = request.user.name 788 if not username: 789 return None # anon user 790 791 homewiki = request.cfg.user_homewiki 792 if homewiki == request.cfg.interwikiname: 793 homewiki = 'Self' 794 795 return homewiki, username 796 797 798def AbsPageName(request, context, pagename): 799 """ 800 Return the absolute pagename for a (possibly) relative pagename. 801 802 @param context: name of the page where "pagename" appears on 803 @param pagename: the (possibly relative) page name 804 @rtype: string 805 @return: the absolute page name 806 """ 807 if pagename.startswith(PARENT_PREFIX): 808 pagename = '/'.join(filter(None, context.split('/')[:-1] + [pagename[PARENT_PREFIX_LEN:]])) 809 elif pagename.startswith(CHILD_PREFIX): 810 pagename = context + '/' + pagename[CHILD_PREFIX_LEN:] 811 return pagename 812 813def pagelinkmarkup(pagename): 814 """ return markup that can be used as link to page <pagename> """ 815 from MoinMoin.parser.text_moin_wiki import Parser 816 if re.match(Parser.word_rule + "$", pagename): 817 return pagename 818 else: 819 return u'["%s"]' % pagename # XXX use quoteName(pagename) later 820 821############################################################################# 822### mimetype support 823############################################################################# 824import mimetypes 825 826MIMETYPES_MORE = { 827 # OpenOffice 2.x & other open document stuff 828 '.odt': 'application/vnd.oasis.opendocument.text', 829 '.ods': 'application/vnd.oasis.opendocument.spreadsheet', 830 '.odp': 'application/vnd.oasis.opendocument.presentation', 831 '.odg': 'application/vnd.oasis.opendocument.graphics', 832 '.odc': 'application/vnd.oasis.opendocument.chart', 833 '.odf': 'application/vnd.oasis.opendocument.formula', 834 '.odb': 'application/vnd.oasis.opendocument.database', 835 '.odi': 'application/vnd.oasis.opendocument.image', 836 '.odm': 'application/vnd.oasis.opendocument.text-master', 837 '.ott': 'application/vnd.oasis.opendocument.text-template', 838 '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template', 839 '.otp': 'application/vnd.oasis.opendocument.presentation-template', 840 '.otg': 'application/vnd.oasis.opendocument.graphics-template', 841} 842[mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()] 843 844MIMETYPES_sanitize_mapping = { 845 # this stuff is text, but got application/* for unknown reasons 846 ('application', 'docbook+xml'): ('text', 'docbook'), 847 ('application', 'x-latex'): ('text', 'latex'), 848 ('application', 'x-tex'): ('text', 'tex'), 849 ('application', 'javascript'): ('text', 'javascript'), 850} 851 852MIMETYPES_spoil_mapping = {} # inverse mapping of above 853for key, value in MIMETYPES_sanitize_mapping.items(): 854 MIMETYPES_spoil_mapping[value] = key 855 856 857class MimeType(object): 858 """ represents a mimetype like text/plain """ 859 860 def __init__(self, mimestr=None, filename=None): 861 self.major = self.minor = None # sanitized mime type and subtype 862 self.params = {} # parameters like "charset" or others 863 self.charset = None # this stays None until we know for sure! 864 self.raw_mimestr = mimestr 865 866 if mimestr: 867 self.parse_mimetype(mimestr) 868 elif filename: 869 self.parse_filename(filename) 870 871 def parse_filename(self, filename): 872 mtype, encoding = mimetypes.guess_type(filename) 873 if mtype is None: 874 mtype = 'application/octet-stream' 875 self.parse_mimetype(mtype) 876 877 def parse_mimetype(self, mimestr): 878 """ take a string like used in content-type and parse it into components, 879 alternatively it also can process some abbreviated string like "wiki" 880 """ 881 parameters = mimestr.split(";") 882 parameters = [p.strip() for p in parameters] 883 mimetype, parameters = parameters[0], parameters[1:] 884 mimetype = mimetype.split('/') 885 if len(mimetype) >= 2: 886 major, minor = mimetype[:2] # we just ignore more than 2 parts 887 else: 888 major, minor = self.parse_format(mimetype[0]) 889 self.major = major.lower() 890 self.minor = minor.lower() 891 for param in parameters: 892 key, value = param.split('=') 893 if value[0] == '"' and value[-1] == '"': # remove quotes 894 value = value[1:-1] 895 self.params[key.lower()] = value 896 if self.params.has_key('charset'): 897 self.charset = self.params['charset'].lower() 898 self.sanitize() 899 900 def parse_format(self, format): 901 """ maps from what we currently use on-page in a #format xxx processing 902 instruction to a sanitized mimetype major, minor tuple. 903 can also be user later for easier entry by the user, so he can just 904 type "wiki" instead of "text/moin-wiki". 905 """ 906 format = format.lower() 907 if format in ('plain', 'csv', 'rst', 'docbook', 'latex', 'tex', 'html', 'css', 908 'xml', 'python', 'perl', 'php', 'ruby', 'javascript', 909 'cplusplus', 'java', 'pascal', 'diff', 'gettext', 'xslt', ): 910 mimetype = 'text', format 911 else: 912 mapping = { 913 'wiki': ('text', 'moin-wiki'), 914 'irc': ('text', 'irssi'), 915 } 916 try: 917 mimetype = mapping[format] 918 except KeyError: 919 mimetype = 'text', 'x-%s' % format 920 return mimetype 921 922 def sanitize(self): 923 """ convert to some representation that makes sense - this is not necessarily 924 conformant to /etc/mime.types or IANA listing, but if something is 925 readable text, we will return some text/* mimetype, not application/*, 926 because we need text/plain as fallback and not application/octet-stream. 927 """ 928 self.major, self.minor = MIMETYPES_sanitize_mapping.get((self.major, self.minor), (self.major, self.minor)) 929 930 def spoil(self): 931 """ this returns something conformant to /etc/mime.type or IANA as a string, 932 kind of inverse operation of sanitize(), but doesn't change self 933 """ 934 major, minor = MIMETYPES_spoil_mapping.get((self.major, self.minor), (self.major, self.minor)) 935 return self.content_type(major, minor) 936 937 def content_type(self, major=None, minor=None, charset=None, params=None): 938 """ return a string suitable for Content-Type header 939 """ 940 major = major or self.major 941 minor = minor or self.minor 942 params = params or self.params or {} 943 if major == 'text': 944 charset = charset or self.charset or params.get('charset', config.charset) 945 params['charset'] = charset 946 mimestr = "%s/%s" % (major, minor) 947 params = ['%s="%s"' % (key.lower(), value) for key, value in params.items()] 948 params.insert(0, mimestr) 949 return "; ".join(params) 950 951 def mime_type(self): 952 """ return a string major/minor only, no params """ 953 return "%s/%s" % (self.major, self.minor) 954 955 def module_name(self): 956 """ convert this mimetype to a string useable as python module name, 957 we yield the exact module name first and then proceed to shorter 958 module names (useful for falling back to them, if the more special 959 module is not found) - e.g. first "text_python", next "text". 960 Finally, we yield "application_octet_stream" as the most general 961 mimetype we have. 962 Hint: the fallback handler module for text/* should be implemented 963 in module "text" (not "text_plain") 964 """ 965 mimetype = self.mime_type() 966 modname = mimetype.replace("/", "_").replace("-", "_").replace(".", "_") 967 fragments = modname.split('_') 968 for length in range(len(fragments), 1, -1): 969 yield "_".join(fragments[:length]) 970 yield self.raw_mimestr 971 yield fragments[0] 972 yield "application_octet_stream" 973 974 975############################################################################# 976### Plugins 977############################################################################# 978 979class PluginError(Exception): 980 """ Base class for plugin errors """ 981 982class PluginMissingError(PluginError): 983 """ Raised when a plugin is not found """ 984 985class PluginAttributeError(PluginError): 986 """ Raised when plugin does not contain an attribtue """ 987 988 989def importPlugin(cfg, kind, name, function="execute"): 990 """ Import wiki or builtin plugin 991 992 Returns function from a plugin module name. If name can not be 993 imported, raise PluginMissingError. If function is missing, raise 994 PluginAttributeError. 995 996 kind may be one of 'action', 'formatter', 'macro', 'parser' or any other 997 directory that exist in MoinMoin or data/plugin. 998 999 Wiki plugins will always override builtin plugins. If you want 1000 specific plugin, use either importWikiPlugin or importBuiltinPlugin 1001 directly. 1002 1003 @param cfg: wiki config instance 1004 @param kind: what kind of module we want to import 1005 @param name: the name of the module 1006 @param function: the function name 1007 @rtype: any object 1008 @return: "function" of module "name" of kind "kind", or None 1009 """ 1010 try: 1011 return importWikiPlugin(cfg, kind, name, function) 1012 except PluginMissingError: 1013 return importBuiltinPlugin(kind, name, function) 1014 1015 1016def importWikiPlugin(cfg, kind, name, function="execute"): 1017 """ Import plugin from the wiki data directory 1018 1019 See importPlugin docstring. 1020 """ 1021 if not name in wikiPlugins(kind, cfg): 1022 raise PluginMissingError 1023 moduleName = '%s.plugin.%s.%s' % (cfg.siteid, kind, name) 1024 return importNameFromPlugin(moduleName, function) 1025 1026 1027def importBuiltinPlugin(kind, name, function="execute"): 1028 """ Import builtin plugin from MoinMoin package 1029 1030 See importPlugin docstring. 1031 """ 1032 if not name in builtinPlugins(kind): 1033 raise PluginMissingError 1034 moduleName = 'MoinMoin.%s.%s' % (kind, name) 1035 return importNameFromPlugin(moduleName, function) 1036 1037 1038def importNameFromPlugin(moduleName, name): 1039 """ Return name from plugin module 1040 1041 Raise PluginAttributeError if name does not exists. 1042 """ 1043 module = __import__(moduleName, globals(), {}, [name]) 1044 try: 1045 return getattr(module, name) 1046 except AttributeError: 1047 raise PluginAttributeError 1048 1049 1050def builtinPlugins(kind): 1051 """ Gets a list of modules in MoinMoin.'kind' 1052 1053 @param kind: what kind of modules we look for 1054 @rtype: list 1055 @return: module names 1056 """ 1057 modulename = "MoinMoin." + kind 1058 return pysupport.importName(modulename, "modules") 1059 1060 1061def wikiPlugins(kind, cfg): 1062 """ Gets a list of modules in data/plugin/'kind' 1063 1064 Require valid plugin directory. e.g missing 'parser' directory or 1065 missing '__init__.py' file will raise errors. 1066 1067 @param kind: what kind of modules we look for 1068 @rtype: list 1069 @return: module names 1070 """ 1071 # Wiki plugins are located in wikiconfig.plugin module 1072 modulename = '%s.plugin.%s' % (cfg.siteid, kind) 1073 return pysupport.importName(modulename, "modules") 1074 1075 1076def getPlugins(kind, cfg): 1077 """ Gets a list of plugin names of kind 1078 1079 @param kind: what kind of modules we look for 1080 @rtype: list 1081 @return: module names 1082 """ 1083 # Copy names from builtin plugins - so we dont destroy the value 1084 all_plugins = builtinPlugins(kind)[:] 1085 1086 # Add extension plugins without duplicates 1087 for plugin in wikiPlugins(kind, cfg): 1088 if plugin not in all_plugins: 1089 all_plugins.append(plugin) 1090 1091 return all_plugins 1092 1093 1094def searchAndImportPlugin(cfg, type, name, what=None): 1095 type2classname = {"parser": "Parser", 1096 "formatter": "Formatter", 1097 } 1098 if what is None: 1099 what = type2classname[type] 1100 mt = MimeType(name) 1101 plugin = None 1102 for module_name in mt.module_name(): 1103 try: 1104 plugin = importPlugin(cfg, type, module_name, what) 1105 break 1106 except PluginMissingError: 1107 pass 1108 else: 1109 raise PluginMissingError("Plugin not found!") 1110 return plugin 1111 1112 1113############################################################################# 1114### Parsers 1115############################################################################# 1116 1117def getParserForExtension(cfg, extension): 1118 """ 1119 Returns the Parser class of the parser fit to handle a file 1120 with the given extension. The extension should be in the same 1121 format as os.path.splitext returns it (i.e. with the dot). 1122 Returns None if no parser willing to handle is found. 1123 The dict of extensions is cached in the config object. 1124 1125 @param cfg: the Config instance for the wiki in question 1126 @param extension: the filename extension including the dot 1127 @rtype: class, None 1128 @returns: the parser class or None 1129 """ 1130 if not hasattr(cfg.cache, 'EXT_TO_PARSER'): 1131 etp, etd = {}, None 1132 for pname in getPlugins('parser', cfg): 1133 try: 1134 Parser = importPlugin(cfg, 'parser', pname, 'Parser') 1135 except PluginMissingError: 1136 continue 1137 if hasattr(Parser, 'extensions'): 1138 exts = Parser.extensions 1139 if isinstance(exts, list): 1140 for ext in Parser.extensions: 1141 etp[ext] = Parser 1142 elif str(exts) == '*': 1143 etd = Parser 1144 cfg.cache.EXT_TO_PARSER = etp 1145 cfg.cache.EXT_TO_PARSER_DEFAULT = etd 1146 1147 return cfg.cache.EXT_TO_PARSER.get(extension, cfg.cache.EXT_TO_PARSER_DEFAULT) 1148 1149 1150############################################################################# 1151### Parameter parsing 1152############################################################################# 1153 1154def parseAttributes(request, attrstring, endtoken=None, extension=None): 1155 """ 1156 Parse a list of attributes and return a dict plus a possible 1157 error message. 1158 If extension is passed, it has to be a callable that returns 1159 a tuple (found_flag, msg). found_flag is whether it did find and process 1160 something, msg is '' when all was OK or any other string to return an error 1161 message. 1162 1163 @param request: the request object 1164 @param attrstring: string containing the attributes to be parsed 1165 @param endtoken: token terminating parsing 1166 @param extension: extension function - 1167 gets called with the current token, the parser and the dict 1168 @rtype: dict, msg 1169 @return: a dict plus a possible error message 1170 """ 1171 import shlex, StringIO 1172 1173 _ = request.getText 1174 1175 parser = shlex.shlex(StringIO.StringIO(attrstring)) 1176 parser.commenters = '' 1177 msg = None 1178 attrs = {} 1179 1180 while not msg: 1181 try: 1182 key = parser.get_token() 1183 except ValueError, err: 1184 msg = str(err) 1185 break 1186 if not key: break 1187 if endtoken and key == endtoken: break 1188 1189 # call extension function with the current token, the parser, and the dict 1190 if extension: 1191 found_flag, msg = extension(key, parser, attrs) 1192 #request.log("%r = extension(%r, parser, %r)" % (msg, key, attrs)) 1193 if found_flag: 1194 continue 1195 elif msg: 1196 break 1197 #else (we found nothing, but also didn't have an error msg) we just continue below: 1198 1199 try: 1200 eq = parser.get_token() 1201 except ValueError, err: 1202 msg = str(err) 1203 break 1204 if eq != "=": 1205 msg = _('Expected "=" to follow "%(token)s"') % {'token': key} 1206 break 1207 1208 try: 1209 val = parser.get_token() 1210 except ValueError, err: 1211 msg = str(err) 1212 break 1213 if not val: 1214 msg = _('Expected a value for key "%(token)s"') % {'token': key} 1215 break 1216 1217 key = escape(key) # make sure nobody cheats 1218 1219 # safely escape and quote value 1220 if val[0] in ["'", '"']: 1221 val = escape(val) 1222 else: 1223 val = '"%s"' % escape(val, 1) 1224 1225 attrs[key.lower()] = val 1226 1227 return attrs, msg or '' 1228 1229 1230class ParameterParser: 1231 """ MoinMoin macro parameter parser 1232 1233 Parses a given parameter string, separates the individual parameters 1234 and detects their type. 1235 1236 Possible parameter types are: 1237 1238 Name | short | example 1239 ---------------------------- 1240 Integer | i | -374 1241 Float | f | 234.234 23.345E-23 1242 String | s | 'Stri\'ng' 1243 Boolean | b | 0 1 True false 1244 Name | | case_sensitive | converted to string 1245 1246 So say you want to parse three things, name, age and if the 1247 person is male or not: 1248 1249 The pattern will be: %(name)s%(age)i%(male)b 1250 1251 As a result, the returned dict will put the first value into 1252 male, second into age etc. If some argument is missing, it will 1253 get None as its value. This also means that all the identifiers 1254 in the pattern will exist in the dict, they will just have the 1255 value None if they were not specified by the caller. 1256 1257 So if we call it with the parameters as follows: 1258 ("John Smith", 18) 1259 this will result in the following dict: 1260 {"name": "John Smith", "age": 18, "male": None} 1261 1262 Another way of calling would be: 1263 ("John Smith", male=True) 1264 this will result in the following dict: 1265 {"name": "John Smith", "age": None, "male": True} 1266 1267 @copyright: 2004 by Florian Festi, 1268 2006 by Mikko Virkkil� 1269 @license: GNU GPL, see COPYING for details. 1270 """ 1271 1272 def __init__(self, pattern): 1273 #parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]" 1274 name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)" 1275 int_re = r"(?P<int>-?\d+)" 1276 bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))" 1277 float_re = r"(?P<float>-?\d+\.\d+([eE][+-]?\d+)?)" 1278 string_re = (r"(?P<string>('([^']|(\'))*?')|" + 1279 r'("([^"]|(\"))*?"))') 1280 name_re = name % "name" 1281 name_param_re = name % "name_param" 1282 1283 param_re = r"\s*(\s*%s\s*=\s*)?(%s|%s|%s|%s|%s)\s*(,|$)" % ( 1284 name_re, float_re, int_re, bool_re, string_re, name_param_re) 1285 self.param_re = re.compile(param_re, re.U) 1286 self._parse_pattern(pattern) 1287 1288 def _parse_pattern(self, pattern): 1289 param_re = r"(%(?P<name>\(.*?\))?(?P<type>[ibfs]{1,3}))|\|" 1290 i = 0 1291 # TODO: Optionals aren't checked. 1292 self.optional = [] 1293 named = False 1294 self.param_list = [] 1295 self.param_dict = {} 1296 1297 for match in re.finditer(param_re, pattern): 1298 if match.group() == "|": 1299 self.optional.append(i) 1300 continue 1301 self.param_list.append(match.group('type')) 1302 if match.group('name'): 1303 named = True 1304 self.param_dict[match.group('name')[1:-1]] = i 1305 elif named: 1306 raise ValueError, "Named parameter expected" 1307 i += 1 1308 1309 def __str__(self): 1310 return "%s, %s, optional:%s" % (self.param_list, self.param_dict, 1311 self.optional) 1312 1313 def parse_parameters(self, input): 1314 """ 1315 (4, 2) 1316 """ 1317 #Default list to "None"s 1318 parameter_list = [None] * len(self.param_list) 1319 parameter_dict = {} 1320 check_list = [0] * len(self.param_list) 1321 1322 i = 0 1323 start = 0 1324 named = False 1325 while start < len(input): 1326 match = re.match(self.param_re, input[start:]) 1327 if not match: 1328 raise ValueError, "Misformatted value" 1329 start += match.end() 1330 value = None 1331 if match.group("int"): 1332 value = int(match.group("int")) 1333 type = 'i' 1334 elif match.group("bool"): 1335 value = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true") 1336 type = 'b' 1337 elif match.group("float"): 1338 value = float(match.group("float")) 1339 type = 'f' 1340 elif match.group("string"): 1341 value = match.group("string")[1:-1] 1342 type = 's' 1343 elif match.group("name_param"): 1344 value = match.group("name_param") 1345 type = 'n' 1346 else: 1347 value = None 1348 1349 parameter_list.append(value) 1350 if match.group("name"): 1351 if not self.param_dict.has_key(match.group("name")): 1352 raise ValueError, "Unknown parameter name '%s'" % match.group("name") 1353 nr = self.param_dict[match.group("name")] 1354 if check_list[nr]: 1355 #raise ValueError, "Parameter specified twice" 1356 #TODO: Something saner that raising an exception. This is pretty good, since it ignores it. 1357 pass 1358 else: 1359 check_list[nr] = 1 1360 parameter_dict[match.group("name")] = value 1361 parameter_list[nr] = value 1362 named = True 1363 elif named: 1364 raise ValueError, "Only named parameters allowed" 1365 else: 1366 nr = i 1367 parameter_list[nr] = value 1368 1369 #Let's populate and map our dictionary to what's been found 1370 for name in self.param_dict.keys(): 1371 tmp = self.param_dict[name] 1372 parameter_dict[name]=parameter_list[tmp] 1373 1374 i += 1 1375 1376 return parameter_list, parameter_dict 1377 1378 1379""" never used: 1380 def _check_type(value, type, format): 1381 if type == 'n' and 's' in format: # n as s 1382 return value 1383 1384 if type in format: 1385 return value # x -> x 1386 1387 if type == 'i': 1388 if 'f' in format: 1389 return float(value) # i -> f 1390 elif 'b' in format: 1391 return value # i -> b 1392 elif type == 'f': 1393 if 'b' in format: 1394 return value # f -> b 1395 elif type == 's': 1396 if 'b' in format: 1397 return value.lower() != 'false' # s-> b 1398 1399 if 's' in format: # * -> s 1400 return str(value) 1401 else: 1402 pass # XXX error 1403 1404def main(): 1405 pattern = "%i%sf%s%ifs%(a)s|%(b)s" 1406 param = ' 4,"DI\'NG", b=retry, a="DING"' 1407 1408 #p_list, p_dict = parse_parameters(param) 1409 1410 print 'Pattern :', pattern 1411 print 'Param :', param 1412 1413 P = ParameterParser(pattern) 1414 print P 1415 print P.parse_parameters(param) 1416 1417 1418if __name__=="__main__": 1419 main() 1420""" 1421 1422############################################################################# 1423### Misc 1424############################################################################# 1425def taintfilename(basename): 1426 """ 1427 Make a filename that is supposed to be a plain name secure, i.e. 1428 remove any possible path components that compromise our system. 1429 1430 @param basename: (possibly unsafe) filename 1431 @rtype: string 1432 @return: (safer) filename 1433 """ 1434 for x in (os.pardir, ':', '/', '\\', '<', '>'): 1435 basename = basename.replace(x, '_') 1436 1437 return basename 1438 1439 1440def mapURL(request, url): 1441 """ 1442 Map URLs according to 'cfg.url_mappings'. 1443 1444 @param url: a URL 1445 @rtype: string 1446 @return: mapped URL 1447 """ 1448 # check whether we have to map URLs 1449 if request.cfg.url_mappings: 1450 # check URL for the configured prefixes 1451 for prefix in request.cfg.url_mappings.keys(): 1452 if url.startswith(prefix): 1453 # substitute prefix with replacement value 1454 return request.cfg.url_mappings[prefix] + url[len(prefix):] 1455 1456 # return unchanged url 1457 return url 1458 1459 1460def getUnicodeIndexGroup(name): 1461 """ 1462 Return a group letter for `name`, which must be a unicode string. 1463 Currently supported: Hangul Syllables (U+AC00 - U+D7AF) 1464 1465 @param name: a string 1466 @rtype: string 1467 @return: group letter or None 1468 """ 1469 c = name[0] 1470 if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables 1471 return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588) 1472 else: 1473 return c.upper() # we put lower and upper case words into the same index group 1474 1475 1476def isStrictWikiname(name, word_re=re.compile(ur"^(?:[%(u)s][%(l)s]+){2,}$" % {'u': config.chars_upper, 'l': config.chars_lower})): 1477 """ 1478 Check whether this is NOT an extended name. 1479 1480 @param name: the wikiname in question 1481 @rtype: bool 1482 @return: true if name matches the word_re 1483 """ 1484 return word_re.match(name) 1485 1486 1487def isPicture(url): 1488 """ 1489 Is this a picture's url? 1490 1491 @param url: the url in question 1492 @rtype: bool 1493 @return: true if url points to a picture 1494 """ 1495 extpos = url.rfind(".") 1496 return extpos > 0 and url[extpos:].lower() in ['.gif', '.jpg', '.jpeg', '.png', '.bmp', '.ico', ] 1497 1498 1499def link_tag(request, params, text=None, formatter=None, on=None, **kw): 1500 """ Create a link. 1501 1502 TODO: cleanup css_class 1503 1504 @param request: the request object 1505 @param params: parameter string appended to the URL after the scriptname/ 1506 @param text: text / inner part of the <a>...</a> link - does NOT get 1507 escaped, so you can give HTML here and it will be used verbatim 1508 @param formatter: the formatter object to use 1509 @param on: opening/closing tag only 1510 @keyword attrs: additional attrs (HTMLified string) (removed in 1.5.3) 1511 @rtype: string 1512 @return: formatted link tag 1513 """ 1514 if formatter is None: 1515 formatter = request.html_formatter 1516 if kw.has_key('css_class'): 1517 css_class = kw['css_class'] 1518 del kw['css_class'] # one time is enough 1519 else: 1520 css_class = None 1521 id = kw.get('id', None) 1522 name = kw.get('name', None) 1523 if text is None: 1524 text = params # default 1525 if formatter: 1526 url = "%s/%s" % (request.script_root, params) 1527 # formatter.url will escape the url part 1528 if on is not None: 1529 tag = formatter.url(on, url, css_class, **kw) 1530 else: 1531 tag = (formatter.url(1, url, css_class, **kw) + 1532 formatter.rawHTML(text) + 1533 formatter.url(0)) 1534 else: # this shouldn't be used any more: 1535 if on is not None and not on: 1536 tag = '</a>' 1537 else: 1538 attrs = '' 1539 if css_class: 1540 attrs += ' class="%s"' % css_class 1541 if id: 1542 attrs += ' id="%s"' % id 1543 if name: 1544 attrs += ' name="%s"' % name 1545 tag = '<a%s href="%s/%s">' % (attrs, request.script_root, params) 1546 if not on: 1547 tag = "%s%s</a>" % (tag, text) 1548 request.log("Warning: wikiutil.link_tag called without formatter and without request.html_formatter. tag=%r" % (tag, )) 1549 return tag 1550 1551def containsConflictMarker(text): 1552 """ Returns true if there is a conflict marker in the text. """ 1553 return "/!\\ '''Edit conflict" in text 1554 1555def pagediff(request, pagename1, rev1, pagename2, rev2, **kw): 1556 """ 1557 Calculate the "diff" between two page contents. 1558 1559 @param pagename1: name of first page 1560 @param rev1: revision of first page 1561 @param pagename2: name of second page 1562 @param rev2: revision of second page 1563 @keyword ignorews: if 1: ignore pure-whitespace changes. 1564 @rtype: list 1565 @return: lines of diff output 1566 """ 1567 from MoinMoin.Page import Page 1568 from MoinMoin.util import diff_text 1569 lines1 = Page(request, pagename1, rev=rev1).getlines() 1570 lines2 = Page(request, pagename2, rev=rev2).getlines() 1571 1572 lines = diff_text.diff(lines1, lines2, **kw) 1573 return lines 1574 1575 1576######################################################################## 1577### Tickets - used by RenamePage and DeletePage 1578######################################################################## 1579 1580def createTicket(request, tm=None): 1581 """Create a ticket using a site-specific secret (the config)""" 1582 ticket = tm or "%010x" % time.time() 1583 digest = hashlib.new('sha1', ticket) 1584 1585 varnames = ['data_dir', 'data_underlay_dir', 'language_default', 1586 'mail_smarthost', 'mail_from', 'page_front_page', 1587 'theme_default', 'sitename', 'logo_string', 1588 'interwikiname', 'user_homewiki', 'acl_rights_before', ] 1589 for varname in varnames: 1590 var = getattr(request.cfg, varname, None) 1591 if isinstance(var, (str, unicode)): 1592 digest.update(repr(var)) 1593 1594 return "%s.%s" % (ticket, digest.hexdigest()) 1595 1596 1597def checkTicket(request, ticket): 1598 """Check validity of a previously created ticket""" 1599 try: 1600 timestamp_str = ticket.split('.')[0] 1601 timestamp = int(timestamp_str, 16) 1602 except ValueError: 1603 # invalid or empty ticket 1604 return False 1605 now = time.time() 1606 if timestamp < now - 10 * 3600: 1607 # we don't accept tickets older than 10h 1608 return False 1609 ourticket = createTicket(request, timestamp_str) 1610 return ticket == ourticket 1611 1612 1613def renderText(request, Parser, text, line_anchors=False): 1614 """executes raw wiki markup with all page elements""" 1615 import StringIO 1616 out = StringIO.StringIO() 1617 request.redirect(out) 1618 wikiizer = Parser(text, request) 1619 wikiizer.format(request.formatter, inhibit_p=True) 1620 result = out.getvalue() 1621 request.redirect() 1622 del out 1623 return result 1624 1625 1626def getProcessingInstructions(text): 1627 """creates dict of processing instructions from raw wiki markup""" 1628 kw = {} 1629 for line in text.split('\n'): 1630 if line.startswith('#'): 1631 for pi in ("format", "refresh", "redirect", "deprecated", "pragma", "form", "acl", "language"): 1632 if line[1:].lower().startswith(pi): 1633 kw[pi] = line[len(pi)+1:].strip() 1634 break 1635 return kw 1636 1637 1638def getParser(request, text): 1639 """gets the parser from raw wiki murkup""" 1640 # check for XML content 1641 if text and text[:5] == '<?xml': 1642 pi_format = "xslt" 1643 else: 1644 # check processing instructions 1645 pi = getProcessingInstructions(text) 1646 pi_format = pi.get("format", request.cfg.default_markup or "wiki").lower() 1647 1648 Parser = searchAndImportPlugin(request.cfg, "parser", pi_format) 1649 return Parser 1650 1651