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