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.
  • [get | view] (2010-02-05 17:13:18, 2.6 KB) [[attachment:ApprovePage-191.py]]
  • [get | view] (2005-10-18 14:16:34, 2.8 KB) [[attachment:ApprovePage.py]]
  • [get | view] (2005-11-08 14:48:57, 2.9 KB) [[attachment:ApprovePage.py-20050811]]
  • [get | view] (2005-11-08 14:51:44, 2.9 KB) [[attachment:ApprovePage.py-20050811-2]]
  • [get | view] (2010-11-18 09:43:04, 7.1 KB) [[attachment:Page.diff-v193-18112010]]
  • [get | view] (2010-02-05 17:15:25, 78.1 KB) [[attachment:Page.py]]
  • [get | view] (2010-11-18 09:41:22, 79.2 KB) [[attachment:Page.py-v193-18112010]]
  • [get | view] (2010-02-05 17:15:58, 4.3 KB) [[attachment:Page.py.patch]]
  • [get | view] (2008-02-12 01:09:14, 4.3 KB) [[attachment:PageApproval.diff]]
  • [get | view] (2010-02-05 17:25:50, 21.4 KB) [[attachment:approval-history.png]]
  • [get | view] (2010-11-18 10:19:10, 2.4 KB) [[attachment:approvePage.py-v193-18112010]]
  • [get | view] (2006-12-04 21:40:20, 3.0 KB) [[attachment:approvepage2.py]]
  • [get | view] (2005-10-18 14:17:03, 4.5 KB) [[attachment:modern.py]]
  • [get | view] (2010-11-18 09:42:23, 3.8 KB) [[attachment:modern_iso.diff-v193-18112010]]
  • [get | view] (2005-11-08 14:48:07, 4.6 KB) [[attachment:modern_iso.py]]
  • [get | view] (2010-11-18 09:40:06, 6.2 KB) [[attachment:modern_iso.py-v193-18112010]]
  • [get | view] (2006-12-04 21:40:00, 4.9 KB) [[attachment:modern_iso2.py]]
  • [get | view] (2010-11-18 09:40:39, 15.0 KB) [[attachment:modernized_iso.py-v193-18112010]]
  • [get | view] (2006-12-04 21:40:39, 1.5 KB) [[attachment:page.diff]]
  • [get | view] (2010-11-18 10:10:42, 7.2 KB) [[attachment:rightsidebar_iso.py-v193-18112010]]
  • [get | view] (2010-02-05 17:13:56, 29.0 KB) [[attachment:solenoid_iso-191.py]]
  • [get | view] (2010-02-05 17:15:05, 1.4 KB) [[attachment:solenoid_iso-191.py.patch]]
 All files | Selected Files: delete move to page copy to page

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