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