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