1 # -*- coding: iso-8859-1 -*-
3 MoinMoin - Wiki Utility Functions
5 @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
10 @license: GNU GPL, see COPYING for details.
20 from MoinMoin import log
21 logging = log.getLogger(__name__)
23 from MoinMoin import config
24 from MoinMoin.util import pysupport, lock
25 from MoinMoin.support.python_compatibility import rsplit
26 from inspect import getargspec, isfunction, isclass, ismethod
28 from MoinMoin import web # needed so that next line works:
32 class InvalidFileNameError(Exception):
33 """ Called when we find an invalid file name """
36 # constants for page names
38 PARENT_PREFIX_LEN = len(PARENT_PREFIX)
40 CHILD_PREFIX_LEN = len(CHILD_PREFIX)
42 #############################################################################
43 ### Getting data from user/Sending data to user
44 #############################################################################
46 def decodeUnknownInput(text):
47 """ Decode unknown input, like text attachments
49 First we try utf-8 because it has special format, and it will decode
50 only utf-8 files. Then we try config.charset, then iso-8859-1 using
51 'replace'. We will never raise an exception, but may return junk
54 WARNING: Use this function only for data that you view, not for data
55 that you save in the wiki.
57 @param text: the text to decode, string
59 @return: decoded text (maybe wrong)
61 # Shortcut for unicode input
62 if isinstance(text, unicode):
66 return unicode(text, 'utf-8')
68 if config.charset not in ['utf-8', 'iso-8859-1']:
70 return unicode(text, config.charset)
73 return unicode(text, 'iso-8859-1', 'replace')
76 def decodeUserInput(s, charsets=[config.charset]):
78 Decodes input from the user.
80 @param s: the string to unquote
81 @param charsets: list of charsets to assume the string is in
83 @return: the unquoted string as unicode
85 for charset in charsets:
87 return s.decode(charset)
90 raise UnicodeError('The string %r cannot be decoded.' % s)
93 def url_quote(s, safe='/', want_unicode=None):
94 """ see werkzeug.utils.url_quote, we use a different safe param default value """
96 assert want_unicode is None
97 except AssertionError:
98 log.exception("call with deprecated want_unicode param, please fix caller")
99 return werkzeug.utils.url_quote(s, charset=config.charset, safe=safe)
101 def url_quote_plus(s, safe='/', want_unicode=None):
102 """ see werkzeug.utils.url_quote_plus, we use a different safe param default value """
104 assert want_unicode is None
105 except AssertionError:
106 log.exception("call with deprecated want_unicode param, please fix caller")
107 return werkzeug.utils.url_quote_plus(s, charset=config.charset, safe=safe)
109 def url_unquote(s, want_unicode=None):
110 """ see werkzeug.utils.url_unquote """
112 assert want_unicode is None
113 except AssertionError:
114 log.exception("call with deprecated want_unicode param, please fix caller")
115 return werkzeug.utils.url_unquote(s, charset=config.charset, errors='fallback:iso-8859-1')
118 def parseQueryString(qstr, want_unicode=None):
119 """ see werkzeug.utils.url_decode """
121 assert want_unicode is None
122 except AssertionError:
123 log.exception("call with deprecated want_unicode param, please fix caller")
124 return werkzeug.utils.url_decode(qstr, charset=config.charset, errors='fallback:iso-8859-1',
125 decode_keys=False, include_empty=False)
127 def makeQueryString(qstr=None, want_unicode=None, **kw):
128 """ Make a querystring from arguments.
130 kw arguments overide values in qstr.
132 If a string is passed in, it's returned verbatim and keyword parameters are ignored.
134 See also: werkzeug.utils.url_encode
136 @param qstr: dict to format as query string, using either ascii or unicode
137 @param kw: same as dict when using keywords, using ascii or unicode
139 @return: query string ready to use in a url
142 assert want_unicode is None
143 except AssertionError:
144 log.exception("call with deprecated want_unicode param, please fix caller")
147 elif isinstance(qstr, (str, unicode)):
149 if isinstance(qstr, dict):
151 return werkzeug.utils.url_encode(qstr, charset=config.charset, encode_keys=True)
153 raise ValueError("Unsupported argument type, should be dict.")
156 def quoteWikinameURL(pagename, charset=config.charset):
157 """ Return a url encoding of filename in plain ascii
159 Use urllib.quote to quote any character that is not always safe.
161 @param pagename: the original pagename (unicode)
162 @param charset: url text encoding, 'utf-8' recommended. Other charset
163 might not be able to encode the page name and raise
164 UnicodeError. (default config.charset ('utf-8')).
166 @return: the quoted filename, all unsafe characters encoded
168 # XXX please note that urllib.quote and werkzeug.utils.url_quote have
169 # XXX different defaults for safe=...
170 return werkzeug.utils.url_quote(pagename, charset=charset, safe='/')
173 escape = werkzeug.utils.escape
176 def clean_input(text, max_len=201):
178 replace CR, LF, TAB by whitespace
181 @param text: unicode text to clean
183 @return: cleaned text
185 # we only have input fields with max 200 chars, but spammers send us more
187 if length == 0 or length > max_len:
190 return text.translate(config.clean_input_translation_map)
193 def make_breakable(text, maxlen):
194 """ make a text breakable by inserting spaces into nonbreakable parts
196 text = text.split(" ")
199 if len(part) > maxlen:
201 newtext.append(part[:maxlen])
205 return " ".join(newtext)
207 ########################################################################
209 ########################################################################
211 # Precompiled patterns for file name [un]quoting
212 UNSAFE = re.compile(r'[^a-zA-Z0-9_]+')
213 QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)')
216 def quoteWikinameFS(wikiname, charset=config.charset):
217 """ Return file system representation of a Unicode WikiName.
219 Warning: will raise UnicodeError if wikiname can not be encoded using
220 charset. The default value of config.charset, 'utf-8' can encode any
223 @param wikiname: Unicode string possibly containing non-ascii characters
224 @param charset: charset to encode string
226 @return: quoted name, safe for any file system
228 filename = wikiname.encode(charset)
232 for needle in UNSAFE.finditer(filename):
233 # append leading safe stuff
234 quoted.append(filename[location:needle.start()])
235 location = needle.end()
236 # Quote and append unsafe stuff
238 for character in needle.group():
239 quoted.append('%02x' % ord(character))
242 # append rest of string
243 quoted.append(filename[location:])
244 return ''.join(quoted)
247 def unquoteWikiname(filename, charsets=[config.charset]):
248 """ Return Unicode WikiName from quoted file name.
250 We raise an InvalidFileNameError if we find an invalid name, so the
251 wiki could alarm the admin or suggest the user to rename a page.
252 Invalid file names should never happen in normal use, but are rather
255 This function should be used only to unquote file names, not page
256 names we receive from the user. These are handled in request by
257 urllib.unquote, decodePagename and normalizePagename.
259 Todo: search clients of unquoteWikiname and check for exceptions.
261 @param filename: string using charset and possibly quoted parts
262 @param charsets: list of charsets used by string
263 @rtype: Unicode String
266 ### Temporary fix start ###
267 # From some places we get called with Unicode strings
268 if isinstance(filename, type(u'')):
269 filename = filename.encode(config.charset)
270 ### Temporary fix end ###
274 for needle in QUOTED.finditer(filename):
275 # append leading unquoted stuff
276 parts.append(filename[start:needle.start()])
278 # Append quoted stuff
279 group = needle.group(1)
280 # Filter invalid filenames
281 if (len(group) % 2 != 0):
282 raise InvalidFileNameError(filename)
284 for i in range(0, len(group), 2):
286 character = chr(int(byte, 16))
287 parts.append(character)
289 # byte not in hex, e.g 'xy'
290 raise InvalidFileNameError(filename)
292 # append rest of string
296 parts.append(filename[start:len(filename)])
297 wikiname = ''.join(parts)
299 # FIXME: This looks wrong, because at this stage "()" can be both errors
300 # like open "(" without close ")", or unquoted valid characters in the file name.
301 # Filter invalid filenames. Any left (xx) must be invalid
302 #if '(' in wikiname or ')' in wikiname:
303 # raise InvalidFileNameError(filename)
305 wikiname = decodeUserInput(wikiname, charsets)
309 def timestamp2version(ts):
310 """ Convert UNIX timestamp (may be float or int) to our version
312 We don't want to use floats, so we just scale by 1e6 to get
315 return long(ts*1000000L) # has to be long for py 2.2.x
317 def version2timestamp(v):
318 """ Convert version number to UNIX timestamp (float).
319 This must ONLY be used for display purposes.
324 # This is the list of meta attribute names to be treated as integers.
325 # IMPORTANT: do not use any meta attribute names with "-" (or any other chars
326 # invalid in python attribute names), use e.g. _ instead.
327 INTEGER_METAS = ['current', 'revision', # for page storage (moin 2.0)
328 'data_format_revision', # for data_dir format spec (use by mig scripts)
331 class MetaDict(dict):
332 """ store meta informations as a dict.
334 def __init__(self, metafilename, cache_directory):
335 """ create a MetaDict from metafilename """
337 self.metafilename = metafilename
339 lock_dir = os.path.join(cache_directory, '__metalock__')
340 self.rlock = lock.ReadLock(lock_dir, 60.0)
341 self.wlock = lock.WriteLock(lock_dir, 60.0)
343 if not self.rlock.acquire(3.0):
344 raise EnvironmentError("Could not lock in MetaDict")
351 """ get the meta dict from an arbitrary filename.
352 does not keep state, does uncached, direct disk access.
353 @param metafilename: the name of the file to read
354 @return: dict with all values or {} if empty or error
358 metafile = codecs.open(self.metafilename, "r", "utf-8")
359 meta = metafile.read() # this is much faster than the file's line-by-line iterator
363 for line in meta.splitlines():
364 key, value = line.split(':', 1)
365 value = value.strip()
366 if key in INTEGER_METAS:
368 dict.__setitem__(self, key, value)
371 """ put the meta dict into an arbitrary filename.
372 does not keep or modify state, does uncached, direct disk access.
373 @param metafilename: the name of the file to write
374 @param metadata: dict of the data to write to the file
377 for key, value in self.items():
378 if key in INTEGER_METAS:
380 meta.append("%s: %s" % (key, value))
381 meta = '\r\n'.join(meta)
383 metafile = codecs.open(self.metafilename, "w", "utf-8")
388 def sync(self, mtime_usecs=None):
389 """ No-Op except for that parameter """
390 if not mtime_usecs is None:
391 self.__setitem__('mtime', str(mtime_usecs))
394 def __getitem__(self, key):
395 """ We don't care for cache coherency here. """
396 return dict.__getitem__(self, key)
398 def __setitem__(self, key, value):
399 """ Sets a dictionary entry. """
400 if not self.wlock.acquire(5.0):
401 raise EnvironmentError("Could not lock in MetaDict")
403 self._get_meta() # refresh cache
405 oldvalue = dict.__getitem__(self, key)
408 if value != oldvalue:
409 dict.__setitem__(self, key, value)
410 self._put_meta() # sync cache
415 # Quoting of wiki names, file names, etc. (in the wiki markup) -----------------------------------
417 # don't ever change this - DEPRECATED, only needed for 1.5 > 1.6 migration conversion
421 #############################################################################
423 #############################################################################
424 INTERWIKI_PAGE = "InterWikiMap"
426 def generate_file_list(request):
427 """ generates a list of all files. for internal use. """
429 # order is important here, the local intermap file takes
430 # precedence over the shared one, and is thus read AFTER
432 intermap_files = request.cfg.shared_intermap
433 if not isinstance(intermap_files, list):
434 intermap_files = [intermap_files]
436 intermap_files = intermap_files[:]
437 intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt"))
438 request.cfg.shared_intermap_files = [filename for filename in intermap_files
439 if filename and os.path.isfile(filename)]
442 def get_max_mtime(file_list, page):
443 """ Returns the highest modification time of the files in file_list and the
445 timestamps = [os.stat(filename).st_mtime for filename in file_list]
447 # exists() is cached and thus cheaper than mtime_usecs()
448 timestamps.append(version2timestamp(page.mtime_usecs()))
450 return max(timestamps)
452 return 0 # no files / pages there
454 def load_wikimap(request):
455 """ load interwiki map (once, and only on demand) """
456 from MoinMoin.Page import Page
458 now = int(time.time())
459 if getattr(request.cfg, "shared_intermap_files", None) is None:
460 generate_file_list(request)
463 _interwiki_list = request.cfg.cache.interwiki_list
464 old_mtime = request.cfg.cache.interwiki_mtime
465 if request.cfg.cache.interwiki_ts + (1*60) < now: # 1 minutes caching time
466 max_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
467 if max_mtime > old_mtime:
468 raise AttributeError # refresh cache
470 request.cfg.cache.interwiki_ts = now
471 except AttributeError:
475 for filename in request.cfg.shared_intermap_files:
476 f = codecs.open(filename, "r", config.charset)
477 lines.extend(f.readlines())
480 # add the contents of the InterWikiMap page
481 lines += Page(request, INTERWIKI_PAGE).get_raw_body().splitlines()
484 if not line or line[0] == '#':
487 line = "%s %s/InterWiki" % (line, request.script_root)
488 wikitag, urlprefix, dummy = line.split(None, 2)
492 _interwiki_list[wikitag] = urlprefix
496 # add own wiki as "Self" and by its configured name
497 _interwiki_list['Self'] = request.script_root + '/'
498 if request.cfg.interwikiname:
499 _interwiki_list[request.cfg.interwikiname] = request.script_root + '/'
502 request.cfg.cache.interwiki_list = _interwiki_list
503 request.cfg.cache.interwiki_ts = now
504 request.cfg.cache.interwiki_mtime = get_max_mtime(request.cfg.shared_intermap_files, Page(request, INTERWIKI_PAGE))
506 return _interwiki_list
508 def split_wiki(wikiurl):
512 *** DEPRECATED FUNCTION FOR OLD 1.5 SYNTAX - ONLY STILL HERE FOR THE 1.5 -> 1.6 MIGRATION ***
513 Use split_interwiki(), see below.
515 @param wikiurl: the url to split
519 # !!! use a regex here!
521 wikitag, tail = wikiurl.split(":", 1)
524 wikitag, tail = wikiurl.split("/", 1)
526 wikitag, tail = 'Self', wikiurl
529 def split_interwiki(wikiurl):
530 """ Split a interwiki name, into wikiname and pagename, e.g:
533 'FrontPage' -> "Self", "FrontPage"
537 can also be used for:
539 'attachment:filename with blanks.txt' -> "attachment", "filename with blanks.txt"
541 @param wikiurl: the url to split
543 @return: (wikiname, pagename)
546 wikiname, pagename = wikiurl.split(":", 1)
548 wikiname, pagename = 'Self', wikiurl
549 return wikiname, pagename
551 def resolve_wiki(request, wikiurl):
553 Resolve an interwiki link.
555 *** DEPRECATED FUNCTION FOR OLD 1.5 SYNTAX - ONLY STILL HERE FOR THE 1.5 -> 1.6 MIGRATION ***
556 Use resolve_interwiki(), see below.
558 @param request: the request object
559 @param wikiurl: the InterWiki:PageName link
561 @return: (wikitag, wikiurl, wikitail, err)
563 _interwiki_list = load_wikimap(request)
565 wikiname, pagename = split_wiki(wikiurl)
567 # return resolved url
568 if wikiname in _interwiki_list:
569 return (wikiname, _interwiki_list[wikiname], pagename, False)
571 return (wikiname, request.script_root, "/InterWiki", True)
573 def resolve_interwiki(request, wikiname, pagename):
574 """ Resolve an interwiki reference (wikiname:pagename).
576 @param request: the request object
577 @param wikiname: interwiki wiki name
578 @param pagename: interwiki page name
580 @return: (wikitag, wikiurl, wikitail, err)
582 _interwiki_list = load_wikimap(request)
583 if wikiname in _interwiki_list:
584 return (wikiname, _interwiki_list[wikiname], pagename, False)
586 return (wikiname, request.script_root, "/InterWiki", True)
588 def join_wiki(wikiurl, wikitail):
590 Add a (url_quoted) page name to an interwiki url.
592 Note: We can't know what kind of URL quoting a remote wiki expects.
593 We just use a utf-8 encoded string with standard URL quoting.
595 @param wikiurl: wiki url, maybe including a $PAGE placeholder
596 @param wikitail: page name
598 @return: generated URL of the page in the other wiki
600 wikitail = url_quote(wikitail)
601 if '$PAGE' in wikiurl:
602 return wikiurl.replace('$PAGE', wikitail)
604 return wikiurl + wikitail
607 #############################################################################
608 ### Page types (based on page names)
609 #############################################################################
611 def isSystemPage(request, pagename):
612 """ Is this a system page? Uses AllSystemPagesGroup internally.
614 @param request: the request object
615 @param pagename: the page name
617 @return: true if page is a system page
619 return (request.dicts.has_member('SystemPagesGroup', pagename) or
620 isTemplatePage(request, pagename))
623 def isTemplatePage(request, pagename):
624 """ Is this a template page?
626 @param pagename: the page name
628 @return: true if page is a template page
630 return request.cfg.cache.page_template_regexact.search(pagename) is not None
633 def isGroupPage(pagename, cfg):
634 """ Is this a name of group page?
636 @param pagename: the page name
638 @return: true if page is a form page
640 return cfg.cache.page_group_regexact.search(pagename) is not None
643 def filterCategoryPages(request, pagelist):
644 """ Return category pages in pagelist
646 WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use
647 getPageList with a filter function.
649 If you pass a list with a single pagename, either that is returned
650 or an empty list, thus you can use this function like a `isCategoryPage`
653 @param pagelist: a list of pages
655 @return: only the category pages of pagelist
657 func = request.cfg.cache.page_category_regexact.search
658 return [pn for pn in pagelist if func(pn)]
661 def getLocalizedPage(request, pagename): # was: getSysPage
662 """ Get a system page according to user settings and available translations.
664 We include some special treatment for the case that <pagename> is the
665 currently rendered page, as this is the case for some pages used very
666 often, like FrontPage, RecentChanges etc. - in that case we reuse the
667 already existing page object instead creating a new one.
669 @param request: the request object
670 @param pagename: the name of the page
672 @return: the page object of that system page, using a translated page,
675 from MoinMoin.Page import Page
676 i18n_name = request.getText(pagename)
678 if i18n_name != pagename:
679 if request.page and i18n_name == request.page.page_name:
680 # do not create new object for current page
681 i18n_page = request.page
682 if i18n_page.exists():
685 i18n_page = Page(request, i18n_name)
686 if i18n_page.exists():
689 # if we failed getting a translated version of <pagename>,
690 # we fall back to english
692 if request.page and pagename == request.page.page_name:
693 # do not create new object for current page
694 pageobj = request.page
696 pageobj = Page(request, pagename)
700 def getFrontPage(request):
701 """ Convenience function to get localized front page
703 @param request: current request
705 @return localized page_front_page, if there is a translation
707 return getLocalizedPage(request, request.cfg.page_front_page)
710 def getHomePage(request, username=None):
712 Get a user's homepage, or return None for anon users and
713 those who have not created a homepage.
715 DEPRECATED - try to use getInterwikiHomePage (see below)
717 @param request: the request object
718 @param username: the user's name
720 @return: user's homepage object - or None
722 from MoinMoin.Page import Page
723 # default to current user
724 if username is None and request.user.valid:
725 username = request.user.name
730 page = Page(request, username)
737 def getInterwikiHomePage(request, username=None):
739 Get a user's homepage.
741 cfg.user_homewiki influences behaviour of this:
742 'Self' does mean we store user homepage in THIS wiki.
743 When set to our own interwikiname, it behaves like with 'Self'.
745 'SomeOtherWiki' means we store user homepages in another wiki.
747 @param request: the request object
748 @param username: the user's name
749 @rtype: tuple (or None for anon users)
750 @return: (wikiname, pagename)
752 # default to current user
753 if username is None and request.user.valid:
754 username = request.user.name
756 return None # anon user
758 homewiki = request.cfg.user_homewiki
759 if homewiki == request.cfg.interwikiname:
762 return homewiki, username
765 def AbsPageName(context, pagename):
767 Return the absolute pagename for a (possibly) relative pagename.
769 @param context: name of the page where "pagename" appears on
770 @param pagename: the (possibly relative) page name
772 @return: the absolute page name
774 if pagename.startswith(PARENT_PREFIX):
775 while context and pagename.startswith(PARENT_PREFIX):
776 context = '/'.join(context.split('/')[:-1])
777 pagename = pagename[PARENT_PREFIX_LEN:]
778 pagename = '/'.join(filter(None, [context, pagename, ]))
779 elif pagename.startswith(CHILD_PREFIX):
781 pagename = context + '/' + pagename[CHILD_PREFIX_LEN:]
783 pagename = pagename[CHILD_PREFIX_LEN:]
786 def RelPageName(context, pagename):
788 Return the relative pagename for some context.
790 @param context: name of the page where "pagename" appears on
791 @param pagename: the absolute page name
793 @return: the relative page name
796 # special case, context is some "virtual root" page with name == ''
797 # every page is a subpage of this virtual root
798 return CHILD_PREFIX + pagename
799 elif pagename.startswith(context + CHILD_PREFIX):
801 return pagename[len(context):]
803 # some kind of sister/aunt
804 context_frags = context.split('/') # A, B, C, D, E
805 pagename_frags = pagename.split('/') # A, B, C, F
806 # first throw away common parents:
808 for cf, pf in zip(context_frags, pagename_frags):
813 context_frags = context_frags[common:] # D, E
814 pagename_frags = pagename_frags[common:] # F
815 go_up = len(context_frags)
816 return PARENT_PREFIX * go_up + '/'.join(pagename_frags)
819 def pagelinkmarkup(pagename, text=None):
820 """ return markup that can be used as link to page <pagename> """
821 from MoinMoin.parser.text_moin_wiki import Parser
822 if re.match(Parser.word_rule + "$", pagename, re.U|re.X) and \
823 (text is None or text == pagename):
826 if text is None or text == pagename:
830 return u'[[%s%s]]' % (pagename, text)
832 #############################################################################
834 #############################################################################
838 # OpenOffice 2.x & other open document stuff
839 '.odt': 'application/vnd.oasis.opendocument.text',
840 '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
841 '.odp': 'application/vnd.oasis.opendocument.presentation',
842 '.odg': 'application/vnd.oasis.opendocument.graphics',
843 '.odc': 'application/vnd.oasis.opendocument.chart',
844 '.odf': 'application/vnd.oasis.opendocument.formula',
845 '.odb': 'application/vnd.oasis.opendocument.database',
846 '.odi': 'application/vnd.oasis.opendocument.image',
847 '.odm': 'application/vnd.oasis.opendocument.text-master',
848 '.ott': 'application/vnd.oasis.opendocument.text-template',
849 '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
850 '.otp': 'application/vnd.oasis.opendocument.presentation-template',
851 '.otg': 'application/vnd.oasis.opendocument.graphics-template',
852 # some systems (like Mac OS X) don't have some of these:
853 '.patch': 'text/x-diff',
854 '.diff': 'text/x-diff',
855 '.py': 'text/x-python',
856 '.cfg': 'text/plain',
857 '.conf': 'text/plain',
858 '.irc': 'text/plain',
860 [mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
862 MIMETYPES_sanitize_mapping = {
863 # this stuff is text, but got application/* for unknown reasons
864 ('application', 'docbook+xml'): ('text', 'docbook'),
865 ('application', 'x-latex'): ('text', 'latex'),
866 ('application', 'x-tex'): ('text', 'tex'),
867 ('application', 'javascript'): ('text', 'javascript'),
870 MIMETYPES_spoil_mapping = {} # inverse mapping of above
871 for _key, _value in MIMETYPES_sanitize_mapping.items():
872 MIMETYPES_spoil_mapping[_value] = _key
875 class MimeType(object):
876 """ represents a mimetype like text/plain """
878 def __init__(self, mimestr=None, filename=None):
879 self.major = self.minor = None # sanitized mime type and subtype
880 self.params = {} # parameters like "charset" or others
881 self.charset = None # this stays None until we know for sure!
882 self.raw_mimestr = mimestr
885 self.parse_mimetype(mimestr)
887 self.parse_filename(filename)
889 def parse_filename(self, filename):
890 mtype, encoding = mimetypes.guess_type(filename)
892 mtype = 'application/octet-stream'
893 self.parse_mimetype(mtype)
895 def parse_mimetype(self, mimestr):
896 """ take a string like used in content-type and parse it into components,
897 alternatively it also can process some abbreviated string like "wiki"
899 parameters = mimestr.split(";")
900 parameters = [p.strip() for p in parameters]
901 mimetype, parameters = parameters[0], parameters[1:]
902 mimetype = mimetype.split('/')
903 if len(mimetype) >= 2:
904 major, minor = mimetype[:2] # we just ignore more than 2 parts
906 major, minor = self.parse_format(mimetype[0])
907 self.major = major.lower()
908 self.minor = minor.lower()
909 for param in parameters:
910 key, value = param.split('=')
911 if value[0] == '"' and value[-1] == '"': # remove quotes
913 self.params[key.lower()] = value
914 if 'charset' in self.params:
915 self.charset = self.params['charset'].lower()
918 def parse_format(self, format):
919 """ maps from what we currently use on-page in a #format xxx processing
920 instruction to a sanitized mimetype major, minor tuple.
921 can also be user later for easier entry by the user, so he can just
922 type "wiki" instead of "text/moin-wiki".
924 format = format.lower()
925 if format in config.parser_text_mimetype:
926 mimetype = 'text', format
929 'wiki': ('text', 'moin-wiki'),
930 'irc': ('text', 'irssi'),
933 mimetype = mapping[format]
935 mimetype = 'text', 'x-%s' % format
939 """ convert to some representation that makes sense - this is not necessarily
940 conformant to /etc/mime.types or IANA listing, but if something is
941 readable text, we will return some text/* mimetype, not application/*,
942 because we need text/plain as fallback and not application/octet-stream.
944 self.major, self.minor = MIMETYPES_sanitize_mapping.get((self.major, self.minor), (self.major, self.minor))
947 """ this returns something conformant to /etc/mime.type or IANA as a string,
948 kind of inverse operation of sanitize(), but doesn't change self
950 major, minor = MIMETYPES_spoil_mapping.get((self.major, self.minor), (self.major, self.minor))
951 return self.content_type(major, minor)
953 def content_type(self, major=None, minor=None, charset=None, params=None):
954 """ return a string suitable for Content-Type header
956 major = major or self.major
957 minor = minor or self.minor
958 params = params or self.params or {}
960 charset = charset or self.charset or params.get('charset', config.charset)
961 params['charset'] = charset
962 mimestr = "%s/%s" % (major, minor)
963 params = ['%s="%s"' % (key.lower(), value) for key, value in params.items()]
964 params.insert(0, mimestr)
965 return "; ".join(params)
968 """ return a string major/minor only, no params """
969 return "%s/%s" % (self.major, self.minor)
971 def module_name(self):
972 """ convert this mimetype to a string useable as python module name,
973 we yield the exact module name first and then proceed to shorter
974 module names (useful for falling back to them, if the more special
975 module is not found) - e.g. first "text_python", next "text".
976 Finally, we yield "application_octet_stream" as the most general
978 Hint: the fallback handler module for text/* should be implemented
979 in module "text" (not "text_plain")
981 mimetype = self.mime_type()
982 modname = mimetype.replace("/", "_").replace("-", "_").replace(".", "_")
983 fragments = modname.split('_')
984 for length in range(len(fragments), 1, -1):
985 yield "_".join(fragments[:length])
986 yield self.raw_mimestr
988 yield "application_octet_stream"
991 #############################################################################
993 #############################################################################
995 class PluginError(Exception):
996 """ Base class for plugin errors """
998 class PluginMissingError(PluginError):
999 """ Raised when a plugin is not found """
1001 class PluginAttributeError(PluginError):
1002 """ Raised when plugin does not contain an attribtue """
1005 def importPlugin(cfg, kind, name, function="execute"):
1006 """ Import wiki or builtin plugin
1008 Returns <function> attr from a plugin module <name>.
1009 If <function> attr is missing, raise PluginAttributeError.
1010 If <function> is None, return the whole module object.
1012 If <name> plugin can not be imported, raise PluginMissingError.
1014 kind may be one of 'action', 'formatter', 'macro', 'parser' or any other
1015 directory that exist in MoinMoin or data/plugin.
1017 Wiki plugins will always override builtin plugins. If you want
1018 specific plugin, use either importWikiPlugin or importBuiltinPlugin
1021 @param cfg: wiki config instance
1022 @param kind: what kind of module we want to import
1023 @param name: the name of the module
1024 @param function: the function name
1026 @return: "function" of module "name" of kind "kind", or None
1029 return importWikiPlugin(cfg, kind, name, function)
1030 except PluginMissingError:
1031 return importBuiltinPlugin(kind, name, function)
1034 def importWikiPlugin(cfg, kind, name, function="execute"):
1035 """ Import plugin from the wiki data directory
1037 See importPlugin docstring.
1039 plugins = wikiPlugins(kind, cfg)
1040 modname = plugins.get(name, None)
1042 raise PluginMissingError()
1043 moduleName = '%s.%s' % (modname, name)
1044 return importNameFromPlugin(moduleName, function)
1047 def importBuiltinPlugin(kind, name, function="execute"):
1048 """ Import builtin plugin from MoinMoin package
1050 See importPlugin docstring.
1052 if not name in builtinPlugins(kind):
1053 raise PluginMissingError()
1054 moduleName = 'MoinMoin.%s.%s' % (kind, name)
1055 return importNameFromPlugin(moduleName, function)
1058 def importNameFromPlugin(moduleName, name):
1059 """ Return <name> attr from <moduleName> module,
1060 raise PluginAttributeError if name does not exist.
1062 If name is None, return the <moduleName> module object.
1068 module = __import__(moduleName, globals(), {}, fromlist)
1070 # module has the obj for module <moduleName>
1072 return getattr(module, name)
1073 except AttributeError:
1074 raise PluginAttributeError
1076 # module now has the toplevel module of <moduleName> (see __import__ docs!)
1077 components = moduleName.split('.')
1078 for comp in components[1:]:
1079 module = getattr(module, comp)
1083 def builtinPlugins(kind):
1084 """ Gets a list of modules in MoinMoin.'kind'
1086 @param kind: what kind of modules we look for
1088 @return: module names
1090 modulename = "MoinMoin." + kind
1091 return pysupport.importName(modulename, "modules")
1094 def wikiPlugins(kind, cfg):
1096 Gets a dict containing the names of all plugins of @kind
1097 as the key and the containing module name as the value.
1099 @param kind: what kind of modules we look for
1101 @return: plugin name to containing module name mapping
1103 # short-cut if we've loaded the dict already
1104 # (or already failed to load it)
1105 cache = cfg._site_plugin_lists
1107 result = cache[kind]
1110 for modname in cfg._plugin_modules:
1112 module = pysupport.importName(modname, kind)
1113 packagepath = os.path.dirname(module.__file__)
1114 plugins = pysupport.getPluginModules(packagepath)
1117 result[p] = '%s.%s' % (modname, kind)
1118 except AttributeError:
1120 cache[kind] = result
1124 def getPlugins(kind, cfg):
1125 """ Gets a list of plugin names of kind
1127 @param kind: what kind of modules we look for
1129 @return: module names
1131 # Copy names from builtin plugins - so we dont destroy the value
1132 all_plugins = builtinPlugins(kind)[:]
1134 # Add extension plugins without duplicates
1135 for plugin in wikiPlugins(kind, cfg):
1136 if plugin not in all_plugins:
1137 all_plugins.append(plugin)
1142 def searchAndImportPlugin(cfg, type, name, what=None):
1143 type2classname = {"parser": "Parser",
1144 "formatter": "Formatter",
1147 what = type2classname[type]
1150 for module_name in mt.module_name():
1152 plugin = importPlugin(cfg, type, module_name, what)
1154 except PluginMissingError:
1157 raise PluginMissingError("Plugin not found!")
1161 #############################################################################
1163 #############################################################################
1165 def getParserForExtension(cfg, extension):
1167 Returns the Parser class of the parser fit to handle a file
1168 with the given extension. The extension should be in the same
1169 format as os.path.splitext returns it (i.e. with the dot).
1170 Returns None if no parser willing to handle is found.
1171 The dict of extensions is cached in the config object.
1173 @param cfg: the Config instance for the wiki in question
1174 @param extension: the filename extension including the dot
1176 @returns: the parser class or None
1178 if not hasattr(cfg.cache, 'EXT_TO_PARSER'):
1180 for pname in getPlugins('parser', cfg):
1182 Parser = importPlugin(cfg, 'parser', pname, 'Parser')
1183 except PluginMissingError:
1185 if hasattr(Parser, 'extensions'):
1186 exts = Parser.extensions
1187 if isinstance(exts, list):
1188 for ext in Parser.extensions:
1190 elif str(exts) == '*':
1192 cfg.cache.EXT_TO_PARSER = etp
1193 cfg.cache.EXT_TO_PARSER_DEFAULT = etd
1195 return cfg.cache.EXT_TO_PARSER.get(extension, cfg.cache.EXT_TO_PARSER_DEFAULT)
1198 #############################################################################
1199 ### Parameter parsing
1200 #############################################################################
1202 class BracketError(Exception):
1205 class BracketUnexpectedCloseError(BracketError):
1206 def __init__(self, bracket):
1207 self.bracket = bracket
1208 BracketError.__init__(self, "Unexpected closing bracket %s" % bracket)
1210 class BracketMissingCloseError(BracketError):
1211 def __init__(self, bracket):
1212 self.bracket = bracket
1213 BracketError.__init__(self, "Missing closing bracket %s" % bracket)
1217 Trivial container-class holding a single character for
1218 the possible prefixes for parse_quoted_separated_ext
1219 and implementing rich equal comparison.
1221 def __init__(self, prefix):
1222 self.prefix = prefix
1224 def __eq__(self, other):
1225 return isinstance(other, ParserPrefix) and other.prefix == self.prefix
1228 return '<ParserPrefix(%s)>' % self.prefix.encode('utf-8')
1230 def parse_quoted_separated_ext(args, separator=None, name_value_separator=None,
1231 brackets=None, seplimit=0, multikey=False,
1232 prefixes=None, quotes='"'):
1234 Parses the given string according to the other parameters.
1236 Items can be quoted with any character from the quotes parameter
1237 and each quote can be escaped by doubling it, the separator and
1238 name_value_separator can both be quoted, when name_value_separator
1239 is set then the name can also be quoted.
1241 Values that are not given are returned as None, while the
1242 empty string as a value can be achieved by quoting it.
1244 If a name or value does not start with a quote, then the quote
1245 looses its special meaning for that name or value, unless it
1246 starts with one of the given prefixes (the parameter is unicode
1247 containing all allowed prefixes.) The prefixes will be returned
1248 as ParserPrefix() instances in the first element of the tuple
1249 for that particular argument.
1251 If multiple separators follow each other, this is treated as
1252 having None arguments inbetween, that is also true for when
1253 space is used as separators (when separator is None), filter
1254 them out afterwards.
1256 The function can also do bracketing, i.e. parse expressions
1257 that contain things like
1258 "(a (a b))" to ['(', 'a', ['(', 'a', 'b']],
1259 in this case, as in this example, the returned list will
1260 contain sub-lists and the brackets parameter must be a list
1261 of opening and closing brackets, e.g.
1262 brackets = ['()', '<>']
1263 Each sub-list's first item is the opening bracket used for
1265 Nesting will be observed between the different types of
1266 brackets given. If bracketing doesn't match, a BracketError
1267 instance is raised with a 'bracket' property indicating the
1268 type of missing or unexpected bracket, the instance will be
1269 either of the class BracketMissingCloseError or of the class
1270 BracketUnexpectedCloseError.
1272 If multikey is True (along with setting name_value_separator),
1273 then the returned tuples for (key, value) pairs can also have
1275 "a=b=c" -> ('a', 'b', 'c')
1277 @param args: arguments to parse
1278 @param separator: the argument separator, defaults to None, meaning any
1279 space separates arguments
1280 @param name_value_separator: separator for name=value, default '=',
1281 name=value keywords not parsed if evaluates to False
1282 @param brackets: a list of two-character strings giving
1283 opening and closing brackets
1284 @param seplimit: limits the number of parsed arguments
1285 @param multikey: multiple keys allowed for a single value
1287 @returns: list of unicode strings and tuples containing
1288 unicode strings, or lists containing the same for
1292 assert name_value_separator is None or name_value_separator != separator
1293 assert name_value_separator is None or len(name_value_separator) == 1
1294 if not isinstance(args, unicode):
1295 raise TypeError('args must be unicode')
1297 result = [] # result list
1298 cur = [None] # current item
1299 quoted = None # we're inside quotes, indicates quote character used
1300 skipquote = 0 # next quote is a quoted quote
1301 noquote = False # no quotes expected because word didn't start with one
1302 seplimit_reached = False # number of separators exhausted
1303 separator_count = 0 # number of separators encountered
1304 SPACE = [' ', '\t', ]
1305 nextitemsep = [separator] # used for skipping trailing space
1306 SPACE = [' ', '\t', ]
1307 if separator is None:
1308 nextitemsep = SPACE[:]
1311 nextitemsep = [separator] # used for skipping trailing space
1312 separators = [separator]
1313 if name_value_separator:
1314 nextitemsep.append(name_value_separator)
1316 # bracketing support
1320 matchingbracket = {}
1322 for o, c in brackets:
1323 assert not o in opening
1325 assert not c in closing
1327 matchingbracket[o] = c
1329 def additem(result, cur, separator_count, nextitemsep):
1333 result.append(tuple(cur))
1336 separator_count += 1
1337 seplimit_reached = False
1338 if seplimit and separator_count >= seplimit:
1339 seplimit_reached = True
1340 nextitemsep = [n for n in nextitemsep if n in separators]
1342 return cur, noquote, separator_count, seplimit_reached, nextitemsep
1351 if not separator is None and not quoted and char in SPACE:
1353 # accumulate all space
1354 while char in SPACE and idx < max - 1:
1358 # remove space if args end with it
1359 if char in SPACE and idx == max - 1:
1361 # remove space at end of argument
1362 if char in nextitemsep:
1365 if len(cur) and cur[-1]:
1366 cur[-1] = cur[-1] + spaces
1367 elif not quoted and char == name_value_separator:
1368 if multikey or len(cur) == 1:
1374 cur[-1] += name_value_separator
1378 elif not quoted and not seplimit_reached and char in separators:
1379 (cur, noquote, separator_count, seplimit_reached,
1380 nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1381 elif not quoted and not noquote and char in quotes:
1382 if len(cur) and cur[-1] is None:
1386 elif char == quoted and not skipquote:
1388 skipquote = 2 # will be decremented right away
1391 elif not quoted and char in opening:
1392 while len(cur) and cur[-1] is None:
1394 (cur, noquote, separator_count, seplimit_reached,
1395 nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1396 bracketstack.append((matchingbracket[char], result))
1398 elif not quoted and char in closing:
1399 while len(cur) and cur[-1] is None:
1401 (cur, noquote, separator_count, seplimit_reached,
1402 nextitemsep) = additem(result, cur, separator_count, nextitemsep)
1404 if not bracketstack:
1405 raise BracketUnexpectedCloseError(char)
1406 expected, oldresult = bracketstack[-1]
1407 if not expected == char:
1408 raise BracketUnexpectedCloseError(char)
1409 del bracketstack[-1]
1410 oldresult.append(result)
1412 elif not quoted and prefixes and char in prefixes and cur == [None]:
1413 cur = [ParserPrefix(char)]
1428 raise BracketMissingCloseError(bracketstack[-1][0])
1435 cur[-1] = quoted + cur[-1]
1439 additem(result, cur, separator_count, nextitemsep)
1443 def parse_quoted_separated(args, separator=',', name_value=True, seplimit=0):
1447 name_value_separator = '='
1451 name_value_separator = None
1453 l = parse_quoted_separated_ext(args, separator=separator,
1454 name_value_separator=name_value_separator,
1457 if isinstance(item, tuple):
1461 keywords[key] = value
1462 positional = trailing
1464 positional.append(item)
1467 return result, keywords, trailing
1470 def get_bool(request, arg, name=None, default=None):
1472 For use with values returned from parse_quoted_separated or given
1473 as macro parameters, return a boolean from a unicode string.
1474 Valid input is 'true'/'false', 'yes'/'no' and '1'/'0' or None for
1477 @param request: A request instance
1478 @param arg: The argument, may be None or a unicode string
1479 @param name: Name of the argument, for error messages
1480 @param default: default value if arg is None
1481 @rtype: boolean or None
1482 @returns: the boolean value of the string according to above rules
1486 assert default is None or isinstance(default, bool)
1489 elif not isinstance(arg, unicode):
1490 raise TypeError('Argument must be None or unicode')
1492 if arg in [u'0', u'false', u'no']:
1494 elif arg in [u'1', u'true', u'yes']:
1499 _('Argument "%s" must be a boolean value, not "%s"') % (
1503 _('Argument must be a boolean value, not "%s"') % arg)
1506 def get_int(request, arg, name=None, default=None):
1508 For use with values returned from parse_quoted_separated or given
1509 as macro parameters, return an integer from a unicode string
1510 containing the decimal representation of a number.
1511 None is a valid input and yields the default value.
1513 @param request: A request instance
1514 @param arg: The argument, may be None or a unicode string
1515 @param name: Name of the argument, for error messages
1516 @param default: default value if arg is None
1518 @returns: the integer value of the string (or default value)
1521 assert default is None or isinstance(default, (int, long))
1524 elif not isinstance(arg, unicode):
1525 raise TypeError('Argument must be None or unicode')
1531 _('Argument "%s" must be an integer value, not "%s"') % (
1535 _('Argument must be an integer value, not "%s"') % arg)
1538 def get_float(request, arg, name=None, default=None):
1540 For use with values returned from parse_quoted_separated or given
1541 as macro parameters, return a float from a unicode string.
1542 None is a valid input and yields the default value.
1544 @param request: A request instance
1545 @param arg: The argument, may be None or a unicode string
1546 @param name: Name of the argument, for error messages
1547 @param default: default return value if arg is None
1548 @rtype: float or None
1549 @returns: the float value of the string (or default value)
1552 assert default is None or isinstance(default, (int, long, float))
1555 elif not isinstance(arg, unicode):
1556 raise TypeError('Argument must be None or unicode')
1562 _('Argument "%s" must be a floating point value, not "%s"') % (
1566 _('Argument must be a floating point value, not "%s"') % arg)
1569 def get_complex(request, arg, name=None, default=None):
1571 For use with values returned from parse_quoted_separated or given
1572 as macro parameters, return a complex from a unicode string.
1573 None is a valid input and yields the default value.
1575 @param request: A request instance
1576 @param arg: The argument, may be None or a unicode string
1577 @param name: Name of the argument, for error messages
1578 @param default: default return value if arg is None
1579 @rtype: complex or None
1580 @returns: the complex value of the string (or default value)
1583 assert default is None or isinstance(default, (int, long, float, complex))
1586 elif not isinstance(arg, unicode):
1587 raise TypeError('Argument must be None or unicode')
1589 # allow writing 'i' instead of 'j'
1590 arg = arg.replace('i', 'j').replace('I', 'j')
1595 _('Argument "%s" must be a complex value, not "%s"') % (
1599 _('Argument must be a complex value, not "%s"') % arg)
1602 def get_unicode(request, arg, name=None, default=None):
1604 For use with values returned from parse_quoted_separated or given
1605 as macro parameters, return a unicode string from a unicode string.
1606 None is a valid input and yields the default value.
1608 @param request: A request instance
1609 @param arg: The argument, may be None or a unicode string
1610 @param name: Name of the argument, for error messages
1611 @param default: default return value if arg is None;
1612 @rtype: unicode or None
1613 @returns: the unicode string (or default value)
1615 assert default is None or isinstance(default, unicode)
1618 elif not isinstance(arg, unicode):
1619 raise TypeError('Argument must be None or unicode')
1624 def get_choice(request, arg, name=None, choices=[None]):
1626 For use with values returned from parse_quoted_separated or given
1627 as macro parameters, return a unicode string that must be in the
1628 choices given. None is a valid input and yields first of the valid
1631 @param request: A request instance
1632 @param arg: The argument, may be None or a unicode string
1633 @param name: Name of the argument, for error messages
1634 @param choices: the possible choices
1635 @rtype: unicode or None
1636 @returns: the unicode string (or default value)
1638 assert isinstance(choices, (tuple, list))
1641 elif not isinstance(arg, unicode):
1642 raise TypeError('Argument must be None or unicode')
1643 elif not arg in choices:
1647 _('Argument "%s" must be one of "%s", not "%s"') % (
1648 name, '", "'.join(choices), arg))
1651 _('Argument must be one of "%s", not "%s"') % (
1652 '", "'.join(choices), arg))
1659 Base class for new argument parsers for
1660 invoke_extension_function.
1665 def parse_argument(self, s):
1667 Parse the argument given in s (a string) and return
1668 the argument for the extension function.
1670 raise NotImplementedError
1672 def get_default(self):
1674 Return the default for this argument.
1676 raise NotImplementedError
1679 class UnitArgument(IEFArgument):
1681 Argument class for invoke_extension_function that forces
1682 having any of the specified units given for a value.
1684 Note that the default unit is "mm".
1686 Use, for example, "UnitArgument('7mm', float, ['%', 'mm'])".
1688 If the defaultunit parameter is given, any argument that
1689 can be converted into the given argtype is assumed to have
1690 the default unit. NOTE: This doesn't work with a choice
1691 (tuple or list) argtype.
1693 def __init__(self, default, argtype, units=['mm'], defaultunit=None):
1695 Initialise a UnitArgument giving the default,
1696 argument type and the permitted units.
1698 IEFArgument.__init__(self)
1699 self._units = list(units)
1700 self._units.sort(lambda x, y: len(y) - len(x))
1701 self._type = argtype
1702 self._defaultunit = defaultunit
1703 assert defaultunit is None or defaultunit in units
1704 if default is not None:
1705 self._default = self.parse_argument(default)
1707 self._default = None
1709 def parse_argument(self, s):
1710 for unit in self._units:
1711 if s.endswith(unit):
1712 ret = (self._type(s[:len(s) - len(unit)]), unit)
1714 if self._defaultunit is not None:
1716 return (self._type(s), self._defaultunit)
1719 units = ', '.join(self._units)
1720 ## XXX: how can we translate this?
1721 raise ValueError("Invalid unit in value %s (allowed units: %s)" % (s, units))
1723 def get_default(self):
1724 return self._default
1729 Wrap a type in this class and give it as default argument
1730 for a function passed to invoke_extension_function() in
1731 order to get generic checking that the argument is given.
1733 def __init__(self, argtype):
1735 Initialise a required_arg
1736 @param argtype: the type the argument should have
1738 if not (argtype in (bool, int, long, float, complex, unicode) or
1739 isinstance(argtype, (IEFArgument, tuple, list))):
1740 raise TypeError("argtype must be a valid type")
1741 self.argtype = argtype
1744 def invoke_extension_function(request, function, args, fixed_args=[]):
1746 Parses arguments for an extension call and calls the extension
1747 function with the arguments.
1749 If the macro function has a default value that is a bool,
1750 int, long, float or unicode object, then the given value
1751 is converted to the type of that default value before passing
1752 it to the macro function. That way, macros need not call the
1753 wikiutil.get_* functions for any arguments that have a default.
1755 @param request: the request object
1756 @param function: the function to invoke
1757 @param args: unicode string with arguments (or evaluating to False)
1758 @param fixed_args: fixed arguments to pass as the first arguments
1759 @returns: the return value from the function called
1762 def _convert_arg(request, value, default, name=None):
1764 Using the get_* functions, convert argument to the type of the default
1765 if that is any of bool, int, long, float or unicode; if the default
1766 is the type itself then convert to that type (keeps None) or if the
1767 default is a list require one of the list items.
1769 In other cases return the value itself.
1771 # if extending this, extend required_arg as well!
1772 if isinstance(default, bool):
1773 return get_bool(request, value, name, default)
1774 elif isinstance(default, (int, long)):
1775 return get_int(request, value, name, default)
1776 elif isinstance(default, float):
1777 return get_float(request, value, name, default)
1778 elif isinstance(default, complex):
1779 return get_complex(request, value, name, default)
1780 elif isinstance(default, unicode):
1781 return get_unicode(request, value, name, default)
1782 elif isinstance(default, (tuple, list)):
1783 return get_choice(request, value, name, default)
1784 elif default is bool:
1785 return get_bool(request, value, name)
1786 elif default is int or default is long:
1787 return get_int(request, value, name)
1788 elif default is float:
1789 return get_float(request, value, name)
1790 elif default is complex:
1791 return get_complex(request, value, name)
1792 elif isinstance(default, IEFArgument):
1793 # defaults handled later
1796 return default.parse_argument(value)
1797 elif isinstance(default, required_arg):
1798 if isinstance(default.argtype, (tuple, list)):
1799 # treat choice specially and return None if no choice
1800 # is given in the value
1801 choices = [None] + list(default.argtype)
1802 return get_choice(request, value, name, choices)
1804 return _convert_arg(request, value, default.argtype, name)
1807 assert isinstance(fixed_args, (list, tuple))
1816 assert isinstance(args, unicode)
1818 positional, keyword, trailing = parse_quoted_separated(args)
1822 kwargs[str(kw)] = keyword[kw]
1823 except UnicodeEncodeError:
1824 kwargs_to_pass[kw] = keyword[kw]
1826 trailing_args.extend(trailing)
1831 if isfunction(function) or ismethod(function):
1832 argnames, varargs, varkw, defaultlist = getargspec(function)
1833 elif isclass(function):
1835 varkw, defaultlist) = getargspec(function.__init__.im_func)
1837 raise TypeError('function must be a function, method or class')
1840 if ismethod(function) or isclass(function):
1841 argnames = argnames[1:]
1843 fixed_argc = len(fixed_args)
1844 argnames = argnames[fixed_argc:]
1845 argc = len(argnames)
1849 # if the fixed parameters have defaults too...
1850 if argc < len(defaultlist):
1851 defaultlist = defaultlist[fixed_argc:]
1852 defstart = argc - len(defaultlist)
1855 # reverse to be able to pop() things off
1856 positional.reverse()
1857 allow_kwargs = False
1858 allow_trailing = False
1859 # convert all arguments to keyword arguments,
1860 # fill all arguments that weren't given with None
1861 for idx in range(argc):
1862 argname = argnames[idx]
1863 if argname == '_kwargs':
1866 if argname == '_trailing_args':
1867 allow_trailing = True
1870 kwargs[argname] = positional.pop()
1871 if not argname in kwargs:
1872 kwargs[argname] = None
1874 defaults[argname] = defaultlist[idx - defstart]
1877 if not allow_trailing:
1878 raise ValueError(_('Too many arguments'))
1879 trailing_args.extend(positional)
1882 if not allow_trailing:
1883 raise ValueError(_('Cannot have arguments without name following'
1884 ' named arguments'))
1885 kwargs['_trailing_args'] = trailing_args
1887 # type-convert all keyword arguments to the type
1888 # that the default value indicates
1889 for argname in kwargs.keys()[:]:
1890 if argname in defaults:
1891 # the value of 'argname' from kwargs will be put into the
1892 # macro's 'argname' argument, so convert that giving the
1893 # name to the converter so the user is told which argument
1894 # went wrong (if it does)
1895 kwargs[argname] = _convert_arg(request, kwargs[argname],
1896 defaults[argname], argname)
1897 if kwargs[argname] is None:
1898 if isinstance(defaults[argname], required_arg):
1899 raise ValueError(_('Argument "%s" is required') % argname)
1900 if isinstance(defaults[argname], IEFArgument):
1901 kwargs[argname] = defaults[argname].get_default()
1903 if not argname in argnames:
1904 # move argname into _kwargs parameter
1905 kwargs_to_pass[argname] = kwargs[argname]
1909 kwargs['_kwargs'] = kwargs_to_pass
1910 if not allow_kwargs:
1911 raise ValueError(_(u'No argument named "%s"') % (
1912 kwargs_to_pass.keys()[0]))
1914 return function(*fixed_args, **kwargs)
1917 def parseAttributes(request, attrstring, endtoken=None, extension=None):
1919 Parse a list of attributes and return a dict plus a possible
1921 If extension is passed, it has to be a callable that returns
1922 a tuple (found_flag, msg). found_flag is whether it did find and process
1923 something, msg is '' when all was OK or any other string to return an error
1926 @param request: the request object
1927 @param attrstring: string containing the attributes to be parsed
1928 @param endtoken: token terminating parsing
1929 @param extension: extension function -
1930 gets called with the current token, the parser and the dict
1932 @return: a dict plus a possible error message
1934 import shlex, StringIO
1938 parser = shlex.shlex(StringIO.StringIO(attrstring))
1939 parser.commenters = ''
1945 key = parser.get_token()
1946 except ValueError, err:
1951 if endtoken and key == endtoken:
1954 # call extension function with the current token, the parser, and the dict
1956 found_flag, msg = extension(key, parser, attrs)
1957 #logging.debug("%r = extension(%r, parser, %r)" % (msg, key, attrs))
1962 #else (we found nothing, but also didn't have an error msg) we just continue below:
1965 eq = parser.get_token()
1966 except ValueError, err:
1970 msg = _('Expected "=" to follow "%(token)s"') % {'token': key}
1974 val = parser.get_token()
1975 except ValueError, err:
1979 msg = _('Expected a value for key "%(token)s"') % {'token': key}
1982 key = escape(key) # make sure nobody cheats
1984 # safely escape and quote value
1985 if val[0] in ["'", '"']:
1988 val = '"%s"' % escape(val, 1)
1990 attrs[key.lower()] = val
1992 return attrs, msg or ''
1995 class ParameterParser:
1996 """ MoinMoin macro parameter parser
1998 Parses a given parameter string, separates the individual parameters
1999 and detects their type.
2001 Possible parameter types are:
2003 Name | short | example
2004 ----------------------------
2006 Float | f | 234.234 23.345E-23
2007 String | s | 'Stri\'ng'
2008 Boolean | b | 0 1 True false
2009 Name | | case_sensitive | converted to string
2011 So say you want to parse three things, name, age and if the
2012 person is male or not:
2014 The pattern will be: %(name)s%(age)i%(male)b
2016 As a result, the returned dict will put the first value into
2017 male, second into age etc. If some argument is missing, it will
2018 get None as its value. This also means that all the identifiers
2019 in the pattern will exist in the dict, they will just have the
2020 value None if they were not specified by the caller.
2022 So if we call it with the parameters as follows:
2024 this will result in the following dict:
2025 {"name": "John Smith", "age": 18, "male": None}
2027 Another way of calling would be:
2028 ("John Smith", male=True)
2029 this will result in the following dict:
2030 {"name": "John Smith", "age": None, "male": True}
2033 def __init__(self, pattern):
2034 # parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
2035 name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)"
2036 int_re = r"(?P<int>-?\d+)"
2037 bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))"
2038 float_re = r"(?P<float>-?\d+\.\d+([eE][+-]?\d+)?)"
2039 string_re = (r"(?P<string>('([^']|(\'))*?')|" +
2040 r'("([^"]|(\"))*?"))')
2041 name_re = name % "name"
2042 name_param_re = name % "name_param"
2044 param_re = r"\s*(\s*%s\s*=\s*)?(%s|%s|%s|%s|%s)\s*(,|$)" % (
2045 name_re, float_re, int_re, bool_re, string_re, name_param_re)
2046 self.param_re = re.compile(param_re, re.U)
2047 self._parse_pattern(pattern)
2049 def _parse_pattern(self, pattern):
2050 param_re = r"(%(?P<name>\(.*?\))?(?P<type>[ibfs]{1,3}))|\|"
2052 # TODO: Optionals aren't checked.
2055 self.param_list = []
2056 self.param_dict = {}
2058 for match in re.finditer(param_re, pattern):
2059 if match.group() == "|":
2060 self.optional.append(i)
2062 self.param_list.append(match.group('type'))
2063 if match.group('name'):
2065 self.param_dict[match.group('name')[1:-1]] = i
2067 raise ValueError("Named parameter expected")
2071 return "%s, %s, optional:%s" % (self.param_list, self.param_dict,
2074 def parse_parameters(self, params):
2075 # Default list/dict entries to None
2076 parameter_list = [None] * len(self.param_list)
2077 parameter_dict = dict([(key, None) for key in self.param_dict])
2078 check_list = [0] * len(self.param_list)
2085 while start < len(params):
2086 match = re.match(self.param_re, params[start:])
2088 raise ValueError("malformed parameters")
2089 start += match.end()
2090 if match.group("int"):
2091 pvalue = int(match.group("int"))
2093 elif match.group("bool"):
2094 pvalue = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
2096 elif match.group("float"):
2097 pvalue = float(match.group("float"))
2099 elif match.group("string"):
2100 pvalue = match.group("string")[1:-1]
2102 elif match.group("name_param"):
2103 pvalue = match.group("name_param")
2106 raise ValueError("Parameter parser code does not fit param_re regex")
2108 name = match.group("name")
2110 if name not in self.param_dict:
2111 # TODO we should think on inheritance of parameters
2112 raise ValueError("unknown parameter name '%s'" % name)
2113 nr = self.param_dict[name]
2115 raise ValueError("parameter '%s' specified twice" % name)
2118 pvalue = self._check_type(pvalue, ptype, self.param_list[nr])
2119 parameter_dict[name] = pvalue
2120 parameter_list[nr] = pvalue
2123 raise ValueError("only named parameters allowed after first named parameter")
2126 if nr not in self.param_dict.values():
2127 fixed_count = nr + 1
2128 parameter_list[nr] = self._check_type(pvalue, ptype, self.param_list[nr])
2130 # Let's populate and map our dictionary to what's been found
2131 for name in self.param_dict:
2132 tmp = self.param_dict[name]
2133 parameter_dict[name] = parameter_list[tmp]
2137 for i in range(fixed_count):
2138 parameter_dict[i] = parameter_list[i]
2140 return fixed_count, parameter_dict
2142 def _check_type(self, pvalue, ptype, format):
2143 if ptype == 'n' and 's' in format: # n as s
2147 return pvalue # x -> x
2151 return float(pvalue) # i -> f
2153 return pvalue != 0 # i -> b
2156 if pvalue.lower() == 'false':
2157 return False # s-> b
2158 elif pvalue.lower() == 'true':
2161 raise ValueError('%r does not match format %r' % (pvalue, format))
2163 if 's' in format: # * -> s
2166 raise ValueError('%r does not match format %r' % (pvalue, format))
2169 #############################################################################
2171 #############################################################################
2172 def normalize_pagename(name, cfg):
2173 """ Normalize page name
2175 Prevent creating page names with invisible characters or funny
2176 whitespace that might confuse the users or abuse the wiki, or
2177 just does not make sense.
2179 Restrict even more group pages, so they can be used inside acl lines.
2181 @param name: page name, unicode
2183 @return: decoded and sanitized page name
2185 # Strip invalid characters
2186 name = config.page_invalid_chars_regex.sub(u'', name)
2188 # Split to pages and normalize each one
2189 pages = name.split(u'/')
2192 # Ignore empty or whitespace only pages
2193 if not page or page.isspace():
2196 # Cleanup group pages.
2197 # Strip non alpha numeric characters, keep white space
2198 if isGroupPage(page, cfg):
2199 page = u''.join([c for c in page
2200 if c.isalnum() or c.isspace()])
2202 # Normalize white space. Each name can contain multiple
2203 # words separated with only one space. Split handle all
2204 # 30 unicode spaces (isspace() == True)
2205 page = u' '.join(page.split())
2207 normalized.append(page)
2209 # Assemble components into full pagename
2210 name = u'/'.join(normalized)
2213 def taintfilename(basename):
2215 Make a filename that is supposed to be a plain name secure, i.e.
2216 remove any possible path components that compromise our system.
2218 @param basename: (possibly unsafe) filename
2220 @return: (safer) filename
2222 for x in (os.pardir, ':', '/', '\\', '<', '>'):
2223 basename = basename.replace(x, '_')
2228 def mapURL(request, url):
2230 Map URLs according to 'cfg.url_mappings'.
2236 # check whether we have to map URLs
2237 if request.cfg.url_mappings:
2238 # check URL for the configured prefixes
2239 for prefix in request.cfg.url_mappings:
2240 if url.startswith(prefix):
2241 # substitute prefix with replacement value
2242 return request.cfg.url_mappings[prefix] + url[len(prefix):]
2244 # return unchanged url
2248 def getUnicodeIndexGroup(name):
2250 Return a group letter for `name`, which must be a unicode string.
2251 Currently supported: Hangul Syllables (U+AC00 - U+D7AF)
2253 @param name: a string
2255 @return: group letter or None
2258 if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables
2259 return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588)
2261 return c.upper() # we put lower and upper case words into the same index group
2264 def isStrictWikiname(name, word_re=re.compile(ur"^(?:[%(u)s][%(l)s]+){2,}$" % {'u': config.chars_upper, 'l': config.chars_lower})):
2266 Check whether this is NOT an extended name.
2268 @param name: the wikiname in question
2270 @return: true if name matches the word_re
2272 return word_re.match(name)
2275 def is_URL(arg, schemas=config.url_schemas):
2276 """ Return True if arg is a URL (with a schema given in the schemas list).
2278 Note: there are not that many requirements for generic URLs, basically
2279 the only mandatory requirement is the ':' between schema and rest.
2280 Schema itself could be anything, also the rest (but we only support some
2281 schemas, as given in config.url_schemas, so it is a bit less ambiguous).
2285 for schema in schemas:
2286 if arg.startswith(schema + ':'):
2293 Is this a picture's url?
2295 @param url: the url in question
2297 @return: true if url points to a picture
2299 extpos = url.rfind(".") + 1
2300 return extpos > 1 and url[extpos:].lower() in config.browser_supported_images
2303 def link_tag(request, params, text=None, formatter=None, on=None, **kw):
2306 TODO: cleanup css_class
2308 @param request: the request object
2309 @param params: parameter string appended to the URL after the scriptname/
2310 @param text: text / inner part of the <a>...</a> link - does NOT get
2311 escaped, so you can give HTML here and it will be used verbatim
2312 @param formatter: the formatter object to use
2313 @param on: opening/closing tag only
2314 @keyword attrs: additional attrs (HTMLified string) (removed in 1.5.3)
2316 @return: formatted link tag
2318 if formatter is None:
2319 formatter = request.html_formatter
2320 if 'css_class' in kw:
2321 css_class = kw['css_class']
2322 del kw['css_class'] # one time is enough
2325 id = kw.get('id', None)
2326 name = kw.get('name', None)
2328 text = params # default
2330 url = "%s/%s" % (request.script_root, params)
2331 # formatter.url will escape the url part
2333 tag = formatter.url(on, url, css_class, **kw)
2335 tag = (formatter.url(1, url, css_class, **kw) +
2336 formatter.rawHTML(text) +
2338 else: # this shouldn't be used any more:
2339 if on is not None and not on:
2344 attrs += ' class="%s"' % css_class
2346 attrs += ' id="%s"' % id
2348 attrs += ' name="%s"' % name
2349 tag = '<a%s href="%s/%s">' % (attrs, request.script_root, params)
2351 tag = "%s%s</a>" % (tag, text)
2352 logging.warning("wikiutil.link_tag called without formatter and without request.html_formatter. tag=%r" % (tag, ))
2355 def containsConflictMarker(text):
2356 """ Returns true if there is a conflict marker in the text. """
2357 return "/!\\ '''Edit conflict" in text
2359 def pagediff(request, pagename1, rev1, pagename2, rev2, **kw):
2361 Calculate the "diff" between two page contents.
2363 @param pagename1: name of first page
2364 @param rev1: revision of first page
2365 @param pagename2: name of second page
2366 @param rev2: revision of second page
2367 @keyword ignorews: if 1: ignore pure-whitespace changes.
2369 @return: lines of diff output
2371 from MoinMoin.Page import Page
2372 from MoinMoin.util import diff_text
2373 lines1 = Page(request, pagename1, rev=rev1).getlines()
2374 lines2 = Page(request, pagename2, rev=rev2).getlines()
2376 lines = diff_text.diff(lines1, lines2, **kw)
2379 def anchor_name_from_text(text):
2381 Generate an anchor name from the given text.
2382 This function generates valid HTML IDs matching: [A-Za-z][A-Za-z0-9:_.-]*
2383 Note: this transformation has a special feature: when you feed it with a
2384 valid ID/name, it will return it without modification (identity
2387 quoted = urllib.quote_plus(text.encode('utf-7'), safe=':')
2388 res = quoted.replace('%', '.').replace('+', '_')
2389 if not res[:1].isalpha():
2393 def split_anchor(pagename):
2395 Split a pagename that (optionally) has an anchor into the real pagename
2396 and the anchor part. If there is no anchor, it returns an empty string
2399 Note: if pagename contains a # (as part of the pagename, not as anchor),
2400 you can use a trick to make it work nevertheless: just append a
2402 "C##" returns ("C#", "")
2403 "Problem #1#" returns ("Problem #1", "")
2405 TODO: We shouldn't deal with composite pagename#anchor strings, but keep
2407 Current approach: [[pagename#anchor|label|attr=val,&qarg=qval]]
2408 Future approach: [[pagename|label|attr=val,&qarg=qval,#anchor]]
2409 The future approach will avoid problems when there is a # in the
2410 pagename part (and no anchor). Also, we need to append #anchor
2411 at the END of the generated URL (AFTER the query string).
2413 parts = rsplit(pagename, '#', 1)
2419 ########################################################################
2420 ### Tickets - used by RenamePage and DeletePage
2421 ########################################################################
2423 def createTicket(request, tm=None, action=None):
2424 """ Create a ticket using a configured secret
2426 @param tm: unix timestamp (optional, uses current time if not given)
2427 @param action: action name (optional, uses current action if not given)
2428 Note: if you create a ticket for a form that calls another
2429 action than the current one, you MUST specify the
2430 action you call when posting the form.
2433 from MoinMoin.support.python_compatibility import hash_new
2435 tm = "%010x" % time.time()
2437 # make the ticket specific to the page and action:
2439 pagename = quoteWikinameURL(request.page.page_name)
2445 action = request.action
2449 secret = request.cfg.secrets['wikiutil/tickets']
2450 digest = hash_new('sha1', secret)
2452 ticket = "%s.%s.%s" % (tm, pagename, action)
2453 digest.update(ticket)
2455 return "%s.%s" % (ticket, digest.hexdigest())
2458 def checkTicket(request, ticket):
2459 """Check validity of a previously created ticket"""
2461 timestamp_str = ticket.split('.')[0]
2462 timestamp = int(timestamp_str, 16)
2464 # invalid or empty ticket
2465 logging.debug("checkTicket: invalid or empty ticket %r" % ticket)
2468 if timestamp < now - 10 * 3600:
2469 # we don't accept tickets older than 10h
2470 logging.debug("checkTicket: too old ticket, timestamp %r" % timestamp)
2472 ourticket = createTicket(request, timestamp_str)
2473 logging.debug("checkTicket: returning %r, got %r, expected %r" % (ticket == ourticket, ticket, ourticket))
2474 return ticket == ourticket
2477 def renderText(request, Parser, text):
2478 """executes raw wiki markup with all page elements"""
2480 out = StringIO.StringIO()
2481 request.redirect(out)
2482 wikiizer = Parser(text, request)
2483 wikiizer.format(request.formatter, inhibit_p=True)
2484 result = out.getvalue()
2489 def get_processing_instructions(body):
2490 """ Extract the processing instructions / acl / etc. at the beginning of a page's body.
2492 Hint: if you have a Page object p, you already have the result of this function in
2493 p.meta and (even better) parsed/processed stuff in p.pi.
2495 Returns a list of (pi, restofline) tuples and a string with the rest of the body.
2498 while body.startswith('#'):
2500 line, body = body.split('\n', 1) # extract first line
2505 # end parsing on empty (invalid) PI
2507 body = line + '\n' + body
2510 if line[1] == '#':# two hash marks are a comment
2512 if not comment.startswith(' '):
2513 # we don't require a blank after the ##, so we put one there
2514 comment = ' ' + comment
2515 line = '##%s' % comment
2517 verb, args = (line[1:] + ' ').split(' ', 1) # split at the first blank
2518 pi.append((verb.lower(), args.strip()))