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.
  • [get | view] (2010-08-19 16:16:12, 73.3 KB) [[attachment:Page.py]]
  • [get | view] (2010-08-19 19:29:46, 13.9 KB) [[attachment:RecentChanges.py]]
  • [get | view] (2010-08-22 20:41:59, 10.7 KB) [[attachment:recentchanges.diff]]
  • [get | view] (2010-08-19 16:16:54, 91.8 KB) [[attachment:wikiutil.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.