comparison MoinMoin/wikiutil.py @ 0:77665d8e2254

tag of nonpublic@localhost--archive/moin--enterprise--1.5--base-0 (automatically generated log message) imported from: moin--main--1.5--base-0
author Thomas Waldmann <tw-public@gmx.de>
date Thu, 22 Sep 2005 15:09:50 +0000
parents
children 883dd7b59979
comparison
equal deleted inserted replaced
-1:000000000000 0:77665d8e2254
1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - Wiki Utility Functions
4
5 @copyright: 2000 - 2004 by Jürgen Hermann <jh@web.de>
6 @license: GNU GPL, see COPYING for details.
7 """
8
9 import os, re, difflib, urllib
10
11 from MoinMoin import util, version, config
12 from MoinMoin.util import pysupport
13
14 # Exceptions
15 class InvalidFileNameError(Exception):
16 """ Called when we find an invalid file name """
17 pass
18
19 # constants for page names
20 PARENT_PREFIX = "../"
21 PARENT_PREFIX_LEN = len(PARENT_PREFIX)
22 CHILD_PREFIX = "/"
23 CHILD_PREFIX_LEN = len(CHILD_PREFIX)
24
25 #############################################################################
26 ### Getting data from user/Sending data to user
27 #############################################################################
28
29 def decodeWindowsPath(text):
30 """ Decode Windows path names correctly. This is needed because many CGI
31 servers follow the RFC recommendation and re-encode the path_info variable
32 according to the file system semantics.
33
34 @param text: the text to decode, string
35 @rtype: unicode
36 @return: decoded text
37 """
38
39 import locale
40 import codecs
41 cur_charset = locale.getdefaultlocale()[1]
42 try:
43 return unicode(text, 'utf-8')
44 except UnicodeError:
45 try:
46 return unicode(text, cur_charset, 'replace')
47 except LookupError:
48 return unicode(text, 'iso-8859-1', 'replace')
49
50 def decodeUnknownInput(text):
51 """ Decode unknown input, like text attachments
52
53 First we try utf-8 because it has special format, and it will decode
54 only utf-8 files. Then we try config.charset, then iso-8859-1 using
55 'replace'. We will never raise an exception, but may return junk
56 data.
57
58 WARNING: Use this function only for data that you view, not for data
59 that you save in the wiki.
60
61 @param text: the text to decode, string
62 @rtype: unicode
63 @return: decoded text (maybe wrong)
64 """
65 # Shortcut for unicode input
66 if isinstance(text, unicode):
67 return text
68
69 try:
70 return unicode(text, 'utf-8')
71 except UnicodeError:
72 if config.charset not in ['utf-8', 'iso-8859-1']:
73 try:
74 return unicode(text, config.charset)
75 except UnicodeError:
76 pass
77 return unicode(text, 'iso-8859-1', 'replace')
78
79
80 def decodeUserInput(s, charsets=[config.charset]):
81 """
82 Decodes input from the user.
83
84 @param s: the string to unquote
85 @param charset: the charset to assume the string is in
86 @rtype: unicode
87 @return: the unquoted string as unicode
88 """
89 for charset in charsets:
90 try:
91 return s.decode(charset)
92 except UnicodeError:
93 pass
94 raise UnicodeError('The string %r cannot be decoded.' % s)
95
96
97 # FIXME: better name would be quoteURL, as this is useful for any
98 # string, not only wiki names.
99 def quoteWikinameURL(pagename, charset=config.charset):
100 """ Return a url encoding of filename in plain ascii
101
102 Use urllib.quote to quote any character that is not always safe.
103
104 @param pagename: the original pagename (unicode)
105 @charset: url text encoding, 'utf-8' recommended. Other charsert
106 might not be able to encode the page name and raise
107 UnicodeError. (default config.charset ('utf-8')).
108 @rtype: string
109 @return: the quoted filename, all unsafe characters encoded
110 """
111 pagename = pagename.replace(u' ', u'_')
112 pagename = pagename.encode(charset)
113 return urllib.quote(pagename)
114
115
116 def escape(s, quote=0):
117 """ Escape possible html tags
118
119 Replace special characters '&', '<' and '>' by SGML entities.
120 (taken from cgi.escape so we don't have to include that, even if we
121 don't use cgi at all)
122
123 FIXME: should return string or unicode?
124
125 @param s: (unicode) string to escape
126 @param quote: bool, should transform '\"' to '&quot;'
127 @rtype: (unicode) string
128 @return: escaped version of s
129 """
130 if not isinstance(s, (str, unicode)):
131 s = str(s)
132
133 # Must first replace &
134 s = s.replace("&", "&amp;")
135
136 # Then other...
137 s = s.replace("<", "&lt;")
138 s = s.replace(">", "&gt;")
139 if quote:
140 s = s.replace('"', "&quot;")
141 return s
142
143
144 ########################################################################
145 ### Storage
146 ########################################################################
147
148 # FIXME: These functions might be moved to storage module, when we have
149 # one. Then they will be called transparently whenever a page is saved.
150
151 # Precompiled patterns for file name [un]quoting
152 UNSAFE = re.compile(r'[^a-zA-Z0-9_]+')
153 QUOTED = re.compile(r'\(([a-fA-F0-9]+)\)')
154
155
156 # FIXME: better name would be quoteWikiname
157 def quoteWikinameFS(wikiname, charset=config.charset):
158 """ Return file system representation of a Unicode WikiName.
159
160 Warning: will raise UnicodeError if wikiname can not be encoded using
161 charset. The default value of config.charset, 'utf-8' can encode any
162 character.
163
164 @param wikiname: Unicode string possibly containing non-ascii characters
165 @param charset: charset to encode string
166 @rtype: string
167 @return: quoted name, safe for any file system
168 """
169 wikiname = wikiname.replace(u' ', u'_') # " " -> "_"
170 filename = wikiname.encode(charset)
171
172 quoted = []
173 location = 0
174 for needle in UNSAFE.finditer(filename):
175 # append leading safe stuff
176 quoted.append(filename[location:needle.start()])
177 location = needle.end()
178 # Quote and append unsafe stuff
179 quoted.append('(')
180 for character in needle.group():
181 quoted.append('%02x' % ord(character))
182 quoted.append(')')
183
184 # append rest of string
185 quoted.append(filename[location:])
186 return ''.join(quoted)
187
188
189 # FIXME: better name would be unquoteFilename
190 def unquoteWikiname(filename, charsets=[config.charset]):
191 """ Return Unicode WikiName from quoted file name.
192
193 We raise an InvalidFileNameError if we find an invalid name, so the
194 wiki could alarm the admin or suggest the user to rename a page.
195 Invalid file names should never happen in normal use, but are rather
196 cheap to find.
197
198 This function should be used only to unquote file names, not page
199 names we receive from the user. These are handled in request by
200 urllib.unquote, decodePagename and normalizePagename.
201
202 Todo: search clients of unquoteWikiname and check for exceptions.
203
204 @param filename: string using charset and possibly quoted parts
205 @param charset: charset used by string
206 @rtype: Unicode String
207 @return: WikiName
208 """
209 ### Temporary fix start ###
210 # From some places we get called with Unicode strings
211 if isinstance(filename, type(u'')):
212 filename = filename.encode(config.charset)
213 ### Temporary fix end ###
214
215 parts = []
216 start = 0
217 for needle in QUOTED.finditer(filename):
218 # append leading unquoted stuff
219 parts.append(filename[start:needle.start()])
220 start = needle.end()
221 # Append quoted stuff
222 group = needle.group(1)
223 # Filter invalid filenames
224 if (len(group) % 2 != 0):
225 raise InvalidFileNameError(filename)
226 try:
227 for i in range(0, len(group), 2):
228 byte = group[i:i+2]
229 character = chr(int(byte, 16))
230 parts.append(character)
231 except ValueError:
232 # byte not in hex, e.g 'xy'
233 raise InvalidFileNameError(filename)
234
235 # append rest of string
236 if start == 0:
237 wikiname = filename
238 else:
239 parts.append(filename[start:len(filename)])
240 wikiname = ''.join(parts)
241
242 # This looks wrong, because at this stage "()" can be both errors
243 # like open "(" without close ")", or unquoted valid characters in
244 # the file name. FIXME: check this.
245 # Filter invalid filenames. Any left (xx) must be invalid
246 #if '(' in wikiname or ')' in wikiname:
247 # raise InvalidFileNameError(filename)
248
249 wikiname = decodeUserInput(wikiname, charsets)
250 wikiname = wikiname.replace(u'_', u' ') # "_" -> " "
251 return wikiname
252
253 # time scaling
254 def timestamp2version(ts):
255 """ Convert UNIX timestamp (may be float or int) to our version
256 (long) int.
257 We don't want to use floats, so we just scale by 1e6 to get
258 an integer in usecs.
259 """
260 return long(ts*1000000L) # has to be long for py 2.2.x
261
262 def version2timestamp(v):
263 """ Convert version number to UNIX timestamp (float).
264 This must ONLY be used for display purposes.
265 """
266 return v/1000000.0
267
268 #############################################################################
269 ### InterWiki
270 #############################################################################
271
272 def split_wiki(wikiurl):
273 """
274 Split a wiki url.
275
276 @param wikiurl: the url to split
277 @rtype: tuple
278 @return: (tag, tail)
279 """
280 # !!! use a regex here!
281 try:
282 wikitag, tail = wikiurl.split(":", 1)
283 except ValueError:
284 try:
285 wikitag, tail = wikiurl.split("/", 1)
286 except ValueError:
287 wikitag = None
288 tail = None
289
290 return (wikitag, tail)
291
292
293 def join_wiki(wikiurl, wikitail):
294 """
295 Add a page name to an interwiki url.
296
297 @param wikiurl: wiki url, maybe including a $PAGE placeholder
298 @param wikitail: page name
299 @rtype: string
300 @return: generated URL of the page in the other wiki
301 """
302 if wikiurl.find('$PAGE') == -1:
303 return wikiurl + wikitail
304 else:
305 return wikiurl.replace('$PAGE', wikitail)
306
307
308 def resolve_wiki(request, wikiurl):
309 """
310 Resolve an interwiki link.
311
312 @param request: the request object
313 @param wikiurl: the InterWiki:PageName link
314 @rtype: tuple
315 @return: (wikitag, wikiurl, wikitail, err)
316 """
317 # load map (once, and only on demand)
318 try:
319 _interwiki_list = request.cfg._interwiki_list
320 except AttributeError:
321 _interwiki_list = {}
322 lines = []
323
324 # order is important here, the local intermap file takes
325 # precedence over the shared one, and is thus read AFTER
326 # the shared one
327 intermap_files = request.cfg.shared_intermap
328 if not isinstance(intermap_files, type([])):
329 intermap_files = [intermap_files]
330 intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt"))
331
332 for filename in intermap_files:
333 if filename and os.path.isfile(filename):
334 f = open(filename, "r")
335 lines.extend(f.readlines())
336 f.close()
337
338 for line in lines:
339 if not line or line[0] == '#': continue
340 try:
341 line = "%s %s/InterWiki" % (line, request.getScriptname())
342 wikitag, urlprefix, trash = line.split(None, 2)
343 except ValueError:
344 pass
345 else:
346 _interwiki_list[wikitag] = urlprefix
347
348 del lines
349
350 # add own wiki as "Self" and by its configured name
351 _interwiki_list['Self'] = request.getScriptname() + '/'
352 if request.cfg.interwikiname:
353 _interwiki_list[request.cfg.interwikiname] = request.getScriptname() + '/'
354
355 # save for later
356 request.cfg._interwiki_list = _interwiki_list
357
358 # split wiki url
359 wikitag, tail = split_wiki(wikiurl)
360
361 # return resolved url
362 if wikitag and _interwiki_list.has_key(wikitag):
363 return (wikitag, _interwiki_list[wikitag], tail, False)
364 else:
365 return (wikitag, request.getScriptname(), "/InterWiki", True)
366
367
368 #############################################################################
369 ### Page types (based on page names)
370 #############################################################################
371
372 def isSystemPage(request, pagename):
373 """ Is this a system page? Uses AllSystemPagesGroup internally.
374
375 @param request: the request object
376 @param pagename: the page name
377 @rtype: bool
378 @return: true if page is a system page
379 """
380 return (request.dicts.has_member('SystemPagesGroup', pagename) or
381 isTemplatePage(request, pagename) or
382 isFormPage(request, pagename))
383
384
385 def isTemplatePage(request, pagename):
386 """ Is this a template page?
387
388 @param pagename: the page name
389 @rtype: bool
390 @return: true if page is a template page
391 """
392 filter = re.compile(request.cfg.page_template_regex, re.UNICODE)
393 return filter.search(pagename) is not None
394
395
396 def isFormPage(request, pagename):
397 """ Is this a form page?
398
399 @param pagename: the page name
400 @rtype: bool
401 @return: true if page is a form page
402 """
403 filter = re.compile(request.cfg.page_form_regex, re.UNICODE)
404 return filter.search(pagename) is not None
405
406 def isGroupPage(request, pagename):
407 """ Is this a name of group page?
408
409 @param pagename: the page name
410 @rtype: bool
411 @return: true if page is a form page
412 """
413 filter = re.compile(request.cfg.page_group_regex, re.UNICODE)
414 return filter.search(pagename) is not None
415
416
417 def filterCategoryPages(request, pagelist):
418 """ Return category pages in pagelist
419
420 WARNING: DO NOT USE THIS TO FILTER THE FULL PAGE LIST! Use
421 getPageList with a filter function.
422
423 If you pass a list with a single pagename, either that is returned
424 or an empty list, thus you can use this function like a `isCategoryPage`
425 one.
426
427 @param pagelist: a list of pages
428 @rtype: list
429 @return: only the category pages of pagelist
430 """
431 func = re.compile(request.cfg.page_category_regex, re.UNICODE).search
432 return filter(func, pagelist)
433
434
435 # TODO: we may rename this to getLocalizedPage because it returns page
436 # that have translations.
437 def getSysPage(request, pagename):
438 """ Get a system page according to user settings and available translations.
439
440 We include some special treatment for the case that <pagename> is the
441 currently rendered page, as this is the case for some pages used very
442 often, like FrontPage, RecentChanges etc. - in that case we reuse the
443 already existing page object instead creating a new one.
444
445 @param request: the request object
446 @param pagename: the name of the page
447 @rtype: Page object
448 @return: the page object of that system page, using a translated page,
449 if it exists
450 """
451 from MoinMoin.Page import Page
452 i18n_name = request.getText(pagename, formatted=False)
453 pageobj = None
454 if i18n_name != pagename:
455 if request.page and i18n_name == request.page.page_name:
456 # do not create new object for current page
457 i18n_page = request.page
458 if i18n_page.exists():
459 pageobj = i18n_page
460 else:
461 i18n_page = Page(request, i18n_name)
462 if i18n_page.exists():
463 pageobj = i18n_page
464
465 # if we failed getting a translated version of <pagename>,
466 # we fall back to english
467 if not pageobj:
468 if request.page and pagename == request.page.page_name:
469 # do not create new object for current page
470 pageobj = request.page
471 else:
472 pageobj = Page(request, pagename)
473 return pageobj
474
475
476 def getFrontPage(request):
477 """ Convenience function to get localized front page
478
479 @param request: current request
480 @rtype: Page object
481 @return localized FrontPage
482 """
483 return getSysPage(request, request.cfg.page_front_page)
484
485
486 def getHomePage(request, username=None):
487 """
488 Get a user's homepage, or return None for anon users and
489 those who have not created a homepage.
490
491 DEPRECATED - try to use getInterwikiHomePage (see below)
492
493 @param request: the request object
494 @param username: the user's name
495 @rtype: Page
496 @return: user's homepage object - or None
497 """
498 from MoinMoin.Page import Page
499 # default to current user
500 if username is None and request.user.valid:
501 username = request.user.name
502
503 # known user?
504 if username:
505 # Return home page
506 page = Page(request, username)
507 if page.exists():
508 return page
509
510 return None
511
512
513 def getInterwikiHomePage(request, username=None):
514 """
515 Get a user's homepage.
516
517 cfg.user_homewiki influences behaviour of this:
518 'Self' does mean we store user homepage in THIS wiki.
519 When set to our own interwikiname, it behaves like with 'Self'.
520
521 'SomeOtherWiki' means we store user homepages in another wiki.
522
523 @param request: the request object
524 @param username: the user's name
525 @rtype: tuple (or None for anon users)
526 @return: (wikiname, pagename)
527 """
528 # default to current user
529 if username is None and request.user.valid:
530 username = request.user.name
531 if not username:
532 return None # anon user
533
534 homewiki = request.cfg.user_homewiki
535 if homewiki == request.cfg.interwikiname:
536 homewiki = 'Self'
537
538 return homewiki, username
539
540
541 def AbsPageName(request, context, pagename):
542 """
543 Return the absolute pagename for a (possibly) relative pagename.
544
545 @param context: name of the page where "pagename" appears on
546 @param pagename: the (possibly relative) page name
547 @rtype: string
548 @return: the absolute page name
549 """
550 if pagename.startswith(PARENT_PREFIX):
551 pagename = '/'.join(filter(None, context.split('/')[:-1] + [pagename[PARENT_PREFIX_LEN:]]))
552 elif pagename.startswith(CHILD_PREFIX):
553 pagename = context + '/' + pagename[CHILD_PREFIX_LEN:]
554 return pagename
555
556 def pagelinkmarkup(pagename):
557 """ return markup that can be used as link to page <pagename> """
558 from MoinMoin.parser.wiki import Parser
559 if re.match(Parser.word_rule + "$", pagename):
560 return pagename
561 else:
562 return u'["%s"]' % pagename
563
564 #############################################################################
565 ### Plugins
566 #############################################################################
567
568 def importPlugin(cfg, kind, name, function="execute"):
569 """ Import wiki or builtin plugin
570
571 Returns an object from a plugin module or None if module or
572 'function' is not found.
573
574 kind may be one of 'action', 'formatter', 'macro', 'processor',
575 'parser' or any other directory that exist in MoinMoin or
576 data/plugin
577
578 Wiki plugins will always override builtin plugins. If you want
579 specific plugin, use either importWikiPlugin or importName directly.
580
581 @param cfg: wiki config instance
582 @param kind: what kind of module we want to import
583 @param name: the name of the module
584 @param function: the function name
585 @rtype: callable
586 @return: "function" of module "name" of kind "kind", or None
587 """
588 # Try to import from the wiki
589 plugin = importWikiPlugin(cfg, kind, name, function)
590 if plugin is None:
591 # Try to get the plugin from MoinMoin
592 modulename = 'MoinMoin.%s.%s' % (kind, name)
593 plugin = pysupport.importName(modulename, function)
594
595 return plugin
596
597 def importWikiPlugin(cfg, kind, name, function):
598 """ Import plugin from the wiki data directory
599
600 We try to import only ONCE - then cache the plugin, even if we got
601 None. This way we prevent expensive import of existing plugins for
602 each call to a plugin.
603
604 @param cfg: wiki config instance
605 @param kind: what kind of module we want to import
606 @param name: the name of the module
607 @param function: the function name
608 @rtype: callable
609 @return: "function" of module "name" of kind "kind", or None
610 """
611
612 # Wiki plugins are located under 'wikiconfigname.plugin' module.
613 modulename = '%s.plugin.%s.%s' % (cfg.siteid, kind, name)
614 key = (modulename, function)
615 try:
616 # Try cache first - fast!
617 plugin = cfg._wiki_plugins[key]
618 except (KeyError, AttributeError):
619 # Try to import from disk and cache result - slow!
620 plugin = pysupport.importName(modulename, function)
621 try:
622 cfg._wiki_plugins[key] = plugin
623 except AttributeError:
624 cfg._wiki_plugins = {key: plugin}
625 return plugin
626
627 # If we use threads, make this function thread safe
628 if config.use_threads:
629 importWikiPlugin = pysupport.makeThreadSafe(importWikiPlugin)
630
631 def builtinPlugins(kind):
632 """ Gets a list of modules in MoinMoin.'kind'
633
634 @param kind: what kind of modules we look for
635 @rtype: list
636 @return: module names
637 """
638 modulename = "MoinMoin." + kind
639 plugins = pysupport.importName(modulename, "modules")
640 return plugins or []
641
642
643 def wikiPlugins(kind, cfg):
644 """ Gets a list of modules in data/plugin/'kind'
645
646 @param kind: what kind of modules we look for
647 @rtype: list
648 @return: module names
649 """
650 # Wiki plugins are located in wikiconfig.plugin module
651 modulename = '%s.plugin.%s' % (cfg.siteid, kind)
652 plugins = pysupport.importName(modulename, "modules")
653 return plugins or []
654
655
656 def getPlugins(kind, cfg):
657 """ Gets a list of plugin names of kind
658
659 @param kind: what kind of modules we look for
660 @rtype: list
661 @return: module names
662 """
663 # Copy names from builtin plugins - so we dont destroy the value
664 all_plugins = builtinPlugins(kind)[:]
665
666 # Add extension plugins without duplicates
667 for plugin in wikiPlugins(kind, cfg):
668 if plugin not in all_plugins:
669 all_plugins.append(plugin)
670
671 return all_plugins
672
673
674 #############################################################################
675 ### Parsers
676 #############################################################################
677
678 def getParserForExtension(cfg, extension):
679 """
680 Returns the Parser class of the parser fit to handle a file
681 with the given extension. The extension should be in the same
682 format as os.path.splitext returns it (i.e. with the dot).
683 Returns None if no parser willing to handle is found.
684 The dict of extensions is cached in the config object.
685
686 @param cfg: the Config instance for the wiki in question
687 @param extension: the filename extension including the dot
688 @rtype: class, None
689 @returns: the parser class or None
690 """
691 if not hasattr(cfg, '_EXT_TO_PARSER'):
692 import types
693 etp, etd = {}, None
694 for pname in getPlugins('parser', cfg):
695 Parser = importPlugin(cfg, 'parser', pname, 'Parser')
696 if Parser is not None:
697 if hasattr(Parser, 'extensions'):
698 exts = Parser.extensions
699 if type(exts) == types.ListType:
700 for ext in Parser.extensions:
701 etp[ext] = Parser
702 elif str(exts) == '*':
703 etd = Parser
704 cfg._EXT_TO_PARSER = etp
705 cfg._EXT_TO_PARSER_DEFAULT = etd
706
707 return cfg._EXT_TO_PARSER.get(extension, cfg._EXT_TO_PARSER_DEFAULT)
708
709
710 #############################################################################
711 ### Misc
712 #############################################################################
713
714 def parseAttributes(request, attrstring, endtoken=None, extension=None):
715 """
716 Parse a list of attributes and return a dict plus a possible
717 error message.
718 If extension is passed, it has to be a callable that returns
719 None when it was not interested into the token, '' when all was OK
720 and it did eat the token, and any other string to return an error
721 message.
722
723 @param request: the request object
724 @param attrstring: string containing the attributes to be parsed
725 @param endtoken: token terminating parsing
726 @param extension: extension function -
727 gets called with the current token, the parser and the dict
728 @rtype: dict, msg
729 @return: a dict plus a possible error message
730 """
731 import shlex, StringIO
732
733 _ = request.getText
734
735 parser = shlex.shlex(StringIO.StringIO(attrstring))
736 parser.commenters = ''
737 msg = None
738 attrs = {}
739
740 while not msg:
741 try:
742 key = parser.get_token()
743 except ValueError, err:
744 msg = str(err)
745 break
746 if not key: break
747 if endtoken and key == endtoken: break
748
749 # call extension function with the current token, the parser, and the dict
750 if extension:
751 msg = extension(key, parser, attrs)
752 if msg == '': continue
753 if msg: break
754
755 try:
756 eq = parser.get_token()
757 except ValueError, err:
758 msg = str(err)
759 break
760 if eq != "=":
761 msg = _('Expected "=" to follow "%(token)s"') % {'token': key}
762 break
763
764 try:
765 val = parser.get_token()
766 except ValueError, err:
767 msg = str(err)
768 break
769 if not val:
770 msg = _('Expected a value for key "%(token)s"') % {'token': key}
771 break
772
773 key = escape(key) # make sure nobody cheats
774
775 # safely escape and quote value
776 if val[0] in ["'", '"']:
777 val = escape(val)
778 else:
779 val = '"%s"' % escape(val, 1)
780
781 attrs[key.lower()] = val
782
783 return attrs, msg or ''
784
785
786 def taintfilename(basename):
787 """
788 Make a filename that is supposed to be a plain name secure, i.e.
789 remove any possible path components that compromise our system.
790
791 @param basename: (possibly unsafe) filename
792 @rtype: string
793 @return: (safer) filename
794 """
795 for x in (os.pardir, ':', '/', '\\', '<', '>'):
796 basename = basename.replace(x, '_')
797
798 return basename
799
800
801 def mapURL(request, url):
802 """
803 Map URLs according to 'cfg.url_mappings'.
804
805 @param url: a URL
806 @rtype: string
807 @return: mapped URL
808 """
809 # check whether we have to map URLs
810 if request.cfg.url_mappings:
811 # check URL for the configured prefixes
812 for prefix in request.cfg.url_mappings.keys():
813 if url.startswith(prefix):
814 # substitute prefix with replacement value
815 return request.cfg.url_mappings[prefix] + url[len(prefix):]
816
817 # return unchanged url
818 return url
819
820
821 def getUnicodeIndexGroup(name):
822 """
823 Return a group letter for `name`, which must be a unicode string.
824 Currently supported: Hangul Syllables (U+AC00 - U+D7AF)
825
826 @param name: a string
827 @rtype: string
828 @return: group letter or None
829 """
830 c = name[0]
831 if u'\uAC00' <= c <= u'\uD7AF': # Hangul Syllables
832 return unichr(0xac00 + (int(ord(c) - 0xac00) / 588) * 588)
833 else:
834 return c
835
836
837 def isStrictWikiname(name, word_re=re.compile(ur"^(?:[%(u)s][%(l)s]+){2,}$" % {'u':config.chars_upper, 'l':config.chars_lower})):
838 """
839 Check whether this is NOT an extended name.
840
841 @param name: the wikiname in question
842 @rtype: bool
843 @return: true if name matches the word_re
844 """
845 return word_re.match(name)
846
847
848 def isPicture(url):
849 """
850 Is this a picture's url?
851
852 @param url: the url in question
853 @rtype: bool
854 @return: true if url points to a picture
855 """
856 extpos = url.rfind(".")
857 return extpos > 0 and url[extpos:].lower() in ['.gif', '.jpg', '.jpeg', '.png']
858
859
860 def link_tag(request, params, text=None, formatter=None, on=None, **kw):
861 """ Create a link.
862
863 @param request: the request object
864 @param params: parameter string appended to the URL after the scriptname/
865 @param text: text / inner part of the <a>...</a> link - does NOT get
866 escaped, so you can give HTML here and it will be used verbatim
867 @param formatter: the formatter object to use
868 @keyword on: opening/closing tag only
869 @keyword attrs: additional attrs (HTMLified string)
870 @rtype: string
871 @return: formatted link tag
872 """
873 css_class = kw.get('css_class', None)
874 if text is None:
875 text = params # default
876 if formatter:
877 url = "%s/%s" % (request.getScriptname(), params)
878 if on != None:
879 return formatter.url(on, url, css_class, **kw)
880 return (formatter.url(1, url, css_class, **kw) +
881 formatter.rawHTML(text) +
882 formatter.url(0))
883 if on != None and not on:
884 return '</a>'
885
886 attrs = ''
887 if kw.has_key('attrs'):
888 attrs += ' ' + kw['attrs']
889 if css_class:
890 attrs += ' class="%s"' % css_class
891 result = '<a%s href="%s/%s">' % (attrs, request.getScriptname(), params)
892 if on:
893 return result
894 else:
895 return "%s%s</a>" % (result, text)
896
897
898 def linediff(oldlines, newlines, **kw):
899 """
900 Find changes between oldlines and newlines.
901
902 @param oldlines: list of old text lines
903 @param newlines: list of new text lines
904 @keyword ignorews: if 1: ignore whitespace
905 @rtype: list
906 @return: lines like diff tool does output.
907 """
908 false = lambda s: None
909 if kw.get('ignorews', 0):
910 d = difflib.Differ(false)
911 else:
912 d = difflib.Differ(false, false)
913
914 lines = list(d.compare(oldlines,newlines))
915
916 # return empty list if there were no changes
917 changed = 0
918 for l in lines:
919 if l[0] != ' ':
920 changed = 1
921 break
922 if not changed: return []
923
924 if not "we want the unchanged lines, too":
925 if "no questionmark lines":
926 lines = filter(lambda line : line[0]!='?', lines)
927 return lines
928
929
930 # calculate the hunks and remove the unchanged lines between them
931 i = 0 # actual index in lines
932 count = 0 # number of unchanged lines
933 lcount_old = 0 # line count old file
934 lcount_new = 0 # line count new file
935 while i < len(lines):
936 marker = lines[i][0]
937 if marker == ' ':
938 count = count + 1
939 i = i + 1
940 lcount_old = lcount_old + 1
941 lcount_new = lcount_new + 1
942 elif marker in ['-', '+']:
943 if (count == i) and count > 3:
944 lines[:i-3] = []
945 i = 4
946 count = 0
947 elif count > 6:
948 # remove lines and insert new hunk indicator
949 lines[i-count+3:i-3] = ['@@ -%i, +%i @@\n' %
950 (lcount_old, lcount_new)]
951 i = i - count + 8
952 count = 0
953 else:
954 count = 0
955 i = i + 1
956 if marker == '-': lcount_old = lcount_old + 1
957 else: lcount_new = lcount_new + 1
958 elif marker == '?':
959 lines[i:i+1] = []
960
961 # remove unchanged lines a the end
962 if count > 3:
963 lines[-count+3:] = []
964
965 return lines
966
967
968 def pagediff(request, pagename1, rev1, pagename2, rev2, **kw):
969 """
970 Calculate the "diff" between two page contents.
971
972 @param pagename1: name of first page
973 @param rev1: revision of first page
974 @param pagename2: name of second page
975 @param rev2: revision of second page
976 @keyword ignorews: if 1: ignore pure-whitespace changes.
977 @rtype: list
978 @return: lines of diff output
979 """
980 from MoinMoin.Page import Page
981 lines1 = Page(request, pagename1, rev=rev1).getlines()
982 lines2 = Page(request, pagename2, rev=rev2).getlines()
983
984 lines = linediff(lines1, lines2, **kw)
985 return lines
986
987
988 #############################################################################
989 ### Page header / footer
990 #############################################################################
991
992 # FIXME - this is theme code, move to theme
993 # Could be simplified by using a template
994
995 def send_title(request, text, **keywords):
996 """
997 Output the page header (and title).
998
999 TODO: check all code that call us and add page keyword for the
1000 current page being rendered.
1001
1002 @param request: the request object
1003 @param text: the title text
1004 @keyword link: URL for the title
1005 @keyword msg: additional message (after saving)
1006 @keyword pagename: 'PageName'
1007 @keyword page: the page instance that called us.
1008 @keyword print_mode: 1 (or 0)
1009 @keyword editor_mode: 1 (or 0)
1010 @keyword media: css media type, defaults to 'screen'
1011 @keyword allow_doubleclick: 1 (or 0)
1012 @keyword html_head: additional <head> code
1013 @keyword body_attr: additional <body> attributes
1014 @keyword body_onload: additional "onload" JavaScript code
1015 """
1016 from MoinMoin.Page import Page
1017 _ = request.getText
1018
1019 if keywords.has_key('page'):
1020 page = keywords['page']
1021 pagename = page.page_name
1022 else:
1023 pagename = keywords.get('pagename', '')
1024 page = Page(request, pagename)
1025
1026 scriptname = request.getScriptname()
1027 pagename_quoted = quoteWikinameURL(pagename)
1028
1029 # get name of system pages
1030 page_front_page = getFrontPage(request).page_name
1031 page_help_contents = getSysPage(request, 'HelpContents').page_name
1032 page_title_index = getSysPage(request, 'TitleIndex').page_name
1033 page_site_navigation = getSysPage(request, 'SiteNavigation').page_name
1034 page_word_index = getSysPage(request, 'WordIndex').page_name
1035 page_user_prefs = getSysPage(request, 'UserPreferences').page_name
1036 page_help_formatting = getSysPage(request, 'HelpOnFormatting').page_name
1037 page_find_page = getSysPage(request, 'FindPage').page_name
1038 home_page = getInterwikiHomePage(request) # XXX sorry theme API change!!! Either None or tuple (wikiname,pagename) now.
1039 page_parent_page = getattr(page.getParentPage(), 'page_name', None)
1040
1041 # Prepare the HTML <head> element
1042 user_head = [request.cfg.html_head]
1043
1044 # include charset information - needed for moin_dump or any other case
1045 # when reading the html without a web server
1046 user_head.append('''<meta http-equiv="Content-Type" content="text/html;charset=%s">\n''' % config.charset)
1047
1048 meta_keywords = request.getPragma('keywords')
1049 meta_desc = request.getPragma('description')
1050 if meta_keywords:
1051 user_head.append('<meta name="keywords" content="%s">\n' % escape(meta_keywords, 1))
1052 if meta_desc:
1053 user_head.append('<meta name="description" content="%s">\n' % escape(meta_desc, 1))
1054
1055 # search engine precautions / optimization:
1056 # if it is an action or edit/search, send query headers (noindex,nofollow):
1057 if request.query_string:
1058 user_head.append(request.cfg.html_head_queries)
1059 elif request.request_method == 'POST':
1060 user_head.append(request.cfg.html_head_posts)
1061 # if it is a special page, index it and follow the links - we do it
1062 # for the original, English pages as well as for (the possibly
1063 # modified) frontpage:
1064 elif pagename in [page_front_page, request.cfg.page_front_page,
1065 page_title_index, 'TitleIndex',
1066 page_find_page, 'FindPage',
1067 page_site_navigation, 'SiteNavigation',
1068 'RecentChanges',]:
1069 user_head.append(request.cfg.html_head_index)
1070 # if it is a normal page, index it, but do not follow the links, because
1071 # there are a lot of illegal links (like actions) or duplicates:
1072 else:
1073 user_head.append(request.cfg.html_head_normal)
1074
1075 if keywords.has_key('pi_refresh') and keywords['pi_refresh']:
1076 user_head.append('<meta http-equiv="refresh" content="%(delay)d;URL=%(url)s">' % keywords['pi_refresh'])
1077
1078 # output buffering increases latency but increases throughput as well
1079 output = []
1080 # later: <html xmlns=\"http://www.w3.org/1999/xhtml\">
1081 output.append("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
1082 <html>
1083 <head>
1084 %s
1085 %s
1086 %s
1087 """ % (
1088 ''.join(user_head),
1089 keywords.get('html_head', ''),
1090 request.theme.html_head({
1091 'title': escape(text),
1092 'sitename': escape(request.cfg.html_pagetitle or request.cfg.sitename),
1093 'print_mode': keywords.get('print_mode', False),
1094 'media': keywords.get('media', 'screen'),
1095 })
1096 ))
1097
1098 # Links
1099 output.append('<link rel="Start" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_front_page)))
1100 if pagename:
1101 output.append('<link rel="Alternate" title="%s" href="%s/%s?action=raw">\n' % (
1102 _('Wiki Markup'), scriptname, pagename_quoted,))
1103 output.append('<link rel="Alternate" media="print" title="%s" href="%s/%s?action=print">\n' % (
1104 _('Print View'), scriptname, pagename_quoted,))
1105
1106 # !!! currently disabled due to Mozilla link prefetching, see
1107 # http://www.mozilla.org/projects/netlib/Link_Prefetching_FAQ.html
1108 #~ all_pages = request.getPageList()
1109 #~ if all_pages:
1110 #~ try:
1111 #~ pos = all_pages.index(pagename)
1112 #~ except ValueError:
1113 #~ # this shopuld never happend in theory, but let's be sure
1114 #~ pass
1115 #~ else:
1116 #~ request.write('<link rel="First" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[0]))
1117 #~ if pos > 0:
1118 #~ request.write('<link rel="Previous" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[pos-1])))
1119 #~ if pos+1 < len(all_pages):
1120 #~ request.write('<link rel="Next" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[pos+1])))
1121 #~ request.write('<link rel="Last" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[-1])))
1122
1123 if page_parent_page:
1124 output.append('<link rel="Up" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_parent_page)))
1125
1126 # write buffer because we call AttachFile
1127 request.write(''.join(output))
1128 output = []
1129
1130 if pagename:
1131 from MoinMoin.action import AttachFile
1132 AttachFile.send_link_rel(request, pagename)
1133
1134 output.extend([
1135 '<link rel="Search" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_find_page)),
1136 '<link rel="Index" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_title_index)),
1137 '<link rel="Glossary" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_word_index)),
1138 '<link rel="Help" href="%s/%s">\n' % (scriptname, quoteWikinameURL(page_help_formatting)),
1139 ])
1140
1141 output.append("</head>\n")
1142 request.write(''.join(output))
1143 output = []
1144 request.flush()
1145
1146 # start the <body>
1147 bodyattr = []
1148 if keywords.has_key('body_attr'):
1149 bodyattr.append(' ')
1150 bodyattr.append(keywords['body_attr'])
1151
1152 # Add doubleclick edit action
1153 if (pagename and keywords.get('allow_doubleclick', 0) and
1154 not keywords.get('print_mode', 0) and
1155 request.user.edit_on_doubleclick):
1156 if request.user.may.write(pagename): # separating this gains speed
1157 querystr = escape(util.web.makeQueryString({'action': 'edit'}))
1158 # TODO: remove escape=0 in 1.4
1159 url = page.url(request, querystr, escape=0)
1160 bodyattr.append(''' ondblclick="location.href='%s'"''' % url)
1161
1162 # Set body to the user interface language and direction
1163 bodyattr.append(' %s' % request.theme.ui_lang_attr())
1164
1165 body_onload = keywords.get('body_onload', '')
1166 if body_onload:
1167 bodyattr.append(''' onload="%s"''' % body_onload)
1168 output.append('\n<body%s>\n' % ''.join(bodyattr))
1169
1170 # Output -----------------------------------------------------------
1171
1172 theme = request.theme
1173
1174 # If in print mode, start page div and emit the title
1175 if keywords.get('print_mode', 0):
1176 d = {'title_text': text, 'title_link': None, 'page': page,}
1177 request.themedict = d
1178 output.append(theme.startPage())
1179 output.append(theme.title(d))
1180
1181 # In standard mode, emit theme.header
1182 else:
1183 # prepare dict for theme code:
1184 d = {
1185 'theme': theme.name,
1186 'script_name': scriptname,
1187 'title_text': text,
1188 'title_link': keywords.get('link', ''),
1189 'logo_string': request.cfg.logo_string,
1190 'site_name': request.cfg.sitename,
1191 'page': page,
1192 'pagesize': pagename and page.size() or 0,
1193 'last_edit_info': pagename and page.lastEditInfo() or '',
1194 'page_name': pagename or '',
1195 'page_find_page': page_find_page,
1196 'page_front_page': page_front_page,
1197 'home_page': home_page,
1198 'page_help_contents': page_help_contents,
1199 'page_help_formatting': page_help_formatting,
1200 'page_parent_page': page_parent_page,
1201 'page_title_index': page_title_index,
1202 'page_word_index': page_word_index,
1203 'page_user_prefs': page_user_prefs,
1204 'user_name': request.user.name,
1205 'user_valid': request.user.valid,
1206 'user_prefs': (page_user_prefs, request.user.name)[request.user.valid],
1207 'msg': keywords.get('msg', ''),
1208 'trail': keywords.get('trail', None),
1209 # Discontinued keys, keep for a while for 3rd party theme developers
1210 'titlesearch': 'use self.searchform(d)',
1211 'textsearch': 'use self.searchform(d)',
1212 'navibar': ['use self.navibar(d)'],
1213 'available_actions': ['use self.request.availableActions(page)'],
1214 }
1215
1216 # add quoted versions of pagenames
1217 newdict = {}
1218 for key in d:
1219 if key.startswith('page_'):
1220 if not d[key] is None:
1221 newdict['q_'+key] = quoteWikinameURL(d[key])
1222 else:
1223 newdict['q_'+key] = None
1224 d.update(newdict)
1225 request.themedict = d
1226
1227 # now call the theming code to do the rendering
1228 if keywords.get('editor_mode', 0):
1229 output.append(theme.editorheader(d))
1230 else:
1231 output.append(theme.header(d))
1232
1233 # emit it
1234 request.write(''.join(output))
1235 output = []
1236 request.flush()
1237
1238
1239 def send_footer(request, pagename, **keywords):
1240 """
1241 Output the page footer.
1242
1243 @param request: the request object
1244 @param pagename: WikiName of the page
1245 @keyword editable: true, when page is editable (default: true)
1246 @keyword showpage: true, when link back to page is wanted (default: false)
1247 @keyword print_mode: true, when page is displayed in Print mode
1248 """
1249 d = request.themedict
1250 theme = request.theme
1251
1252 # Emit end of page in print mode, or complete footer in standard mode
1253 if keywords.get('print_mode', 0):
1254 request.write(theme.pageinfo(d['page']))
1255 request.write(theme.endPage())
1256 else:
1257 # This is used only by classic now, kill soon
1258 d['footer_fragments'] = request._footer_fragments
1259 request.write(theme.footer(d, **keywords))
1260
1261
1262 ########################################################################
1263 ### Tickets - used by RenamePage and DeletePage
1264 ########################################################################
1265
1266 def createTicket(tm = None):
1267 """Create a ticket using a site-specific secret (the config)"""
1268 import sha, time, types
1269 ticket = tm or "%010x" % time.time()
1270 digest = sha.new()
1271 digest.update(ticket)
1272
1273 cfgvars = vars(config)
1274 for var in cfgvars.values():
1275 if type(var) is types.StringType:
1276 digest.update(repr(var))
1277
1278 return "%s.%s" % (ticket, digest.hexdigest())
1279
1280
1281 def checkTicket(ticket):
1282 """Check validity of a previously created ticket"""
1283 timestamp = ticket.split('.')[0]
1284 ourticket = createTicket(timestamp)
1285 return ticket == ourticket
1286
1287