Attachment 'wikiutil.patch'

Download

   1 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/__init__.py
   2 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
   3 +++ b/MoinMoin/wikiutil/__init__.py	Tue May 25 16:30:42 2010 -0300
   4 @@ -0,0 +1,54 @@
   5 +# -*- coding: iso-8859-1 -*-
   6 +"""
   7 +    MoinMoin - Wiki Utility Functions
   8 +
   9 +    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
  10 +                2004 by Florian Festi,
  11 +                2006 by Mikko Virkkil,
  12 +                2005-2009 MoinMoin:ThomasWaldmann,
  13 +                2007 MoinMoin:ReimarBauer,
  14 +                2008 MoinMoin:ChristopherDenter
  15 +    @license: GNU GPL, see COPYING for details.
  16 +"""
  17 +
  18 +import cgi
  19 +import codecs
  20 +import os
  21 +import re
  22 +import time
  23 +import urllib
  24 +
  25 +from MoinMoin import log
  26 +logging = log.getLogger(__name__)
  27 +
  28 +from MoinMoin import config
  29 +from MoinMoin.util import pysupport, lock
  30 +from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError
  31 +from MoinMoin.support.python_compatibility import rsplit
  32 +
  33 +from MoinMoin import web # needed so that next line works:
  34 +import werkzeug
  35 +
  36 +# Exceptions
  37 +class InvalidFileNameError(Exception):
  38 +    """ Called when we find an invalid file name """
  39 +    pass
  40 +
  41 +# constants for page names
  42 +PARENT_PREFIX = "../"
  43 +PARENT_PREFIX_LEN = len(PARENT_PREFIX)
  44 +CHILD_PREFIX = "/"
  45 +CHILD_PREFIX_LEN = len(CHILD_PREFIX)
  46 +
  47 +# Import without break MoinMoin
  48 +from parsers import *
  49 +from data import *
  50 +from plugins import *
  51 +from pagetypes import *
  52 +from pageedit import *
  53 +from misc import *
  54 +from interwiki import *
  55 +from parsers import *
  56 +from mimetype import *
  57 +from forms import *
  58 +from storage import *
  59 \ No newline at end of file
  60 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/data.py
  61 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
  62 +++ b/MoinMoin/wikiutil/data.py	Tue May 25 16:30:42 2010 -0300
  63 @@ -0,0 +1,177 @@
  64 +from MoinMoin import config
  65 +import werkzeug
  66 +
  67 +#############################################################################
  68 +### Getting data from user/Sending data to user
  69 +#############################################################################
  70 +
  71 +def decodeUnknownInput(text):
  72 +    """ Decode input in unknown encoding
  73 +
  74 +    First we try utf-8 because it has special format, and it will decode
  75 +    only utf-8 files. Then we try config.charset, then iso-8859-1 using
  76 +    'replace'. We will never raise an exception, but may return junk
  77 +    data.
  78 +
  79 +    WARNING: Use this function only for data that you view, not for data
  80 +    that you save in the wiki.
  81 +
  82 +    @param text: the text to decode, string
  83 +    @rtype: unicode
  84 +    @return: decoded text (maybe wrong)
  85 +    """
  86 +    # Shortcut for unicode input
  87 +    if isinstance(text, unicode):
  88 +        return text
  89 +
  90 +    try:
  91 +        return unicode(text, 'utf-8')
  92 +    except UnicodeError:
  93 +        if config.charset not in ['utf-8', 'iso-8859-1']:
  94 +            try:
  95 +                return unicode(text, config.charset)
  96 +            except UnicodeError:
  97 +                pass
  98 +        return unicode(text, 'iso-8859-1', 'replace')
  99 +
 100 +
 101 +def decodeUserInput(s, charsets=[config.charset]):
 102 +    """
 103 +    Decodes input from the user.
 104 +
 105 +    @param s: the string to unquote
 106 +    @param charsets: list of charsets to assume the string is in
 107 +    @rtype: unicode
 108 +    @return: the unquoted string as unicode
 109 +    """
 110 +    for charset in charsets:
 111 +        try:
 112 +            return s.decode(charset)
 113 +        except UnicodeError:
 114 +            pass
 115 +    raise UnicodeError('The string %r cannot be decoded.' % s)
 116 +
 117 +
 118 +def url_quote(s, safe='/', want_unicode=None):
 119 +    """ see werkzeug.url_quote, we use a different safe param default value """
 120 +    try:
 121 +        assert want_unicode is None
 122 +    except AssertionError:
 123 +        log.exception("call with deprecated want_unicode param, please fix caller")
 124 +    return werkzeug.url_quote(s, charset=config.charset, safe=safe)
 125 +
 126 +def url_quote_plus(s, safe='/', want_unicode=None):
 127 +    """ see werkzeug.url_quote_plus, we use a different safe param default value """
 128 +    try:
 129 +        assert want_unicode is None
 130 +    except AssertionError:
 131 +        log.exception("call with deprecated want_unicode param, please fix caller")
 132 +    return werkzeug.url_quote_plus(s, charset=config.charset, safe=safe)
 133 +
 134 +def url_unquote(s, want_unicode=None):
 135 +    """ see werkzeug.url_unquote """
 136 +    try:
 137 +        assert want_unicode is None
 138 +    except AssertionError:
 139 +        log.exception("call with deprecated want_unicode param, please fix caller")
 140 +    if isinstance(s, unicode):
 141 +        s = s.encode(config.charset)
 142 +    return werkzeug.url_unquote(s, charset=config.charset, errors='fallback:iso-8859-1')
 143 +
 144 +
 145 +def parseQueryString(qstr, want_unicode=None):
 146 +    """ see werkzeug.url_decode
 147 +
 148 +        Please note: this returns a MultiDict, you might need to use dict() on
 149 +                     the result if your code expects a "normal" dict.
 150 +    """
 151 +    try:
 152 +        assert want_unicode is None
 153 +    except AssertionError:
 154 +        log.exception("call with deprecated want_unicode param, please fix caller")
 155 +    return werkzeug.url_decode(qstr, charset=config.charset, errors='fallback:iso-8859-1',
 156 +                               decode_keys=False, include_empty=False)
 157 +
 158 +def makeQueryString(qstr=None, want_unicode=None, **kw):
 159 +    """ Make a querystring from arguments.
 160 +
 161 +    kw arguments overide values in qstr.
 162 +
 163 +    If a string is passed in, it's returned verbatim and keyword parameters are ignored.
 164 +
 165 +    See also: werkzeug.url_encode
 166 +
 167 +    @param qstr: dict to format as query string, using either ascii or unicode
 168 +    @param kw: same as dict when using keywords, using ascii or unicode
 169 +    @rtype: string
 170 +    @return: query string ready to use in a url
 171 +    """
 172 +    try:
 173 +        assert want_unicode is None
 174 +    except AssertionError:
 175 +        log.exception("call with deprecated want_unicode param, please fix caller")
 176 +    if qstr is None:
 177 +        qstr = {}
 178 +    elif isinstance(qstr, (str, unicode)):
 179 +        return qstr
 180 +    if isinstance(qstr, dict):
 181 +        qstr.update(kw)
 182 +        return werkzeug.url_encode(qstr, charset=config.charset, encode_keys=True)
 183 +    else:
 184 +        raise ValueError("Unsupported argument type, should be dict.")
 185 +
 186 +
 187 +def quoteWikinameURL(pagename, charset=config.charset):
 188 +    """ Return a url encoding of filename in plain ascii
 189 +
 190 +    Use urllib.quote to quote any character that is not always safe.
 191 +
 192 +    @param pagename: the original pagename (unicode)
 193 +    @param charset: url text encoding, 'utf-8' recommended. Other charset
 194 +                    might not be able to encode the page name and raise
 195 +                    UnicodeError. (default config.charset ('utf-8')).
 196 +    @rtype: string
 197 +    @return: the quoted filename, all unsafe characters encoded
 198 +    """
 199 +    # XXX please note that urllib.quote and werkzeug.url_quote have
 200 +    # XXX different defaults for safe=...
 201 +    return werkzeug.url_quote(pagename, charset=charset, safe='/')
 202 +
 203 +
 204 +escape = werkzeug.escape
 205 +
 206 +
 207 +def clean_input(text, max_len=201):
 208 +    """ Clean input:
 209 +        replace CR, LF, TAB by whitespace
 210 +        delete control chars
 211 +
 212 +        @param text: unicode text to clean (if we get str, we decode)
 213 +        @rtype: unicode
 214 +        @return: cleaned text
 215 +    """
 216 +    # we only have input fields with max 200 chars, but spammers send us more
 217 +    length = len(text)
 218 +    if length == 0 or length > max_len:
 219 +        return u''
 220 +    else:
 221 +        if isinstance(text, str):
 222 +            # the translate() below can ONLY process unicode, thus, if we get
 223 +            # str, we try to decode it using the usual coding:
 224 +            text = text.decode(config.charset)
 225 +        return text.translate(config.clean_input_translation_map)
 226 +
 227 +
 228 +def make_breakable(text, maxlen):
 229 +    """ make a text breakable by inserting spaces into nonbreakable parts
 230 +    """
 231 +    text = text.split(" ")
 232 +    newtext = []
 233 +    for part in text:
 234 +        if len(part) > maxlen:
 235 +            while part:
 236 +                newtext.append(part[:maxlen])
 237 +                part = part[maxlen:]
 238 +        else:
 239 +            newtext.append(part)
 240 +    return " ".join(newtext)
 241 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/forms.py
 242 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
 243 +++ b/MoinMoin/wikiutil/forms.py	Tue May 25 16:30:42 2010 -0300
 244 @@ -0,0 +1,227 @@
 245 +import re
 246 +from MoinMoin import config
 247 +
 248 +########################################################################
 249 +### Tickets - usually used in forms to make sure that form submissions
 250 +### are in response to a form the same user got from moin for the same
 251 +### action and same page.
 252 +########################################################################
 253 +
 254 +def createTicket(request, tm=None, action=None, pagename=None):
 255 +    """ Create a ticket using a configured secret
 256 +
 257 +        @param tm: unix timestamp (optional, uses current time if not given)
 258 +        @param action: action name (optional, uses current action if not given)
 259 +                       Note: if you create a ticket for a form that calls another
 260 +                             action than the current one, you MUST specify the
 261 +                             action you call when posting the form.
 262 +        @param pagename: page name (optional, uses current page name if not given)
 263 +                       Note: if you create a ticket for a form that posts to another
 264 +                             page than the current one, you MUST specify the
 265 +                             page name you use when posting the form.
 266 +    """
 267 +
 268 +    from MoinMoin.support.python_compatibility import hmac_new
 269 +    if tm is None:
 270 +        # for age-check of ticket
 271 +        tm = "%010x" % time.time()
 272 +
 273 +    # make the ticket very specific:
 274 +    if pagename is None:
 275 +        try:
 276 +            pagename = request.page.page_name
 277 +        except:
 278 +            pagename = ''
 279 +
 280 +    if action is None:
 281 +        action = request.action
 282 +
 283 +    if request.session:
 284 +        # either a user is logged in or we have a anon session -
 285 +        # if session times out, ticket will get invalid
 286 +        sid = request.session.sid
 287 +    else:
 288 +        sid = ''
 289 +
 290 +    if request.user.valid:
 291 +        uid = request.user.id
 292 +    else:
 293 +        uid = ''
 294 +
 295 +    hmac_data = []
 296 +    for value in [tm, pagename, action, sid, uid, ]:
 297 +        if isinstance(value, unicode):
 298 +            value = value.encode('utf-8')
 299 +        hmac_data.append(value)
 300 +
 301 +    hmac = hmac_new(request.cfg.secrets['wikiutil/tickets'],
 302 +                    ''.join(hmac_data))
 303 +    return "%s.%s" % (tm, hmac.hexdigest())
 304 +
 305 +
 306 +def checkTicket(request, ticket):
 307 +    """Check validity of a previously created ticket"""
 308 +    try:
 309 +        timestamp_str = ticket.split('.')[0]
 310 +        timestamp = int(timestamp_str, 16)
 311 +    except ValueError:
 312 +        # invalid or empty ticket
 313 +        logging.debug("checkTicket: invalid or empty ticket %r" % ticket)
 314 +        return False
 315 +    now = time.time()
 316 +    if timestamp < now - 10 * 3600:
 317 +        # we don't accept tickets older than 10h
 318 +        logging.debug("checkTicket: too old ticket, timestamp %r" % timestamp)
 319 +        return False
 320 +    # Note: if the session timed out, that will also invalidate the ticket,
 321 +    #       if the ticket was created within a session.
 322 +    ourticket = createTicket(request, timestamp_str)
 323 +    logging.debug("checkTicket: returning %r, got %r, expected %r" % (ticket == ourticket, ticket, ourticket))
 324 +    return ticket == ourticket
 325 +
 326 +
 327 +def renderText(request, Parser, text):
 328 +    """executes raw wiki markup with all page elements"""
 329 +    import StringIO
 330 +    out = StringIO.StringIO()
 331 +    request.redirect(out)
 332 +    wikiizer = Parser(text, request)
 333 +    wikiizer.format(request.formatter, inhibit_p=True)
 334 +    result = out.getvalue()
 335 +    request.redirect()
 336 +    del out
 337 +    return result
 338 +
 339 +
 340 +def split_body(body):
 341 +    """ Extract the processing instructions / acl / etc. at the beginning of a page's body.
 342 +
 343 +        Hint: if you have a Page object p, you already have the result of this function in
 344 +              p.meta and (even better) parsed/processed stuff in p.pi.
 345 +
 346 +        Returns a list of (pi, restofline) tuples and a string with the rest of the body.
 347 +    """
 348 +    pi = {}
 349 +    while body.startswith('#'):
 350 +        try:
 351 +            line, body = body.split('\n', 1) # extract first line
 352 +        except ValueError:
 353 +            line = body
 354 +            body = ''
 355 +
 356 +        # end parsing on empty (invalid) PI
 357 +        if line == "#":
 358 +            body = line + '\n' + body
 359 +            break
 360 +
 361 +        if line[1] == '#':# two hash marks are a comment
 362 +            comment = line[2:]
 363 +            if not comment.startswith(' '):
 364 +                # we don't require a blank after the ##, so we put one there
 365 +                comment = ' ' + comment
 366 +                line = '##%s' % comment
 367 +
 368 +        verb, args = (line[1:] + ' ').split(' ', 1) # split at the first blank
 369 +        pi.setdefault(verb.lower(), []).append(args.strip())
 370 +
 371 +    for key, value in pi.iteritems():
 372 +        if len(value) == 1:
 373 +            pi[key] = value[0]
 374 +        else:
 375 +            pi[key] = tuple(value)
 376 +
 377 +    return pi, body
 378 +
 379 +
 380 +def add_metadata_to_body(metadata, data):
 381 +    """
 382 +    Adds the processing instructions to the data.
 383 +    """
 384 +    from MoinMoin.items import SIZE, EDIT_LOG
 385 +    READONLY_METADATA = [SIZE] + list(EDIT_LOCK) + EDIT_LOG
 386 +
 387 +    parsing_instructions = ["format", "language", "refresh", "acl",
 388 +                            "redirect", "deprecated", "openiduser",
 389 +                            "pragma", "internal", "external"]
 390 +
 391 +    metadata_data = ""
 392 +    for key, value in metadata.iteritems():
 393 +        if key not in parsing_instructions:
 394 +            continue
 395 +        # special handling for list metadata like acls
 396 +        if isinstance(value, list):
 397 +            for line in value:
 398 +                metadata_data += "#%s %s\n" % (key, line)
 399 +        else:
 400 +            metadata_data += "#%s %s\n" % (key, value)
 401 +    return metadata_data + data
 402 +
 403 +
 404 +def get_hostname(request, addr):
 405 +    """
 406 +    Looks up the hostname depending on the configuration.
 407 +    """
 408 +    if request.cfg.log_reverse_dns_lookups:
 409 +        import socket
 410 +        try:
 411 +            hostname = socket.gethostbyaddr(addr)[0]
 412 +            hostname = unicode(hostname, config.charset)
 413 +        except (socket.error, UnicodeError):
 414 +            hostname = addr
 415 +    else:
 416 +        hostname = addr
 417 +    return hostname
 418 +
 419 +
 420 +class Version(tuple):
 421 +    """
 422 +    Version objects store versions like 1.2.3-4.5alpha6 in a structured
 423 +    way and support version comparisons and direct version component access.
 424 +    1: major version (digits only)
 425 +    2: minor version (digits only)
 426 +    3: (maintenance) release version (digits only)
 427 +    4.5alpha6: optional additional version specification (str)
 428 +
 429 +    You can create a Version instance either by giving the components, like:
 430 +        Version(1,2,3,'4.5alpha6')
 431 +    or by giving the composite version string, like:
 432 +        Version(version="1.2.3-4.5alpha6").
 433 +
 434 +    Version subclasses tuple, so comparisons to tuples should work.
 435 +    Also, we inherit all the comparison logic from tuple base class.
 436 +    """
 437 +    VERSION_RE = re.compile(
 438 +        r"""(?P<major>\d+)
 439 +            \.
 440 +            (?P<minor>\d+)
 441 +            \.
 442 +            (?P<release>\d+)
 443 +            (-
 444 +             (?P<additional>.+)
 445 +            )?""",
 446 +            re.VERBOSE)
 447 +
 448 +    @classmethod
 449 +    def parse_version(cls, version):
 450 +        match = cls.VERSION_RE.match(version)
 451 +        if match is None:
 452 +            raise ValueError("Unexpected version string format: %r" % version)
 453 +        v = match.groupdict()
 454 +        return int(v['major']), int(v['minor']), int(v['release']), str(v['additional'] or '')
 455 +
 456 +    def __new__(cls, major=0, minor=0, release=0, additional='', version=None):
 457 +        if version:
 458 +            major, minor, release, additional = cls.parse_version(version)
 459 +        return tuple.__new__(cls, (major, minor, release, additional))
 460 +
 461 +    # properties for easy access of version components
 462 +    major = property(lambda self: self[0])
 463 +    minor = property(lambda self: self[1])
 464 +    release = property(lambda self: self[2])
 465 +    additional = property(lambda self: self[3])
 466 +
 467 +    def __str__(self):
 468 +        version_str = "%d.%d.%d" % (self.major, self.minor, self.release)
 469 +        if self.additional:
 470 +            version_str += "-%s" % self.additional
 471 +        return version_str
 472 \ No newline at end of file
 473 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/interwiki.py
 474 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
 475 +++ b/MoinMoin/wikiutil/interwiki.py	Tue May 25 16:30:42 2010 -0300
 476 @@ -0,0 +1,137 @@
 477 +from MoinMoin import config
 478 +
 479 +#############################################################################
 480 +### InterWiki
 481 +#############################################################################
 482 +INTERWIKI_PAGE = "InterWikiMap"
 483 +
 484 +def generate_file_list(request):
 485 +    """ generates a list of all files. for internal use. """
 486 +
 487 +    # order is important here, intermap files read later overwrite
 488 +    # data from files read earlier!
 489 +    intermap_files = request.cfg.shared_intermap
 490 +    if not isinstance(intermap_files, list):
 491 +        intermap_files = [intermap_files]
 492 +    else:
 493 +        intermap_files = intermap_files[:]
 494 +    request.cfg.shared_intermap_files = [filename for filename in intermap_files
 495 +                                         if filename and os.path.isfile(filename)]
 496 +
 497 +
 498 +def get_max_mtime(file_list, page):
 499 +    """ Returns the highest modification time of the files in file_list and the
 500 +    page page. """
 501 +    timestamps = [os.stat(filename).st_mtime for filename in file_list]
 502 +    if page.exists():
 503 +        timestamps.append(page.mtime())
 504 +    if timestamps:
 505 +        return max(timestamps)
 506 +    else:
 507 +        return 0 # no files / pages there
 508 +
 509 +def load_wikimap(request):
 510 +    """ load interwiki map (once, and only on demand) """
 511 +    from MoinMoin.Page import Page
 512 +
 513 +    now = int(time.time())
 514 +    if getattr(request.cfg, "shared_intermap_files", None) is None:
 515 +        generate_file_list(request)
 516 +
 517 +    try:
 518 +        _interwiki_list = request.cfg.cache.interwiki_list
 519 +        old_mtime = request.cfg.cache.interwiki_mtime
 520 +        if request.cfg.cache.interwiki_ts + (1*60) < now: # 1 minutes caching time
 521 +            max_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
 522 +            if max_mtime > old_mtime:
 523 +                raise AttributeError # refresh cache
 524 +            else:
 525 +                request.cfg.cache.interwiki_ts = now
 526 +    except AttributeError:
 527 +        _interwiki_list = {}
 528 +        lines = []
 529 +
 530 +        for filename in request.cfg.shared_intermap_files:
 531 +            f = codecs.open(filename, "r", config.charset)
 532 +            lines.extend(f.readlines())
 533 +            f.close()
 534 +
 535 +        # add the contents of the InterWikiMap page
 536 +        lines += Page(request, INTERWIKI_PAGE).get_raw_body().splitlines()
 537 +
 538 +        for line in lines:
 539 +            if not line or line[0] == '#':
 540 +                continue
 541 +            try:
 542 +                line = "%s %s/InterWiki" % (line, request.script_root)
 543 +                wikitag, urlprefix, dummy = line.split(None, 2)
 544 +            except ValueError:
 545 +                pass
 546 +            else:
 547 +                _interwiki_list[wikitag] = urlprefix
 548 +
 549 +        del lines
 550 +
 551 +        # add own wiki as "Self" and by its configured name
 552 +        _interwiki_list['Self'] = request.script_root + '/'
 553 +        if request.cfg.interwikiname:
 554 +            _interwiki_list[request.cfg.interwikiname] = request.script_root + '/'
 555 +
 556 +        # save for later
 557 +        request.cfg.cache.interwiki_list = _interwiki_list
 558 +        request.cfg.cache.interwiki_ts = now
 559 +        request.cfg.cache.interwiki_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
 560 +
 561 +    return _interwiki_list
 562 +
 563 +def split_interwiki(wikiurl):
 564 +    """ Split a interwiki name, into wikiname and pagename, e.g:
 565 +
 566 +    'MoinMoin:FrontPage' -> "MoinMoin", "FrontPage"
 567 +    'FrontPage' -> "Self", "FrontPage"
 568 +    'MoinMoin:Page with blanks' -> "MoinMoin", "Page with blanks"
 569 +    'MoinMoin:' -> "MoinMoin", ""
 570 +
 571 +    @param wikiurl: the url to split
 572 +    @rtype: tuple
 573 +    @return: (wikiname, pagename)
 574 +    """
 575 +    try:
 576 +        wikiname, pagename = wikiurl.split(":", 1)
 577 +    except ValueError:
 578 +        wikiname, pagename = 'Self', wikiurl
 579 +    return wikiname, pagename
 580 +
 581 +def resolve_interwiki(request, wikiname, pagename):
 582 +    """ Resolve an interwiki reference (wikiname:pagename).
 583 +
 584 +    @param request: the request object
 585 +    @param wikiname: interwiki wiki name
 586 +    @param pagename: interwiki page name
 587 +    @rtype: tuple
 588 +    @return: (wikitag, wikiurl, wikitail, err)
 589 +    """
 590 +    _interwiki_list = load_wikimap(request)
 591 +    if wikiname in _interwiki_list:
 592 +        return (wikiname, _interwiki_list[wikiname], pagename, False)
 593 +    else:
 594 +        return (wikiname, request.script_root, "/InterWiki", True)
 595 +
 596 +def join_wiki(wikiurl, wikitail):
 597 +    """
 598 +    Add a (url_quoted) page name to an interwiki url.
 599 +
 600 +    Note: We can't know what kind of URL quoting a remote wiki expects.
 601 +          We just use a utf-8 encoded string with standard URL quoting.
 602 +
 603 +    @param wikiurl: wiki url, maybe including a $PAGE placeholder
 604 +    @param wikitail: page name
 605 +    @rtype: string
 606 +    @return: generated URL of the page in the other wiki
 607 +    """
 608 +    wikitail = url_quote(wikitail)
 609 +    if '$PAGE' in wikiurl:
 610 +        return wikiurl.replace('$PAGE', wikitail)
 611 +    else:
 612 +        return wikiurl + wikitail
 613 +
 614 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/mimetype.py
 615 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
 616 +++ b/MoinMoin/wikiutil/mimetype.py	Tue May 25 16:30:42 2010 -0300
 617 @@ -0,0 +1,191 @@
 618 +from MoinMoin import config
 619 +
 620 +#############################################################################
 621 +### mimetype support
 622 +#############################################################################
 623 +import mimetypes
 624 +
 625 +MIMETYPES_MORE = {
 626 + # OpenOffice 2.x & other open document stuff
 627 + '.odt': 'application/vnd.oasis.opendocument.text',
 628 + '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
 629 + '.odp': 'application/vnd.oasis.opendocument.presentation',
 630 + '.odg': 'application/vnd.oasis.opendocument.graphics',
 631 + '.odc': 'application/vnd.oasis.opendocument.chart',
 632 + '.odf': 'application/vnd.oasis.opendocument.formula',
 633 + '.odb': 'application/vnd.oasis.opendocument.database',
 634 + '.odi': 'application/vnd.oasis.opendocument.image',
 635 + '.odm': 'application/vnd.oasis.opendocument.text-master',
 636 + '.ott': 'application/vnd.oasis.opendocument.text-template',
 637 + '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
 638 + '.otp': 'application/vnd.oasis.opendocument.presentation-template',
 639 + '.otg': 'application/vnd.oasis.opendocument.graphics-template',
 640 + # some systems (like Mac OS X) don't have some of these:
 641 + '.patch': 'text/x-diff',
 642 + '.diff': 'text/x-diff',
 643 + '.py': 'text/x-python',
 644 + '.cfg': 'text/plain',
 645 + '.conf': 'text/plain',
 646 + '.irc': 'text/plain',
 647 + '.md5': 'text/plain',
 648 + '.csv': 'text/csv',
 649 + '.flv': 'video/x-flv',
 650 + '.wmv': 'video/x-ms-wmv',
 651 + '.swf': 'application/x-shockwave-flash',
 652 + '.moin': 'text/x.moin.wiki',
 653 + '.creole': 'text/x.moin.creole',
 654 +}
 655 +
 656 +# add all mimetype patterns of pygments
 657 +import pygments.lexers
 658 +
 659 +for name, short, patterns, mime in pygments.lexers.get_all_lexers():
 660 +    for pattern in patterns:
 661 +        if pattern.startswith('*.') and mime:
 662 +            MIMETYPES_MORE[pattern[1:]] = mime[0]
 663 +
 664 +[mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
 665 +
 666 +MIMETYPES_sanitize_mapping = {
 667 +    # this stuff is text, but got application/* for unknown reasons
 668 +    ('application', 'docbook+xml'): ('text', 'docbook'),
 669 +    ('application', 'x-latex'): ('text', 'latex'),
 670 +    ('application', 'x-tex'): ('text', 'tex'),
 671 +    ('application', 'javascript'): ('text', 'javascript'),
 672 +}
 673 +
 674 +MIMETYPES_spoil_mapping = {} # inverse mapping of above
 675 +for _key, _value in MIMETYPES_sanitize_mapping.items():
 676 +    MIMETYPES_spoil_mapping[_value] = _key
 677 +
 678 +
 679 +class MimeType(object):
 680 +    """ represents a mimetype like text/plain """
 681 +
 682 +    def __init__(self, mimestr=None, filename=None):
 683 +        self.major = self.minor = None # sanitized mime type and subtype
 684 +        self.params = {} # parameters like "charset" or others
 685 +        self.charset = None # this stays None until we know for sure!
 686 +        self.raw_mimestr = mimestr
 687 +        self.filename = filename
 688 +        if mimestr:
 689 +            self.parse_mimetype(mimestr)
 690 +        elif filename:
 691 +            self.parse_filename(filename)
 692 +
 693 +    def parse_filename(self, filename):
 694 +        mtype, encoding = mimetypes.guess_type(filename)
 695 +        if mtype is None:
 696 +            mtype = 'application/octet-stream'
 697 +        self.parse_mimetype(mtype)
 698 +
 699 +    def parse_mimetype(self, mimestr):
 700 +        """ take a string like used in content-type and parse it into components,
 701 +            alternatively it also can process some abbreviated string like "wiki"
 702 +        """
 703 +        parameters = mimestr.split(";")
 704 +        parameters = [p.strip() for p in parameters]
 705 +        mimetype, parameters = parameters[0], parameters[1:]
 706 +        mimetype = mimetype.split('/')
 707 +        if len(mimetype) >= 2:
 708 +            major, minor = mimetype[:2] # we just ignore more than 2 parts
 709 +        else:
 710 +            major, minor = self.parse_format(mimetype[0])
 711 +        self.major = major.lower()
 712 +        self.minor = minor.lower()
 713 +        for param in parameters:
 714 +            key, value = param.split('=')
 715 +            if value[0] == '"' and value[-1] == '"': # remove quotes
 716 +                value = value[1:-1]
 717 +            self.params[key.lower()] = value
 718 +        if 'charset' in self.params:
 719 +            self.charset = self.params['charset'].lower()
 720 +        self.sanitize()
 721 +
 722 +    def parse_format(self, format):
 723 +        """ maps from what we currently use on-page in a #format xxx processing
 724 +            instruction to a sanitized mimetype major, minor tuple.
 725 +            can also be user later for easier entry by the user, so he can just
 726 +            type "wiki" instead of "text/x.moin.wiki".
 727 +        """
 728 +        format = format.lower()
 729 +        if format in config.parser_text_mimetype:
 730 +            mimetype = 'text', format
 731 +        else:
 732 +            mapping = {
 733 +                'wiki': ('text', 'x.moin.wiki'),
 734 +                'irc': ('text', 'irssi'),
 735 +            }
 736 +            try:
 737 +                mimetype = mapping[format]
 738 +            except KeyError:
 739 +                mimetype = 'text', 'x-%s' % format
 740 +        return mimetype
 741 +
 742 +    def sanitize(self):
 743 +        """ convert to some representation that makes sense - this is not necessarily
 744 +            conformant to /etc/mime.types or IANA listing, but if something is
 745 +            readable text, we will return some text/* mimetype, not application/*,
 746 +            because we need text/plain as fallback and not application/octet-stream.
 747 +        """
 748 +        self.major, self.minor = MIMETYPES_sanitize_mapping.get((self.major, self.minor), (self.major, self.minor))
 749 +
 750 +    def spoil(self):
 751 +        """ this returns something conformant to /etc/mime.type or IANA as a string,
 752 +            kind of inverse operation of sanitize(), but doesn't change self
 753 +        """
 754 +        major, minor = MIMETYPES_spoil_mapping.get((self.major, self.minor), (self.major, self.minor))
 755 +        return self.content_type(major, minor)
 756 +
 757 +    def content_type(self, major=None, minor=None, charset=None, params=None):
 758 +        """ return a string suitable for Content-Type header
 759 +        """
 760 +        major = major or self.major
 761 +        minor = minor or self.minor
 762 +        params = params or self.params or {}
 763 +        if major == 'text':
 764 +            charset = charset or self.charset or params.get('charset', config.charset)
 765 +            params['charset'] = charset
 766 +        mimestr = "%s/%s" % (major, minor)
 767 +        params = ['%s="%s"' % (key.lower(), value) for key, value in params.items()]
 768 +        params.insert(0, mimestr)
 769 +        return "; ".join(params)
 770 +
 771 +    def mime_type(self):
 772 +        """ return a string major/minor only, no params """
 773 +        return "%s/%s" % (self.major, self.minor)
 774 +
 775 +    def content_disposition(self, cfg):
 776 +        # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
 777 +        # we just let the user store them to disk ('attachment').
 778 +        # For safe files, we directly show them inline (this also works better for IE).
 779 +        mime_type = self.mime_type()
 780 +        dangerous = mime_type in cfg.mimetypes_xss_protect
 781 +        content_disposition = dangerous and 'attachment' or 'inline'
 782 +        filename = self.filename
 783 +        if filename is not None:
 784 +            # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
 785 +            # There is no solution that is compatible to IE except stripping non-ascii chars
 786 +            if isinstance(filename, unicode):
 787 +                filename = filename.encode(config.charset)
 788 +            content_disposition += '; filename="%s"' % filename
 789 +        return content_disposition
 790 +
 791 +    def module_name(self):
 792 +        """ convert this mimetype to a string useable as python module name,
 793 +            we yield the exact module name first and then proceed to shorter
 794 +            module names (useful for falling back to them, if the more special
 795 +            module is not found) - e.g. first "text_python", next "text".
 796 +            Finally, we yield "application_octet_stream" as the most general
 797 +            mimetype we have.
 798 +            Hint: the fallback handler module for text/* should be implemented
 799 +                  in module "text" (not "text_plain")
 800 +        """
 801 +        mimetype = self.mime_type()
 802 +        modname = mimetype.replace("/", "_").replace("-", "_").replace(".", "_")
 803 +        fragments = modname.split('_')
 804 +        for length in range(len(fragments), 1, -1):
 805 +            yield "_".join(fragments[:length])
 806 +        yield self.raw_mimestr
 807 +        yield fragments[0]
 808 +        yield "application_octet_stream"
 809 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/misc.py
 810 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
 811 +++ b/MoinMoin/wikiutil/misc.py	Tue May 25 16:30:42 2010 -0300
 812 @@ -0,0 +1,258 @@
 813 +import urllib
 814 +from MoinMoin import config
 815 +from MoinMoin.support.python_compatibility import rsplit
 816 +
 817 +from pagetypes import isGroupPage
 818 +
 819 +#############################################################################
 820 +### Misc
 821 +#############################################################################
 822 +def normalize_pagename(name, cfg):
 823 +    """ Normalize page name
 824 +
 825 +    Prevent creating page names with invisible characters or funny
 826 +    whitespace that might confuse the users or abuse the wiki, or
 827 +    just does not make sense.
 828 +
 829 +    Restrict even more group pages, so they can be used inside acl lines.
 830 +
 831 +    @param name: page name, unicode
 832 +    @rtype: unicode
 833 +    @return: decoded and sanitized page name
 834 +    """
 835 +    # Strip invalid characters
 836 +    name = config.page_invalid_chars_regex.sub(u'', name)
 837 +
 838 +    # Split to pages and normalize each one
 839 +    pages = name.split(u'/')
 840 +    normalized = []
 841 +    for page in pages:
 842 +        # Ignore empty or whitespace only pages
 843 +        if not page or page.isspace():
 844 +            continue
 845 +
 846 +        # Cleanup group pages.
 847 +        # Strip non alpha numeric characters, keep white space
 848 +        if isGroupPage(page, cfg):
 849 +            page = u''.join([c for c in page
 850 +                             if c.isalnum() or c.isspace()])
 851 +
 852 +        # Normalize white space. Each name can contain multiple
 853 +        # words separated with only one space. Split handle all
 854 +        # 30 unicode spaces (isspace() == True)
 855 +        page = u' '.join(page.split())
 856 +
 857 +        normalized.append(page)
 858 +
 859 +    # Assemble components into full pagename
 860 +    name = u'/'.join(normalized)
 861 +    return name
 862 +
 863 +def taintfilename(basename):
 864 +    """
 865 +    Make a filename that is supposed to be a plain name secure, i.e.
 866 +    remove any possible path components that compromise our system.
 867 +
 868 +    @param basename: (possibly unsafe) filename
 869 +    @rtype: string
 870 +    @return: (safer) filename
 871 +    """
 872 +    for x in (os.pardir, ':', '/', '\\', '<', '>'):
 873 +        basename = basename.replace(x, '_')
 874 +
 875 +    return basename
 876 +
 877 +
 878 +def drawing2fname(drawing):
 879 +    config.drawing_extensions = ['.tdraw', '.adraw',
 880 +                                 '.svg',
 881 +                                 '.png', '.jpg', '.jpeg', '.gif',
 882 +                                ]
 883 +    fname, ext = os.path.splitext(drawing)
 884 +    # note: do not just check for empty extension or stuff like drawing:foo.bar
 885 +    # will fail, instead of being expanded to foo.bar.tdraw
 886 +    if ext not in config.drawing_extensions:
 887 +        # for backwards compatibility, twikidraw is the default:
 888 +        drawing += '.tdraw'
 889 +    return drawing
 890 +
 891 +
 892 +def mapURL(request, url):
 893 +    """
 894 +    Map URLs according to 'cfg.url_mappings'.
 895 +
 896 +    @param url: a URL
 897 +    @rtype: string
 898 +    @return: mapped URL
 899 +    """
 900 +    # check whether we have to map URLs
 901 +    if request.cfg.url_mappings:
 902 +        # check URL for the configured prefixes
 903 +        for prefix in request.cfg.url_mappings:
 904 +            if url.startswith(prefix):
 905 +                # substitute prefix with replacement value
 906 +                return request.cfg.url_mappings[prefix] + url[len(prefix):]
 907 +
 908 +    # return unchanged url
 909 +    return url
 910 +
 911 +
 912 +def getUnicodeIndexGroup(name):
 913 +    """
 914 +    Return a group letter for `name`, which must be a unicode string.
 915 +    Currently supported: Hangul Syllables (U+AC00 - U+D7AF)
 916 +
 917 +    @param name: a string
 918 +    @rtype: string
 919 +    @return: group letter or None
 920 +    """
 921 +    c = name[0]
 922 +    if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables
 923 +        return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588)
 924 +    else:
 925 +        return c.upper() # we put lower and upper case words into the same index group
 926 +
 927 +
 928 +def is_URL(arg, schemas=config.url_schemas):
 929 +    """ Return True if arg is a URL (with a schema given in the schemas list).
 930 +
 931 +        Note: there are not that many requirements for generic URLs, basically
 932 +        the only mandatory requirement is the ':' between schema and rest.
 933 +        Schema itself could be anything, also the rest (but we only support some
 934 +        schemas, as given in config.url_schemas, so it is a bit less ambiguous).
 935 +    """
 936 +    if ':' not in arg:
 937 +        return False
 938 +    for schema in schemas:
 939 +        if arg.startswith(schema + ':'):
 940 +            return True
 941 +    return False
 942 +
 943 +
 944 +def isPicture(url):
 945 +    """
 946 +    Is this a picture's url?
 947 +
 948 +    @param url: the url in question
 949 +    @rtype: bool
 950 +    @return: true if url points to a picture
 951 +    """
 952 +    extpos = url.rfind(".") + 1
 953 +    return extpos > 1 and url[extpos:].lower() in config.browser_supported_images
 954 +
 955 +
 956 +def link_tag(request, params, text=None, formatter=None, on=None, **kw):
 957 +    """ Create a link.
 958 +
 959 +    TODO: cleanup css_class
 960 +
 961 +    @param request: the request object
 962 +    @param params: parameter string appended to the URL after the scriptname/
 963 +    @param text: text / inner part of the <a>...</a> link - does NOT get
 964 +                 escaped, so you can give HTML here and it will be used verbatim
 965 +    @param formatter: the formatter object to use
 966 +    @param on: opening/closing tag only
 967 +    @keyword attrs: additional attrs (HTMLified string) (removed in 1.5.3)
 968 +    @rtype: string
 969 +    @return: formatted link tag
 970 +    """
 971 +    if formatter is None:
 972 +        formatter = request.html_formatter
 973 +    if 'css_class' in kw:
 974 +        css_class = kw['css_class']
 975 +        del kw['css_class'] # one time is enough
 976 +    else:
 977 +        css_class = None
 978 +    id = kw.get('id', None)
 979 +    name = kw.get('name', None)
 980 +    if text is None:
 981 +        text = params # default
 982 +    if formatter:
 983 +        url = "%s/%s" % (request.script_root, params)
 984 +        # formatter.url will escape the url part
 985 +        if on is not None:
 986 +            tag = formatter.url(on, url, css_class, **kw)
 987 +        else:
 988 +            tag = (formatter.url(1, url, css_class, **kw) +
 989 +                formatter.rawHTML(text) +
 990 +                formatter.url(0))
 991 +    else: # this shouldn't be used any more:
 992 +        if on is not None and not on:
 993 +            tag = '</a>'
 994 +        else:
 995 +            attrs = ''
 996 +            if css_class:
 997 +                attrs += ' class="%s"' % css_class
 998 +            if id:
 999 +                attrs += ' id="%s"' % id
1000 +            if name:
1001 +                attrs += ' name="%s"' % name
1002 +            tag = '<a%s href="%s/%s">' % (attrs, request.script_root, params)
1003 +            if not on:
1004 +                tag = "%s%s</a>" % (tag, text)
1005 +        logging.warning("wikiutil.link_tag called without formatter and without request.html_formatter. tag=%r" % (tag, ))
1006 +    return tag
1007 +
1008 +def containsConflictMarker(text):
1009 +    """ Returns true if there is a conflict marker in the text. """
1010 +    return "/!\\ '''Edit conflict" in text
1011 +
1012 +def pagediff(request, pagename1, rev1, pagename2, rev2, **kw):
1013 +    """
1014 +    Calculate the "diff" between two page contents.
1015 +
1016 +    @param pagename1: name of first page
1017 +    @param rev1: revision of first page
1018 +    @param pagename2: name of second page
1019 +    @param rev2: revision of second page
1020 +    @keyword ignorews: if 1: ignore pure-whitespace changes.
1021 +    @rtype: list
1022 +    @return: lines of diff output
1023 +    """
1024 +    from MoinMoin.Page import Page
1025 +    from MoinMoin.util import diff_text
1026 +    lines1 = Page(request, pagename1, rev=rev1).getlines()
1027 +    lines2 = Page(request, pagename2, rev=rev2).getlines()
1028 +
1029 +    lines = diff_text.diff(lines1, lines2, **kw)
1030 +    return lines
1031 +
1032 +def anchor_name_from_text(text):
1033 +    '''
1034 +    Generate an anchor name from the given text.
1035 +    This function generates valid HTML IDs matching: [A-Za-z][A-Za-z0-9:_.-]*
1036 +    Note: this transformation has a special feature: when you feed it with a
1037 +          valid ID/name, it will return it without modification (identity
1038 +          transformation).
1039 +    '''
1040 +    quoted = urllib.quote_plus(text.encode('utf-7'), safe=':')
1041 +    res = quoted.replace('%', '.').replace('+', '_')
1042 +    if not res[:1].isalpha():
1043 +        return 'A%s' % res
1044 +    return res
1045 +
1046 +def split_anchor(pagename):
1047 +    """
1048 +    Split a pagename that (optionally) has an anchor into the real pagename
1049 +    and the anchor part. If there is no anchor, it returns an empty string
1050 +    for the anchor.
1051 +
1052 +    Note: if pagename contains a # (as part of the pagename, not as anchor),
1053 +          you can use a trick to make it work nevertheless: just append a
1054 +          # at the end:
1055 +          "C##" returns ("C#", "")
1056 +          "Problem #1#" returns ("Problem #1", "")
1057 +
1058 +    TODO: We shouldn't deal with composite pagename#anchor strings, but keep
1059 +          it separate.
1060 +          Current approach: [[pagename#anchor|label|attr=val,&qarg=qval]]
1061 +          Future approach:  [[pagename|label|attr=val,&qarg=qval,#anchor]]
1062 +          The future approach will avoid problems when there is a # in the
1063 +          pagename part (and no anchor). Also, we need to append #anchor
1064 +          at the END of the generated URL (AFTER the query string).
1065 +    """
1066 +    parts = rsplit(pagename, '#', 1)
1067 +    if len(parts) == 2:
1068 +        return parts
1069 +    else:
1070 +        return pagename, ""
1071 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/pageedit.py
1072 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
1073 +++ b/MoinMoin/wikiutil/pageedit.py	Tue May 25 16:30:42 2010 -0300
1074 @@ -0,0 +1,42 @@
1075 +from MoinMoin import config
1076 +
1077 +#############################################################################
1078 +### Page edit locking
1079 +#############################################################################
1080 +
1081 +EDIT_LOCK_TIMESTAMP = "edit_lock_timestamp"
1082 +EDIT_LOCK_ADDR = "edit_lock_addr"
1083 +EDIT_LOCK_HOSTNAME = "edit_lock_hostname"
1084 +EDIT_LOCK_USERID = "edit_lock_userid"
1085 +
1086 +EDIT_LOCK = (EDIT_LOCK_TIMESTAMP, EDIT_LOCK_ADDR, EDIT_LOCK_HOSTNAME, EDIT_LOCK_USERID)
1087 +
1088 +def get_edit_lock(item):
1089 +    """
1090 +    Given an Item, get a tuple containing the timestamp of the edit-lock and the user.
1091 +    """
1092 +    for key in EDIT_LOCK:
1093 +        if not key in item:
1094 +            return (False, 0.0, "", "", "")
1095 +        else:
1096 +            return (True, float(item[EDIT_LOCK_TIMESTAMP]), item[EDIT_LOCK_ADDR],
1097 +                    item[EDIT_LOCK_HOSTNAME], item[EDIT_LOCK_USERID])
1098 +
1099 +def set_edit_lock(item, request):
1100 +    """
1101 +    Set the lock property to True or False.
1102 +    """
1103 +    timestamp = time.time()
1104 +    addr = request.remote_addr
1105 +    hostname = wikiutil.get_hostname(request, addr)
1106 +    if hasattr(request, "user"):
1107 +        userid = request.user.valid and request.user.id or ''
1108 +    else:
1109 +        userid = ''
1110 +
1111 +    item.change_metadata()
1112 +    item[EDIT_LOCK_TIMESTAMP] = str(timestamp)
1113 +    item[EDIT_LOCK_ADDR] = addr
1114 +    item[EDIT_LOCK_HOSTNAME] = hostname
1115 +    item[EDIT_LOCK_USERID] = userid
1116 +    item.publish_metadata()
1117 \ No newline at end of file
1118 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/pagetypes.py
1119 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
1120 +++ b/MoinMoin/wikiutil/pagetypes.py	Tue May 25 16:30:42 2010 -0300
1121 @@ -0,0 +1,230 @@
1122 +from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError
1123 +
1124 +
1125 +#############################################################################
1126 +### Page types (based on page names)
1127 +#############################################################################
1128 +
1129 +def isSystemPage(request, pagename):
1130 +    """ Is this a system page?
1131 +
1132 +    @param request: the request object
1133 +    @param pagename: the page name
1134 +    @rtype: bool
1135 +    @return: true if page is a system page
1136 +    """
1137 +    from MoinMoin.items import IS_SYSPAGE
1138 +    try:
1139 +        item = request.storage.get_item(pagename)
1140 +        return item.get_revision(-1)[IS_SYSPAGE]
1141 +    except (NoSuchItemError, NoSuchRevisionError, KeyError):
1142 +        pass
1143 +
1144 +    return isTemplatePage(request, pagename)
1145 +
1146 +
1147 +def isTemplatePage(request, pagename):
1148 +    """ Is this a template page?
1149 +
1150 +    @param pagename: the page name
1151 +    @rtype: bool
1152 +    @return: true if page is a template page
1153 +    """
1154 +    return request.cfg.cache.page_template_regexact.search(pagename) is not None
1155 +
1156 +
1157 +def isGroupPage(pagename, cfg):
1158 +    """ Is this a name of group page?
1159 +
1160 +    @param pagename: the page name
1161 +    @rtype: bool
1162 +    @return: true if page is a form page
1163 +    """
1164 +    return cfg.cache.page_group_regexact.search(pagename) is not None
1165 +
1166 +
1167 +def filterCategoryPages(request, pagelist):
1168 +    """ Return category pages in pagelist
1169 +
1170 +    WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use
1171 +    getPageList with a filter function.
1172 +
1173 +    If you pass a list with a single pagename, either that is returned
1174 +    or an empty list, thus you can use this function like a `isCategoryPage`
1175 +    one.
1176 +
1177 +    @param pagelist: a list of pages
1178 +    @rtype: list
1179 +    @return: only the category pages of pagelist
1180 +    """
1181 +    func = request.cfg.cache.page_category_regexact.search
1182 +    return [pn for pn in pagelist if func(pn)]
1183 +
1184 +
1185 +def getLocalizedPage(request, pagename): # was: getSysPage
1186 +    """ Get a system page according to user settings and available translations.
1187 +
1188 +    We include some special treatment for the case that <pagename> is the
1189 +    currently rendered page, as this is the case for some pages used very
1190 +    often, like FrontPage, etc. - in that case we reuse the already existing
1191 +    page object instead creating a new one.
1192 +
1193 +    @param request: the request object
1194 +    @param pagename: the name of the page
1195 +    @rtype: Page object
1196 +    @return: the page object of that system page, using a translated page,
1197 +             if it exists
1198 +    """
1199 +    from MoinMoin.Page import Page
1200 +    i18n_name = request.getText(pagename)
1201 +    pageobj = None
1202 +    if i18n_name != pagename:
1203 +        if request.page and i18n_name == request.page.page_name:
1204 +            # do not create new object for current page
1205 +            i18n_page = request.page
1206 +            if i18n_page.exists():
1207 +                pageobj = i18n_page
1208 +        else:
1209 +            i18n_page = Page(request, i18n_name)
1210 +            if i18n_page.exists():
1211 +                pageobj = i18n_page
1212 +
1213 +    # if we failed getting a translated version of <pagename>,
1214 +    # we fall back to english
1215 +    if not pageobj:
1216 +        if request.page and pagename == request.page.page_name:
1217 +            # do not create new object for current page
1218 +            pageobj = request.page
1219 +        else:
1220 +            pageobj = Page(request, pagename)
1221 +    return pageobj
1222 +
1223 +
1224 +def getFrontPage(request):
1225 +    """ Convenience function to get localized front page
1226 +
1227 +    @param request: current request
1228 +    @rtype: Page object
1229 +    @return localized page_front_page, if there is a translation
1230 +    """
1231 +    return getLocalizedPage(request, request.cfg.page_front_page)
1232 +
1233 +
1234 +def getHomePage(request, username=None):
1235 +    """
1236 +    Get a user's homepage, or return None for anon users and
1237 +    those who have not created a homepage.
1238 +
1239 +    DEPRECATED - try to use getInterwikiHomePage (see below)
1240 +
1241 +    @param request: the request object
1242 +    @param username: the user's name
1243 +    @rtype: Page
1244 +    @return: user's homepage object - or None
1245 +    """
1246 +    from MoinMoin.Page import Page
1247 +    # default to current user
1248 +    if username is None and request.user.valid:
1249 +        username = request.user.name
1250 +
1251 +    # known user?
1252 +    if username:
1253 +        # Return home page
1254 +        page = Page(request, username)
1255 +        if page.exists():
1256 +            return page
1257 +
1258 +    return None
1259 +
1260 +
1261 +def getInterwikiHomePage(request, username=None):
1262 +    """
1263 +    Get a user's homepage.
1264 +
1265 +    cfg.user_homewiki influences behaviour of this:
1266 +    'Self' does mean we store user homepage in THIS wiki.
1267 +    When set to our own interwikiname, it behaves like with 'Self'.
1268 +
1269 +    'SomeOtherWiki' means we store user homepages in another wiki.
1270 +
1271 +    @param request: the request object
1272 +    @param username: the user's name
1273 +    @rtype: tuple (or None for anon users)
1274 +    @return: (wikiname, pagename)
1275 +    """
1276 +    # default to current user
1277 +    if username is None and request.user.valid:
1278 +        username = request.user.name
1279 +    if not username:
1280 +        return None # anon user
1281 +
1282 +    homewiki = request.cfg.user_homewiki
1283 +    if homewiki == request.cfg.interwikiname:
1284 +        homewiki = u'Self'
1285 +
1286 +    return homewiki, username
1287 +
1288 +
1289 +def AbsPageName(context, pagename):
1290 +    """
1291 +    Return the absolute pagename for a (possibly) relative pagename.
1292 +
1293 +    @param context: name of the page where "pagename" appears on
1294 +    @param pagename: the (possibly relative) page name
1295 +    @rtype: string
1296 +    @return: the absolute page name
1297 +    """
1298 +    if pagename.startswith(PARENT_PREFIX):
1299 +        while context and pagename.startswith(PARENT_PREFIX):
1300 +            context = '/'.join(context.split('/')[:-1])
1301 +            pagename = pagename[PARENT_PREFIX_LEN:]
1302 +        pagename = '/'.join(filter(None, [context, pagename, ]))
1303 +    elif pagename.startswith(CHILD_PREFIX):
1304 +        if context:
1305 +            pagename = context + '/' + pagename[CHILD_PREFIX_LEN:]
1306 +        else:
1307 +            pagename = pagename[CHILD_PREFIX_LEN:]
1308 +    return pagename
1309 +
1310 +def RelPageName(context, pagename):
1311 +    """
1312 +    Return the relative pagename for some context.
1313 +
1314 +    @param context: name of the page where "pagename" appears on
1315 +    @param pagename: the absolute page name
1316 +    @rtype: string
1317 +    @return: the relative page name
1318 +    """
1319 +    if context == '':
1320 +        # special case, context is some "virtual root" page with name == ''
1321 +        # every page is a subpage of this virtual root
1322 +        return CHILD_PREFIX + pagename
1323 +    elif pagename.startswith(context + CHILD_PREFIX):
1324 +        # simple child
1325 +        return pagename[len(context):]
1326 +    else:
1327 +        # some kind of sister/aunt
1328 +        context_frags = context.split('/')   # A, B, C, D, E
1329 +        pagename_frags = pagename.split('/') # A, B, C, F
1330 +        # first throw away common parents:
1331 +        common = 0
1332 +        for cf, pf in zip(context_frags, pagename_frags):
1333 +            if cf == pf:
1334 +                common += 1
1335 +            else:
1336 +                break
1337 +        context_frags = context_frags[common:] # D, E
1338 +        pagename_frags = pagename_frags[common:] # F
1339 +        go_up = len(context_frags)
1340 +        return PARENT_PREFIX * go_up + '/'.join(pagename_frags)
1341 +
1342 +
1343 +def pagelinkmarkup(pagename, text=None):
1344 +    """ return markup that can be used as link to page <pagename> """
1345 +    # XXX: This used to check for CamelCase
1346 +    # TODO: To be replaced by a converter application/x-moin-document -> real markup
1347 +    if text is None or text == pagename:
1348 +        text = ''
1349 +    else:
1350 +        text = u'|%s' % text
1351 +    return u'[[%s%s]]' % (pagename, text)
1352 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/parsers.py
1353 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
1354 +++ b/MoinMoin/wikiutil/parsers.py	Tue May 25 16:30:42 2010 -0300
1355 @@ -0,0 +1,1022 @@
1356 +
1357 +#############################################################################
1358 +### Parsers
1359 +#############################################################################
1360 +
1361 +def getParserForExtension(cfg, extension):
1362 +    """
1363 +    Returns the Parser class of the parser fit to handle a file
1364 +    with the given extension. The extension should be in the same
1365 +    format as os.path.splitext returns it (i.e. with the dot).
1366 +    Returns None if no parser willing to handle is found.
1367 +    The dict of extensions is cached in the config object.
1368 +
1369 +    @param cfg: the Config instance for the wiki in question
1370 +    @param extension: the filename extension including the dot
1371 +    @rtype: class, None
1372 +    @returns: the parser class or None
1373 +    """
1374 +    if not hasattr(cfg.cache, 'EXT_TO_PARSER'):
1375 +        etp, etd = {}, None
1376 +        parser_plugins = getPlugins('parser', cfg)
1377 +        # force the 'highlight' parser to be the first entry in the list
1378 +        # this makes it possible to overwrite some mapping entries later, so that
1379 +        # moin will use some "better" parser for some filename extensions
1380 +        parser_plugins.remove('highlight')
1381 +        parser_plugins = ['highlight'] + parser_plugins
1382 +        for pname in parser_plugins:
1383 +            try:
1384 +                Parser = importPlugin(cfg, 'parser', pname, 'Parser')
1385 +            except PluginMissingError:
1386 +                continue
1387 +            if hasattr(Parser, 'extensions'):
1388 +                exts = Parser.extensions
1389 +                if isinstance(exts, list):
1390 +                    for ext in exts:
1391 +                        etp[ext] = Parser
1392 +                elif str(exts) == '*':
1393 +                    etd = Parser
1394 +        cfg.cache.EXT_TO_PARSER = etp
1395 +        cfg.cache.EXT_TO_PARSER_DEFAULT = etd
1396 +
1397 +    return cfg.cache.EXT_TO_PARSER.get(extension, cfg.cache.EXT_TO_PARSER_DEFAULT)
1398 +
1399 +
1400 +#############################################################################
1401 +### Parameter parsing
1402 +#############################################################################
1403 +
1404 +class BracketError(Exception):
1405 +    pass
1406 +
1407 +class BracketUnexpectedCloseError(BracketError):
1408 +    def __init__(self, bracket):
1409 +        self.bracket = bracket
1410 +        BracketError.__init__(self, "Unexpected closing bracket %s" % bracket)
1411 +
1412 +class BracketMissingCloseError(BracketError):
1413 +    def __init__(self, bracket):
1414 +        self.bracket = bracket
1415 +        BracketError.__init__(self, "Missing closing bracket %s" % bracket)
1416 +
1417 +class ParserPrefix:
1418 +    """
1419 +    Trivial container-class holding a single character for
1420 +    the possible prefixes for parse_quoted_separated_ext
1421 +    and implementing rich equal comparison.
1422 +    """
1423 +    def __init__(self, prefix):
1424 +        self.prefix = prefix
1425 +
1426 +    def __eq__(self, other):
1427 +        return isinstance(other, ParserPrefix) and other.prefix == self.prefix
1428 +
1429 +    def __repr__(self):
1430 +        return '<ParserPrefix(%s)>' % self.prefix.encode('utf-8')
1431 +
1432 +def parse_quoted_separated_ext(args, separator=None, name_value_separator=None,
1433 +                               brackets=None, seplimit=0, multikey=False,
1434 +                               prefixes=None, quotes='"'):
1435 +    """
1436 +    Parses the given string according to the other parameters.
1437 +
1438 +    Items can be quoted with any character from the quotes parameter
1439 +    and each quote can be escaped by doubling it, the separator and
1440 +    name_value_separator can both be quoted, when name_value_separator
1441 +    is set then the name can also be quoted.
1442 +
1443 +    Values that are not given are returned as None, while the
1444 +    empty string as a value can be achieved by quoting it.
1445 +
1446 +    If a name or value does not start with a quote, then the quote
1447 +    looses its special meaning for that name or value, unless it
1448 +    starts with one of the given prefixes (the parameter is unicode
1449 +    containing all allowed prefixes.) The prefixes will be returned
1450 +    as ParserPrefix() instances in the first element of the tuple
1451 +    for that particular argument.
1452 +
1453 +    If multiple separators follow each other, this is treated as
1454 +    having None arguments inbetween, that is also true for when
1455 +    space is used as separators (when separator is None), filter
1456 +    them out afterwards.
1457 +
1458 +    The function can also do bracketing, i.e. parse expressions
1459 +    that contain things like
1460 +        "(a (a b))" to ['(', 'a', ['(', 'a', 'b']],
1461 +    in this case, as in this example, the returned list will
1462 +    contain sub-lists and the brackets parameter must be a list
1463 +    of opening and closing brackets, e.g.
1464 +        brackets = ['()', '<>']
1465 +    Each sub-list's first item is the opening bracket used for
1466 +    grouping.
1467 +    Nesting will be observed between the different types of
1468 +    brackets given. If bracketing doesn't match, a BracketError
1469 +    instance is raised with a 'bracket' property indicating the
1470 +    type of missing or unexpected bracket, the instance will be
1471 +    either of the class BracketMissingCloseError or of the class
1472 +    BracketUnexpectedCloseError.
1473 +
1474 +    If multikey is True (along with setting name_value_separator),
1475 +    then the returned tuples for (key, value) pairs can also have
1476 +    multiple keys, e.g.
1477 +        "a=b=c" -> ('a', 'b', 'c')
1478 +
1479 +    @param args: arguments to parse
1480 +    @param separator: the argument separator, defaults to None, meaning any
1481 +        space separates arguments
1482 +    @param name_value_separator: separator for name=value, default '=',
1483 +        name=value keywords not parsed if evaluates to False
1484 +    @param brackets: a list of two-character strings giving
1485 +        opening and closing brackets
1486 +    @param seplimit: limits the number of parsed arguments
1487 +    @param multikey: multiple keys allowed for a single value
1488 +    @rtype: list
1489 +    @returns: list of unicode strings and tuples containing
1490 +        unicode strings, or lists containing the same for
1491 +        bracketing support
1492 +    """
1493 +    idx = 0
1494 +    assert name_value_separator is None or name_value_separator != separator
1495 +    assert name_value_separator is None or len(name_value_separator) == 1
1496 +    if not isinstance(args, unicode):
1497 +        raise TypeError('args must be unicode')
1498 +    max = len(args)
1499 +    result = []         # result list
1500 +    cur = [None]        # current item
1501 +    quoted = None       # we're inside quotes, indicates quote character used
1502 +    skipquote = 0       # next quote is a quoted quote
1503 +    noquote = False     # no quotes expected because word didn't start with one
1504 +    seplimit_reached = False # number of separators exhausted
1505 +    separator_count = 0 # number of separators encountered
1506 +    SPACE = [' ', '\t', ]
1507 +    nextitemsep = [separator]   # used for skipping trailing space
1508 +    SPACE = [' ', '\t', ]
1509 +    if separator is None:
1510 +        nextitemsep = SPACE[:]
1511 +        separators = SPACE
1512 +    else:
1513 +        nextitemsep = [separator]   # used for skipping trailing space
1514 +        separators = [separator]
1515 +    if name_value_separator:
1516 +        nextitemsep.append(name_value_separator)
1517 +
1518 +    # bracketing support
1519 +    opening = []
1520 +    closing = []
1521 +    bracketstack = []
1522 +    matchingbracket = {}
1523 +    if brackets:
1524 +        for o, c in brackets:
1525 +            assert not o in opening
1526 +            opening.append(o)
1527 +            assert not c in closing
1528 +            closing.append(c)
1529 +            matchingbracket[o] = c
1530 +
1531 +    def additem(result, cur, separator_count, nextitemsep):
1532 +        if len(cur) == 1:
1533 +            result.extend(cur)
1534 +        elif cur:
1535 +            result.append(tuple(cur))
1536 +        cur = [None]
1537 +        noquote = False
1538 +        separator_count += 1
1539 +        seplimit_reached = False
1540 +        if seplimit and separator_count >= seplimit:
1541 +            seplimit_reached = True
1542 +            nextitemsep = [n for n in nextitemsep if n in separators]
1543 +
1544 +        return cur, noquote, separator_count, seplimit_reached, nextitemsep
1545 +
1546 +    while idx < max:
1547 +        char = args[idx]
1548 +        next = None
1549 +        if idx + 1 < max:
1550 +            next = args[idx+1]
1551 +        if skipquote:
1552 +            skipquote -= 1
1553 +        if not separator is None and not quoted and char in SPACE:
1554 +            spaces = ''
1555 +            # accumulate all space
1556 +            while char in SPACE and idx < max - 1:
1557 +                spaces += char
1558 +                idx += 1
1559 +                char = args[idx]
1560 +            # remove space if args end with it
1561 +            if char in SPACE and idx == max - 1:
1562 +                break
1563 +            # remove space at end of argument
1564 +            if char in nextitemsep:
1565 +                continue
1566 +            idx -= 1
1567 +            if len(cur) and cur[-1]:
1568 +                cur[-1] = cur[-1] + spaces
1569 +        elif not quoted and char == name_value_separator:
1570 +            if multikey or len(cur) == 1:
1571 +                cur.append(None)
1572 +            else:
1573 +                if not multikey:
1574 +                    if cur[-1] is None:
1575 +                        cur[-1] = ''
1576 +                    cur[-1] += name_value_separator
1577 +                else:
1578 +                    cur.append(None)
1579 +            noquote = False
1580 +        elif not quoted and not seplimit_reached and char in separators:
1581 +            (cur, noquote, separator_count, seplimit_reached,
1582 +             nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1583 +        elif not quoted and not noquote and char in quotes:
1584 +            if len(cur) and cur[-1] is None:
1585 +                del cur[-1]
1586 +            cur.append(u'')
1587 +            quoted = char
1588 +        elif char == quoted and not skipquote:
1589 +            if next == quoted:
1590 +                skipquote = 2 # will be decremented right away
1591 +            else:
1592 +                quoted = None
1593 +        elif not quoted and char in opening:
1594 +            while len(cur) and cur[-1] is None:
1595 +                del cur[-1]
1596 +            (cur, noquote, separator_count, seplimit_reached,
1597 +             nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1598 +            bracketstack.append((matchingbracket[char], result))
1599 +            result = [char]
1600 +        elif not quoted and char in closing:
1601 +            while len(cur) and cur[-1] is None:
1602 +                del cur[-1]
1603 +            (cur, noquote, separator_count, seplimit_reached,
1604 +             nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1605 +            cur = []
1606 +            if not bracketstack:
1607 +                raise BracketUnexpectedCloseError(char)
1608 +            expected, oldresult = bracketstack[-1]
1609 +            if not expected == char:
1610 +                raise BracketUnexpectedCloseError(char)
1611 +            del bracketstack[-1]
1612 +            oldresult.append(result)
1613 +            result = oldresult
1614 +        elif not quoted and prefixes and char in prefixes and cur == [None]:
1615 +            cur = [ParserPrefix(char)]
1616 +            cur.append(None)
1617 +        else:
1618 +            if len(cur):
1619 +                if cur[-1] is None:
1620 +                    cur[-1] = char
1621 +                else:
1622 +                    cur[-1] += char
1623 +            else:
1624 +                cur.append(char)
1625 +            noquote = True
1626 +
1627 +        idx += 1
1628 +
1629 +    if bracketstack:
1630 +        raise BracketMissingCloseError(bracketstack[-1][0])
1631 +
1632 +    if quoted:
1633 +        if len(cur):
1634 +            if cur[-1] is None:
1635 +                cur[-1] = quoted
1636 +            else:
1637 +                cur[-1] = quoted + cur[-1]
1638 +        else:
1639 +            cur.append(quoted)
1640 +
1641 +    additem(result, cur, separator_count, nextitemsep)
1642 +
1643 +    return result
1644 +
1645 +def parse_quoted_separated(args, separator=',', name_value=True, seplimit=0):
1646 +    result = []
1647 +    positional = result
1648 +    if name_value:
1649 +        name_value_separator = '='
1650 +        trailing = []
1651 +        keywords = {}
1652 +    else:
1653 +        name_value_separator = None
1654 +
1655 +    l = parse_quoted_separated_ext(args, separator=separator,
1656 +                                   name_value_separator=name_value_separator,
1657 +                                   seplimit=seplimit)
1658 +    for item in l:
1659 +        if isinstance(item, tuple):
1660 +            key, value = item
1661 +            if key is None:
1662 +                key = u''
1663 +            keywords[key] = value
1664 +            positional = trailing
1665 +        else:
1666 +            positional.append(item)
1667 +
1668 +    if name_value:
1669 +        return result, keywords, trailing
1670 +    return result
1671 +
1672 +def get_bool(request, arg, name=None, default=None):
1673 +    """
1674 +    For use with values returned from parse_quoted_separated or given
1675 +    as macro parameters, return a boolean from a unicode string.
1676 +    Valid input is 'true'/'false', 'yes'/'no' and '1'/'0' or None for
1677 +    the default value.
1678 +
1679 +    @param request: A request instance
1680 +    @param arg: The argument, may be None or a unicode string
1681 +    @param name: Name of the argument, for error messages
1682 +    @param default: default value if arg is None
1683 +    @rtype: boolean or None
1684 +    @returns: the boolean value of the string according to above rules
1685 +              (or default value)
1686 +    """
1687 +    _ = request.getText
1688 +    assert default is None or isinstance(default, bool)
1689 +    if arg is None:
1690 +        return default
1691 +    elif not isinstance(arg, unicode):
1692 +        raise TypeError('Argument must be None or unicode')
1693 +    arg = arg.lower()
1694 +    if arg in [u'0', u'false', u'no']:
1695 +        return False
1696 +    elif arg in [u'1', u'true', u'yes']:
1697 +        return True
1698 +    else:
1699 +        if name:
1700 +            raise ValueError(
1701 +                _('Argument "%s" must be a boolean value, not "%s"') % (
1702 +                    name, arg))
1703 +        else:
1704 +            raise ValueError(
1705 +                _('Argument must be a boolean value, not "%s"') % arg)
1706 +
1707 +
1708 +def get_int(request, arg, name=None, default=None):
1709 +    """
1710 +    For use with values returned from parse_quoted_separated or given
1711 +    as macro parameters, return an integer from a unicode string
1712 +    containing the decimal representation of a number.
1713 +    None is a valid input and yields the default value.
1714 +
1715 +    @param request: A request instance
1716 +    @param arg: The argument, may be None or a unicode string
1717 +    @param name: Name of the argument, for error messages
1718 +    @param default: default value if arg is None
1719 +    @rtype: int or None
1720 +    @returns: the integer value of the string (or default value)
1721 +    """
1722 +    _ = request.getText
1723 +    assert default is None or isinstance(default, (int, long))
1724 +    if arg is None:
1725 +        return default
1726 +    elif not isinstance(arg, unicode):
1727 +        raise TypeError('Argument must be None or unicode')
1728 +    try:
1729 +        return int(arg)
1730 +    except ValueError:
1731 +        if name:
1732 +            raise ValueError(
1733 +                _('Argument "%s" must be an integer value, not "%s"') % (
1734 +                    name, arg))
1735 +        else:
1736 +            raise ValueError(
1737 +                _('Argument must be an integer value, not "%s"') % arg)
1738 +
1739 +
1740 +def get_float(request, arg, name=None, default=None):
1741 +    """
1742 +    For use with values returned from parse_quoted_separated or given
1743 +    as macro parameters, return a float from a unicode string.
1744 +    None is a valid input and yields the default value.
1745 +
1746 +    @param request: A request instance
1747 +    @param arg: The argument, may be None or a unicode string
1748 +    @param name: Name of the argument, for error messages
1749 +    @param default: default return value if arg is None
1750 +    @rtype: float or None
1751 +    @returns: the float value of the string (or default value)
1752 +    """
1753 +    _ = request.getText
1754 +    assert default is None or isinstance(default, (int, long, float))
1755 +    if arg is None:
1756 +        return default
1757 +    elif not isinstance(arg, unicode):
1758 +        raise TypeError('Argument must be None or unicode')
1759 +    try:
1760 +        return float(arg)
1761 +    except ValueError:
1762 +        if name:
1763 +            raise ValueError(
1764 +                _('Argument "%s" must be a floating point value, not "%s"') % (
1765 +                    name, arg))
1766 +        else:
1767 +            raise ValueError(
1768 +                _('Argument must be a floating point value, not "%s"') % arg)
1769 +
1770 +
1771 +def get_complex(request, arg, name=None, default=None):
1772 +    """
1773 +    For use with values returned from parse_quoted_separated or given
1774 +    as macro parameters, return a complex from a unicode string.
1775 +    None is a valid input and yields the default value.
1776 +
1777 +    @param request: A request instance
1778 +    @param arg: The argument, may be None or a unicode string
1779 +    @param name: Name of the argument, for error messages
1780 +    @param default: default return value if arg is None
1781 +    @rtype: complex or None
1782 +    @returns: the complex value of the string (or default value)
1783 +    """
1784 +    _ = request.getText
1785 +    assert default is None or isinstance(default, (int, long, float, complex))
1786 +    if arg is None:
1787 +        return default
1788 +    elif not isinstance(arg, unicode):
1789 +        raise TypeError('Argument must be None or unicode')
1790 +    try:
1791 +        # allow writing 'i' instead of 'j'
1792 +        arg = arg.replace('i', 'j').replace('I', 'j')
1793 +        return complex(arg)
1794 +    except ValueError:
1795 +        if name:
1796 +            raise ValueError(
1797 +                _('Argument "%s" must be a complex value, not "%s"') % (
1798 +                    name, arg))
1799 +        else:
1800 +            raise ValueError(
1801 +                _('Argument must be a complex value, not "%s"') % arg)
1802 +
1803 +
1804 +def get_unicode(request, arg, name=None, default=None):
1805 +    """
1806 +    For use with values returned from parse_quoted_separated or given
1807 +    as macro parameters, return a unicode string from a unicode string.
1808 +    None is a valid input and yields the default value.
1809 +
1810 +    @param request: A request instance
1811 +    @param arg: The argument, may be None or a unicode string
1812 +    @param name: Name of the argument, for error messages
1813 +    @param default: default return value if arg is None;
1814 +    @rtype: unicode or None
1815 +    @returns: the unicode string (or default value)
1816 +    """
1817 +    assert default is None or isinstance(default, unicode)
1818 +    if arg is None:
1819 +        return default
1820 +    elif not isinstance(arg, unicode):
1821 +        raise TypeError('Argument must be None or unicode')
1822 +
1823 +    return arg
1824 +
1825 +
1826 +def get_choice(request, arg, name=None, choices=[None], default_none=False):
1827 +    """
1828 +    For use with values returned from parse_quoted_separated or given
1829 +    as macro parameters, return a unicode string that must be in the
1830 +    choices given. None is a valid input and yields first of the valid
1831 +    choices.
1832 +
1833 +    @param request: A request instance
1834 +    @param arg: The argument, may be None or a unicode string
1835 +    @param name: Name of the argument, for error messages
1836 +    @param choices: the possible choices
1837 +    @param default_none: If False (default), get_choice returns first available
1838 +                         choice if arg is None. If True, get_choice returns
1839 +                         None if arg is None. This is useful if some arg value
1840 +                         is required (no default choice).
1841 +    @rtype: unicode or None
1842 +    @returns: the unicode string (or default value)
1843 +    """
1844 +    assert isinstance(choices, (tuple, list))
1845 +    if arg is None:
1846 +        if default_none:
1847 +            return None
1848 +        else:
1849 +            return choices[0]
1850 +    elif not isinstance(arg, unicode):
1851 +        raise TypeError('Argument must be None or unicode')
1852 +    elif not arg in choices:
1853 +        _ = request.getText
1854 +        if name:
1855 +            raise ValueError(
1856 +                _('Argument "%s" must be one of "%s", not "%s"') % (
1857 +                    name, '", "'.join([repr(choice) for choice in choices]),
1858 +                    arg))
1859 +        else:
1860 +            raise ValueError(
1861 +                _('Argument must be one of "%s", not "%s"') % (
1862 +                    '", "'.join([repr(choice) for choice in choices]), arg))
1863 +
1864 +    return arg
1865 +
1866 +
1867 +class IEFArgument:
1868 +    """
1869 +    Base class for new argument parsers for
1870 +    invoke_extension_function.
1871 +    """
1872 +    def __init__(self):
1873 +        pass
1874 +
1875 +    def parse_argument(self, s):
1876 +        """
1877 +        Parse the argument given in s (a string) and return
1878 +        the argument for the extension function.
1879 +        """
1880 +        raise NotImplementedError
1881 +
1882 +    def get_default(self):
1883 +        """
1884 +        Return the default for this argument.
1885 +        """
1886 +        raise NotImplementedError
1887 +
1888 +
1889 +class UnitArgument(IEFArgument):
1890 +    """
1891 +    Argument class for invoke_extension_function that forces
1892 +    having any of the specified units given for a value.
1893 +
1894 +    Note that the default unit is "mm".
1895 +
1896 +    Use, for example, "UnitArgument('7mm', float, ['%', 'mm'])".
1897 +
1898 +    If the defaultunit parameter is given, any argument that
1899 +    can be converted into the given argtype is assumed to have
1900 +    the default unit. NOTE: This doesn't work with a choice
1901 +    (tuple or list) argtype.
1902 +    """
1903 +    def __init__(self, default, argtype, units=['mm'], defaultunit=None):
1904 +        """
1905 +        Initialise a UnitArgument giving the default,
1906 +        argument type and the permitted units.
1907 +        """
1908 +        IEFArgument.__init__(self)
1909 +        self._units = list(units)
1910 +        self._units.sort(lambda x, y: len(y) - len(x))
1911 +        self._type = argtype
1912 +        self._defaultunit = defaultunit
1913 +        assert defaultunit is None or defaultunit in units
1914 +        if default is not None:
1915 +            self._default = self.parse_argument(default)
1916 +        else:
1917 +            self._default = None
1918 +
1919 +    def parse_argument(self, s):
1920 +        for unit in self._units:
1921 +            if s.endswith(unit):
1922 +                ret = (self._type(s[:len(s) - len(unit)]), unit)
1923 +                return ret
1924 +        if self._defaultunit is not None:
1925 +            try:
1926 +                return (self._type(s), self._defaultunit)
1927 +            except ValueError:
1928 +                pass
1929 +        units = ', '.join(self._units)
1930 +        ## XXX: how can we translate this?
1931 +        raise ValueError("Invalid unit in value %s (allowed units: %s)" % (s, units))
1932 +
1933 +    def get_default(self):
1934 +        return self._default
1935 +
1936 +
1937 +class required_arg:
1938 +    """
1939 +    Wrap a type in this class and give it as default argument
1940 +    for a function passed to invoke_extension_function() in
1941 +    order to get generic checking that the argument is given.
1942 +    """
1943 +    def __init__(self, argtype):
1944 +        """
1945 +        Initialise a required_arg
1946 +        @param argtype: the type the argument should have
1947 +        """
1948 +        if not (argtype in (bool, int, long, float, complex, unicode) or
1949 +                isinstance(argtype, (IEFArgument, tuple, list))):
1950 +            raise TypeError("argtype must be a valid type")
1951 +        self.argtype = argtype
1952 +
1953 +
1954 +def invoke_extension_function(request, function, args, fixed_args=[]):
1955 +    """
1956 +    Parses arguments for an extension call and calls the extension
1957 +    function with the arguments.
1958 +
1959 +    If the macro function has a default value that is a bool,
1960 +    int, long, float or unicode object, then the given value
1961 +    is converted to the type of that default value before passing
1962 +    it to the macro function. That way, macros need not call the
1963 +    wikiutil.get_* functions for any arguments that have a default.
1964 +
1965 +    @param request: the request object
1966 +    @param function: the function to invoke
1967 +    @param args: unicode string with arguments (or evaluating to False)
1968 +    @param fixed_args: fixed arguments to pass as the first arguments
1969 +    @returns: the return value from the function called
1970 +    """
1971 +    from inspect import getargspec, isfunction, isclass, ismethod
1972 +
1973 +    def _convert_arg(request, value, default, name=None):
1974 +        """
1975 +        Using the get_* functions, convert argument to the type of the default
1976 +        if that is any of bool, int, long, float or unicode; if the default
1977 +        is the type itself then convert to that type (keeps None) or if the
1978 +        default is a list require one of the list items.
1979 +
1980 +        In other cases return the value itself.
1981 +        """
1982 +        # if extending this, extend required_arg as well!
1983 +        if isinstance(default, bool):
1984 +            return get_bool(request, value, name, default)
1985 +        elif isinstance(default, (int, long)):
1986 +            return get_int(request, value, name, default)
1987 +        elif isinstance(default, float):
1988 +            return get_float(request, value, name, default)
1989 +        elif isinstance(default, complex):
1990 +            return get_complex(request, value, name, default)
1991 +        elif isinstance(default, unicode):
1992 +            return get_unicode(request, value, name, default)
1993 +        elif isinstance(default, (tuple, list)):
1994 +            return get_choice(request, value, name, default)
1995 +        elif default is bool:
1996 +            return get_bool(request, value, name)
1997 +        elif default is int or default is long:
1998 +            return get_int(request, value, name)
1999 +        elif default is float:
2000 +            return get_float(request, value, name)
2001 +        elif default is complex:
2002 +            return get_complex(request, value, name)
2003 +        elif isinstance(default, IEFArgument):
2004 +            # defaults handled later
2005 +            if value is None:
2006 +                return None
2007 +            return default.parse_argument(value)
2008 +        elif isinstance(default, required_arg):
2009 +            if isinstance(default.argtype, (tuple, list)):
2010 +                # treat choice specially and return None if no choice
2011 +                # is given in the value
2012 +                return get_choice(request, value, name, list(default.argtype),
2013 +                       default_none=True)
2014 +            else:
2015 +                return _convert_arg(request, value, default.argtype, name)
2016 +        return value
2017 +
2018 +    assert isinstance(fixed_args, (list, tuple))
2019 +
2020 +    _ = request.getText
2021 +
2022 +    kwargs = {}
2023 +    kwargs_to_pass = {}
2024 +    trailing_args = []
2025 +
2026 +    if args:
2027 +        assert isinstance(args, unicode)
2028 +
2029 +        positional, keyword, trailing = parse_quoted_separated(args)
2030 +
2031 +        for kw in keyword:
2032 +            try:
2033 +                kwargs[str(kw)] = keyword[kw]
2034 +            except UnicodeEncodeError:
2035 +                kwargs_to_pass[kw] = keyword[kw]
2036 +
2037 +        trailing_args.extend(trailing)
2038 +
2039 +    else:
2040 +        positional = []
2041 +
2042 +    if isfunction(function) or ismethod(function):
2043 +        argnames, varargs, varkw, defaultlist = getargspec(function)
2044 +    elif isclass(function):
2045 +        (argnames, varargs,
2046 +         varkw, defaultlist) = getargspec(function.__init__.im_func)
2047 +    else:
2048 +        raise TypeError('function must be a function, method or class')
2049 +
2050 +    # self is implicit!
2051 +    if ismethod(function) or isclass(function):
2052 +        argnames = argnames[1:]
2053 +
2054 +    fixed_argc = len(fixed_args)
2055 +    argnames = argnames[fixed_argc:]
2056 +    argc = len(argnames)
2057 +    if not defaultlist:
2058 +        defaultlist = []
2059 +
2060 +    # if the fixed parameters have defaults too...
2061 +    if argc < len(defaultlist):
2062 +        defaultlist = defaultlist[fixed_argc:]
2063 +    defstart = argc - len(defaultlist)
2064 +
2065 +    defaults = {}
2066 +    # reverse to be able to pop() things off
2067 +    positional.reverse()
2068 +    allow_kwargs = False
2069 +    allow_trailing = False
2070 +    # convert all arguments to keyword arguments,
2071 +    # fill all arguments that weren't given with None
2072 +    for idx in range(argc):
2073 +        argname = argnames[idx]
2074 +        if argname == '_kwargs':
2075 +            allow_kwargs = True
2076 +            continue
2077 +        if argname == '_trailing_args':
2078 +            allow_trailing = True
2079 +            continue
2080 +        if positional:
2081 +            kwargs[argname] = positional.pop()
2082 +        if not argname in kwargs:
2083 +            kwargs[argname] = None
2084 +        if idx >= defstart:
2085 +            defaults[argname] = defaultlist[idx - defstart]
2086 +
2087 +    if positional:
2088 +        if not allow_trailing:
2089 +            raise ValueError(_('Too many arguments'))
2090 +        trailing_args.extend(positional)
2091 +
2092 +    if trailing_args:
2093 +        if not allow_trailing:
2094 +            raise ValueError(_('Cannot have arguments without name following'
2095 +                               ' named arguments'))
2096 +        kwargs['_trailing_args'] = trailing_args
2097 +
2098 +    # type-convert all keyword arguments to the type
2099 +    # that the default value indicates
2100 +    for argname in kwargs.keys()[:]:
2101 +        if argname in defaults:
2102 +            # the value of 'argname' from kwargs will be put into the
2103 +            # macro's 'argname' argument, so convert that giving the
2104 +            # name to the converter so the user is told which argument
2105 +            # went wrong (if it does)
2106 +            kwargs[argname] = _convert_arg(request, kwargs[argname],
2107 +                                           defaults[argname], argname)
2108 +            if kwargs[argname] is None:
2109 +                if isinstance(defaults[argname], required_arg):
2110 +                    raise ValueError(_('Argument "%s" is required') % argname)
2111 +                if isinstance(defaults[argname], IEFArgument):
2112 +                    kwargs[argname] = defaults[argname].get_default()
2113 +
2114 +        if not argname in argnames:
2115 +            # move argname into _kwargs parameter
2116 +            kwargs_to_pass[argname] = kwargs[argname]
2117 +            del kwargs[argname]
2118 +
2119 +    if kwargs_to_pass:
2120 +        kwargs['_kwargs'] = kwargs_to_pass
2121 +        if not allow_kwargs:
2122 +            raise ValueError(_(u'No argument named "%s"') % (
2123 +                kwargs_to_pass.keys()[0]))
2124 +
2125 +    return function(*fixed_args, **kwargs)
2126 +
2127 +
2128 +def parseAttributes(request, attrstring, endtoken=None, extension=None):
2129 +    """
2130 +    Parse a list of attributes and return a dict plus a possible
2131 +    error message.
2132 +    If extension is passed, it has to be a callable that returns
2133 +    a tuple (found_flag, msg). found_flag is whether it did find and process
2134 +    something, msg is '' when all was OK or any other string to return an error
2135 +    message.
2136 +
2137 +    @param request: the request object
2138 +    @param attrstring: string containing the attributes to be parsed
2139 +    @param endtoken: token terminating parsing
2140 +    @param extension: extension function -
2141 +                      gets called with the current token, the parser and the dict
2142 +    @rtype: dict, msg
2143 +    @return: a dict plus a possible error message
2144 +    """
2145 +    import shlex, StringIO
2146 +
2147 +    _ = request.getText
2148 +
2149 +    parser = shlex.shlex(StringIO.StringIO(attrstring))
2150 +    parser.commenters = ''
2151 +    msg = None
2152 +    attrs = {}
2153 +
2154 +    while not msg:
2155 +        try:
2156 +            key = parser.get_token()
2157 +        except ValueError, err:
2158 +            msg = str(err)
2159 +            break
2160 +        if not key:
2161 +            break
2162 +        if endtoken and key == endtoken:
2163 +            break
2164 +
2165 +        # call extension function with the current token, the parser, and the dict
2166 +        if extension:
2167 +            found_flag, msg = extension(key, parser, attrs)
2168 +            #logging.debug("%r = extension(%r, parser, %r)" % (msg, key, attrs))
2169 +            if found_flag:
2170 +                continue
2171 +            elif msg:
2172 +                break
2173 +            #else (we found nothing, but also didn't have an error msg) we just continue below:
2174 +
2175 +        try:
2176 +            eq = parser.get_token()
2177 +        except ValueError, err:
2178 +            msg = str(err)
2179 +            break
2180 +        if eq != "=":
2181 +            msg = _('Expected "=" to follow "%(token)s"') % {'token': key}
2182 +            break
2183 +
2184 +        try:
2185 +            val = parser.get_token()
2186 +        except ValueError, err:
2187 +            msg = str(err)
2188 +            break
2189 +        if not val:
2190 +            msg = _('Expected a value for key "%(token)s"') % {'token': key}
2191 +            break
2192 +
2193 +        key = escape(key) # make sure nobody cheats
2194 +
2195 +        # safely escape and quote value
2196 +        if val[0] in ["'", '"']:
2197 +            val = escape(val)
2198 +        else:
2199 +            val = '"%s"' % escape(val, 1)
2200 +
2201 +        attrs[key.lower()] = val
2202 +
2203 +    return attrs, msg or ''
2204 +
2205 +
2206 +class ParameterParser:
2207 +    """ MoinMoin macro parameter parser
2208 +
2209 +        Parses a given parameter string, separates the individual parameters
2210 +        and detects their type.
2211 +
2212 +        Possible parameter types are:
2213 +
2214 +        Name      | short  | example
2215 +        ----------------------------
2216 +         Integer  | i      | -374
2217 +         Float    | f      | 234.234 23.345E-23
2218 +         String   | s      | 'Stri\'ng'
2219 +         Boolean  | b      | 0 1 True false
2220 +         Name     |        | case_sensitive | converted to string
2221 +
2222 +        So say you want to parse three things, name, age and if the
2223 +        person is male or not:
2224 +
2225 +        The pattern will be: %(name)s%(age)i%(male)b
2226 +
2227 +        As a result, the returned dict will put the first value into
2228 +        male, second into age etc. If some argument is missing, it will
2229 +        get None as its value. This also means that all the identifiers
2230 +        in the pattern will exist in the dict, they will just have the
2231 +        value None if they were not specified by the caller.
2232 +
2233 +        So if we call it with the parameters as follows:
2234 +            ("John Smith", 18)
2235 +        this will result in the following dict:
2236 +            {"name": "John Smith", "age": 18, "male": None}
2237 +
2238 +        Another way of calling would be:
2239 +            ("John Smith", male=True)
2240 +        this will result in the following dict:
2241 +            {"name": "John Smith", "age": None, "male": True}
2242 +    """
2243 +
2244 +    def __init__(self, pattern):
2245 +        # parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
2246 +        name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)"
2247 +        int_re = r"(?P<int>-?\d+)"
2248 +        bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))"
2249 +        float_re = r"(?P<float>-?\d+\.\d+([eE][+-]?\d+)?)"
2250 +        string_re = (r"(?P<string>('([^']|(\'))*?')|" +
2251 +                                r'("([^"]|(\"))*?"))')
2252 +        name_re = name % "name"
2253 +        name_param_re = name % "name_param"
2254 +
2255 +        param_re = r"\s*(\s*%s\s*=\s*)?(%s|%s|%s|%s|%s)\s*(,|$)" % (
2256 +                   name_re, float_re, int_re, bool_re, string_re, name_param_re)
2257 +        self.param_re = re.compile(param_re, re.U)
2258 +        self._parse_pattern(pattern)
2259 +
2260 +    def _parse_pattern(self, pattern):
2261 +        param_re = r"(%(?P<name>\(.*?\))?(?P<type>[ibfs]{1,3}))|\|"
2262 +        i = 0
2263 +        # TODO: Optionals aren't checked.
2264 +        self.optional = []
2265 +        named = False
2266 +        self.param_list = []
2267 +        self.param_dict = {}
2268 +
2269 +        for match in re.finditer(param_re, pattern):
2270 +            if match.group() == "|":
2271 +                self.optional.append(i)
2272 +                continue
2273 +            self.param_list.append(match.group('type'))
2274 +            if match.group('name'):
2275 +                named = True
2276 +                self.param_dict[match.group('name')[1:-1]] = i
2277 +            elif named:
2278 +                raise ValueError("Named parameter expected")
2279 +            i += 1
2280 +
2281 +    def __str__(self):
2282 +        return "%s, %s, optional:%s" % (self.param_list, self.param_dict,
2283 +                                        self.optional)
2284 +
2285 +    def parse_parameters(self, params):
2286 +        # Default list/dict entries to None
2287 +        parameter_list = [None] * len(self.param_list)
2288 +        parameter_dict = dict([(key, None) for key in self.param_dict])
2289 +        check_list = [0] * len(self.param_list)
2290 +
2291 +        i = 0
2292 +        start = 0
2293 +        fixed_count = 0
2294 +        named = False
2295 +
2296 +        while start < len(params):
2297 +            match = re.match(self.param_re, params[start:])
2298 +            if not match:
2299 +                raise ValueError("malformed parameters")
2300 +            start += match.end()
2301 +            if match.group("int"):
2302 +                pvalue = int(match.group("int"))
2303 +                ptype = 'i'
2304 +            elif match.group("bool"):
2305 +                pvalue = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
2306 +                ptype = 'b'
2307 +            elif match.group("float"):
2308 +                pvalue = float(match.group("float"))
2309 +                ptype = 'f'
2310 +            elif match.group("string"):
2311 +                pvalue = match.group("string")[1:-1]
2312 +                ptype = 's'
2313 +            elif match.group("name_param"):
2314 +                pvalue = match.group("name_param")
2315 +                ptype = 'n'
2316 +            else:
2317 +                raise ValueError("Parameter parser code does not fit param_re regex")
2318 +
2319 +            name = match.group("name")
2320 +            if name:
2321 +                if name not in self.param_dict:
2322 +                    # TODO we should think on inheritance of parameters
2323 +                    raise ValueError("unknown parameter name '%s'" % name)
2324 +                nr = self.param_dict[name]
2325 +                if check_list[nr]:
2326 +                    raise ValueError("parameter '%s' specified twice" % name)
2327 +                else:
2328 +                    check_list[nr] = 1
2329 +                pvalue = self._check_type(pvalue, ptype, self.param_list[nr])
2330 +                parameter_dict[name] = pvalue
2331 +                parameter_list[nr] = pvalue
2332 +                named = True
2333 +            elif named:
2334 +                raise ValueError("only named parameters allowed after first named parameter")
2335 +            else:
2336 +                nr = i
2337 +                if nr not in self.param_dict.values():
2338 +                    fixed_count = nr + 1
2339 +                parameter_list[nr] = self._check_type(pvalue, ptype, self.param_list[nr])
2340 +
2341 +            # Let's populate and map our dictionary to what's been found
2342 +            for name in self.param_dict:
2343 +                tmp = self.param_dict[name]
2344 +                parameter_dict[name] = parameter_list[tmp]
2345 +
2346 +            i += 1
2347 +
2348 +        for i in range(fixed_count):
2349 +            parameter_dict[i] = parameter_list[i]
2350 +
2351 +        return fixed_count, parameter_dict
2352 +
2353 +    def _check_type(self, pvalue, ptype, format):
2354 +        if ptype == 'n' and 's' in format: # n as s
2355 +            return pvalue
2356 +
2357 +        if ptype in format:
2358 +            return pvalue # x -> x
2359 +
2360 +        if ptype == 'i':
2361 +            if 'f' in format:
2362 +                return float(pvalue) # i -> f
2363 +            elif 'b' in format:
2364 +                return pvalue != 0 # i -> b
2365 +        elif ptype == 's':
2366 +            if 'b' in format:
2367 +                if pvalue.lower() == 'false':
2368 +                    return False # s-> b
2369 +                elif pvalue.lower() == 'true':
2370 +                    return True # s-> b
2371 +                else:
2372 +                    raise ValueError('%r does not match format %r' % (pvalue, format))
2373 +
2374 +        if 's' in format: # * -> s
2375 +            return str(pvalue)
2376 +
2377 +        raise ValueError('%r does not match format %r' % (pvalue, format))
2378 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/plugins.py
2379 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
2380 +++ b/MoinMoin/wikiutil/plugins.py	Tue May 25 16:30:42 2010 -0300
2381 @@ -0,0 +1,172 @@
2382 +import os
2383 +from MoinMoin.util import pysupport, lock
2384 +from MoinMoin import config
2385 +
2386 +#############################################################################
2387 +### Plugins
2388 +#############################################################################
2389 +
2390 +class PluginError(Exception):
2391 +    """ Base class for plugin errors """
2392 +
2393 +class PluginMissingError(PluginError):
2394 +    """ Raised when a plugin is not found """
2395 +
2396 +class PluginAttributeError(PluginError):
2397 +    """ Raised when plugin does not contain an attribtue """
2398 +
2399 +
2400 +def importPlugin(cfg, kind, name, function="execute"):
2401 +    """ Import wiki or builtin plugin
2402 +
2403 +    Returns <function> attr from a plugin module <name>.
2404 +    If <function> attr is missing, raise PluginAttributeError.
2405 +    If <function> is None, return the whole module object.
2406 +
2407 +    If <name> plugin can not be imported, raise PluginMissingError.
2408 +
2409 +    kind may be one of 'action', 'formatter', 'macro', 'parser' or any other
2410 +    directory that exist in MoinMoin or data/plugin.
2411 +
2412 +    Wiki plugins will always override builtin plugins. If you want
2413 +    specific plugin, use either importWikiPlugin or importBuiltinPlugin
2414 +    directly.
2415 +
2416 +    @param cfg: wiki config instance
2417 +    @param kind: what kind of module we want to import
2418 +    @param name: the name of the module
2419 +    @param function: the function name
2420 +    @rtype: any object
2421 +    @return: "function" of module "name" of kind "kind", or None
2422 +    """
2423 +    try:
2424 +        return importWikiPlugin(cfg, kind, name, function)
2425 +    except PluginMissingError:
2426 +        return importBuiltinPlugin(kind, name, function)
2427 +
2428 +
2429 +def importWikiPlugin(cfg, kind, name, function="execute"):
2430 +    """ Import plugin from the wiki data directory
2431 +
2432 +    See importPlugin docstring.
2433 +    """
2434 +    plugins = wikiPlugins(kind, cfg)
2435 +    modname = plugins.get(name, None)
2436 +    if modname is None:
2437 +        raise PluginMissingError()
2438 +    moduleName = '%s.%s' % (modname, name)
2439 +    return importNameFromPlugin(moduleName, function)
2440 +
2441 +
2442 +def importBuiltinPlugin(kind, name, function="execute"):
2443 +    """ Import builtin plugin from MoinMoin package
2444 +
2445 +    See importPlugin docstring.
2446 +    """
2447 +    if not name in builtinPlugins(kind):
2448 +        raise PluginMissingError()
2449 +    moduleName = 'MoinMoin.%s.%s' % (kind, name)
2450 +    return importNameFromPlugin(moduleName, function)
2451 +
2452 +
2453 +def importNameFromPlugin(moduleName, name):
2454 +    """ Return <name> attr from <moduleName> module,
2455 +        raise PluginAttributeError if name does not exist.
2456 +
2457 +        If name is None, return the <moduleName> module object.
2458 +    """
2459 +    if name is None:
2460 +        fromlist = []
2461 +    else:
2462 +        fromlist = [name]
2463 +    module = __import__(moduleName, globals(), {}, fromlist)
2464 +    if fromlist:
2465 +        # module has the obj for module <moduleName>
2466 +        try:
2467 +            return getattr(module, name)
2468 +        except AttributeError:
2469 +            raise PluginAttributeError
2470 +    else:
2471 +        # module now has the toplevel module of <moduleName> (see __import__ docs!)
2472 +        components = moduleName.split('.')
2473 +        for comp in components[1:]:
2474 +            module = getattr(module, comp)
2475 +        return module
2476 +
2477 +
2478 +def builtinPlugins(kind):
2479 +    """ Gets a list of modules in MoinMoin.'kind'
2480 +
2481 +    @param kind: what kind of modules we look for
2482 +    @rtype: list
2483 +    @return: module names
2484 +    """
2485 +    modulename = "MoinMoin." + kind
2486 +    return pysupport.importName(modulename, "modules")
2487 +
2488 +
2489 +def wikiPlugins(kind, cfg):
2490 +    """
2491 +    Gets a dict containing the names of all plugins of @kind
2492 +    as the key and the containing module name as the value.
2493 +
2494 +    @param kind: what kind of modules we look for
2495 +    @rtype: dict
2496 +    @return: plugin name to containing module name mapping
2497 +    """
2498 +    # short-cut if we've loaded the dict already
2499 +    # (or already failed to load it)
2500 +    cache = cfg._site_plugin_lists
2501 +    if kind in cache:
2502 +        result = cache[kind]
2503 +    else:
2504 +        result = {}
2505 +        for modname in cfg._plugin_modules:
2506 +            try:
2507 +                module = pysupport.importName(modname, kind)
2508 +                packagepath = os.path.dirname(module.__file__)
2509 +                plugins = pysupport.getPluginModules(packagepath)
2510 +                for p in plugins:
2511 +                    if not p in result:
2512 +                        result[p] = '%s.%s' % (modname, kind)
2513 +            except AttributeError:
2514 +                pass
2515 +        cache[kind] = result
2516 +    return result
2517 +
2518 +
2519 +def getPlugins(kind, cfg):
2520 +    """ Gets a list of plugin names of kind
2521 +
2522 +    @param kind: what kind of modules we look for
2523 +    @rtype: list
2524 +    @return: module names
2525 +    """
2526 +    # Copy names from builtin plugins - so we dont destroy the value
2527 +    all_plugins = builtinPlugins(kind)[:]
2528 +
2529 +    # Add extension plugins without duplicates
2530 +    for plugin in wikiPlugins(kind, cfg):
2531 +        if plugin not in all_plugins:
2532 +            all_plugins.append(plugin)
2533 +
2534 +    return all_plugins
2535 +
2536 +
2537 +def searchAndImportPlugin(cfg, type, name, what=None):
2538 +    type2classname = {"parser": "Parser",
2539 +                      "formatter": "Formatter",
2540 +    }
2541 +    if what is None:
2542 +        what = type2classname[type]
2543 +    mt = MimeType(name)
2544 +    plugin = None
2545 +    for module_name in mt.module_name():
2546 +        try:
2547 +            plugin = importPlugin(cfg, type, module_name, what)
2548 +            break
2549 +        except PluginMissingError:
2550 +            pass
2551 +    else:
2552 +        raise PluginMissingError("Plugin not found! (%r %r %r)" % (type, name, what))
2553 +    return plugin
2554 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/storage.py
2555 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
2556 +++ b/MoinMoin/wikiutil/storage.py	Tue May 25 16:30:42 2010 -0300
2557 @@ -0,0 +1,209 @@
2558 +from MoinMoin import config
2559 +import re
2560 +
2561 +########################################################################
2562 +### Storage
2563 +########################################################################
2564 +
2565 +# Precompiled patterns for file name [un]quoting
2566 +UNSAFE = re.compile(r'[^a-zA-Z0-9_]+')
2567 +QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)')
2568 +
2569 +
2570 +def quoteWikinameFS(wikiname, charset=config.charset):
2571 +    """ Return file system representation of a Unicode WikiName.
2572 +
2573 +    Warning: will raise UnicodeError if wikiname can not be encoded using
2574 +    charset. The default value of config.charset, 'utf-8' can encode any
2575 +    character.
2576 +
2577 +    @param wikiname: Unicode string possibly containing non-ascii characters
2578 +    @param charset: charset to encode string
2579 +    @rtype: string
2580 +    @return: quoted name, safe for any file system
2581 +    """
2582 +    filename = wikiname.encode(charset)
2583 +
2584 +    quoted = []
2585 +    location = 0
2586 +    for needle in UNSAFE.finditer(filename):
2587 +        # append leading safe stuff
2588 +        quoted.append(filename[location:needle.start()])
2589 +        location = needle.end()
2590 +        # Quote and append unsafe stuff
2591 +        quoted.append('(')
2592 +        for character in needle.group():
2593 +            quoted.append('%02x' % ord(character))
2594 +        quoted.append(')')
2595 +
2596 +    # append rest of string
2597 +    quoted.append(filename[location:])
2598 +    return ''.join(quoted)
2599 +
2600 +
2601 +def unquoteWikiname(filename, charsets=[config.charset]):
2602 +    """ Return Unicode WikiName from quoted file name.
2603 +
2604 +    We raise an InvalidFileNameError if we find an invalid name, so the
2605 +    wiki could alarm the admin or suggest the user to rename a page.
2606 +    Invalid file names should never happen in normal use, but are rather
2607 +    cheap to find.
2608 +
2609 +    This function should be used only to unquote file names, not page
2610 +    names we receive from the user. These are handled in request by
2611 +    urllib.unquote, decodePagename and normalizePagename.
2612 +
2613 +    Todo: search clients of unquoteWikiname and check for exceptions.
2614 +
2615 +    @param filename: string using charset and possibly quoted parts
2616 +    @param charsets: list of charsets used by string
2617 +    @rtype: Unicode String
2618 +    @return: WikiName
2619 +    """
2620 +    ### Temporary fix start ###
2621 +    # From some places we get called with Unicode strings
2622 +    if isinstance(filename, type(u'')):
2623 +        filename = filename.encode(config.charset)
2624 +    ### Temporary fix end ###
2625 +
2626 +    parts = []
2627 +    start = 0
2628 +    for needle in QUOTED.finditer(filename):
2629 +        # append leading unquoted stuff
2630 +        parts.append(filename[start:needle.start()])
2631 +        start = needle.end()
2632 +        # Append quoted stuff
2633 +        group = needle.group(1)
2634 +        # Filter invalid filenames
2635 +        if (len(group) % 2 != 0):
2636 +            raise InvalidFileNameError(filename)
2637 +        try:
2638 +            for i in range(0, len(group), 2):
2639 +                byte = group[i:i+2]
2640 +                character = chr(int(byte, 16))
2641 +                parts.append(character)
2642 +        except ValueError:
2643 +            # byte not in hex, e.g 'xy'
2644 +            raise InvalidFileNameError(filename)
2645 +
2646 +    # append rest of string
2647 +    if start == 0:
2648 +        wikiname = filename
2649 +    else:
2650 +        parts.append(filename[start:len(filename)])
2651 +        wikiname = ''.join(parts)
2652 +
2653 +    # FIXME: This looks wrong, because at this stage "()" can be both errors
2654 +    # like open "(" without close ")", or unquoted valid characters in the file name.
2655 +    # Filter invalid filenames. Any left (xx) must be invalid
2656 +    #if '(' in wikiname or ')' in wikiname:
2657 +    #    raise InvalidFileNameError(filename)
2658 +
2659 +    wikiname = decodeUserInput(wikiname, charsets)
2660 +    return wikiname
2661 +
2662 +# time scaling
2663 +def timestamp2version(ts):
2664 +    """ Convert UNIX timestamp (may be float or int) to our version
2665 +        (long) int.
2666 +        We don't want to use floats, so we just scale by 1e6 to get
2667 +        an integer in usecs.
2668 +    """
2669 +    return long(ts*1000000L)
2670 +
2671 +def version2timestamp(v):
2672 +    """ Convert version number to UNIX timestamp (float).
2673 +        This must ONLY be used for display purposes.
2674 +    """
2675 +    return v / 1000000.0
2676 +
2677 +
2678 +# This is the list of meta attribute names to be treated as integers.
2679 +# IMPORTANT: do not use any meta attribute names with "-" (or any other chars
2680 +# invalid in python attribute names), use e.g. _ instead.
2681 +INTEGER_METAS = ['current', 'revision', # for page storage (moin 2.0)
2682 +                 'data_format_revision', # for data_dir format spec (use by mig scripts)
2683 +                ]
2684 +
2685 +class MetaDict(dict):
2686 +    """ store meta informations as a dict.
2687 +    """
2688 +    def __init__(self, metafilename, cache_directory):
2689 +        """ create a MetaDict from metafilename """
2690 +        dict.__init__(self)
2691 +        self.metafilename = metafilename
2692 +        self.dirty = False
2693 +        lock_dir = os.path.join(cache_directory, '__metalock__')
2694 +        self.rlock = lock.ReadLock(lock_dir, 60.0)
2695 +        self.wlock = lock.WriteLock(lock_dir, 60.0)
2696 +
2697 +        if not self.rlock.acquire(3.0):
2698 +            raise EnvironmentError("Could not lock in MetaDict")
2699 +        try:
2700 +            self._get_meta()
2701 +        finally:
2702 +            self.rlock.release()
2703 +
2704 +    def _get_meta(self):
2705 +        """ get the meta dict from an arbitrary filename.
2706 +            does not keep state, does uncached, direct disk access.
2707 +            @param metafilename: the name of the file to read
2708 +            @return: dict with all values or {} if empty or error
2709 +        """
2710 +
2711 +        try:
2712 +            metafile = codecs.open(self.metafilename, "r", "utf-8")
2713 +            meta = metafile.read() # this is much faster than the file's line-by-line iterator
2714 +            metafile.close()
2715 +        except IOError:
2716 +            meta = u''
2717 +        for line in meta.splitlines():
2718 +            key, value = line.split(':', 1)
2719 +            value = value.strip()
2720 +            if key in INTEGER_METAS:
2721 +                value = int(value)
2722 +            dict.__setitem__(self, key, value)
2723 +
2724 +    def _put_meta(self):
2725 +        """ put the meta dict into an arbitrary filename.
2726 +            does not keep or modify state, does uncached, direct disk access.
2727 +            @param metafilename: the name of the file to write
2728 +            @param metadata: dict of the data to write to the file
2729 +        """
2730 +        meta = []
2731 +        for key, value in self.items():
2732 +            if key in INTEGER_METAS:
2733 +                value = str(value)
2734 +            meta.append("%s: %s" % (key, value))
2735 +        meta = '\r\n'.join(meta)
2736 +
2737 +        metafile = codecs.open(self.metafilename, "w", "utf-8")
2738 +        metafile.write(meta)
2739 +        metafile.close()
2740 +        self.dirty = False
2741 +
2742 +    def sync(self, mtime_usecs=None):
2743 +        """ No-Op except for that parameter """
2744 +        if not mtime_usecs is None:
2745 +            self.__setitem__('mtime', str(mtime_usecs))
2746 +        # otherwise no-op
2747 +
2748 +    def __getitem__(self, key):
2749 +        """ We don't care for cache coherency here. """
2750 +        return dict.__getitem__(self, key)
2751 +
2752 +    def __setitem__(self, key, value):
2753 +        """ Sets a dictionary entry. """
2754 +        if not self.wlock.acquire(5.0):
2755 +            raise EnvironmentError("Could not lock in MetaDict")
2756 +        try:
2757 +            self._get_meta() # refresh cache
2758 +            try:
2759 +                oldvalue = dict.__getitem__(self, key)
2760 +            except KeyError:
2761 +                oldvalue = None
2762 +            if value != oldvalue:
2763 +                dict.__setitem__(self, key, value)
2764 +                self._put_meta() # sync cache
2765 +        finally:
2766 +            self.wlock.release()
2767 \ No newline at end of file
2768 diff -r 30b6a04fa95b -r 7d36954db686 MoinMoin/wikiutil/tickets.py
2769 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
2770 +++ b/MoinMoin/wikiutil/tickets.py	Tue May 25 16:30:42 2010 -0300
2771 @@ -0,0 +1,229 @@
2772 +from MoinMoin import log
2773 +logging = log.getLogger(__name__)
2774 +from MoinMoin import config
2775 +
2776 +########################################################################
2777 +### Tickets - usually used in forms to make sure that form submissions
2778 +### are in response to a form the same user got from moin for the same
2779 +### action and same page.
2780 +########################################################################
2781 +
2782 +def createTicket(request, tm=None, action=None, pagename=None):
2783 +    """ Create a ticket using a configured secret
2784 +
2785 +        @param tm: unix timestamp (optional, uses current time if not given)
2786 +        @param action: action name (optional, uses current action if not given)
2787 +                       Note: if you create a ticket for a form that calls another
2788 +                             action than the current one, you MUST specify the
2789 +                             action you call when posting the form.
2790 +        @param pagename: page name (optional, uses current page name if not given)
2791 +                       Note: if you create a ticket for a form that posts to another
2792 +                             page than the current one, you MUST specify the
2793 +                             page name you use when posting the form.
2794 +    """
2795 +
2796 +    from MoinMoin.support.python_compatibility import hmac_new
2797 +    if tm is None:
2798 +        # for age-check of ticket
2799 +        tm = "%010x" % time.time()
2800 +
2801 +    # make the ticket very specific:
2802 +    if pagename is None:
2803 +        try:
2804 +            pagename = request.page.page_name
2805 +        except:
2806 +            pagename = ''
2807 +
2808 +    if action is None:
2809 +        action = request.action
2810 +
2811 +    if request.session:
2812 +        # either a user is logged in or we have a anon session -
2813 +        # if session times out, ticket will get invalid
2814 +        sid = request.session.sid
2815 +    else:
2816 +        sid = ''
2817 +
2818 +    if request.user.valid:
2819 +        uid = request.user.id
2820 +    else:
2821 +        uid = ''
2822 +
2823 +    hmac_data = []
2824 +    for value in [tm, pagename, action, sid, uid, ]:
2825 +        if isinstance(value, unicode):
2826 +            value = value.encode('utf-8')
2827 +        hmac_data.append(value)
2828 +
2829 +    hmac = hmac_new(request.cfg.secrets['wikiutil/tickets'],
2830 +                    ''.join(hmac_data))
2831 +    return "%s.%s" % (tm, hmac.hexdigest())
2832 +
2833 +
2834 +def checkTicket(request, ticket):
2835 +    """Check validity of a previously created ticket"""
2836 +    try:
2837 +        timestamp_str = ticket.split('.')[0]
2838 +        timestamp = int(timestamp_str, 16)
2839 +    except ValueError:
2840 +        # invalid or empty ticket
2841 +        logging.debug("checkTicket: invalid or empty ticket %r" % ticket)
2842 +        return False
2843 +    now = time.time()
2844 +    if timestamp < now - 10 * 3600:
2845 +        # we don't accept tickets older than 10h
2846 +        logging.debug("checkTicket: too old ticket, timestamp %r" % timestamp)
2847 +        return False
2848 +    # Note: if the session timed out, that will also invalidate the ticket,
2849 +    #       if the ticket was created within a session.
2850 +    ourticket = createTicket(request, timestamp_str)
2851 +    logging.debug("checkTicket: returning %r, got %r, expected %r" % (ticket == ourticket, ticket, ourticket))
2852 +    return ticket == ourticket
2853 +
2854 +
2855 +def renderText(request, Parser, text):
2856 +    """executes raw wiki markup with all page elements"""
2857 +    import StringIO
2858 +    out = StringIO.StringIO()
2859 +    request.redirect(out)
2860 +    wikiizer = Parser(text, request)
2861 +    wikiizer.format(request.formatter, inhibit_p=True)
2862 +    result = out.getvalue()
2863 +    request.redirect()
2864 +    del out
2865 +    return result
2866 +
2867 +
2868 +def split_body(body):
2869 +    """ Extract the processing instructions / acl / etc. at the beginning of a page's body.
2870 +
2871 +        Hint: if you have a Page object p, you already have the result of this function in
2872 +              p.meta and (even better) parsed/processed stuff in p.pi.
2873 +
2874 +        Returns a list of (pi, restofline) tuples and a string with the rest of the body.
2875 +    """
2876 +    pi = {}
2877 +    while body.startswith('#'):
2878 +        try:
2879 +            line, body = body.split('\n', 1) # extract first line
2880 +        except ValueError:
2881 +            line = body
2882 +            body = ''
2883 +
2884 +        # end parsing on empty (invalid) PI
2885 +        if line == "#":
2886 +            body = line + '\n' + body
2887 +            break
2888 +
2889 +        if line[1] == '#':# two hash marks are a comment
2890 +            comment = line[2:]
2891 +            if not comment.startswith(' '):
2892 +                # we don't require a blank after the ##, so we put one there
2893 +                comment = ' ' + comment
2894 +                line = '##%s' % comment
2895 +
2896 +        verb, args = (line[1:] + ' ').split(' ', 1) # split at the first blank
2897 +        pi.setdefault(verb.lower(), []).append(args.strip())
2898 +
2899 +    for key, value in pi.iteritems():
2900 +        if len(value) == 1:
2901 +            pi[key] = value[0]
2902 +        else:
2903 +            pi[key] = tuple(value)
2904 +
2905 +    return pi, body
2906 +
2907 +
2908 +def add_metadata_to_body(metadata, data):
2909 +    """
2910 +    Adds the processing instructions to the data.
2911 +    """
2912 +    from MoinMoin.items import SIZE, EDIT_LOG
2913 +    READONLY_METADATA = [SIZE] + list(EDIT_LOCK) + EDIT_LOG
2914 +
2915 +    parsing_instructions = ["format", "language", "refresh", "acl",
2916 +                            "redirect", "deprecated", "openiduser",
2917 +                            "pragma", "internal", "external"]
2918 +
2919 +    metadata_data = ""
2920 +    for key, value in metadata.iteritems():
2921 +        if key not in parsing_instructions:
2922 +            continue
2923 +        # special handling for list metadata like acls
2924 +        if isinstance(value, list):
2925 +            for line in value:
2926 +                metadata_data += "#%s %s\n" % (key, line)
2927 +        else:
2928 +            metadata_data += "#%s %s\n" % (key, value)
2929 +    return metadata_data + data
2930 +
2931 +
2932 +def get_hostname(request, addr):
2933 +    """
2934 +    Looks up the hostname depending on the configuration.
2935 +    """
2936 +    if request.cfg.log_reverse_dns_lookups:
2937 +        import socket
2938 +        try:
2939 +            hostname = socket.gethostbyaddr(addr)[0]
2940 +            hostname = unicode(hostname, config.charset)
2941 +        except (socket.error, UnicodeError):
2942 +            hostname = addr
2943 +    else:
2944 +        hostname = addr
2945 +    return hostname
2946 +
2947 +
2948 +class Version(tuple):
2949 +    """
2950 +    Version objects store versions like 1.2.3-4.5alpha6 in a structured
2951 +    way and support version comparisons and direct version component access.
2952 +    1: major version (digits only)
2953 +    2: minor version (digits only)
2954 +    3: (maintenance) release version (digits only)
2955 +    4.5alpha6: optional additional version specification (str)
2956 +
2957 +    You can create a Version instance either by giving the components, like:
2958 +        Version(1,2,3,'4.5alpha6')
2959 +    or by giving the composite version string, like:
2960 +        Version(version="1.2.3-4.5alpha6").
2961 +
2962 +    Version subclasses tuple, so comparisons to tuples should work.
2963 +    Also, we inherit all the comparison logic from tuple base class.
2964 +    """
2965 +    VERSION_RE = re.compile(
2966 +        r"""(?P<major>\d+)
2967 +            \.
2968 +            (?P<minor>\d+)
2969 +            \.
2970 +            (?P<release>\d+)
2971 +            (-
2972 +             (?P<additional>.+)
2973 +            )?""",
2974 +            re.VERBOSE)
2975 +
2976 +    @classmethod
2977 +    def parse_version(cls, version):
2978 +        match = cls.VERSION_RE.match(version)
2979 +        if match is None:
2980 +            raise ValueError("Unexpected version string format: %r" % version)
2981 +        v = match.groupdict()
2982 +        return int(v['major']), int(v['minor']), int(v['release']), str(v['additional'] or '')
2983 +
2984 +    def __new__(cls, major=0, minor=0, release=0, additional='', version=None):
2985 +        if version:
2986 +            major, minor, release, additional = cls.parse_version(version)
2987 +        return tuple.__new__(cls, (major, minor, release, additional))
2988 +
2989 +    # properties for easy access of version components
2990 +    major = property(lambda self: self[0])
2991 +    minor = property(lambda self: self[1])
2992 +    release = property(lambda self: self[2])
2993 +    additional = property(lambda self: self[3])
2994 +
2995 +    def __str__(self):
2996 +        version_str = "%d.%d.%d" % (self.major, self.minor, self.release)
2997 +        if self.additional:
2998 +            version_str += "-%s" % self.additional
2999 +        return version_str
3000 +
3001 

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2010-05-25 19:39:42, 107.1 KB) [[attachment:wikiutil.patch]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.