Back to index

moin  1.9.0~rc2
wikiutil160a.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Wiki Utility Functions
00004 
00005     @copyright: 2000 - 2004 by Jürgen Hermann <jh@web.de>
00006                 2007 by Reimar Bauer
00007     @license: GNU GPL, see COPYING for details.
00008 """
00009 
00010 import cgi
00011 import codecs
00012 import os
00013 import re
00014 import time
00015 import urllib
00016 
00017 from MoinMoin import config
00018 from MoinMoin.util import pysupport, lock
00019 
00020 # Exceptions
00021 class InvalidFileNameError(Exception):
00022     """ Called when we find an invalid file name """
00023     pass
00024 
00025 # constants for page names
00026 PARENT_PREFIX = "../"
00027 PARENT_PREFIX_LEN = len(PARENT_PREFIX)
00028 CHILD_PREFIX = "/"
00029 CHILD_PREFIX_LEN = len(CHILD_PREFIX)
00030 
00031 #############################################################################
00032 ### Getting data from user/Sending data to user
00033 #############################################################################
00034 
00035 def decodeUnknownInput(text):
00036     """ Decode unknown input, like text attachments
00037 
00038     First we try utf-8 because it has special format, and it will decode
00039     only utf-8 files. Then we try config.charset, then iso-8859-1 using
00040     'replace'. We will never raise an exception, but may return junk
00041     data.
00042 
00043     WARNING: Use this function only for data that you view, not for data
00044     that you save in the wiki.
00045 
00046     @param text: the text to decode, string
00047     @rtype: unicode
00048     @return: decoded text (maybe wrong)
00049     """
00050     # Shortcut for unicode input
00051     if isinstance(text, unicode):
00052         return text
00053 
00054     try:
00055         return unicode(text, 'utf-8')
00056     except UnicodeError:
00057         if config.charset not in ['utf-8', 'iso-8859-1']:
00058             try:
00059                 return unicode(text, config.charset)
00060             except UnicodeError:
00061                 pass
00062         return unicode(text, 'iso-8859-1', 'replace')
00063 
00064 
00065 def decodeUserInput(s, charsets=[config.charset]):
00066     """
00067     Decodes input from the user.
00068 
00069     @param s: the string to unquote
00070     @param charsets: list of charsets to assume the string is in
00071     @rtype: unicode
00072     @return: the unquoted string as unicode
00073     """
00074     for charset in charsets:
00075         try:
00076             return s.decode(charset)
00077         except UnicodeError:
00078             pass
00079     raise UnicodeError('The string %r cannot be decoded.' % s)
00080 
00081 
00082 # this is a thin wrapper around urllib (urllib only handles str, not unicode)
00083 # with py <= 2.4.1, it would give incorrect results with unicode
00084 # with py == 2.4.2, it crashes with unicode, if it contains non-ASCII chars
00085 def url_quote(s, safe='/', want_unicode=False):
00086     """
00087     Wrapper around urllib.quote doing the encoding/decoding as usually wanted:
00088 
00089     @param s: the string to quote (can be str or unicode, if it is unicode,
00090               config.charset is used to encode it before calling urllib)
00091     @param safe: just passed through to urllib
00092     @param want_unicode: for the less usual case that you want to get back
00093                          unicode and not str, set this to True
00094                          Default is False.
00095     """
00096     if isinstance(s, unicode):
00097         s = s.encode(config.charset)
00098     elif not isinstance(s, str):
00099         s = str(s)
00100     s = urllib.quote(s, safe)
00101     if want_unicode:
00102         s = s.decode(config.charset) # ascii would also work
00103     return s
00104 
00105 def url_quote_plus(s, safe='/', want_unicode=False):
00106     """
00107     Wrapper around urllib.quote_plus doing the encoding/decoding as usually wanted:
00108 
00109     @param s: the string to quote (can be str or unicode, if it is unicode,
00110               config.charset is used to encode it before calling urllib)
00111     @param safe: just passed through to urllib
00112     @param want_unicode: for the less usual case that you want to get back
00113                          unicode and not str, set this to True
00114                          Default is False.
00115     """
00116     if isinstance(s, unicode):
00117         s = s.encode(config.charset)
00118     elif not isinstance(s, str):
00119         s = str(s)
00120     s = urllib.quote_plus(s, safe)
00121     if want_unicode:
00122         s = s.decode(config.charset) # ascii would also work
00123     return s
00124 
00125 def url_unquote(s, want_unicode=True):
00126     """
00127     Wrapper around urllib.unquote doing the encoding/decoding as usually wanted:
00128 
00129     @param s: the string to unquote (can be str or unicode, if it is unicode,
00130               config.charset is used to encode it before calling urllib)
00131     @param want_unicode: for the less usual case that you want to get back
00132                          str and not unicode, set this to False.
00133                          Default is True.
00134     """
00135     if isinstance(s, unicode):
00136         s = s.encode(config.charset) # ascii would also work
00137     s = urllib.unquote(s)
00138     if want_unicode:
00139         s = s.decode(config.charset)
00140     return s
00141 
00142 def parseQueryString(qstr, want_unicode=True):
00143     """ Parse a querystring "key=value&..." into a dict.
00144     """
00145     is_unicode = isinstance(qstr, unicode)
00146     if is_unicode:
00147         qstr = qstr.encode(config.charset)
00148     values = {}
00149     for key, value in cgi.parse_qs(qstr).items():
00150         if len(value) < 2:
00151             v = ''.join(value)
00152             if want_unicode:
00153                 try:
00154                     v = unicode(v, config.charset)
00155                 except UnicodeDecodeError:
00156                     v = unicode(v, 'iso-8859-1', 'replace')
00157             values[key] = v
00158     return values
00159 
00160 def makeQueryString(qstr=None, want_unicode=False, **kw):
00161     """ Make a querystring from arguments.
00162 
00163     kw arguments overide values in qstr.
00164 
00165     If a string is passed in, it's returned verbatim and
00166     keyword parameters are ignored.
00167 
00168     @param qstr: dict to format as query string, using either ascii or unicode
00169     @param kw: same as dict when using keywords, using ascii or unicode
00170     @rtype: string
00171     @return: query string ready to use in a url
00172     """
00173     if qstr is None:
00174         qstr = {}
00175     if isinstance(qstr, dict):
00176         qstr.update(kw)
00177         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()]
00178         qstr = '&'.join(items)
00179     return qstr
00180 
00181 
00182 def quoteWikinameURL(pagename, charset=config.charset):
00183     """ Return a url encoding of filename in plain ascii
00184 
00185     Use urllib.quote to quote any character that is not always safe.
00186 
00187     @param pagename: the original pagename (unicode)
00188     @param charset: url text encoding, 'utf-8' recommended. Other charset
00189                     might not be able to encode the page name and raise
00190                     UnicodeError. (default config.charset ('utf-8')).
00191     @rtype: string
00192     @return: the quoted filename, all unsafe characters encoded
00193     """
00194     pagename = pagename.encode(charset)
00195     return urllib.quote(pagename)
00196 
00197 
00198 def escape(s, quote=0):
00199     """ Escape possible html tags
00200 
00201     Replace special characters '&', '<' and '>' by SGML entities.
00202     (taken from cgi.escape so we don't have to include that, even if we
00203     don't use cgi at all)
00204 
00205     @param s: (unicode) string to escape
00206     @param quote: bool, should transform '\"' to '&quot;'
00207     @rtype: when called with a unicode object, return unicode object - otherwise return string object
00208     @return: escaped version of s
00209     """
00210     if not isinstance(s, (str, unicode)):
00211         s = str(s)
00212 
00213     # Must first replace &
00214     s = s.replace("&", "&amp;")
00215 
00216     # Then other...
00217     s = s.replace("<", "&lt;")
00218     s = s.replace(">", "&gt;")
00219     if quote:
00220         s = s.replace('"', "&quot;")
00221     return s
00222 
00223 def clean_comment(comment):
00224     """ Clean comment - replace CR, LF, TAB by whitespace, delete control chars
00225         TODO: move this to config, create on first call then return cached.
00226     """
00227     # we only have input fields with max 200 chars, but spammers send us more
00228     if len(comment) > 201:
00229         comment = u''
00230     remap_chars = {
00231         ord(u'\t'): u' ',
00232         ord(u'\r'): u' ',
00233         ord(u'\n'): u' ',
00234     }
00235     control_chars = u'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f' \
00236                     '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f'
00237     for c in control_chars:
00238         remap_chars[c] = None
00239     comment = comment.translate(remap_chars)
00240     return comment
00241 
00242 def make_breakable(text, maxlen):
00243     """ make a text breakable by inserting spaces into nonbreakable parts
00244     """
00245     text = text.split(" ")
00246     newtext = []
00247     for part in text:
00248         if len(part) > maxlen:
00249             while part:
00250                 newtext.append(part[:maxlen])
00251                 part = part[maxlen:]
00252         else:
00253             newtext.append(part)
00254     return " ".join(newtext)
00255 
00256 ########################################################################
00257 ### Storage
00258 ########################################################################
00259 
00260 # Precompiled patterns for file name [un]quoting
00261 UNSAFE = re.compile(r'[^a-zA-Z0-9_]+')
00262 QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)')
00263 
00264 
00265 def quoteWikinameFS(wikiname, charset=config.charset):
00266     """ Return file system representation of a Unicode WikiName.
00267 
00268     Warning: will raise UnicodeError if wikiname can not be encoded using
00269     charset. The default value of config.charset, 'utf-8' can encode any
00270     character.
00271 
00272     @param wikiname: Unicode string possibly containing non-ascii characters
00273     @param charset: charset to encode string
00274     @rtype: string
00275     @return: quoted name, safe for any file system
00276     """
00277     filename = wikiname.encode(charset)
00278 
00279     quoted = []
00280     location = 0
00281     for needle in UNSAFE.finditer(filename):
00282         # append leading safe stuff
00283         quoted.append(filename[location:needle.start()])
00284         location = needle.end()
00285         # Quote and append unsafe stuff
00286         quoted.append('(')
00287         for character in needle.group():
00288             quoted.append('%02x' % ord(character))
00289         quoted.append(')')
00290 
00291     # append rest of string
00292     quoted.append(filename[location:])
00293     return ''.join(quoted)
00294 
00295 
00296 def unquoteWikiname(filename, charsets=[config.charset]):
00297     """ Return Unicode WikiName from quoted file name.
00298 
00299     We raise an InvalidFileNameError if we find an invalid name, so the
00300     wiki could alarm the admin or suggest the user to rename a page.
00301     Invalid file names should never happen in normal use, but are rather
00302     cheap to find.
00303 
00304     This function should be used only to unquote file names, not page
00305     names we receive from the user. These are handled in request by
00306     urllib.unquote, decodePagename and normalizePagename.
00307 
00308     Todo: search clients of unquoteWikiname and check for exceptions.
00309 
00310     @param filename: string using charset and possibly quoted parts
00311     @param charsets: list of charsets used by string
00312     @rtype: Unicode String
00313     @return: WikiName
00314     """
00315     ### Temporary fix start ###
00316     # From some places we get called with Unicode strings
00317     if isinstance(filename, type(u'')):
00318         filename = filename.encode(config.charset)
00319     ### Temporary fix end ###
00320 
00321     parts = []
00322     start = 0
00323     for needle in QUOTED.finditer(filename):
00324         # append leading unquoted stuff
00325         parts.append(filename[start:needle.start()])
00326         start = needle.end()
00327         # Append quoted stuff
00328         group = needle.group(1)
00329         # Filter invalid filenames
00330         if (len(group) % 2 != 0):
00331             raise InvalidFileNameError(filename)
00332         try:
00333             for i in range(0, len(group), 2):
00334                 byte = group[i:i+2]
00335                 character = chr(int(byte, 16))
00336                 parts.append(character)
00337         except ValueError:
00338             # byte not in hex, e.g 'xy'
00339             raise InvalidFileNameError(filename)
00340 
00341     # append rest of string
00342     if start == 0:
00343         wikiname = filename
00344     else:
00345         parts.append(filename[start:len(filename)])
00346         wikiname = ''.join(parts)
00347 
00348     # FIXME: This looks wrong, because at this stage "()" can be both errors
00349     # like open "(" without close ")", or unquoted valid characters in the file name.
00350     # Filter invalid filenames. Any left (xx) must be invalid
00351     #if '(' in wikiname or ')' in wikiname:
00352     #    raise InvalidFileNameError(filename)
00353 
00354     wikiname = decodeUserInput(wikiname, charsets)
00355     return wikiname
00356 
00357 # time scaling
00358 def timestamp2version(ts):
00359     """ Convert UNIX timestamp (may be float or int) to our version
00360         (long) int.
00361         We don't want to use floats, so we just scale by 1e6 to get
00362         an integer in usecs.
00363     """
00364     return long(ts*1000000L) # has to be long for py 2.2.x
00365 
00366 def version2timestamp(v):
00367     """ Convert version number to UNIX timestamp (float).
00368         This must ONLY be used for display purposes.
00369     """
00370     return v / 1000000.0
00371 
00372 
00373 # This is the list of meta attribute names to be treated as integers.
00374 # IMPORTANT: do not use any meta attribute names with "-" (or any other chars
00375 # invalid in python attribute names), use e.g. _ instead.
00376 INTEGER_METAS = ['current', 'revision', # for page storage (moin 2.0)
00377                  'data_format_revision', # for data_dir format spec (use by mig scripts)
00378                 ]
00379 
00380 class MetaDict(dict):
00381     """ store meta informations as a dict.
00382     """
00383     def __init__(self, metafilename, cache_directory):
00384         """ create a MetaDict from metafilename """
00385         dict.__init__(self)
00386         self.metafilename = metafilename
00387         self.dirty = False
00388         lock_dir = os.path.join(cache_directory, '__metalock__')
00389         self.rlock = lock.ReadLock(lock_dir, 60.0)
00390         self.wlock = lock.WriteLock(lock_dir, 60.0)
00391 
00392         if not self.rlock.acquire(3.0):
00393             raise EnvironmentError("Could not lock in MetaDict")
00394         try:
00395             self._get_meta()
00396         finally:
00397             self.rlock.release()
00398 
00399     def _get_meta(self):
00400         """ get the meta dict from an arbitrary filename.
00401             does not keep state, does uncached, direct disk access.
00402             @param metafilename: the name of the file to read
00403             @return: dict with all values or {} if empty or error
00404         """
00405 
00406         try:
00407             metafile = codecs.open(self.metafilename, "r", "utf-8")
00408             meta = metafile.read() # this is much faster than the file's line-by-line iterator
00409             metafile.close()
00410         except IOError:
00411             meta = u''
00412         for line in meta.splitlines():
00413             key, value = line.split(':', 1)
00414             value = value.strip()
00415             if key in INTEGER_METAS:
00416                 value = int(value)
00417             dict.__setitem__(self, key, value)
00418 
00419     def _put_meta(self):
00420         """ put the meta dict into an arbitrary filename.
00421             does not keep or modify state, does uncached, direct disk access.
00422             @param metafilename: the name of the file to write
00423             @param metadata: dict of the data to write to the file
00424         """
00425         meta = []
00426         for key, value in self.items():
00427             if key in INTEGER_METAS:
00428                 value = str(value)
00429             meta.append("%s: %s" % (key, value))
00430         meta = '\r\n'.join(meta)
00431 
00432         metafile = codecs.open(self.metafilename, "w", "utf-8")
00433         metafile.write(meta)
00434         metafile.close()
00435         self.dirty = False
00436 
00437     def sync(self, mtime_usecs=None):
00438         """ No-Op except for that parameter """
00439         if not mtime_usecs is None:
00440             self.__setitem__('mtime', str(mtime_usecs))
00441         # otherwise no-op
00442 
00443     def __getitem__(self, key):
00444         """ We don't care for cache coherency here. """
00445         return dict.__getitem__(self, key)
00446 
00447     def __setitem__(self, key, value):
00448         """ Sets a dictionary entry. """
00449         if not self.wlock.acquire(5.0):
00450             raise EnvironmentError("Could not lock in MetaDict")
00451         try:
00452             self._get_meta() # refresh cache
00453             try:
00454                 oldvalue = dict.__getitem__(self, key)
00455             except KeyError:
00456                 oldvalue = None
00457             if value != oldvalue:
00458                 dict.__setitem__(self, key, value)
00459                 self._put_meta() # sync cache
00460         finally:
00461             self.wlock.release()
00462 
00463 
00464 # Quoting of wiki names, file names, etc. (in the wiki markup) -----------------------------------
00465 
00466 QUOTE_CHARS = u"'\""
00467 
00468 def quoteName(name):
00469     """ put quotes around a given name """
00470     for quote_char in QUOTE_CHARS:
00471         if quote_char not in name:
00472             return u"%s%s%s" % (quote_char, name, quote_char)
00473     else:
00474         return name # XXX we need to be able to escape the quote char for worst case
00475 
00476 def unquoteName(name):
00477     """ if there are quotes around the name, strip them """
00478     for quote_char in QUOTE_CHARS:
00479         if quote_char == name[0] == name[-1]:
00480             return name[1:-1]
00481     else:
00482         return name
00483 
00484 #############################################################################
00485 ### InterWiki
00486 #############################################################################
00487 INTERWIKI_PAGE = "InterWikiMap"
00488 
00489 def generate_file_list(request):
00490     """ generates a list of all files. for internal use. """
00491 
00492     # order is important here, the local intermap file takes
00493     # precedence over the shared one, and is thus read AFTER
00494     # the shared one
00495     intermap_files = request.cfg.shared_intermap
00496     if not isinstance(intermap_files, list):
00497         intermap_files = [intermap_files]
00498     else:
00499         intermap_files = intermap_files[:]
00500     intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt"))
00501     request.cfg.shared_intermap_files = [filename for filename in intermap_files
00502                                          if filename and os.path.isfile(filename)]
00503 
00504 
00505 def get_max_mtime(file_list, page):
00506     """ Returns the highest modification time of the files in file_list and the
00507     page page. """
00508     timestamps = [os.stat(filename).st_mtime for filename in file_list]
00509     if page.exists():
00510         # exists() is cached and thus cheaper than mtime_usecs()
00511         timestamps.append(version2timestamp(page.mtime_usecs()))
00512     return max(timestamps)
00513 
00514 
00515 def load_wikimap(request):
00516     """ load interwiki map (once, and only on demand) """
00517     from MoinMoin.Page import Page
00518 
00519     now = int(time.time())
00520     if getattr(request.cfg, "shared_intermap_files", None) is None:
00521         generate_file_list(request)
00522 
00523     try:
00524         _interwiki_list = request.cfg.cache.interwiki_list
00525         old_mtime = request.cfg.cache.interwiki_mtime
00526         if request.cfg.cache.interwiki_ts + (1*60) < now: # 1 minutes caching time
00527             max_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
00528             if max_mtime > old_mtime:
00529                 raise AttributeError # refresh cache
00530             else:
00531                 request.cfg.cache.interwiki_ts = now
00532     except AttributeError:
00533         _interwiki_list = {}
00534         lines = []
00535 
00536         for filename in request.cfg.shared_intermap_files:
00537             f = open(filename, "r")
00538             lines.extend(f.readlines())
00539             f.close()
00540 
00541         # add the contents of the InterWikiMap page
00542         lines += Page(request, INTERWIKI_PAGE).get_raw_body().splitlines()
00543 
00544         for line in lines:
00545             if not line or line[0] == '#': continue
00546             try:
00547                 line = "%s %s/InterWiki" % (line, request.script_root)
00548                 wikitag, urlprefix, dummy = line.split(None, 2)
00549             except ValueError:
00550                 pass
00551             else:
00552                 _interwiki_list[wikitag] = urlprefix
00553 
00554         del lines
00555 
00556         # add own wiki as "Self" and by its configured name
00557         _interwiki_list['Self'] = request.script_root + '/'
00558         if request.cfg.interwikiname:
00559             _interwiki_list[request.cfg.interwikiname] = request.script_root + '/'
00560 
00561         # save for later
00562         request.cfg.cache.interwiki_list = _interwiki_list
00563         request.cfg.cache.interwiki_ts = now
00564         request.cfg.cache.interwiki_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
00565 
00566     return _interwiki_list
00567 
00568 def split_wiki(wikiurl):
00569     """ Split a wiki url, e.g:
00570 
00571     'MoinMoin:FrontPage' -> "MoinMoin", "FrontPage", ""
00572     'FrontPage' -> "Self", "FrontPage", ""
00573     'MoinMoin:"Page with blanks" link title' -> "MoinMoin", "Page with blanks", "link title"
00574 
00575     can also be used for:
00576 
00577     'attachment:"filename with blanks.txt" other title' -> "attachment", "filename with blanks.txt", "other title"
00578 
00579     @param wikiurl: the url to split
00580     @rtype: tuple
00581     @return: (wikiname, pagename, linktext)
00582     """
00583     try:
00584         wikiname, rest = wikiurl.split(":", 1) # e.g. MoinMoin:FrontPage
00585     except ValueError:
00586         try:
00587             wikiname, rest = wikiurl.split("/", 1) # for what is this used?
00588         except ValueError:
00589             wikiname, rest = 'Self', wikiurl
00590     if rest:
00591         first_char = rest[0]
00592         if first_char in QUOTE_CHARS: # quoted pagename
00593             pagename_linktext = rest[1:].split(first_char, 1)
00594         else: # not quoted, split on whitespace
00595             pagename_linktext = rest.split(None, 1)
00596     else:
00597         pagename_linktext = "", ""
00598     if len(pagename_linktext) == 1:
00599         pagename, linktext = pagename_linktext[0], ""
00600     else:
00601         pagename, linktext = pagename_linktext
00602     linktext = linktext.strip()
00603     return wikiname, pagename, linktext
00604 
00605 def resolve_wiki(request, wikiurl):
00606     """ Resolve an interwiki link.
00607 
00608     @param request: the request object
00609     @param wikiurl: the InterWiki:PageName link
00610     @rtype: tuple
00611     @return: (wikitag, wikiurl, wikitail, err)
00612     """
00613     _interwiki_list = load_wikimap(request)
00614     wikiname, pagename, linktext = split_wiki(wikiurl)
00615     if _interwiki_list.has_key(wikiname):
00616         return (wikiname, _interwiki_list[wikiname], pagename, False)
00617     else:
00618         return (wikiname, request.script_root, "/InterWiki", True)
00619 
00620 def join_wiki(wikiurl, wikitail):
00621     """
00622     Add a (url_quoted) page name to an interwiki url.
00623 
00624     Note: We can't know what kind of URL quoting a remote wiki expects.
00625           We just use a utf-8 encoded string with standard URL quoting.
00626 
00627     @param wikiurl: wiki url, maybe including a $PAGE placeholder
00628     @param wikitail: page name
00629     @rtype: string
00630     @return: generated URL of the page in the other wiki
00631     """
00632     wikitail = url_quote(wikitail)
00633     if '$PAGE' in wikiurl:
00634         return wikiurl.replace('$PAGE', wikitail)
00635     else:
00636         return wikiurl + wikitail
00637 
00638 
00639 #############################################################################
00640 ### Page types (based on page names)
00641 #############################################################################
00642 
00643 def isSystemPage(request, pagename):
00644     """ Is this a system page? Uses AllSystemPagesGroup internally.
00645 
00646     @param request: the request object
00647     @param pagename: the page name
00648     @rtype: bool
00649     @return: true if page is a system page
00650     """
00651     return (pagename in request.groups.get(u'SystemPagesGroup', []) or
00652         isTemplatePage(request, pagename))
00653 
00654 
00655 def isTemplatePage(request, pagename):
00656     """ Is this a template page?
00657 
00658     @param pagename: the page name
00659     @rtype: bool
00660     @return: true if page is a template page
00661     """
00662     return request.cfg.cache.page_template_regex.search(pagename) is not None
00663 
00664 
00665 def isGroupPage(request, pagename):
00666     """ Is this a name of group page?
00667 
00668     @param pagename: the page name
00669     @rtype: bool
00670     @return: true if page is a form page
00671     """
00672     return request.cfg.cache.page_group_regex.search(pagename) is not None
00673 
00674 
00675 def filterCategoryPages(request, pagelist):
00676     """ Return category pages in pagelist
00677 
00678     WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use
00679     getPageList with a filter function.
00680 
00681     If you pass a list with a single pagename, either that is returned
00682     or an empty list, thus you can use this function like a `isCategoryPage`
00683     one.
00684 
00685     @param pagelist: a list of pages
00686     @rtype: list
00687     @return: only the category pages of pagelist
00688     """
00689     func = request.cfg.cache.page_category_regex.search
00690     return filter(func, pagelist)
00691 
00692 
00693 def getLocalizedPage(request, pagename): # was: getSysPage
00694     """ Get a system page according to user settings and available translations.
00695 
00696     We include some special treatment for the case that <pagename> is the
00697     currently rendered page, as this is the case for some pages used very
00698     often, like FrontPage, RecentChanges etc. - in that case we reuse the
00699     already existing page object instead creating a new one.
00700 
00701     @param request: the request object
00702     @param pagename: the name of the page
00703     @rtype: Page object
00704     @return: the page object of that system page, using a translated page,
00705              if it exists
00706     """
00707     from MoinMoin.Page import Page
00708     i18n_name = request.getText(pagename, formatted=False)
00709     pageobj = None
00710     if i18n_name != pagename:
00711         if request.page and i18n_name == request.page.page_name:
00712             # do not create new object for current page
00713             i18n_page = request.page
00714             if i18n_page.exists():
00715                 pageobj = i18n_page
00716         else:
00717             i18n_page = Page(request, i18n_name)
00718             if i18n_page.exists():
00719                 pageobj = i18n_page
00720 
00721     # if we failed getting a translated version of <pagename>,
00722     # we fall back to english
00723     if not pageobj:
00724         if request.page and pagename == request.page.page_name:
00725             # do not create new object for current page
00726             pageobj = request.page
00727         else:
00728             pageobj = Page(request, pagename)
00729     return pageobj
00730 
00731 
00732 def getFrontPage(request):
00733     """ Convenience function to get localized front page
00734 
00735     @param request: current request
00736     @rtype: Page object
00737     @return localized page_front_page, if there is a translation
00738     """
00739     return getLocalizedPage(request, request.cfg.page_front_page)
00740 
00741 
00742 def getHomePage(request, username=None):
00743     """
00744     Get a user's homepage, or return None for anon users and
00745     those who have not created a homepage.
00746 
00747     DEPRECATED - try to use getInterwikiHomePage (see below)
00748 
00749     @param request: the request object
00750     @param username: the user's name
00751     @rtype: Page
00752     @return: user's homepage object - or None
00753     """
00754     from MoinMoin.Page import Page
00755     # default to current user
00756     if username is None and request.user.valid:
00757         username = request.user.name
00758 
00759     # known user?
00760     if username:
00761         # Return home page
00762         page = Page(request, username)
00763         if page.exists():
00764             return page
00765 
00766     return None
00767 
00768 
00769 def getInterwikiHomePage(request, username=None):
00770     """
00771     Get a user's homepage.
00772 
00773     cfg.user_homewiki influences behaviour of this:
00774     'Self' does mean we store user homepage in THIS wiki.
00775     When set to our own interwikiname, it behaves like with 'Self'.
00776 
00777     'SomeOtherWiki' means we store user homepages in another wiki.
00778 
00779     @param request: the request object
00780     @param username: the user's name
00781     @rtype: tuple (or None for anon users)
00782     @return: (wikiname, pagename)
00783     """
00784     # default to current user
00785     if username is None and request.user.valid:
00786         username = request.user.name
00787     if not username:
00788         return None # anon user
00789 
00790     homewiki = request.cfg.user_homewiki
00791     if homewiki == request.cfg.interwikiname:
00792         homewiki = 'Self'
00793 
00794     return homewiki, username
00795 
00796 
00797 def AbsPageName(request, context, pagename):
00798     """
00799     Return the absolute pagename for a (possibly) relative pagename.
00800 
00801     @param context: name of the page where "pagename" appears on
00802     @param pagename: the (possibly relative) page name
00803     @rtype: string
00804     @return: the absolute page name
00805     """
00806     if pagename.startswith(PARENT_PREFIX):
00807         pagename = '/'.join(filter(None, context.split('/')[:-1] + [pagename[PARENT_PREFIX_LEN:]]))
00808     elif pagename.startswith(CHILD_PREFIX):
00809         pagename = context + '/' + pagename[CHILD_PREFIX_LEN:]
00810     return pagename
00811 
00812 def pagelinkmarkup(pagename):
00813     """ return markup that can be used as link to page <pagename> """
00814     from MoinMoin.parser.text_moin_wiki import Parser
00815     if re.match(Parser.word_rule + "$", pagename):
00816         return pagename
00817     else:
00818         return u'["%s"]' % pagename # XXX use quoteName(pagename) later
00819 
00820 #############################################################################
00821 ### mimetype support
00822 #############################################################################
00823 import mimetypes
00824 
00825 MIMETYPES_MORE = {
00826  # OpenOffice 2.x & other open document stuff
00827  '.odt': 'application/vnd.oasis.opendocument.text',
00828  '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
00829  '.odp': 'application/vnd.oasis.opendocument.presentation',
00830  '.odg': 'application/vnd.oasis.opendocument.graphics',
00831  '.odc': 'application/vnd.oasis.opendocument.chart',
00832  '.odf': 'application/vnd.oasis.opendocument.formula',
00833  '.odb': 'application/vnd.oasis.opendocument.database',
00834  '.odi': 'application/vnd.oasis.opendocument.image',
00835  '.odm': 'application/vnd.oasis.opendocument.text-master',
00836  '.ott': 'application/vnd.oasis.opendocument.text-template',
00837  '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
00838  '.otp': 'application/vnd.oasis.opendocument.presentation-template',
00839  '.otg': 'application/vnd.oasis.opendocument.graphics-template',
00840 }
00841 [mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
00842 
00843 MIMETYPES_sanitize_mapping = {
00844     # this stuff is text, but got application/* for unknown reasons
00845     ('application', 'docbook+xml'): ('text', 'docbook'),
00846     ('application', 'x-latex'): ('text', 'latex'),
00847     ('application', 'x-tex'): ('text', 'tex'),
00848     ('application', 'javascript'): ('text', 'javascript'),
00849 }
00850 
00851 MIMETYPES_spoil_mapping = {} # inverse mapping of above
00852 for key, value in MIMETYPES_sanitize_mapping.items():
00853     MIMETYPES_spoil_mapping[value] = key
00854 
00855 
00856 class MimeType(object):
00857     """ represents a mimetype like text/plain """
00858 
00859     def __init__(self, mimestr=None, filename=None):
00860         self.major = self.minor = None # sanitized mime type and subtype
00861         self.params = {} # parameters like "charset" or others
00862         self.charset = None # this stays None until we know for sure!
00863         self.raw_mimestr = mimestr
00864 
00865         if mimestr:
00866             self.parse_mimetype(mimestr)
00867         elif filename:
00868             self.parse_filename(filename)
00869 
00870     def parse_filename(self, filename):
00871         mtype, encoding = mimetypes.guess_type(filename)
00872         if mtype is None:
00873             mtype = 'application/octet-stream'
00874         self.parse_mimetype(mtype)
00875 
00876     def parse_mimetype(self, mimestr):
00877         """ take a string like used in content-type and parse it into components,
00878             alternatively it also can process some abbreviated string like "wiki"
00879         """
00880         parameters = mimestr.split(";")
00881         parameters = [p.strip() for p in parameters]
00882         mimetype, parameters = parameters[0], parameters[1:]
00883         mimetype = mimetype.split('/')
00884         if len(mimetype) >= 2:
00885             major, minor = mimetype[:2] # we just ignore more than 2 parts
00886         else:
00887             major, minor = self.parse_format(mimetype[0])
00888         self.major = major.lower()
00889         self.minor = minor.lower()
00890         for param in parameters:
00891             key, value = param.split('=')
00892             if value[0] == '"' and value[-1] == '"': # remove quotes
00893                 value = value[1:-1]
00894             self.params[key.lower()] = value
00895         if self.params.has_key('charset'):
00896             self.charset = self.params['charset'].lower()
00897         self.sanitize()
00898 
00899     def parse_format(self, format):
00900         """ maps from what we currently use on-page in a #format xxx processing
00901             instruction to a sanitized mimetype major, minor tuple.
00902             can also be user later for easier entry by the user, so he can just
00903             type "wiki" instead of "text/moin-wiki".
00904         """
00905         format = format.lower()
00906         if format in ('plain', 'csv', 'rst', 'docbook', 'latex', 'tex', 'html', 'css',
00907                       'xml', 'python', 'perl', 'php', 'ruby', 'javascript',
00908                       'cplusplus', 'java', 'pascal', 'diff', 'gettext', 'xslt', ):
00909             mimetype = 'text', format
00910         else:
00911             mapping = {
00912                 'wiki': ('text', 'moin-wiki'),
00913                 'irc': ('text', 'irssi'),
00914             }
00915             try:
00916                 mimetype = mapping[format]
00917             except KeyError:
00918                 mimetype = 'text', 'x-%s' % format
00919         return mimetype
00920 
00921     def sanitize(self):
00922         """ convert to some representation that makes sense - this is not necessarily
00923             conformant to /etc/mime.types or IANA listing, but if something is
00924             readable text, we will return some text/* mimetype, not application/*,
00925             because we need text/plain as fallback and not application/octet-stream.
00926         """
00927         self.major, self.minor = MIMETYPES_sanitize_mapping.get((self.major, self.minor), (self.major, self.minor))
00928 
00929     def spoil(self):
00930         """ this returns something conformant to /etc/mime.type or IANA as a string,
00931             kind of inverse operation of sanitize(), but doesn't change self
00932         """
00933         major, minor = MIMETYPES_spoil_mapping.get((self.major, self.minor), (self.major, self.minor))
00934         return self.content_type(major, minor)
00935 
00936     def content_type(self, major=None, minor=None, charset=None, params=None):
00937         """ return a string suitable for Content-Type header
00938         """
00939         major = major or self.major
00940         minor = minor or self.minor
00941         params = params or self.params or {}
00942         if major == 'text':
00943             charset = charset or self.charset or params.get('charset', config.charset)
00944             params['charset'] = charset
00945         mimestr = "%s/%s" % (major, minor)
00946         params = ['%s="%s"' % (key.lower(), value) for key, value in params.items()]
00947         params.insert(0, mimestr)
00948         return "; ".join(params)
00949 
00950     def mime_type(self):
00951         """ return a string major/minor only, no params """
00952         return "%s/%s" % (self.major, self.minor)
00953 
00954     def module_name(self):
00955         """ convert this mimetype to a string useable as python module name,
00956             we yield the exact module name first and then proceed to shorter
00957             module names (useful for falling back to them, if the more special
00958             module is not found) - e.g. first "text_python", next "text".
00959             Finally, we yield "application_octet_stream" as the most general
00960             mimetype we have.
00961             Hint: the fallback handler module for text/* should be implemented
00962                   in module "text" (not "text_plain")
00963         """
00964         mimetype = self.mime_type()
00965         modname = mimetype.replace("/", "_").replace("-", "_").replace(".", "_")
00966         fragments = modname.split('_')
00967         for length in range(len(fragments), 1, -1):
00968             yield "_".join(fragments[:length])
00969         yield self.raw_mimestr
00970         yield fragments[0]
00971         yield "application_octet_stream"
00972 
00973 
00974 #############################################################################
00975 ### Plugins
00976 #############################################################################
00977 
00978 class PluginError(Exception):
00979     """ Base class for plugin errors """
00980 
00981 class PluginMissingError(PluginError):
00982     """ Raised when a plugin is not found """
00983 
00984 class PluginAttributeError(PluginError):
00985     """ Raised when plugin does not contain an attribtue """
00986 
00987 
00988 def importPlugin(cfg, kind, name, function="execute"):
00989     """ Import wiki or builtin plugin
00990 
00991     Returns function from a plugin module name. If name can not be
00992     imported, raise PluginMissingError. If function is missing, raise
00993     PluginAttributeError.
00994 
00995     kind may be one of 'action', 'formatter', 'macro', 'parser' or any other
00996     directory that exist in MoinMoin or data/plugin.
00997 
00998     Wiki plugins will always override builtin plugins. If you want
00999     specific plugin, use either importWikiPlugin or importBuiltinPlugin
01000     directly.
01001 
01002     @param cfg: wiki config instance
01003     @param kind: what kind of module we want to import
01004     @param name: the name of the module
01005     @param function: the function name
01006     @rtype: any object
01007     @return: "function" of module "name" of kind "kind", or None
01008     """
01009     try:
01010         return importWikiPlugin(cfg, kind, name, function)
01011     except PluginMissingError:
01012         return importBuiltinPlugin(kind, name, function)
01013 
01014 
01015 def importWikiPlugin(cfg, kind, name, function="execute"):
01016     """ Import plugin from the wiki data directory
01017 
01018     See importPlugin docstring.
01019     """
01020     if not name in wikiPlugins(kind, cfg):
01021         raise PluginMissingError
01022     moduleName = '%s.plugin.%s.%s' % (cfg.siteid, kind, name)
01023     return importNameFromPlugin(moduleName, function)
01024 
01025 
01026 def importBuiltinPlugin(kind, name, function="execute"):
01027     """ Import builtin plugin from MoinMoin package
01028 
01029     See importPlugin docstring.
01030     """
01031     if not name in builtinPlugins(kind):
01032         raise PluginMissingError
01033     moduleName = 'MoinMoin.%s.%s' % (kind, name)
01034     return importNameFromPlugin(moduleName, function)
01035 
01036 
01037 def importNameFromPlugin(moduleName, name):
01038     """ Return name from plugin module
01039 
01040     Raise PluginAttributeError if name does not exists.
01041     """
01042     module = __import__(moduleName, globals(), {}, [name])
01043     try:
01044         return getattr(module, name)
01045     except AttributeError:
01046         raise PluginAttributeError
01047 
01048 
01049 def builtinPlugins(kind):
01050     """ Gets a list of modules in MoinMoin.'kind'
01051 
01052     @param kind: what kind of modules we look for
01053     @rtype: list
01054     @return: module names
01055     """
01056     modulename = "MoinMoin." + kind
01057     return pysupport.importName(modulename, "modules")
01058 
01059 
01060 def wikiPlugins(kind, cfg):
01061     """ Gets a list of modules in data/plugin/'kind'
01062 
01063     Require valid plugin directory. e.g missing 'parser' directory or
01064     missing '__init__.py' file will raise errors.
01065 
01066     @param kind: what kind of modules we look for
01067     @rtype: list
01068     @return: module names
01069     """
01070     # Wiki plugins are located in wikiconfig.plugin module
01071     modulename = '%s.plugin.%s' % (cfg.siteid, kind)
01072     return pysupport.importName(modulename, "modules")
01073 
01074 
01075 def getPlugins(kind, cfg):
01076     """ Gets a list of plugin names of kind
01077 
01078     @param kind: what kind of modules we look for
01079     @rtype: list
01080     @return: module names
01081     """
01082     # Copy names from builtin plugins - so we dont destroy the value
01083     all_plugins = builtinPlugins(kind)[:]
01084 
01085     # Add extension plugins without duplicates
01086     for plugin in wikiPlugins(kind, cfg):
01087         if plugin not in all_plugins:
01088             all_plugins.append(plugin)
01089 
01090     return all_plugins
01091 
01092 
01093 def searchAndImportPlugin(cfg, type, name, what=None):
01094     type2classname = {"parser": "Parser",
01095                       "formatter": "Formatter",
01096     }
01097     if what is None:
01098         what = type2classname[type]
01099     mt = MimeType(name)
01100     plugin = None
01101     for module_name in mt.module_name():
01102         try:
01103             plugin = importPlugin(cfg, type, module_name, what)
01104             break
01105         except PluginMissingError:
01106             pass
01107     else:
01108         raise PluginMissingError("Plugin not found!")
01109     return plugin
01110 
01111 
01112 #############################################################################
01113 ### Parsers
01114 #############################################################################
01115 
01116 def getParserForExtension(cfg, extension):
01117     """
01118     Returns the Parser class of the parser fit to handle a file
01119     with the given extension. The extension should be in the same
01120     format as os.path.splitext returns it (i.e. with the dot).
01121     Returns None if no parser willing to handle is found.
01122     The dict of extensions is cached in the config object.
01123 
01124     @param cfg: the Config instance for the wiki in question
01125     @param extension: the filename extension including the dot
01126     @rtype: class, None
01127     @returns: the parser class or None
01128     """
01129     if not hasattr(cfg.cache, 'EXT_TO_PARSER'):
01130         etp, etd = {}, None
01131         for pname in getPlugins('parser', cfg):
01132             try:
01133                 Parser = importPlugin(cfg, 'parser', pname, 'Parser')
01134             except PluginMissingError:
01135                 continue
01136             if hasattr(Parser, 'extensions'):
01137                 exts = Parser.extensions
01138                 if isinstance(exts, list):
01139                     for ext in Parser.extensions:
01140                         etp[ext] = Parser
01141                 elif str(exts) == '*':
01142                     etd = Parser
01143         cfg.cache.EXT_TO_PARSER = etp
01144         cfg.cache.EXT_TO_PARSER_DEFAULT = etd
01145 
01146     return cfg.cache.EXT_TO_PARSER.get(extension, cfg.cache.EXT_TO_PARSER_DEFAULT)
01147 
01148 
01149 #############################################################################
01150 ### Parameter parsing
01151 #############################################################################
01152 
01153 def parseAttributes(request, attrstring, endtoken=None, extension=None):
01154     """
01155     Parse a list of attributes and return a dict plus a possible
01156     error message.
01157     If extension is passed, it has to be a callable that returns
01158     a tuple (found_flag, msg). found_flag is whether it did find and process
01159     something, msg is '' when all was OK or any other string to return an error
01160     message.
01161 
01162     @param request: the request object
01163     @param attrstring: string containing the attributes to be parsed
01164     @param endtoken: token terminating parsing
01165     @param extension: extension function -
01166                       gets called with the current token, the parser and the dict
01167     @rtype: dict, msg
01168     @return: a dict plus a possible error message
01169     """
01170     import shlex, StringIO
01171 
01172     _ = request.getText
01173 
01174     parser = shlex.shlex(StringIO.StringIO(attrstring))
01175     parser.commenters = ''
01176     msg = None
01177     attrs = {}
01178 
01179     while not msg:
01180         try:
01181             key = parser.get_token()
01182         except ValueError, err:
01183             msg = str(err)
01184             break
01185         if not key: break
01186         if endtoken and key == endtoken: break
01187 
01188         # call extension function with the current token, the parser, and the dict
01189         if extension:
01190             found_flag, msg = extension(key, parser, attrs)
01191             #request.log("%r = extension(%r, parser, %r)" % (msg, key, attrs))
01192             if found_flag:
01193                 continue
01194             elif msg:
01195                 break
01196             #else (we found nothing, but also didn't have an error msg) we just continue below:
01197 
01198         try:
01199             eq = parser.get_token()
01200         except ValueError, err:
01201             msg = str(err)
01202             break
01203         if eq != "=":
01204             msg = _('Expected "=" to follow "%(token)s"') % {'token': key}
01205             break
01206 
01207         try:
01208             val = parser.get_token()
01209         except ValueError, err:
01210             msg = str(err)
01211             break
01212         if not val:
01213             msg = _('Expected a value for key "%(token)s"') % {'token': key}
01214             break
01215 
01216         key = escape(key) # make sure nobody cheats
01217 
01218         # safely escape and quote value
01219         if val[0] in ["'", '"']:
01220             val = escape(val)
01221         else:
01222             val = '"%s"' % escape(val, 1)
01223 
01224         attrs[key.lower()] = val
01225 
01226     return attrs, msg or ''
01227 
01228 
01229 class ParameterParser:
01230     """ MoinMoin macro parameter parser
01231 
01232         Parses a given parameter string, separates the individual parameters
01233         and detects their type.
01234 
01235         Possible parameter types are:
01236 
01237         Name      | short  | example
01238         ----------------------------
01239          Integer  | i      | -374
01240          Float    | f      | 234.234 23.345E-23
01241          String   | s      | 'Stri\'ng'
01242          Boolean  | b      | 0 1 True false
01243          Name     |        | case_sensitive | converted to string
01244 
01245         So say you want to parse three things, name, age and if the
01246         person is male or not:
01247 
01248         The pattern will be: %(name)s%(age)i%(male)b
01249 
01250         As a result, the returned dict will put the first value into
01251         male, second into age etc. If some argument is missing, it will
01252         get None as its value. This also means that all the identifiers
01253         in the pattern will exist in the dict, they will just have the
01254         value None if they were not specified by the caller.
01255 
01256         So if we call it with the parameters as follows:
01257             ("John Smith", 18)
01258         this will result in the following dict:
01259             {"name": "John Smith", "age": 18, "male": None}
01260 
01261         Another way of calling would be:
01262             ("John Smith", male=True)
01263         this will result in the following dict:
01264             {"name": "John Smith", "age": None, "male": True}
01265 
01266         @copyright: 2004 by Florian Festi,
01267                     2006 by Mikko Virkkilä
01268         @license: GNU GPL, see COPYING for details.
01269     """
01270 
01271     def __init__(self, pattern):
01272         #parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
01273         name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)"
01274         int_re = r"(?P<int>-?\d+)"
01275         bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))"
01276         float_re = r"(?P<float>-?\d+\.\d+([eE][+-]?\d+)?)"
01277         string_re = (r"(?P<string>('([^']|(\'))*?')|" +
01278                                 r'("([^"]|(\"))*?"))')
01279         name_re = name % "name"
01280         name_param_re = name % "name_param"
01281 
01282         param_re = r"\s*(\s*%s\s*=\s*)?(%s|%s|%s|%s|%s)\s*(,|$)" % (
01283                    name_re, float_re, int_re, bool_re, string_re, name_param_re)
01284         self.param_re = re.compile(param_re, re.U)
01285         self._parse_pattern(pattern)
01286 
01287     def _parse_pattern(self, pattern):
01288         param_re = r"(%(?P<name>\(.*?\))?(?P<type>[ibfs]{1,3}))|\|"
01289         i = 0
01290         # TODO: Optionals aren't checked.
01291         self.optional = []
01292         named = False
01293         self.param_list = []
01294         self.param_dict = {}
01295 
01296         for match in re.finditer(param_re, pattern):
01297             if match.group() == "|":
01298                 self.optional.append(i)
01299                 continue
01300             self.param_list.append(match.group('type'))
01301             if match.group('name'):
01302                 named = True
01303                 self.param_dict[match.group('name')[1:-1]] = i
01304             elif named:
01305                 raise ValueError, "Named parameter expected"
01306             i += 1
01307 
01308     def __str__(self):
01309         return "%s, %s, optional:%s" % (self.param_list, self.param_dict,
01310                                         self.optional)
01311 
01312     def parse_parameters(self, input):
01313         """
01314         (4, 2)
01315         """
01316         #Default list to "None"s
01317         parameter_list = [None] * len(self.param_list)
01318         parameter_dict = {}
01319         check_list = [0] * len(self.param_list)
01320 
01321         i = 0
01322         start = 0
01323         named = False
01324         while start < len(input):
01325             match = re.match(self.param_re, input[start:])
01326             if not match:
01327                 raise ValueError, "Misformatted value"
01328             start += match.end()
01329             value = None
01330             if match.group("int"):
01331                 value = int(match.group("int"))
01332                 type = 'i'
01333             elif match.group("bool"):
01334                 value = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
01335                 type = 'b'
01336             elif match.group("float"):
01337                 value = float(match.group("float"))
01338                 type = 'f'
01339             elif match.group("string"):
01340                 value = match.group("string")[1:-1]
01341                 type = 's'
01342             elif match.group("name_param"):
01343                 value = match.group("name_param")
01344                 type = 'n'
01345             else:
01346                 value = None
01347 
01348             parameter_list.append(value)
01349             if match.group("name"):
01350                 if not self.param_dict.has_key(match.group("name")):
01351                     raise ValueError, "Unknown parameter name '%s'" % match.group("name")
01352                 nr = self.param_dict[match.group("name")]
01353                 if check_list[nr]:
01354                     #raise ValueError, "Parameter specified twice"
01355                     #TODO: Something saner that raising an exception. This is pretty good, since it ignores it.
01356                     pass
01357                 else:
01358                     check_list[nr] = 1
01359                 parameter_dict[match.group("name")] = value
01360                 parameter_list[nr] = value
01361                 named = True
01362             elif named:
01363                 raise ValueError, "Only named parameters allowed"
01364             else:
01365                 nr = i
01366                 parameter_list[nr] = value
01367 
01368             #Let's populate and map our dictionary to what's been found
01369             for name in self.param_dict.keys():
01370                 tmp = self.param_dict[name]
01371                 parameter_dict[name]=parameter_list[tmp]
01372 
01373             i += 1
01374 
01375         return parameter_list, parameter_dict
01376 
01377 
01378 """ never used:
01379     def _check_type(value, type, format):
01380         if type == 'n' and 's' in format: # n as s
01381             return value
01382 
01383         if type in format:
01384             return value # x -> x
01385 
01386         if type == 'i':
01387             if 'f' in format:
01388                 return float(value) # i -> f
01389             elif 'b' in format:
01390                 return value # i -> b
01391         elif type == 'f':
01392             if 'b' in format:
01393                 return value  # f -> b
01394         elif type == 's':
01395             if 'b' in format:
01396                 return value.lower() != 'false' # s-> b
01397 
01398         if 's' in format: # * -> s
01399             return str(value)
01400         else:
01401             pass # XXX error
01402 
01403 def main():
01404     pattern = "%i%sf%s%ifs%(a)s|%(b)s"
01405     param = ' 4,"DI\'NG", b=retry, a="DING"'
01406 
01407     #p_list, p_dict = parse_parameters(param)
01408 
01409     print 'Pattern :', pattern
01410     print 'Param :', param
01411 
01412     P = ParameterParser(pattern)
01413     print P
01414     print P.parse_parameters(param)
01415 
01416 
01417 if __name__=="__main__":
01418     main()
01419 """
01420 
01421 #############################################################################
01422 ### Misc
01423 #############################################################################
01424 def taintfilename(basename):
01425     """
01426     Make a filename that is supposed to be a plain name secure, i.e.
01427     remove any possible path components that compromise our system.
01428 
01429     @param basename: (possibly unsafe) filename
01430     @rtype: string
01431     @return: (safer) filename
01432     """
01433     for x in (os.pardir, ':', '/', '\\', '<', '>'):
01434         basename = basename.replace(x, '_')
01435 
01436     return basename
01437 
01438 
01439 def mapURL(request, url):
01440     """
01441     Map URLs according to 'cfg.url_mappings'.
01442 
01443     @param url: a URL
01444     @rtype: string
01445     @return: mapped URL
01446     """
01447     # check whether we have to map URLs
01448     if request.cfg.url_mappings:
01449         # check URL for the configured prefixes
01450         for prefix in request.cfg.url_mappings.keys():
01451             if url.startswith(prefix):
01452                 # substitute prefix with replacement value
01453                 return request.cfg.url_mappings[prefix] + url[len(prefix):]
01454 
01455     # return unchanged url
01456     return url
01457 
01458 
01459 def getUnicodeIndexGroup(name):
01460     """
01461     Return a group letter for `name`, which must be a unicode string.
01462     Currently supported: Hangul Syllables (U+AC00 - U+D7AF)
01463 
01464     @param name: a string
01465     @rtype: string
01466     @return: group letter or None
01467     """
01468     c = name[0]
01469     if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables
01470         return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588)
01471     else:
01472         return c.upper() # we put lower and upper case words into the same index group
01473 
01474 
01475 def isStrictWikiname(name, word_re=re.compile(ur"^(?:[%(u)s][%(l)s]+){2,}$" % {'u': config.chars_upper, 'l': config.chars_lower})):
01476     """
01477     Check whether this is NOT an extended name.
01478 
01479     @param name: the wikiname in question
01480     @rtype: bool
01481     @return: true if name matches the word_re
01482     """
01483     return word_re.match(name)
01484 
01485 
01486 def isPicture(url):
01487     """
01488     Is this a picture's url?
01489 
01490     @param url: the url in question
01491     @rtype: bool
01492     @return: true if url points to a picture
01493     """
01494     extpos = url.rfind(".")
01495     return extpos > 0 and url[extpos:].lower() in ['.gif', '.jpg', '.jpeg', '.png', '.bmp', '.ico', ]
01496 
01497 
01498 def link_tag(request, params, text=None, formatter=None, on=None, **kw):
01499     """ Create a link.
01500 
01501     TODO: cleanup css_class
01502 
01503     @param request: the request object
01504     @param params: parameter string appended to the URL after the scriptname/
01505     @param text: text / inner part of the <a>...</a> link - does NOT get
01506                  escaped, so you can give HTML here and it will be used verbatim
01507     @param formatter: the formatter object to use
01508     @param on: opening/closing tag only
01509     @keyword attrs: additional attrs (HTMLified string) (removed in 1.5.3)
01510     @rtype: string
01511     @return: formatted link tag
01512     """
01513     if formatter is None:
01514         formatter = request.html_formatter
01515     if kw.has_key('css_class'):
01516         css_class = kw['css_class']
01517         del kw['css_class'] # one time is enough
01518     else:
01519         css_class = None
01520     id = kw.get('id', None)
01521     name = kw.get('name', None)
01522     if text is None:
01523         text = params # default
01524     if formatter:
01525         url = "%s/%s" % (request.script_root, params)
01526         # formatter.url will escape the url part
01527         if on is not None:
01528             tag = formatter.url(on, url, css_class, **kw)
01529         else:
01530             tag = (formatter.url(1, url, css_class, **kw) +
01531                 formatter.rawHTML(text) +
01532                 formatter.url(0))
01533     else: # this shouldn't be used any more:
01534         if on is not None and not on:
01535             tag = '</a>'
01536         else:
01537             attrs = ''
01538             if css_class:
01539                 attrs += ' class="%s"' % css_class
01540             if id:
01541                 attrs += ' id="%s"' % id
01542             if name:
01543                 attrs += ' name="%s"' % name
01544             tag = '<a%s href="%s/%s">' % (attrs, request.script_root, params)
01545             if not on:
01546                 tag = "%s%s</a>" % (tag, text)
01547         request.log("Warning: wikiutil.link_tag called without formatter and without request.html_formatter. tag=%r" % (tag, ))
01548     return tag
01549 
01550 def containsConflictMarker(text):
01551     """ Returns true if there is a conflict marker in the text. """
01552     return "/!\\ '''Edit conflict" in text
01553 
01554 def pagediff(request, pagename1, rev1, pagename2, rev2, **kw):
01555     """
01556     Calculate the "diff" between two page contents.
01557 
01558     @param pagename1: name of first page
01559     @param rev1: revision of first page
01560     @param pagename2: name of second page
01561     @param rev2: revision of second page
01562     @keyword ignorews: if 1: ignore pure-whitespace changes.
01563     @rtype: list
01564     @return: lines of diff output
01565     """
01566     from MoinMoin.Page import Page
01567     from MoinMoin.util import diff_text
01568     lines1 = Page(request, pagename1, rev=rev1).getlines()
01569     lines2 = Page(request, pagename2, rev=rev2).getlines()
01570 
01571     lines = diff_text.diff(lines1, lines2, **kw)
01572     return lines
01573 
01574 
01575 ########################################################################
01576 ### Tickets - used by RenamePage and DeletePage
01577 ########################################################################
01578 
01579 def createTicket(request, tm=None):
01580     """Create a ticket using a site-specific secret (the config)"""
01581     from MoinMoin.support.python_compatibility import hash_new
01582     ticket = tm or "%010x" % time.time()
01583     digest = hash_new('sha1', ticket)
01584 
01585     varnames = ['data_dir', 'data_underlay_dir', 'language_default',
01586                 'mail_smarthost', 'mail_from', 'page_front_page',
01587                 'theme_default', 'sitename', 'logo_string',
01588                 'interwikiname', 'user_homewiki', 'acl_rights_before', ]
01589     for varname in varnames:
01590         var = getattr(request.cfg, varname, None)
01591         if isinstance(var, (str, unicode)):
01592             digest.update(repr(var))
01593 
01594     return "%s.%s" % (ticket, digest.hexdigest())
01595 
01596 
01597 def checkTicket(request, ticket):
01598     """Check validity of a previously created ticket"""
01599     try:
01600         timestamp_str = ticket.split('.')[0]
01601         timestamp = int(timestamp_str, 16)
01602     except ValueError:
01603         # invalid or empty ticket
01604         return False
01605     now = time.time()
01606     if timestamp < now - 10 * 3600:
01607         # we don't accept tickets older than 10h
01608         return False
01609     ourticket = createTicket(request, timestamp_str)
01610     return ticket == ourticket
01611 
01612 
01613 def renderText(request, Parser, text, line_anchors=False):
01614     """executes raw wiki markup with all page elements"""
01615     import StringIO
01616     out = StringIO.StringIO()
01617     request.redirect(out)
01618     wikiizer = Parser(text, request)
01619     wikiizer.format(request.formatter, inhibit_p=True)
01620     result = out.getvalue()
01621     request.redirect()
01622     del out
01623     return result
01624 
01625 
01626 def getProcessingInstructions(text):
01627     """creates dict of processing instructions from raw wiki markup"""
01628     kw = {}
01629     for line in text.split('\n'):
01630         if line.startswith('#'):
01631             for pi in ("format", "refresh", "redirect", "deprecated", "pragma", "form", "acl", "language"):
01632                 if line[1:].lower().startswith(pi):
01633                     kw[pi] = line[len(pi)+1:].strip()
01634                     break
01635     return kw
01636 
01637 
01638 def getParser(request, text):
01639     """gets the parser from raw wiki murkup"""
01640     # check for XML content
01641     if text and text[:5] == '<?xml':
01642         pi_format = "xslt"
01643     else:
01644         # check processing instructions
01645         pi = getProcessingInstructions(text)
01646         pi_format = pi.get("format", request.cfg.default_markup or "wiki").lower()
01647 
01648     Parser = searchAndImportPlugin(request.cfg, "parser", pi_format)
01649     return Parser
01650