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