Attachment 'Page.py'
Download 1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - Page class
4
5 Page is used for read-only access to a wiki page. For r/w access see PageEditor.
6 A Page object is used to access a wiki page (in general) as well as to access
7 some specific revision of a wiki page.
8
9 The RootPage is some virtual page located at / and is mainly used to do namespace
10 operations like getting the page list.
11
12 Currently, this is all a big mixture between high-level page code, intermediate
13 data/underlay layering code, caching code and low-level filesystem storage code.
14 To see the filesystem storage layout we use, best is to look into data/pages/
15 (underlay dir uses the same format).
16
17 TODO:
18 * Cleanly separate the code into packages for:
19 * Page (or rather: Item)
20 * Layering
21 * Cache
22 * Storage
23 * ACLs should be handled on a low layer, raising an Exception when access
24 is denied, so we won't have security issues just because someone forgot to check
25 user.may.read(secretpage).
26 * The distinction between a item and a item revision should be clearer.
27 * Items can be anything, not just wiki pages, but also files of any mimetype.
28 The mimetype hierarchy should be modelled by a MimeTypeItem class hierarchy.
29
30 @copyright: 2000-2004 by Juergen Hermann <jh@web.de>,
31 2005-2008 by MoinMoin:ThomasWaldmann,
32 2006 by MoinMoin:FlorianFesti,
33 2007 by MoinMoin:ReimarBauer
34 @license: GNU GPL, see COPYING for details.
35 """
36
37 import os, re, codecs
38
39 from MoinMoin import log
40 logging = log.getLogger(__name__)
41
42 from MoinMoin import config, caching, user, util, wikiutil
43 from MoinMoin.logfile import eventlog
44
45 def is_cache_exception(e):
46 args = e.args
47 return not (len(args) != 1 or args[0] != 'CacheNeedsUpdate')
48
49
50 class ItemCache:
51 """ Cache some page item related data, as meta data or pagelist
52
53 We only cache this to RAM in request.cfg (this is the only kind of
54 server object we have), because it might be too big for pickling it
55 in and out.
56 """
57 def __init__(self, name):
58 """ Initialize ItemCache object.
59 @param name: name of the object, used for display in logging and
60 influences behaviour of refresh().
61 """
62 self.name = name
63 self.cache = {}
64 self.log_pos = None # TODO: initialize this to EOF pos of log
65 # to avoid reading in the whole log on first request
66 self.requests = 0
67 self.hits = 0
68 self.loglevel = logging.NOTSET
69
70 def putItem(self, request, name, key, data):
71 """ Remembers some data for item name under a key.
72 @param request: currently unused
73 @param name: name of the item (page), unicode
74 @param key: used as secondary access key after name
75 @param data: the data item that should be remembered
76 """
77 d = self.cache.setdefault(name, {})
78 d[key] = data
79
80 def getItem(self, request, name, key):
81 """ Returns some item stored for item name under key.
82 @param request: the request object
83 @param name: name of the item (page), unicode
84 @param key: used as secondary access key after name
85 @return: the data or None, if there is no such name or key.
86 """
87 self.refresh(request)
88 try:
89 data = self.cache[name][key]
90 self.hits += 1
91 hit_str = 'hit'
92 except KeyError:
93 data = None
94 hit_str = 'miss'
95 self.requests += 1
96 logging.log(self.loglevel, "%s cache %s (h/r %2.1f%%) for %r %r" % (
97 self.name,
98 hit_str,
99 float(self.hits * 100) / self.requests,
100 name,
101 key,
102 ))
103 return data
104
105 def refresh(self, request):
106 """ Refresh the cache - if anything has changed in the wiki, we see it
107 in the edit-log and either delete cached data for the changed items
108 (for 'meta') or the complete cache ('pagelists').
109 @param request: the request object
110 """
111 from MoinMoin.logfile import editlog
112 elog = editlog.EditLog(request)
113 old_pos = self.log_pos
114 new_pos, items = elog.news(old_pos)
115 if items:
116 if self.name == 'meta':
117 for item in items:
118 logging.log(self.loglevel, "cache: removing %r" % item)
119 try:
120 del self.cache[item]
121 except:
122 pass
123 elif self.name == 'pagelists':
124 logging.log(self.loglevel, "cache: clearing pagelist cache")
125 self.cache = {}
126 self.log_pos = new_pos # important to do this at the end -
127 # avoids threading race conditions
128
129
130 class Page(object):
131 """ Page - Manage an (immutable) page associated with a WikiName.
132 To change a page's content, use the PageEditor class.
133 """
134 def __init__(self, request, page_name, **kw):
135 """ Create page object.
136
137 Note that this is a 'lean' operation, since the text for the page
138 is loaded on demand. Thus, things like `Page(name).link_to()` are
139 efficient.
140
141 @param page_name: WikiName of the page
142 @keyword rev: number of older revision
143 @keyword formatter: formatter instance or mimetype str,
144 None or no kw arg will use default formatter
145 @keyword include_self: if 1, include current user (default: 0)
146 """
147 self.request = request
148 self.cfg = request.cfg
149 self.page_name = page_name
150 self.rev = kw.get('rev', 0) # revision of this page
151 self.include_self = kw.get('include_self', 0)
152
153 formatter = kw.get('formatter', None)
154 if isinstance(formatter, (str, unicode)): # mimetype given
155 mimetype = str(formatter)
156 self.formatter = None
157 self.output_mimetype = mimetype
158 self.default_formatter = mimetype == "text/html"
159 elif formatter is not None: # formatter instance given
160 self.formatter = formatter
161 self.default_formatter = 0
162 self.output_mimetype = "text/todo" # TODO where do we get this value from?
163 else:
164 self.formatter = None
165 self.default_formatter = 1
166 self.output_mimetype = "text/html"
167
168 self.output_charset = config.charset # correct for wiki pages
169
170 self._text_filename_force = None
171 self.hilite_re = None
172
173 self.__body = None # unicode page body == metadata + data
174 self.__body_modified = 0 # was __body modified in RAM so it differs from disk?
175 self.__meta = None # list of raw tuples of page metadata (currently: the # stuff at top of the page)
176 self.__pi = None # dict of preprocessed page metadata (processing instructions)
177 self.__data = None # unicode page data = body - metadata
178
179 self.reset()
180
181 def reset(self):
182 """ Reset page state """
183 page_name = self.page_name
184 # page_name quoted for file system usage, needs to be reset to
185 # None when pagename changes
186
187 qpagename = wikiutil.quoteWikinameFS(page_name)
188 self.page_name_fs = qpagename
189
190 # the normal and the underlay path used for this page
191 normalpath = os.path.join(self.cfg.data_dir, "pages", qpagename)
192 if not self.cfg.data_underlay_dir is None:
193 underlaypath = os.path.join(self.cfg.data_underlay_dir, "pages", qpagename)
194 else:
195 underlaypath = None
196
197 # TUNING - remember some essential values
198
199 # does the page come from normal page storage (0) or from
200 # underlay dir (1) (can be used as index into following list)
201 self._underlay = None
202
203 # path to normal / underlay page dir
204 self._pagepath = [normalpath, underlaypath]
205
206 # now we define some properties to lazy load some attributes on first access:
207
208 def get_body(self):
209 if self.__body is None:
210 # try to open file
211 try:
212 f = codecs.open(self._text_filename(), 'rb', config.charset)
213 except IOError, er:
214 import errno
215 if er.errno == errno.ENOENT:
216 # just doesn't exist, return empty text (note that we
217 # never store empty pages, so this is detectable and also
218 # safe when passed to a function expecting a string)
219 return ""
220 else:
221 raise
222
223 # read file content and make sure it is closed properly
224 try:
225 text = f.read()
226 text = self.decodeTextMimeType(text)
227 self.__body = text
228 finally:
229 f.close()
230 return self.__body
231
232 def set_body(self, newbody):
233 self.__body = newbody
234 self.__meta = None
235 self.__data = None
236 body = property(fget=get_body, fset=set_body) # complete page text
237
238 def get_meta(self):
239 if self.__meta is None:
240 self.__meta, self.__data = wikiutil.get_processing_instructions(self.body)
241 return self.__meta
242 meta = property(fget=get_meta) # processing instructions, ACLs (upper part of page text)
243
244 def get_data(self):
245 if self.__data is None:
246 self.__meta, self.__data = wikiutil.get_processing_instructions(self.body)
247 return self.__data
248 data = property(fget=get_data) # content (lower part of page text)
249
250 def get_pi(self):
251 if self.__pi is None:
252 self.__pi = self.parse_processing_instructions()
253 return self.__pi
254 pi = property(fget=get_pi) # processed meta stuff
255
256 def getlines(self):
257 """ Return a list of all lines in body.
258
259 @rtype: list
260 @return: list of strs body_lines
261 """
262 return self.body.split('\n')
263
264 def get_raw_body(self):
265 """ Load the raw markup from the page file.
266
267 @rtype: unicode
268 @return: raw page contents of this page, unicode
269 """
270 return self.body
271
272 def get_raw_body_str(self):
273 """ Returns the raw markup from the page file, as a string.
274
275 @rtype: str
276 @return: raw page contents of this page, utf-8-encoded
277 """
278 return self.body.encode("utf-8")
279
280 def set_raw_body(self, body, modified=0):
281 """ Set the raw body text (prevents loading from disk).
282
283 TODO: this should not be a public function, as Page is immutable.
284
285 @param body: raw body text
286 @param modified: 1 means that we internally modified the raw text and
287 that it is not in sync with the page file on disk. This is
288 used e.g. by PageEditor when previewing the page.
289 """
290 self.body = body
291 self.__body_modified = modified
292
293 def get_current_from_pagedir(self, pagedir):
294 """ Get the current revision number from an arbitrary pagedir.
295 Does not modify page object's state, uncached, direct disk access.
296 @param pagedir: the pagedir with the 'current' file to read
297 @return: int currentrev
298 """
299 revfilename = os.path.join(pagedir, 'current')
300 try:
301 revfile = file(revfilename)
302 revstr = revfile.read().strip()
303 revfile.close()
304 rev = int(revstr)
305 except:
306 rev = 99999999 # XXX do some better error handling
307 return rev
308
309 def get_rev_dir(self, pagedir, rev=0):
310 """ Get a revision of a page from an arbitrary pagedir.
311
312 Does not modify page object's state, uncached, direct disk access.
313
314 @param pagedir: the path to the page storage area
315 @param rev: int revision to get (default is 0 and means the current
316 revision (in this case, the real revint is returned)
317 @return: (str path to file of the revision,
318 int realrevint,
319 bool exists)
320 """
321 if rev == 0:
322 rev = self.get_current_from_pagedir(pagedir)
323
324 revstr = '%08d' % rev
325 pagefile = os.path.join(pagedir, 'revisions', revstr)
326 if rev != 99999999:
327 exists = os.path.exists(pagefile)
328 if exists:
329 self._setRealPageName(pagedir)
330 else:
331 exists = False
332 return pagefile, rev, exists
333
334 def _setRealPageName(self, pagedir):
335 """ Set page_name to the real case of page name
336
337 On case insensitive file system, "pagename" exists even if the
338 real page name is "PageName" or "PAGENAME". This leads to
339 confusion in urls, links and logs.
340 See MoinMoinBugs/MacHfsPlusCaseInsensitive
341
342 Correct the case of the page name. Elements created from the
343 page name in reset() are not updated because it's too messy, and
344 this fix seems to be enough for now.
345
346 Problems to fix later:
347
348 - ["helponnavigation"] link to HelpOnNavigation but not
349 considered as backlink.
350
351 @param pagedir: the storage path to the page directory
352 """
353 if self._text_filename_force is None:
354 # we only do this for normal pages, but not for the MissingPage,
355 # because the code below is wrong in that case
356 realPath = util.filesys.realPathCase(pagedir)
357 if realPath is not None:
358 realPath = wikiutil.unquoteWikiname(realPath)
359 self.page_name = realPath[-len(self.page_name):]
360
361 def get_rev(self, use_underlay=-1, rev=0):
362 """ Get information about a revision.
363
364 filename, number, and (existance test) of this page and revision.
365
366 @param use_underlay: -1 == auto, 0 == normal, 1 == underlay
367 @param rev: int revision to get (default is 0 and means the current
368 revision (in this case, the real revint is returned)
369 @return: (str path to current revision of page,
370 int realrevint,
371 bool exists)
372 """
373 def layername(underlay):
374 if underlay == -1:
375 return 'layer_auto'
376 elif underlay == 0:
377 return 'layer_normal'
378 else: # 1
379 return 'layer_underlay'
380
381 request = self.request
382 cache_name = self.page_name
383 cache_key = layername(use_underlay)
384 if self._text_filename_force is None:
385 cache_data = request.cfg.cache.meta.getItem(request, cache_name, cache_key)
386 if cache_data and (rev == 0 or rev == cache_data[1]):
387 # we got the correct rev data from the cache
388 #logging.debug("got data from cache: %r %r %r" % cache_data)
389 return cache_data
390
391 # Figure out if we should use underlay or not, if needed.
392 if use_underlay == -1:
393 underlay, pagedir = self.getPageStatus(check_create=0)
394 else:
395 underlay, pagedir = use_underlay, self._pagepath[use_underlay]
396
397 # Find current revision, if automatic selection is requested.
398 if rev == 0:
399 realrev = self.get_current_from_pagedir(pagedir)
400 else:
401 realrev = rev
402
403 data = self.get_rev_dir(pagedir, realrev)
404 if rev == 0 and self._text_filename_force is None:
405 # we only save the current rev to the cache
406 request.cfg.cache.meta.putItem(request, cache_name, cache_key, data)
407
408 return data
409
410 def current_rev(self):
411 """ Return number of current revision.
412
413 This is the same as get_rev()[1].
414
415 @return: int revision
416 """
417 pagefile, rev, exists = self.get_rev()
418 return rev
419
420 def get_real_rev(self):
421 """ Returns the real revision number of this page.
422 A rev==0 is translated to the current revision.
423
424 @returns: revision number > 0
425 @rtype: int
426 """
427 if self.rev == 0:
428 return self.current_rev()
429 return self.rev
430
431 def getPageBasePath(self, use_underlay=-1):
432 """ Get full path to a page-specific storage area. `args` can
433 contain additional path components that are added to the base path.
434
435 @param use_underlay: force using a specific pagedir, default -1
436 -1 = automatically choose page dir
437 1 = use underlay page dir
438 0 = use standard page dir
439 @rtype: string
440 @return: int underlay,
441 str the full path to the storage area
442 """
443 standardpath, underlaypath = self._pagepath
444 if underlaypath is None:
445 use_underlay = 0
446
447 if use_underlay == -1: # automatic
448 if self._underlay is None:
449 underlay, path = 0, standardpath
450 pagefile, rev, exists = self.get_rev(use_underlay=0)
451 if not exists:
452 pagefile, rev, exists = self.get_rev(use_underlay=1)
453 if exists:
454 underlay, path = 1, underlaypath
455 self._underlay = underlay
456 else:
457 underlay = self._underlay
458 path = self._pagepath[underlay]
459 else: # normal or underlay
460 underlay, path = use_underlay, self._pagepath[use_underlay]
461
462 return underlay, path
463
464 def getPageStatus(self, *args, **kw):
465 """ Get full path to a page-specific storage area. `args` can
466 contain additional path components that are added to the base path.
467
468 @param args: additional path components
469 @keyword use_underlay: force using a specific pagedir, default '-1'
470 -1 = automatically choose page dir
471 1 = use underlay page dir
472 0 = use standard page dir
473 @keyword check_create: if true, ensures that the path requested really exists
474 (if it doesn't, create all directories automatically).
475 (default true)
476 @keyword isfile: is the last component in args a filename? (default is false)
477 @rtype: string
478 @return: (int underlay (1 if using underlay, 0 otherwise),
479 str the full path to the storage area )
480 """
481 check_create = kw.get('check_create', 1)
482 isfile = kw.get('isfile', 0)
483 use_underlay = kw.get('use_underlay', -1)
484 underlay, path = self.getPageBasePath(use_underlay)
485 fullpath = os.path.join(*((path, ) + args))
486 if check_create:
487 if isfile:
488 dirname, filename = os.path.split(fullpath)
489 else:
490 dirname = fullpath
491 try:
492 os.makedirs(dirname)
493 except OSError, err:
494 if not os.path.exists(dirname):
495 raise
496 return underlay, fullpath
497
498 def getPagePath(self, *args, **kw):
499 """ Return path to the page storage area. """
500 return self.getPageStatus(*args, **kw)[1]
501
502 def _text_filename(self, **kw):
503 """ The name of the page file, possibly of an older page.
504
505 @keyword rev: page revision, overriding self.rev
506 @rtype: string
507 @return: complete filename (including path) to this page
508 """
509 if self._text_filename_force is not None:
510 return self._text_filename_force
511 rev = kw.get('rev', 0)
512 if not rev and self.rev:
513 rev = self.rev
514 fname, rev, exists = self.get_rev(-1, rev)
515 return fname
516
517 def editlog_entry(self):
518 """ Return the edit-log entry for this Page object (can be an old revision).
519 """
520 request = self.request
521 use_cache = self.rev == 0 # use the cache for current rev
522 if use_cache:
523 cache_name, cache_key = self.page_name, 'lastlog'
524 entry = request.cfg.cache.meta.getItem(request, cache_name, cache_key)
525 else:
526 entry = None
527 if entry is None:
528 from MoinMoin.logfile import editlog
529 wanted_rev = "%08d" % self.get_real_rev()
530 edit_log = editlog.EditLog(request, rootpagename=self.page_name)
531 for entry in edit_log.reverse():
532 if entry.rev == wanted_rev:
533 break
534 else:
535 entry = () # don't use None
536 if use_cache:
537 request.cfg.cache.meta.putItem(request, cache_name, cache_key, entry)
538 return entry
539
540 def edit_info(self):
541 """ Return timestamp/editor info for this Page object (can be an old revision).
542
543 Note: if you ask about a deleted revision, it will report timestamp and editor
544 for the delete action (in the edit-log, this is just a SAVE).
545
546 This is used by MoinMoin/xmlrpc/__init__.py.
547
548 @rtype: dict
549 @return: timestamp and editor information
550 """
551 line = self.editlog_entry()
552 if line:
553 editordata = line.getInterwikiEditorData(self.request)
554 if editordata[0] == 'interwiki':
555 editor = "%s:%s" % editordata[1]
556 else:
557 editor = editordata[1] # ip or email or anon
558 result = {
559 'timestamp': line.ed_time_usecs,
560 'editor': editor,
561 }
562 del line
563 else:
564 result = {}
565 return result
566
567 def last_edit(self, request):
568 # XXX usage of last_edit is DEPRECATED - use edit_info()
569 if not self.exists(): # XXX doesn't make much sense, but still kept
570 return None # XXX here until we remove last_edit()
571 return self.edit_info()
572
573 def lastEditInfo(self, request=None):
574 """ Return the last edit info.
575
576 Note: if you ask about a deleted revision, it will report timestamp and editor
577 for the delete action (in the edit-log, this is just a SAVE).
578
579 @param request: the request object (DEPRECATED, unused)
580 @rtype: dict
581 @return: timestamp and editor information
582 """
583 log = self.editlog_entry()
584 if log:
585 request = self.request
586 editor = log.getEditor(request)
587 time = wikiutil.version2timestamp(log.ed_time_usecs)
588 time = request.user.getFormattedDateTime(time) # Use user time format
589 result = {'editor': editor, 'time': time}
590 del log
591 else:
592 result = {}
593 return result
594
595 def isWritable(self):
596 """ Can this page be changed?
597
598 @rtype: bool
599 @return: true, if this page is writable or does not exist
600 """
601 return os.access(self._text_filename(), os.W_OK) or not self.exists()
602
603 def isUnderlayPage(self, includeDeleted=True):
604 """ Does this page live in the underlay dir?
605
606 Return true even if the data dir has a copy of this page. To
607 check for underlay only page, use ifUnderlayPage() and not
608 isStandardPage()
609
610 @param includeDeleted: include deleted pages
611 @rtype: bool
612 @return: true if page lives in the underlay dir
613 """
614 return self.exists(domain='underlay', includeDeleted=includeDeleted)
615
616 def isStandardPage(self, includeDeleted=True):
617 """ Does this page live in the data dir?
618
619 Return true even if this is a copy of an underlay page. To check
620 for data only page, use isStandardPage() and not isUnderlayPage().
621
622 @param includeDeleted: include deleted pages
623 @rtype: bool
624 @return: true if page lives in the data dir
625 """
626 return self.exists(domain='standard', includeDeleted=includeDeleted)
627
628 def exists(self, rev=0, domain=None, includeDeleted=False):
629 """ Does this page exist?
630
631 This is the lower level method for checking page existence. Use
632 the higher level methods isUnderlayPage and isStandardPage for
633 cleaner code.
634
635 @param rev: revision to look for. Default: check current
636 @param domain: where to look for the page. Default: look in all,
637 available values: 'underlay', 'standard'
638 @param includeDeleted: ignore page state, just check its pagedir
639 @rtype: bool
640 @return: true, if page exists
641 """
642 # Edge cases
643 if domain == 'underlay' and not self.request.cfg.data_underlay_dir:
644 return False
645
646 if includeDeleted:
647 # Look for page directory, ignore page state
648 if domain is None:
649 checklist = [0, 1]
650 else:
651 checklist = [domain == 'underlay']
652 for use_underlay in checklist:
653 pagedir = self.getPagePath(use_underlay=use_underlay, check_create=0)
654 if os.path.exists(pagedir):
655 return True
656 return False
657 else:
658 # Look for non-deleted pages only, using get_rev
659 if not rev and self.rev:
660 rev = self.rev
661
662 if domain is None:
663 use_underlay = -1
664 else:
665 use_underlay = domain == 'underlay'
666 d, d, exists = self.get_rev(use_underlay, rev)
667 return exists
668
669 def size(self, rev=0):
670 """ Get Page size.
671
672 @rtype: int
673 @return: page size, 0 for non-existent pages.
674 """
675 if rev == self.rev: # same revision as self
676 if self.__body is not None:
677 return len(self.__body)
678
679 try:
680 return os.path.getsize(self._text_filename(rev=rev))
681 except EnvironmentError, e:
682 import errno
683 if e.errno == errno.ENOENT:
684 return 0
685 raise
686
687 def mtime_usecs(self):
688 """ Get modification timestamp of this page (from edit-log, can be for an old revision).
689
690 @rtype: int
691 @return: mtime of page (or 0 if page / edit-log entry does not exist)
692 """
693 entry = self.editlog_entry()
694 return entry and entry.ed_time_usecs or 0
695
696 def mtime_printable(self, request):
697 """ Get printable (as per user's preferences) modification timestamp of this page.
698
699 @rtype: string
700 @return: formatted string with mtime of page
701 """
702 t = self.mtime_usecs()
703 if not t:
704 result = "0" # TODO: i18n, "Ever", "Beginning of time"...?
705 else:
706 result = request.user.getFormattedDateTime(
707 wikiutil.version2timestamp(t))
708 return result
709
710 def split_title(self):
711 """ Return a string with the page name split by spaces per user preferences.
712
713 @rtype: unicode
714 @return: pagename of this page, split into space separated words
715 """
716 request = self.request
717 if not request.user.wikiname_add_spaces:
718 return self.page_name
719
720 # look for the end of words and the start of a new word,
721 # and insert a space there
722 splitted = config.split_regex.sub(r'\1 \2', self.page_name)
723 return splitted
724
725 def url(self, request, querystr=None, anchor=None, relative=False, **kw):
726 """ Return complete URL for this page, including scriptname.
727 The URL is NOT escaped, if you write it to HTML, use wikiutil.escape
728 (at least if you have a querystr, to escape the & chars).
729
730 @param request: the request object
731 @param querystr: the query string to add after a "?" after the url
732 (str or dict, see wikiutil.makeQueryString)
733 @param anchor: if specified, make a link to this anchor
734 @param relative: create a relative link (default: False), note that this
735 changed in 1.7, in 1.6, the default was True.
736 @rtype: str
737 @return: complete url of this page, including scriptname
738 """
739 assert(isinstance(anchor, (type(None), str, unicode)))
740 # Create url, excluding scriptname
741 url = wikiutil.quoteWikinameURL(self.page_name)
742 if querystr:
743 if isinstance(querystr, dict):
744 action = querystr.get('action', None)
745 else:
746 action = None # we don't support getting the action out of a str
747
748 querystr = wikiutil.makeQueryString(querystr)
749
750 # make action URLs denyable by robots.txt:
751 if action is not None and request.cfg.url_prefix_action is not None:
752 url = "%s/%s/%s" % (request.cfg.url_prefix_action, action, url)
753 url = '%s?%s' % (url, querystr)
754
755 if not relative:
756 url = '%s/%s' % (request.script_root, url)
757
758 # Add anchor
759 if anchor:
760 fmt = getattr(self, 'formatter', request.html_formatter)
761 if fmt:
762 anchor = fmt.sanitize_to_id(anchor)
763 url = "%s#%s" % (url, anchor)
764
765 return url
766
767 def link_to_raw(self, request, text, querystr=None, anchor=None, **kw):
768 """ core functionality of link_to, without the magic """
769 url = self.url(request, querystr, anchor=anchor, relative=True) # scriptName is added by link_tag
770 # escaping is done by link_tag -> formatter.url -> ._open()
771 link = wikiutil.link_tag(request, url, text,
772 formatter=getattr(self, 'formatter', None), **kw)
773 return link
774
775 def link_to(self, request, text=None, querystr=None, anchor=None, no_escape=None, **kw):
776 """ Return HTML markup that links to this page.
777
778 See wikiutil.link_tag() for possible keyword parameters.
779
780 @param request: the request object
781 @param text: inner text of the link - it gets automatically escaped
782 @param querystr: the query string to add after a "?" after the url
783 @param anchor: if specified, make a link to this anchor
784 @keyword on: opening/closing tag only
785 @keyword attachment_indicator: if 1, add attachment indicator after link tag
786 @keyword css_class: css class to use
787 @rtype: string
788 @return: formatted link
789 """
790 if not text:
791 text = self.split_title()
792 if not no_escape:
793 text = wikiutil.escape(text)
794
795 # Add css class for non existing page
796 if not self.exists():
797 kw['css_class'] = 'nonexistent'
798
799 attachment_indicator = kw.get('attachment_indicator')
800 if attachment_indicator is None:
801 attachment_indicator = 0 # default is off
802 else:
803 del kw['attachment_indicator'] # avoid having this as <a> tag attribute
804
805 link = self.link_to_raw(request, text, querystr, anchor, **kw)
806
807 # Create a link to attachments if any exist
808 if attachment_indicator:
809 from MoinMoin.action import AttachFile
810 link += AttachFile.getIndicator(request, self.page_name)
811
812 return link
813
814 def getSubscribers(self, request, **kw):
815 """ Get all subscribers of this page.
816
817 @param request: the request object
818 @keyword include_self: if 1, include current user (default: 0)
819 @keyword return_users: if 1, return user instances (default: 0)
820 @rtype: dict
821 @return: lists of subscribed email addresses in a dict by language key
822 """
823 include_self = kw.get('include_self', self.include_self)
824 return_users = kw.get('return_users', 0)
825
826 # extract categories of this page
827 pageList = self.getCategories(request)
828
829 # add current page name for list matching
830 pageList.append(self.page_name)
831
832 if self.cfg.SecurityPolicy:
833 UserPerms = self.cfg.SecurityPolicy
834 else:
835 from MoinMoin.security import Default as UserPerms
836
837 # get email addresses of the all wiki user which have a profile stored;
838 # add the address only if the user has subscribed to the page and
839 # the user is not the current editor
840 userlist = user.getUserList(request)
841 subscriber_list = {}
842 for uid in userlist:
843 if uid == request.user.id and not include_self:
844 continue # no self notification
845 subscriber = user.User(request, uid)
846
847 # The following tests should be ordered in order of
848 # decreasing computation complexity, in particular
849 # the permissions check may be expensive; see the bug
850 # MoinMoinBugs/GetSubscribersPerformanceProblem
851
852 # This is a bit wrong if return_users=1 (which implies that the caller will process
853 # user attributes and may, for example choose to send an SMS)
854 # So it _should_ be "not (subscriber.email and return_users)" but that breaks at the moment.
855 if not subscriber.email:
856 continue # skip empty email addresses
857
858 # skip people not subscribed
859 if not subscriber.isSubscribedTo(pageList):
860 continue
861
862 # skip people who can't read the page
863 if not UserPerms(subscriber).read(self.page_name):
864 continue
865
866 # add the user to the list
867 lang = subscriber.language or request.cfg.language_default
868 if not lang in subscriber_list:
869 subscriber_list[lang] = []
870 if return_users:
871 subscriber_list[lang].append(subscriber)
872 else:
873 subscriber_list[lang].append(subscriber.email)
874
875 return subscriber_list
876
877 def parse_processing_instructions(self):
878 """ Parse page text and extract processing instructions,
879 return a dict of PIs and the non-PI rest of the body.
880 """
881 from MoinMoin import i18n
882 from MoinMoin import security
883 request = self.request
884 pi = {} # we collect the processing instructions here
885
886 # default language from cfg
887 pi['language'] = self.cfg.language_default or "en"
888
889 body = self.body
890 # TODO: remove this hack once we have separate metadata and can use mimetype there
891 if body.startswith('<?xml'): # check for XML content
892 pi['lines'] = 0
893 pi['format'] = "xslt"
894 pi['formatargs'] = ''
895 pi['acl'] = security.AccessControlList(request.cfg, []) # avoid KeyError on acl check
896 return pi
897
898 meta = self.meta
899
900 # default is wiki markup
901 pi['format'] = self.cfg.default_markup or "wiki"
902 pi['formatargs'] = ''
903 pi['lines'] = len(meta)
904 acl = []
905
906 for verb, args in meta:
907 if verb == "format": # markup format
908 format, formatargs = (args + ' ').split(' ', 1)
909 pi['format'] = format.lower()
910 pi['formatargs'] = formatargs.strip()
911
912 elif verb == "acl":
913 acl.append(args)
914
915 elif verb == "language":
916 # Page language. Check if args is a known moin language
917 if args in i18n.wikiLanguages():
918 pi['language'] = args
919
920 elif verb == "refresh":
921 if self.cfg.refresh:
922 try:
923 mindelay, targetallowed = self.cfg.refresh
924 args = args.split()
925 if len(args) >= 1:
926 delay = max(int(args[0]), mindelay)
927 if len(args) >= 2:
928 target = args[1]
929 else:
930 target = self.page_name
931 if '://' in target:
932 if targetallowed == 'internal':
933 raise ValueError
934 elif targetallowed == 'external':
935 url = target
936 else:
937 url = Page(request, target).url(request)
938 pi['refresh'] = (delay, url)
939 except (ValueError, ):
940 pass
941
942 elif verb == "redirect":
943 pi['redirect'] = args
944
945 elif verb == "deprecated":
946 pi['deprecated'] = True
947
948 elif verb == "openiduser":
949 if request.cfg.openid_server_enable_user:
950 pi['openid.user'] = args
951
952 elif verb == "pragma":
953 try:
954 key, val = args.split(' ', 1)
955 except (ValueError, TypeError):
956 pass
957 else:
958 request.setPragma(key, val)
959
960 pi['acl'] = security.AccessControlList(request.cfg, acl)
961 return pi
962
963 def send_raw(self, content_disposition=None, mimetype=None):
964 """ Output the raw page data (action=raw).
965 With no content_disposition, the browser usually just displays the
966 data on the screen, with content_disposition='attachment', it will
967 offer a dialogue to save it to disk (used by Save action).
968 Supplied mimetype overrides default text/plain.
969 """
970 request = self.request
971 request.mimetype = mimetype or 'text/plain'
972 if self.exists():
973 # use the correct last-modified value from the on-disk file
974 # to ensure cacheability where supported. Because we are sending
975 # RAW (file) content, the file mtime is correct as Last-Modified header.
976 request.status_code = 200
977 request.last_modified = os.path.getmtime(self._text_filename())
978 text = self.encodeTextMimeType(self.body)
979 #request.headers['Content-Length'] = len(text) # XXX WRONG! text is unicode obj, but we send utf-8!
980 if content_disposition:
981 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
982 # There is no solution that is compatible to IE except stripping non-ascii chars
983 filename_enc = "%s.txt" % self.page_name.encode(config.charset)
984 dispo_string = '%s; filename="%s"' % (content_disposition, filename_enc)
985 request.headers['Content-Disposition'] = dispo_string
986 else:
987 request.status_code = 404
988 text = u"Page %s not found." % self.page_name
989
990 request.write(text)
991
992 def send_page(self, **keywords):
993 """ Output the formatted page.
994
995 TODO: "kill send_page(), quick" (since 2002 :)
996
997 @keyword content_only: if 1, omit http headers, page header and footer
998 @keyword content_id: set the id of the enclosing div
999 @keyword count_hit: if 1, add an event to the log
1000 @keyword send_special: if True, this is a special page send
1001 @keyword omit_footnotes: if True, do not send footnotes (used by include macro)
1002 """
1003 request = self.request
1004 _ = request.getText
1005 request.clock.start('send_page')
1006 emit_headers = keywords.get('emit_headers', 1)
1007 content_only = keywords.get('content_only', 0)
1008 omit_footnotes = keywords.get('omit_footnotes', 0)
1009 content_id = keywords.get('content_id', 'content')
1010 do_cache = keywords.get('do_cache', 1)
1011 send_special = keywords.get('send_special', False)
1012 print_mode = keywords.get('print_mode', 0)
1013 if print_mode:
1014 media = request.values.get('media', 'print')
1015 else:
1016 media = 'screen'
1017 self.hilite_re = (keywords.get('hilite_re') or
1018 request.values.get('highlight'))
1019
1020 # count hit?
1021 if keywords.get('count_hit', 0):
1022 eventlog.EventLog(request).add(request, 'VIEWPAGE', {'pagename': self.page_name})
1023
1024 # load the text
1025 body = self.data
1026 pi = self.pi
1027
1028 if 'redirect' in pi and not (
1029 'action' in request.values or 'redirect' in request.values or content_only):
1030 # redirect to another page
1031 # note that by including "action=show", we prevent endless looping
1032 # (see code in "request") or any cascaded redirection
1033 pagename, anchor = wikiutil.split_anchor(pi['redirect'])
1034 redirect_url = Page(request, pagename).url(request,
1035 querystr={'action': 'show', 'redirect': self.page_name, },
1036 anchor=anchor)
1037 request.http_redirect(redirect_url, code=301)
1038 return
1039
1040 # if necessary, load the formatter
1041 if self.default_formatter:
1042 from MoinMoin.formatter.text_html import Formatter
1043 self.formatter = Formatter(request, store_pagelinks=1)
1044 elif not self.formatter:
1045 Formatter = wikiutil.searchAndImportPlugin(request.cfg, "formatter", self.output_mimetype)
1046 self.formatter = Formatter(request)
1047
1048 # save formatter
1049 no_formatter = object()
1050 old_formatter = getattr(request, "formatter", no_formatter)
1051 request.formatter = self.formatter
1052
1053 self.formatter.setPage(self)
1054 if self.hilite_re:
1055 try:
1056 self.formatter.set_highlight_re(self.hilite_re)
1057 except re.error, err:
1058 request.theme.add_msg(_('Invalid highlighting regular expression "%(regex)s": %(error)s') % {
1059 'regex': wikiutil.escape(self.hilite_re),
1060 'error': wikiutil.escape(str(err)),
1061 }, "warning")
1062 self.hilite_re = None
1063
1064 if 'deprecated' in pi:
1065 # deprecated page, append last backup version to current contents
1066 # (which should be a short reason why the page is deprecated)
1067 request.theme.add_msg(_('The backed up content of this page is deprecated and will rank lower in search results!'), "warning")
1068
1069 revisions = self.getRevList()
1070 if len(revisions) >= 2: # XXX shouldn't that be ever the case!? Looks like not.
1071 oldpage = Page(request, self.page_name, rev=revisions[1])
1072 body += oldpage.get_data()
1073 del oldpage
1074
1075 lang = self.pi.get('language', request.cfg.language_default)
1076 request.setContentLanguage(lang)
1077
1078 # start document output
1079 page_exists = self.exists()
1080 if not content_only:
1081 if emit_headers:
1082 request.content_type = "%s; charset=%s" % (self.output_mimetype, self.output_charset)
1083 if page_exists:
1084 if not request.user.may.read(self.page_name):
1085 request.status_code = 403
1086 else:
1087 request.status_code = 200
1088 if not request.cacheable:
1089 # use "nocache" headers if we're using a method that is not simply "display"
1090 request.disableHttpCaching(level=2)
1091 elif request.user.valid:
1092 # use nocache headers if a user is logged in (which triggers personalisation features)
1093 request.disableHttpCaching(level=1)
1094 else:
1095 # TODO: we need to know if a page generates dynamic content -
1096 # if it does, we must not use the page file mtime as last modified value
1097 # The following code is commented because it is incorrect for dynamic pages:
1098 #lastmod = os.path.getmtime(self._text_filename())
1099 #request.headers['Last-Modified'] = util.timefuncs.formathttpdate(lastmod)
1100 pass
1101 else:
1102 request.status_code = 404
1103
1104 if not page_exists and self.request.isSpiderAgent:
1105 # don't send any 404 content to bots
1106 return
1107
1108 request.write(self.formatter.startDocument(self.page_name))
1109
1110 # send the page header
1111 if self.default_formatter:
1112 if self.rev:
1113 request.theme.add_msg("<strong>%s</strong><br>" % (
1114 _('Revision %(rev)d as of %(date)s') % {
1115 'rev': self.rev,
1116 'date': wikiutil.escape(self.mtime_printable(request))
1117 }), "info")
1118
1119 # This redirect message is very annoying.
1120 # Less annoying now without the warning sign.
1121 if 'redirect' in request.values:
1122 redir = request.values['redirect']
1123 request.theme.add_msg('<strong>%s</strong><br>' % (
1124 _('Redirected from page "%(page)s"') % {'page':
1125 wikiutil.link_tag(request, wikiutil.quoteWikinameURL(redir) + "?action=show", self.formatter.text(redir))}), "info")
1126 if 'redirect' in pi:
1127 request.theme.add_msg('<strong>%s</strong><br>' % (
1128 _('This page redirects to page "%(page)s"') % {'page': wikiutil.escape(pi['redirect'])}), "info")
1129
1130 # Page trail
1131 trail = None
1132 if not print_mode:
1133 request.user.addTrail(self)
1134 trail = request.user.getTrail()
1135
1136 title = self.split_title()
1137
1138 html_head = ''
1139 if request.cfg.openid_server_enabled:
1140 openid_username = self.page_name
1141 userid = user.getUserId(request, openid_username)
1142
1143 if userid is None and 'openid.user' in self.pi:
1144 openid_username = self.pi['openid.user']
1145 userid = user.getUserId(request, openid_username)
1146
1147 openid_group_name = request.cfg.openid_server_restricted_users_group
1148 if userid is not None and (
1149 not openid_group_name or (
1150 openid_group_name in request.groups and
1151 openid_username in request.groups[openid_group_name])):
1152 html_head = '<link rel="openid2.provider" href="%s">' % \
1153 wikiutil.escape(request.getQualifiedURL(self.url(request,
1154 querystr={'action': 'serveopenid'})), True)
1155 html_head += '<link rel="openid.server" href="%s">' % \
1156 wikiutil.escape(request.getQualifiedURL(self.url(request,
1157 querystr={'action': 'serveopenid'})), True)
1158 html_head += '<meta http-equiv="x-xrds-location" content="%s">' % \
1159 wikiutil.escape(request.getQualifiedURL(self.url(request,
1160 querystr={'action': 'serveopenid', 'yadis': 'ep'})), True)
1161 elif self.page_name == request.cfg.page_front_page:
1162 html_head = '<meta http-equiv="x-xrds-location" content="%s">' % \
1163 wikiutil.escape(request.getQualifiedURL(self.url(request,
1164 querystr={'action': 'serveopenid', 'yadis': 'idp'})), True)
1165
1166 request.theme.send_title(title, page=self,
1167 print_mode=print_mode,
1168 media=media, pi_refresh=pi.get('refresh'),
1169 allow_doubleclick=1, trail=trail,
1170 html_head=html_head,
1171 )
1172
1173 # special pages handling, including denying access
1174 special = None
1175
1176 if not send_special:
1177 if not page_exists and not body:
1178 special = 'missing'
1179 elif not request.user.may.read(self.page_name):
1180 special = 'denied'
1181
1182 # if we have a special page, output it, unless
1183 # - we should only output content (this is for say the pagelinks formatter)
1184 # - we have a non-default formatter
1185 if special and not content_only and self.default_formatter:
1186 self._specialPageText(request, special) # this recursively calls send_page
1187
1188 # if we didn't short-cut to a special page, output this page
1189 if not special:
1190 # start wiki content div
1191 request.write(self.formatter.startContent(content_id))
1192
1193 # parse the text and send the page content
1194 self.send_page_content(request, body,
1195 format=pi['format'],
1196 format_args=pi['formatargs'],
1197 do_cache=do_cache,
1198 start_line=pi['lines'])
1199
1200 # check for pending footnotes
1201 if getattr(request, 'footnotes', None) and not omit_footnotes:
1202 from MoinMoin.macro.FootNote import emit_footnotes
1203 request.write(emit_footnotes(request, self.formatter))
1204
1205 # end wiki content div
1206 request.write(self.formatter.endContent())
1207
1208 # end document output
1209 if not content_only:
1210 # send the page footer
1211 if self.default_formatter:
1212 request.theme.send_footer(self.page_name, print_mode=print_mode)
1213
1214 request.write(self.formatter.endDocument())
1215
1216 request.clock.stop('send_page')
1217 if not content_only and self.default_formatter:
1218 request.theme.send_closing_html()
1219
1220 # cache the pagelinks
1221 if do_cache and self.default_formatter and page_exists:
1222 cache = caching.CacheEntry(request, self, 'pagelinks', scope='item', use_pickle=True)
1223 if cache.needsUpdate(self._text_filename()):
1224 links = self.formatter.pagelinks
1225 cache.update(links)
1226
1227 # restore old formatter (hopefully we dont throw any exception that is catched again)
1228 if old_formatter is no_formatter:
1229 del request.formatter
1230 else:
1231 request.formatter = old_formatter
1232
1233
1234 def getFormatterName(self):
1235 """ Return a formatter name as used in the caching system
1236
1237 @rtype: string
1238 @return: formatter name as used in caching
1239 """
1240 if not hasattr(self, 'formatter') or self.formatter is None:
1241 return ''
1242 module = self.formatter.__module__
1243 return module[module.rfind('.') + 1:]
1244
1245 def canUseCache(self, parser=None):
1246 """ Is caching available for this request?
1247
1248 This make sure we can try to use the caching system for this
1249 request, but it does not make sure that this will
1250 succeed. Themes can use this to decide if a Refresh action
1251 should be displayed.
1252
1253 @param parser: the parser used to render the page
1254 @rtype: bool
1255 @return: if this page can use caching
1256 """
1257 if (not self.rev and
1258 not self.hilite_re and
1259 not self.__body_modified and
1260 self.getFormatterName() in self.cfg.caching_formats):
1261 # Everything is fine, now check the parser:
1262 if parser is None:
1263 try:
1264 parser = wikiutil.searchAndImportPlugin(self.request.cfg, "parser", self.pi['format'])
1265 except wikiutil.PluginMissingError:
1266 return False
1267 return getattr(parser, 'caching', False)
1268 return False
1269
1270 def send_page_content(self, request, body, format='wiki', format_args='', do_cache=1, **kw):
1271 """ Output the formatted wiki page, using caching if possible
1272
1273 @param request: the request object
1274 @param body: text of the wiki page
1275 @param format: format of content, default 'wiki'
1276 @param format_args: #format arguments, used by some parsers
1277 @param do_cache: if True, use cached content
1278 """
1279 request.clock.start('send_page_content')
1280 # Load the parser
1281 try:
1282 Parser = wikiutil.searchAndImportPlugin(request.cfg, "parser", format)
1283 except wikiutil.PluginMissingError:
1284 Parser = wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain")
1285 parser = Parser(body, request, format_args=format_args, **kw)
1286
1287 if not (do_cache and self.canUseCache(Parser)):
1288 self.format(parser)
1289 else:
1290 try:
1291 code = self.loadCache(request)
1292 self.execute(request, parser, code)
1293 except Exception, e:
1294 if not is_cache_exception(e):
1295 raise
1296 try:
1297 code = self.makeCache(request, parser)
1298 self.execute(request, parser, code)
1299 except Exception, e:
1300 if not is_cache_exception(e):
1301 raise
1302 logging.error('page cache failed after creation')
1303 self.format(parser)
1304
1305 request.clock.stop('send_page_content')
1306
1307 def format(self, parser):
1308 """ Format and write page content without caching """
1309 parser.format(self.formatter)
1310
1311 def execute(self, request, parser, code):
1312 """ Write page content by executing cache code """
1313 formatter = self.formatter
1314 request.clock.start("Page.execute")
1315 try:
1316 from MoinMoin.macro import Macro
1317 macro_obj = Macro(parser)
1318 # Fix __file__ when running from a zip package
1319 import MoinMoin
1320 if hasattr(MoinMoin, '__loader__'):
1321 __file__ = os.path.join(MoinMoin.__loader__.archive, 'dummy')
1322 try:
1323 exec code
1324 except "CacheNeedsUpdate": # convert the exception
1325 raise Exception("CacheNeedsUpdate")
1326 finally:
1327 request.clock.stop("Page.execute")
1328
1329 def loadCache(self, request):
1330 """ Return page content cache or raises 'CacheNeedsUpdate' """
1331 cache = caching.CacheEntry(request, self, self.getFormatterName(), scope='item')
1332 attachmentsPath = self.getPagePath('attachments', check_create=0)
1333 if cache.needsUpdate(self._text_filename(), attachmentsPath):
1334 raise Exception('CacheNeedsUpdate')
1335
1336 import marshal
1337 try:
1338 return marshal.loads(cache.content())
1339 except (EOFError, ValueError, TypeError):
1340 # Bad marshal data, must update the cache.
1341 # See http://docs.python.org/lib/module-marshal.html
1342 raise Exception('CacheNeedsUpdate')
1343 except Exception, err:
1344 logging.info('failed to load "%s" cache: %s' %
1345 (self.page_name, str(err)))
1346 raise Exception('CacheNeedsUpdate')
1347
1348 def makeCache(self, request, parser):
1349 """ Format content into code, update cache and return code """
1350 import marshal
1351 from MoinMoin.formatter.text_python import Formatter
1352 formatter = Formatter(request, ["page"], self.formatter)
1353
1354 # Save request state while formatting page
1355 saved_current_lang = request.current_lang
1356 try:
1357 text = request.redirectedOutput(parser.format, formatter)
1358 finally:
1359 request.current_lang = saved_current_lang
1360
1361 src = formatter.assemble_code(text)
1362 code = compile(src.encode(config.charset),
1363 self.page_name.encode(config.charset), 'exec')
1364 cache = caching.CacheEntry(request, self, self.getFormatterName(), scope='item')
1365 cache.update(marshal.dumps(code))
1366 return code
1367
1368 def _specialPageText(self, request, special_type):
1369 """ Output the default page content for new pages.
1370
1371 @param request: the request object
1372 """
1373 _ = request.getText
1374
1375 if special_type == 'missing':
1376 if request.user.valid and request.user.name == self.page_name and \
1377 request.cfg.user_homewiki in ('Self', request.cfg.interwikiname):
1378 page = wikiutil.getLocalizedPage(request, 'MissingHomePage')
1379 else:
1380 page = wikiutil.getLocalizedPage(request, 'MissingPage')
1381
1382 alternative_text = u"'''<<Action(action=edit, text=\"%s\")>>'''" % _('Create New Page')
1383 elif special_type == 'denied':
1384 page = wikiutil.getLocalizedPage(request, 'PermissionDeniedPage')
1385 alternative_text = u"'''%s'''" % _('You are not allowed to view this page.')
1386 else:
1387 assert False
1388
1389 special_exists = page.exists()
1390
1391 if special_exists:
1392 page._text_filename_force = page._text_filename()
1393 else:
1394 page.body = alternative_text
1395 logging.warn('The page "%s" could not be found. Check your'
1396 ' underlay directory setting.' % page.page_name)
1397 page.page_name = self.page_name
1398
1399 page.send_page(content_only=True, do_cache=not special_exists, send_special=True)
1400
1401
1402 def getRevList(self):
1403 """ Get a page revision list of this page, including the current version,
1404 sorted by revision number in descending order (current page first).
1405
1406 @rtype: list of ints
1407 @return: page revisions
1408 """
1409 revisions = []
1410 if self.page_name:
1411 rev_dir = self.getPagePath('revisions', check_create=0)
1412 if os.path.isdir(rev_dir):
1413 for rev in os.listdir(rev_dir):
1414 try:
1415 revint = int(rev)
1416 revisions.append(revint)
1417 except ValueError:
1418 pass
1419 revisions.sort()
1420 revisions.reverse()
1421 return revisions
1422
1423 def olderrevision(self, rev=0):
1424 """ Get revision of the next older page revision than rev.
1425 rev == 0 means this page objects revision (that may be an old
1426 revision already!)
1427 """
1428 if rev == 0:
1429 rev = self.rev
1430 revisions = self.getRevList()
1431 for r in revisions:
1432 if r < rev:
1433 older = r
1434 break
1435 return older
1436
1437 def getPageText(self, start=0, length=None):
1438 """ Convenience function to get the page text, skipping the header
1439
1440 @rtype: unicode
1441 @return: page text, excluding the header
1442 """
1443 if length is None:
1444 return self.data[start:]
1445 else:
1446 return self.data[start:start+length]
1447
1448 def getPageHeader(self, start=0, length=None):
1449 """ Convenience function to get the page header
1450
1451 @rtype: unicode
1452 @return: page header
1453 """
1454 header = ['#%s %s' % t for t in self.meta]
1455 header = '\n'.join(header)
1456 if header:
1457 if length is None:
1458 return header[start:]
1459 else:
1460 return header[start:start+length]
1461 return ''
1462
1463 def getPageLinks(self, request):
1464 """ Get a list of the links on this page.
1465
1466 @param request: the request object
1467 @rtype: list
1468 @return: page names this page links to
1469 """
1470 if self.exists():
1471 cache = caching.CacheEntry(request, self, 'pagelinks', scope='item', do_locking=False, use_pickle=True)
1472 if cache.needsUpdate(self._text_filename()):
1473 links = self.parsePageLinks(request)
1474 cache.update(links)
1475 else:
1476 try:
1477 links = cache.content()
1478 except caching.CacheError:
1479 links = self.parsePageLinks(request)
1480 cache.update(links)
1481 else:
1482 links = []
1483 return links
1484
1485 def parsePageLinks(self, request):
1486 """ Parse page links by formatting with a pagelinks formatter
1487
1488 This is a old hack to get the pagelinks by rendering the page
1489 with send_page. We can remove this hack after factoring
1490 send_page and send_page_content into small reuseable methods.
1491
1492 More efficient now by using special pagelinks formatter and
1493 redirecting possible output into null file.
1494 """
1495 pagename = self.page_name
1496 if request.parsePageLinks_running.get(pagename, False):
1497 #logging.debug("avoid recursion for page %r" % pagename)
1498 return [] # avoid recursion
1499
1500 #logging.debug("running parsePageLinks for page %r" % pagename)
1501 # remember we are already running this function for this page:
1502 request.parsePageLinks_running[pagename] = True
1503
1504 request.clock.start('parsePageLinks')
1505
1506 class Null:
1507 def write(self, data):
1508 pass
1509
1510 request.redirect(Null())
1511 request.mode_getpagelinks += 1
1512 #logging.debug("mode_getpagelinks == %r" % request.mode_getpagelinks)
1513 try:
1514 try:
1515 from MoinMoin.formatter.pagelinks import Formatter
1516 formatter = Formatter(request, store_pagelinks=1)
1517 page = Page(request, pagename, formatter=formatter)
1518 page.send_page(content_only=1)
1519 except:
1520 logging.exception("pagelinks formatter failed, traceback follows")
1521 finally:
1522 request.mode_getpagelinks -= 1
1523 #logging.debug("mode_getpagelinks == %r" % request.mode_getpagelinks)
1524 request.redirect()
1525 if hasattr(request, '_fmt_hd_counters'):
1526 del request._fmt_hd_counters
1527 request.clock.stop('parsePageLinks')
1528 return formatter.pagelinks
1529
1530 def getCategories(self, request):
1531 """ Get categories this page belongs to.
1532
1533 @param request: the request object
1534 @rtype: list
1535 @return: categories this page belongs to
1536 """
1537 return wikiutil.filterCategoryPages(request, self.getPageLinks(request))
1538
1539 def getParentPage(self):
1540 """ Return parent page or None
1541
1542 @rtype: Page
1543 @return: parent page or None
1544 """
1545 if self.page_name:
1546 pos = self.page_name.rfind('/')
1547 if pos > 0:
1548 parent = Page(self.request, self.page_name[:pos])
1549 if parent.exists():
1550 return parent
1551 return None
1552
1553 def getACL(self, request):
1554 """ Get cached ACLs of this page.
1555
1556 Return cached ACL or invoke parseACL and update the cache.
1557
1558 @param request: the request object
1559 @rtype: MoinMoin.security.AccessControlList
1560 @return: ACL of this page
1561 """
1562 try:
1563 return self.__acl # for request.page, this is n-1 times used
1564 except AttributeError:
1565 # the caching here is still useful for pages != request.page,
1566 # when we have multiple page objects for the same page name.
1567 request.clock.start('getACL')
1568 # Try the cache or parse acl and update the cache
1569 currentRevision = self.current_rev()
1570 cache_name = self.page_name
1571 cache_key = 'acl'
1572 cache_data = request.cfg.cache.meta.getItem(request, cache_name, cache_key)
1573 if cache_data is None:
1574 aclRevision, acl = None, None
1575 else:
1576 aclRevision, acl = cache_data
1577 #logging.debug("currrev: %r, cachedaclrev: %r" % (currentRevision, aclRevision))
1578 if aclRevision != currentRevision:
1579 acl = self.parseACL()
1580 if currentRevision != 99999999:
1581 # don't use cache for non existing pages
1582 # otherwise in the process of creating copies by filesys.copytree (PageEditor.copyPage)
1583 # the first may test will create a cache entry with the default_acls for a non existing page
1584 # At the time the page is created acls on that page would be ignored until the process
1585 # is completed by adding a log entry into edit-log
1586 cache_data = (currentRevision, acl)
1587 request.cfg.cache.meta.putItem(request, cache_name, cache_key, cache_data)
1588 self.__acl = acl
1589 request.clock.stop('getACL')
1590 return acl
1591
1592 def parseACL(self):
1593 """ Return ACLs parsed from the last available revision
1594
1595 The effective ACL is always from the last revision, even if
1596 you access an older revision.
1597 """
1598 from MoinMoin import security
1599 if self.exists() and self.rev == 0:
1600 return self.pi['acl']
1601 try:
1602 lastRevision = self.getRevList()[0]
1603 except IndexError:
1604 return security.AccessControlList(self.request.cfg)
1605 if self.rev == lastRevision:
1606 return self.pi['acl']
1607
1608 return Page(self.request, self.page_name, rev=lastRevision).parseACL()
1609
1610 # Text format -------------------------------------------------------
1611
1612 def encodeTextMimeType(self, text):
1613 """ Encode text from moin internal representation to text/* mime type
1614
1615 Make sure text uses CRLF line ends, keep trailing newline.
1616
1617 @param text: text to encode (unicode)
1618 @rtype: unicode
1619 @return: encoded text
1620 """
1621 if text:
1622 lines = text.splitlines()
1623 # Keep trailing newline
1624 if text.endswith(u'\n') and not lines[-1] == u'':
1625 lines.append(u'')
1626 text = u'\r\n'.join(lines)
1627 return text
1628
1629 def decodeTextMimeType(self, text):
1630 """ Decode text from text/* mime type to moin internal representation
1631
1632 @param text: text to decode (unicode). Text must use CRLF!
1633 @rtype: unicode
1634 @return: text using internal representation
1635 """
1636 text = text.replace(u'\r', u'')
1637 return text
1638
1639 def isConflict(self):
1640 """ Returns true if there is a known editing conflict for that page.
1641
1642 @return: true if there is a known conflict.
1643 """
1644
1645 cache = caching.CacheEntry(self.request, self, 'conflict', scope='item')
1646 return cache.exists()
1647
1648 def setConflict(self, state):
1649 """ Sets the editing conflict flag.
1650
1651 @param state: bool, true if there is a conflict.
1652 """
1653 cache = caching.CacheEntry(self.request, self, 'conflict', scope='item')
1654 if state:
1655 cache.update("") # touch it!
1656 else:
1657 cache.remove()
1658
1659
1660 class RootPage(Page):
1661 """ These functions were removed from the Page class to remove hierarchical
1662 page storage support until after we have a storage api (and really need it).
1663 Currently, there is only 1 instance of this class: request.rootpage
1664 """
1665 def __init__(self, request):
1666 page_name = u''
1667 Page.__init__(self, request, page_name)
1668
1669 def getPageBasePath(self, use_underlay=0):
1670 """ Get full path to a page-specific storage area. `args` can
1671 contain additional path components that are added to the base path.
1672
1673 @param use_underlay: force using a specific pagedir, default 0:
1674 1 = use underlay page dir
1675 0 = use standard page dir
1676 Note: we do NOT have special support for -1
1677 here, that will just behave as 0!
1678 @rtype: string
1679 @return: int underlay,
1680 str the full path to the storage area
1681 """
1682 if self.cfg.data_underlay_dir is None:
1683 use_underlay = 0
1684
1685 # 'auto' doesn't make sense here. maybe not even 'underlay':
1686 if use_underlay == 1:
1687 underlay, path = 1, self.cfg.data_underlay_dir
1688 # no need to check 'standard' case, we just use path in that case!
1689 else:
1690 # this is the location of the virtual root page
1691 underlay, path = 0, self.cfg.data_dir
1692
1693 return underlay, path
1694
1695 def getPageList(self, user=None, exists=1, filter=None, include_underlay=True, return_objects=False):
1696 """ List user readable pages under current page
1697
1698 Currently only request.rootpage is used to list pages, but if we
1699 have true sub pages, any page can list its sub pages.
1700
1701 The default behavior is listing all the pages readable by the
1702 current user. If you want to get a page list for another user,
1703 specify the user name.
1704
1705 If you want to get the full page list, without user filtering,
1706 call with user="". Use this only if really needed, and do not
1707 display pages the user can not read.
1708
1709 filter is usually compiled re match or search method, but can be
1710 any method that get a unicode argument and return bool. If you
1711 want to filter the page list, do it with this filter function,
1712 and NOT on the output of this function. page.exists() and
1713 user.may.read are very expensive, and should be done on the
1714 smallest data set.
1715
1716 @param user: the user requesting the pages (MoinMoin.user.User)
1717 @param filter: filter function
1718 @param exists: filter existing pages
1719 @param include_underlay: determines if underlay pages are returned as well
1720 @param return_objects: lets it return a list of Page objects instead of
1721 names
1722 @rtype: list of unicode strings
1723 @return: user readable wiki page names
1724 """
1725 request = self.request
1726 request.clock.start('getPageList')
1727 # Check input
1728 if user is None:
1729 user = request.user
1730
1731 # Get pages cache or create it
1732 cachedlist = request.cfg.cache.pagelists.getItem(request, 'all', None)
1733 if cachedlist is None:
1734 cachedlist = {}
1735 for name in self._listPages():
1736 # Unquote file system names
1737 pagename = wikiutil.unquoteWikiname(name)
1738
1739 # Filter those annoying editor backups - current moin does not create
1740 # those pages any more, but users have them already in data/pages
1741 # until we remove them by a mig script...
1742 if pagename.endswith(u'/MoinEditorBackup'):
1743 continue
1744
1745 cachedlist[pagename] = None
1746 request.cfg.cache.pagelists.putItem(request, 'all', None, cachedlist)
1747
1748 if user or exists or filter or not include_underlay or return_objects:
1749 # Filter names
1750 pages = []
1751 for name in cachedlist:
1752 # First, custom filter - exists and acl check are very
1753 # expensive!
1754 if filter and not filter(name):
1755 continue
1756
1757 page = Page(request, name)
1758
1759 # Filter underlay pages
1760 if not include_underlay and page.getPageStatus()[0]: # is an underlay page
1761 continue
1762
1763 # Filter deleted pages
1764 if exists and not page.exists():
1765 continue
1766
1767 # Filter out page user may not read.
1768 if user and not user.may.read(name):
1769 continue
1770
1771 if return_objects:
1772 pages.append(page)
1773 else:
1774 pages.append(name)
1775 else:
1776 pages = cachedlist.keys()
1777
1778 request.clock.stop('getPageList')
1779 return pages
1780
1781 def getPageDict(self, user=None, exists=1, filter=None, include_underlay=True):
1782 """ Return a dictionary of filtered page objects readable by user
1783
1784 Invoke getPageList then create a dict from the page list. See
1785 getPageList docstring for more details.
1786
1787 @param user: the user requesting the pages
1788 @param filter: filter function
1789 @param exists: only existing pages
1790 @rtype: dict {unicode: Page}
1791 @return: user readable pages
1792 """
1793 pages = {}
1794 for name in self.getPageList(user=user, exists=exists, filter=filter, include_underlay=include_underlay):
1795 pages[name] = Page(self.request, name)
1796 return pages
1797
1798 def _listPages(self):
1799 """ Return a list of file system page names
1800
1801 This is the lowest level disk access, don't use it unless you
1802 really need it.
1803
1804 NOTE: names are returned in file system encoding, not in unicode!
1805
1806 @rtype: dict
1807 @return: dict of page names using file system encoding
1808 """
1809 # Get pages in standard dir
1810 path = self.getPagePath('pages')
1811 pages = self._listPageInPath(path)
1812
1813 if self.cfg.data_underlay_dir is not None:
1814 # Merge with pages from underlay
1815 path = self.getPagePath('pages', use_underlay=1)
1816 underlay = self._listPageInPath(path)
1817 pages.update(underlay)
1818
1819 return pages
1820
1821 def _listPageInPath(self, path):
1822 """ List page names in domain, using path
1823
1824 This is the lowest level disk access, don't use it unless you
1825 really need it.
1826
1827 NOTE: names are returned in file system encoding, not in unicode!
1828
1829 @param path: directory to list (string)
1830 @rtype: dict
1831 @return: dict of page names using file system encoding
1832 """
1833 pages = {}
1834 for name in os.listdir(path):
1835 # Filter non-pages in quoted wiki names
1836 # List all pages in pages directory - assume flat namespace.
1837 # We exclude everything starting with '.' to get rid of . and ..
1838 # directory entries. If we ever create pagedirs starting with '.'
1839 # it will be with the intention to have them not show up in page
1840 # list (like .name won't show up for ls command under UNIX).
1841 # Note that a . within a wiki page name will be quoted to (2e).
1842 if not name.startswith('.'):
1843 pages[name] = None
1844
1845 if 'CVS' in pages:
1846 del pages['CVS'] # XXX DEPRECATED: remove this directory name just in
1847 # case someone has the pages dir under CVS control.
1848 return pages
1849
1850 def getPageCount(self, exists=0):
1851 """ Return page count
1852
1853 The default value does the fastest listing, and return count of
1854 all pages, including deleted pages, ignoring acl rights.
1855
1856 If you want to get a more accurate number, call with
1857 exists=1. This will be about 100 times slower though.
1858
1859 @param exists: filter existing pages
1860 @rtype: int
1861 @return: number of pages
1862 """
1863 self.request.clock.start('getPageCount')
1864 if exists:
1865 # WARNING: SLOW
1866 pages = self.getPageList(user='')
1867 else:
1868 pages = self._listPages()
1869 count = len(pages)
1870 self.request.clock.stop('getPageCount')
1871
1872 return count
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.