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