Back to index

moin  1.9.0~rc2
PageEditor.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - PageEditor class
00004 
00005     PageEditor is used for r/w access to a wiki page (edit, rename, delete operations).
00006 
00007     TODO:
00008     * See comments in Page.py, most apply here, too.
00009     * The editor code should be modularized so we will be able to use it for any
00010       text/* mimetype data with some special features enabled depending on the
00011       mimetype (e.g. enable wiki markup help when editing wiki mimetype).
00012 
00013     @copyright: 2000-2004 by Juergen Hermann <jh@web.de>,
00014                 2005-2007 by MoinMoin:ThomasWaldmann,
00015                 2007 by MoinMoin:ReimarBauer
00016     @license: GNU GPL, see COPYING for details.
00017 """
00018 
00019 import os, time, codecs, errno
00020 
00021 
00022 from MoinMoin import caching, config, wikiutil, error
00023 from MoinMoin.Page import Page
00024 from MoinMoin.widget import html
00025 from MoinMoin.widget.dialog import Status
00026 from MoinMoin.logfile import editlog, eventlog
00027 from MoinMoin.mail.sendmail import encodeSpamSafeEmail
00028 from MoinMoin.support.python_compatibility import set
00029 from MoinMoin.util import filesys, timefuncs, web
00030 from MoinMoin.events import PageDeletedEvent, PageRenamedEvent, PageCopiedEvent, PageRevertedEvent
00031 from MoinMoin.events import PagePreSaveEvent, Abort, send_event
00032 import MoinMoin.events.notification as notification
00033 
00034 # used for merging
00035 conflict_markers = ("\n---- /!\\ '''Edit conflict - other version:''' ----\n",
00036                     "\n---- /!\\ '''Edit conflict - your version:''' ----\n",
00037                     "\n---- /!\\ '''End of edit conflict''' ----\n")
00038 
00039 
00040 #############################################################################
00041 ### Javascript code for editor page
00042 #############################################################################
00043 
00044 # we avoid the "--" operator to make this XHTML happy!
00045 _countdown_js = """
00046 %(countdown_script)s
00047 <script type="text/javascript">
00048 var countdown_timeout_min = %(lock_timeout)s
00049 var countdown_lock_expire = "%(lock_expire)s"
00050 var countdown_lock_mins = "%(lock_mins)s"
00051 var countdown_lock_secs = "%(lock_secs)s"
00052 addLoadEvent(countdown)
00053 </script>
00054 """
00055 
00056 
00057 #############################################################################
00058 ### PageEditor - Edit pages
00059 #############################################################################
00060 class PageEditor(Page):
00061     """ Editor for a wiki page. """
00062 
00063     # exceptions for .saveText()
00064     class SaveError(error.Error):
00065         pass
00066     class RevertError(SaveError):
00067         pass
00068     class AccessDenied(SaveError):
00069         pass
00070     class Immutable(AccessDenied):
00071         pass
00072     class NoAdmin(AccessDenied):
00073         pass
00074     class EmptyPage(SaveError):
00075         pass
00076     class Unchanged(SaveError):
00077         pass
00078     class EditConflict(SaveError):
00079         pass
00080     class CouldNotLock(SaveError):
00081         pass
00082 
00083     def __init__(self, request, page_name, **keywords):
00084         """ Create page editor object.
00085 
00086         @param page_name: name of the page
00087         @param request: the request object
00088         @keyword do_revision_backup: if 0, suppress making a page backup per revision
00089         @keyword do_editor_backup: if 0, suppress saving of draft copies
00090         @keyword uid_override: override user id and name (default None)
00091         """
00092         Page.__init__(self, request, page_name, **keywords)
00093         self._ = request.getText
00094 
00095         self.do_revision_backup = keywords.get('do_revision_backup', 1)
00096         self.do_editor_backup = keywords.get('do_editor_backup', 1)
00097         self.uid_override = keywords.get('uid_override', None)
00098 
00099         self.lock = PageLock(self)
00100 
00101     def mergeEditConflict(self, origrev):
00102         """ Try to merge current page version with new version the user tried to save
00103 
00104         @param origrev: the original revision the user was editing
00105         @rtype: bool
00106         @return: merge success status
00107         """
00108         from MoinMoin.util import diff3
00109         allow_conflicts = 1
00110 
00111         # Get current editor text
00112         savetext = self.get_raw_body()
00113 
00114         # The original text from the revision the user was editing
00115         original_text = Page(self.request, self.page_name, rev=origrev).get_raw_body()
00116 
00117         # The current revision someone else saved
00118         saved_text = Page(self.request, self.page_name).get_raw_body()
00119 
00120         # And try to merge all into one with edit conflict separators
00121         verynewtext = diff3.text_merge(original_text, saved_text, savetext,
00122                                        allow_conflicts, *conflict_markers)
00123         if verynewtext:
00124             self.set_raw_body(verynewtext)
00125             return True
00126 
00127         # this should never happen, except for empty pages
00128         return False
00129 
00130     def sendconfirmleaving(self):
00131         """ Prevents moving away from the page without saving it. """
00132         _ = self._
00133         self.request.write(u'''\
00134 <script type="text/javascript">
00135     var flgChange = false;
00136     function confirmleaving() {
00137         if ( flgChange )
00138             return "%s";
00139     }
00140 </script>
00141 ''' % _("Your changes are not saved!"))
00142 
00143     def sendEditor(self, **kw):
00144         """ Send the editor form page.
00145 
00146         @keyword preview: if given, show this text in preview mode
00147         @keyword staytop: don't go to #preview
00148         @keyword comment: comment field (when preview is true)
00149         """
00150         from MoinMoin import i18n
00151         from MoinMoin.action import SpellCheck
00152         request = self.request
00153         form = request.form
00154         _ = self._
00155 
00156         raw_body = ''
00157         msg = None
00158         conflict_msg = None
00159         edit_lock_message = None
00160         preview = kw.get('preview', None)
00161         staytop = kw.get('staytop', 0)
00162 
00163         from MoinMoin.formatter.text_html import Formatter
00164         request.formatter = Formatter(request, store_pagelinks=1)
00165 
00166         # check edit permissions
00167         if not request.user.may.write(self.page_name):
00168             msg = _('You are not allowed to edit this page.')
00169         elif not self.isWritable():
00170             msg = _('Page is immutable!')
00171         elif self.rev:
00172             # Trying to edit an old version, this is not possible via
00173             # the web interface, but catch it just in case...
00174             msg = _('Cannot edit old revisions!')
00175         else:
00176             try:
00177                 # try to acquire edit lock
00178                 ok, edit_lock_message = self.lock.acquire()
00179                 if not ok:
00180                     # failed to get the lock
00181                     if preview is not None:
00182                         edit_lock_message = _('The lock you held timed out. Be prepared for editing conflicts!'
00183                             ) + "<br>" + edit_lock_message
00184                     else:
00185                         msg = edit_lock_message
00186             except OSError, err:
00187                 if err.errno == errno.ENAMETOOLONG:
00188                     msg = _("Page name is too long, try shorter name.")
00189                 else:
00190                     raise
00191 
00192         # Did one of the prechecks fail?
00193         if msg:
00194             request.theme.add_msg(msg, "error")
00195             self.send_page()
00196             return
00197 
00198         # Emit http_headers after checks (send_page)
00199         request.disableHttpCaching(level=2)
00200 
00201         # check if we want to load a draft
00202         use_draft = None
00203         if 'button_load_draft' in form:
00204             wanted_draft_timestamp = int(form.get('draft_ts', '0'))
00205             if wanted_draft_timestamp:
00206                 draft = self._load_draft()
00207                 if draft is not None:
00208                     draft_timestamp, draft_rev, draft_text = draft
00209                     if draft_timestamp == wanted_draft_timestamp:
00210                         use_draft = draft_text
00211 
00212         # Check for draft / normal / preview submit
00213         if use_draft is not None:
00214             title = _('Draft of "%(pagename)s"')
00215             # Propagate original revision
00216             rev = int(form['draft_rev'])
00217             self.set_raw_body(use_draft, modified=1)
00218             preview = use_draft
00219         elif preview is None:
00220             title = _('Edit "%(pagename)s"')
00221         else:
00222             title = _('Preview of "%(pagename)s"')
00223             # Propagate original revision
00224             rev = request.rev
00225             self.set_raw_body(preview, modified=1)
00226 
00227         # send header stuff
00228         lock_timeout = self.lock.timeout / 60
00229         lock_page = wikiutil.escape(self.page_name, quote=1)
00230         lock_expire = _("Your edit lock on %(lock_page)s has expired!") % {'lock_page': lock_page}
00231         lock_mins = _("Your edit lock on %(lock_page)s will expire in # minutes.") % {'lock_page': lock_page}
00232         lock_secs = _("Your edit lock on %(lock_page)s will expire in # seconds.") % {'lock_page': lock_page}
00233 
00234         # get request parameters
00235         try:
00236             text_rows = int(form['rows'])
00237         except StandardError:
00238             text_rows = self.cfg.edit_rows
00239             if request.user.valid:
00240                 text_rows = int(request.user.edit_rows)
00241 
00242         if preview is not None:
00243             # Check for editing conflicts
00244             if not self.exists():
00245                 # page does not exist, are we creating it?
00246                 if rev:
00247                     conflict_msg = _('Someone else deleted this page while you were editing!')
00248             elif rev != self.current_rev():
00249                 conflict_msg = _('Someone else changed this page while you were editing!')
00250                 if self.mergeEditConflict(rev):
00251                     conflict_msg = _("""Someone else saved this page while you were editing!
00252 Please review the page and save then. Do not save this page as it is!""")
00253                     rev = self.current_rev()
00254             if conflict_msg:
00255                 # We don't show preview when in conflict
00256                 preview = None
00257 
00258         elif self.exists():
00259             # revision of existing page
00260             rev = self.current_rev()
00261         else:
00262             # page creation
00263             rev = 0
00264 
00265         # Page editing is done using user language
00266         request.setContentLanguage(request.lang)
00267 
00268         # Get the text body for the editor field.
00269         # TODO: what about deleted pages? show the text of the last revision or use the template?
00270         if preview is not None:
00271             raw_body = self.get_raw_body()
00272             if use_draft:
00273                 request.theme.add_msg(_("[Content loaded from draft]"), 'info')
00274         elif self.exists():
00275             # If the page exists, we get the text from the page.
00276             # TODO: maybe warn if template argument was ignored because the page exists?
00277             raw_body = self.get_raw_body()
00278         elif 'template' in request.values:
00279             # If the page does not exist, we try to get the content from the template parameter.
00280             template_page = wikiutil.unquoteWikiname(request.values['template'])
00281             if request.user.may.read(template_page):
00282                 raw_body = Page(request, template_page).get_raw_body()
00283                 if raw_body:
00284                     request.theme.add_msg(_("[Content of new page loaded from %s]") % (template_page, ), 'info')
00285                 else:
00286                     request.theme.add_msg(_("[Template %s not found]") % (template_page, ), 'warning')
00287             else:
00288                 request.theme.add_msg(_("[You may not read %s]") % (template_page, ), 'error')
00289 
00290         # Make backup on previews - but not for new empty pages
00291         if not use_draft and preview and raw_body:
00292             self._save_draft(raw_body, rev)
00293 
00294         draft_message = None
00295         loadable_draft = False
00296         if preview is None:
00297             draft = self._load_draft()
00298             if draft is not None:
00299                 draft_timestamp, draft_rev, draft_text = draft
00300                 if draft_text != raw_body:
00301                     loadable_draft = True
00302                     page_rev = rev
00303                     draft_timestamp_str = request.user.getFormattedDateTime(draft_timestamp)
00304                     draft_message = _(u"'''<<BR>>Your draft based on revision %(draft_rev)d (saved %(draft_timestamp_str)s) can be loaded instead of the current revision %(page_rev)d by using the load draft button - in case you lost your last edit somehow without saving it.''' A draft gets saved for you when you do a preview, cancel an edit or unsuccessfully save.", wiki=True) % locals()
00305 
00306         # Setup status message
00307         status = [kw.get('msg', ''), conflict_msg, edit_lock_message, draft_message]
00308         status = [msg for msg in status if msg]
00309         status = ' '.join(status)
00310         status = Status(request, content=status)
00311         request.theme.add_msg(status, "dialog")
00312 
00313         request.theme.send_title(
00314             title % {'pagename': self.split_title(), },
00315             page=self,
00316             html_head=self.lock.locktype and (
00317                 _countdown_js % {
00318                      'countdown_script': request.theme.externalScript('countdown'),
00319                      'lock_timeout': lock_timeout,
00320                      'lock_expire': lock_expire,
00321                      'lock_mins': lock_mins,
00322                      'lock_secs': lock_secs,
00323                     }) or '',
00324             editor_mode=1,
00325         )
00326 
00327         request.write(request.formatter.startContent("content"))
00328 
00329         # Generate default content for new pages
00330         if not raw_body:
00331             raw_body = _('Describe %s here.') % (self.page_name, )
00332 
00333         # send form
00334         request.write('<form id="editor" method="post" action="%s#preview" onSubmit="flgChange = false;">' % (
00335                 request.href(self.page_name)
00336         ))
00337 
00338         # yet another weird workaround for broken IE6 (it expands the text
00339         # editor area to the right after you begin to type...). IE sucks...
00340         # http://fplanque.net/2003/Articles/iecsstextarea/
00341         request.write('<fieldset style="border:none;padding:0;">')
00342 
00343         request.write(unicode(html.INPUT(type="hidden", name="action", value="edit")))
00344 
00345         # Send revision of the page our edit is based on
00346         request.write('<input type="hidden" name="rev" value="%d">' % (rev, ))
00347 
00348         # Create and send a ticket, so we can check the POST
00349         request.write('<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(request))
00350 
00351         # Save backto in a hidden input
00352         backto = request.values.get('backto')
00353         if backto:
00354             request.write(unicode(html.INPUT(type="hidden", name="backto", value=backto)))
00355 
00356         # button bar
00357         button_spellcheck = '<input class="button" type="submit" name="button_spellcheck" value="%s" onClick="flgChange = false;">' % _('Check Spelling')
00358 
00359         save_button_text = _('Save Changes')
00360         cancel_button_text = _('Cancel')
00361 
00362         if self.cfg.page_license_enabled:
00363             request.write('<p><em>', _(
00364 """By hitting '''%(save_button_text)s''' you put your changes under the %(license_link)s.
00365 If you don't want that, hit '''%(cancel_button_text)s''' to cancel your changes.""", wiki=True) % {
00366                 'save_button_text': save_button_text,
00367                 'cancel_button_text': cancel_button_text,
00368                 'license_link': wikiutil.getLocalizedPage(request, self.cfg.page_license_page).link_to(request),
00369             }, '</em></p>')
00370 
00371 
00372         request.write('''
00373 <input class="button" type="submit" name="button_save" value="%s" onClick="flgChange = false;">
00374 <input class="button" type="submit" name="button_preview" value="%s" onClick="flgChange = false;">
00375 ''' % (save_button_text, _('Preview'), ))
00376 
00377         if not (request.cfg.editor_force and request.cfg.editor_default == 'text'):
00378             request.write('''
00379 <input id="switch2gui" style="display: none;" class="button" type="submit" name="button_switch" value="%s">
00380 ''' % (_('GUI Mode'), ))
00381 
00382         if loadable_draft:
00383             request.write('''
00384 <input class="button" type="submit" name="button_load_draft" value="%s" onClick="flgChange = false;">
00385 <input type="hidden" name="draft_ts" value="%d">
00386 <input type="hidden" name="draft_rev" value="%d">
00387 ''' % (_('Load Draft'), draft_timestamp, draft_rev))
00388 
00389         request.write('''
00390 %s
00391 <input class="button" type="submit" name="button_cancel" value="%s">
00392 <input type="hidden" name="editor" value="text">
00393 ''' % (button_spellcheck, cancel_button_text, ))
00394 
00395         # Trivial Change-checkbox to the top of the page, shows up only if user has JavaScript enabled. It's "linked" with the bottom's box (checking one checks both)
00396         if self.cfg.mail_enabled:
00397             request.write('''
00398 <script type="text/javascript">
00399     <!--
00400     function toggle_trivial(CheckedBox)
00401     {
00402         TrivialBoxes = document.getElementsByName("trivial");
00403         for (var i = 0; i < TrivialBoxes.length; i++)
00404             TrivialBoxes[i].checked = CheckedBox.checked;
00405     }
00406 
00407     document.write('<input type="checkbox" name="trivial" id="chktrivialtop" value="1" %(checked)s onclick="toggle_trivial(this)">');
00408     document.write('<label for="chktrivialtop">%(label)s</label>');
00409     //-->
00410 </script> ''' % {
00411                 'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
00412                 'label': _("Trivial change"),
00413             })
00414 
00415         from MoinMoin.security.textcha import TextCha
00416         request.write(TextCha(request).render())
00417 
00418         # Add textarea with page text
00419         self.sendconfirmleaving()
00420 
00421         lang = self.pi.get('language', request.cfg.language_default)
00422 
00423         request.write(
00424             u'''\
00425 <textarea id="editor-textarea" name="savetext" lang="%(lang)s" dir="%(dir)s" rows="%(rows)d" cols="80"
00426           onChange="flgChange = true;" onKeyPress="flgChange = true;">\
00427 %(text)s\
00428 </textarea>''' % {
00429             'lang': lang,
00430             'dir': i18n.getDirection(lang),
00431             'rows': text_rows,
00432             'text': wikiutil.escape(raw_body)
00433         })
00434 
00435         request.write("<p>")
00436         request.write(_("Comment:"),
00437             ' <input id="editor-comment" type="text" name="comment" value="%s" size="80" maxlength="200"'
00438             ' onChange="flgChange = true;" onKeyPress="flgChange = true;">' % (
00439                 wikiutil.escape(kw.get('comment', ''), 1), ))
00440         request.write("</p>")
00441 
00442         # Category selection
00443         filterfn = self.cfg.cache.page_category_regexact.search
00444         cat_pages = request.rootpage.getPageList(filter=filterfn)
00445         cat_pages.sort()
00446         cat_pages = [wikiutil.pagelinkmarkup(p) for p in cat_pages]
00447         cat_pages.insert(0, ('', _('<No addition>')))
00448         request.write("<p>")
00449         request.write(_('Add to: %(category)s') % {
00450             'category': unicode(web.makeSelection('category', cat_pages)),
00451         })
00452 
00453         if self.cfg.mail_enabled:
00454             request.write('''
00455 &nbsp;
00456 
00457 <input type="checkbox" name="trivial" id="chktrivial" value="1" %(checked)s onclick="toggle_trivial(this)">
00458 <label for="chktrivial">%(label)s</label>
00459 
00460 ''' % {
00461                 'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
00462                 'label': _("Trivial change"),
00463                 })
00464 
00465         request.write('''
00466 &nbsp;
00467 <input type="checkbox" name="rstrip" id="chkrstrip" value="1" %(checked)s>
00468 <label for="chkrstrip">%(label)s</label>
00469 ''' % {
00470             'checked': ('', 'checked')[form.get('rstrip', '0') == '1'],
00471             'label': _('Remove trailing whitespace from each line')
00472             })
00473         request.write("</p>")
00474 
00475         badwords_re = None
00476         if preview is not None:
00477             if 'button_spellcheck' in form or 'button_newwords' in form:
00478                 badwords, badwords_re, msg = SpellCheck.checkSpelling(self, request, own_form=0)
00479                 request.write("<p>%s</p>" % msg)
00480         request.write('</fieldset>')
00481         request.write("</form>")
00482 
00483         # QuickHelp originally by Georg Mischler <schorsch@lightingwiki.com>
00484         markup = self.pi['format'] or request.cfg.default_markup
00485         parser = wikiutil.searchAndImportPlugin(self.request.cfg, "parser", markup)
00486         quickhelp = getattr(parser, 'quickhelp', None)
00487         if quickhelp:
00488             request.write(request.formatter.div(1, id="editor-help"))
00489             request.write(_(quickhelp, wiki=True))
00490             request.write(request.formatter.div(0))
00491 
00492         if preview is not None:
00493             if staytop:
00494                 content_id = 'previewbelow'
00495             else:
00496                 content_id = 'preview'
00497             self.send_page(content_id=content_id, content_only=1, hilite_re=badwords_re)
00498 
00499         request.write(request.formatter.endContent())
00500         request.theme.send_footer(self.page_name)
00501         request.theme.send_closing_html()
00502 
00503     def sendCancel(self, newtext, rev):
00504         """ User clicked on Cancel button.
00505             If edit locking is active, delete the current lock file.
00506 
00507         @param newtext: the edited text (which has been cancelled)
00508         @param rev: not used!?
00509         """
00510         request = self.request
00511         _ = self._
00512         self._save_draft(newtext, rev) # shall we really save a draft on CANCEL?
00513         self.lock.release()
00514 
00515         backto = request.values.get('backto')
00516         if backto:
00517             pg = Page(request, backto)
00518             request.http_redirect(pg.url(request))
00519         else:
00520             request.theme.add_msg(_('Edit was cancelled.'), "error")
00521             self.send_page()
00522 
00523     def copyPage(self, newpagename, comment=u''):
00524         """ Copy the current version of the page (keeping the backups, logs and attachments).
00525 
00526         @param comment: Comment given by user
00527         @rtype: unicode
00528         @return: success flag, error message
00529         """
00530         request = self.request
00531         _ = self._
00532 
00533         if not newpagename:
00534             return False, _("You can't copy to an empty pagename.")
00535 
00536         if not self.request.user.may.write(newpagename):
00537             return False, _('You are not allowed to copy this page!')
00538 
00539         newpage = PageEditor(request, newpagename)
00540 
00541         pageexists_error = _("""'''A page with the name {{{'%s'}}} already exists.'''
00542 
00543 Try a different name.""", wiki=True) % (wikiutil.escape(newpagename), )
00544 
00545         # Check whether a page with the new name already exists
00546         if newpage.exists(includeDeleted=1):
00547             return False, pageexists_error
00548 
00549         # Get old page text
00550         savetext = self.get_raw_body()
00551 
00552         oldpath = self.getPagePath(check_create=0)
00553         newpath = newpage.getPagePath(check_create=0)
00554 
00555         # Copy page
00556         # NOTE: might fail if another process created newpagename just
00557         try:
00558             filesys.copytree(oldpath, newpath)
00559             self.error = None
00560             savetext = u"## page was copied from %s\n%s" % (self.page_name, savetext)
00561             Page.__init__(self, request, newpagename)
00562             self._write_file(savetext, "SAVENEW", comment)
00563 
00564             event = PageCopiedEvent(request, newpage, self, comment)
00565             send_event(event)
00566 
00567             return True, None
00568         except OSError, err:
00569             # Try to understand what happened. Maybe its better to check
00570             # the error code, but I just reused the available code above...
00571             if newpage.exists(includeDeleted=1):
00572                 return False, pageexists_error
00573             else:
00574                 return False, _('Could not copy page because of file system error: %s.') % unicode(err)
00575 
00576     def renamePage(self, newpagename, comment=u''):
00577         """ Rename the current version of the page (making a backup before deletion
00578             and keeping the backups, logs and attachments).
00579 
00580         @param comment: Comment given by user
00581         @rtype: unicode
00582         @return: success flag, error message
00583         """
00584         request = self.request
00585         _ = self._
00586 
00587         if not (request.user.may.delete(self.page_name)
00588                 and request.user.may.write(newpagename)):
00589             msg = _('You are not allowed to rename this page!')
00590             raise self.AccessDenied, msg
00591 
00592         if not newpagename:
00593             return False, _("You can't rename to an empty pagename.")
00594 
00595         newpage = PageEditor(request, newpagename)
00596 
00597         pageexists_error = _("""'''A page with the name {{{'%s'}}} already exists.'''
00598 
00599 Try a different name.""", wiki=True) % (wikiutil.escape(newpagename), )
00600 
00601         # Check whether a page with the new name already exists
00602         if newpage.exists(includeDeleted=1):
00603             return False, pageexists_error
00604 
00605         # Get old page text
00606         savetext = self.get_raw_body()
00607 
00608         oldpath = self.getPagePath(check_create=0)
00609         newpath = newpage.getPagePath(check_create=0)
00610 
00611         # Rename page
00612 
00613         # NOTE: might fail if another process created newpagename just
00614         # NOW, while you read this comment. Rename is atomic for files -
00615         # but for directories, rename will fail if the directory
00616         # exists. We should have global edit-lock to avoid this.
00617         # See http://docs.python.org/lib/os-file-dir.html
00618         try:
00619             os.rename(oldpath, newpath)
00620             self.error = None
00621             # Save page text with a comment about the old name
00622             savetext = u"## page was renamed from %s\n%s" % (self.page_name, savetext)
00623             newpage.saveText(savetext, 0, comment=comment, extra=self.page_name, action='SAVE/RENAME', notify=False)
00624             # delete pagelinks
00625             arena = newpage
00626             key = 'pagelinks'
00627             cache = caching.CacheEntry(request, arena, key, scope='item')
00628             cache.remove()
00629 
00630             # clean the cache
00631             for formatter_name in self.cfg.caching_formats:
00632                 arena = newpage
00633                 key = formatter_name
00634                 cache = caching.CacheEntry(request, arena, key, scope='item')
00635                 cache.remove()
00636 
00637             event = PageRenamedEvent(request, newpage, self, comment)
00638             send_event(event)
00639 
00640             return True, None
00641         except OSError, err:
00642             # Try to understand what happened. Maybe its better to check
00643             # the error code, but I just reused the available code above...
00644             if newpage.exists(includeDeleted=1):
00645                 return False, pageexists_error
00646             else:
00647                 return False, _('Could not rename page because of file system error: %s.') % unicode(err)
00648 
00649 
00650     def revertPage(self, revision, comment=u''):
00651         """ Reverts page to the given revision
00652 
00653         @param revision: revision to revert to
00654         @type revision: int
00655 
00656         """
00657         _ = self.request.getText
00658 
00659         if not self.request.user.may.revert(self.page_name):
00660             # no real message necessary, cannot happen if
00661             # user doesn't try to exploit us
00662             raise self.RevertError('not allowed')
00663         elif revision is None:
00664             # see above
00665             raise self.RevertError('cannot revert to current rev')
00666         else:
00667             revstr = '%08d' % revision
00668             pg = Page(self.request, self.page_name, rev=revision)
00669             msg = self.saveText(pg.get_raw_body(), 0, extra=revstr, action="SAVE/REVERT", notify=False, comment=comment)
00670 
00671             # Remove cache entry (if exists)
00672             pg = Page(self.request, self.page_name)
00673             key = self.request.form.get('key', 'text_html') # XXX see cleanup code in deletePage
00674             caching.CacheEntry(self.request, pg, key, scope='item').remove()
00675             caching.CacheEntry(self.request, pg, "pagelinks", scope='item').remove()
00676 
00677             # Notify observers
00678             e = PageRevertedEvent(self.request, self.page_name, revision, revstr)
00679             send_event(e)
00680 
00681             return msg
00682 
00683     def deletePage(self, comment=None):
00684         """ Delete the current version of the page (making a backup before deletion
00685             and keeping the backups, logs and attachments).
00686 
00687         @param comment: Comment given by user
00688         @rtype: unicode
00689         @return: success flag, error message
00690         """
00691         request = self.request
00692         _ = self._
00693         success = True
00694         if not (request.user.may.write(self.page_name)
00695                 and request.user.may.delete(self.page_name)):
00696             msg = _('You are not allowed to delete this page!')
00697             raise self.AccessDenied, msg
00698 
00699         try:
00700             msg = self.saveText(u"deleted\n", 0, comment=comment or u'', deleted=True, notify=False)
00701             msg = msg.replace(
00702                 _("Thank you for your changes. Your attention to detail is appreciated."),
00703                 _('Page "%s" was successfully deleted!') % (wikiutil.escape(self.page_name), ))
00704 
00705             event = PageDeletedEvent(request, self, comment)
00706             send_event(event)
00707 
00708         except self.SaveError, message:
00709             # XXX do not only catch base class SaveError here, but
00710             # also the derived classes, so we can give better err msgs
00711             success = False
00712             msg = "SaveError has occured in PageEditor.deletePage. We need locking there."
00713 
00714         # delete pagelinks
00715         arena = self
00716         key = 'pagelinks'
00717         cache = caching.CacheEntry(request, arena, key, scope='item')
00718         cache.remove()
00719 
00720         # clean the cache
00721         for formatter_name in self.cfg.caching_formats:
00722             arena = self
00723             key = formatter_name
00724             cache = caching.CacheEntry(request, arena, key, scope='item')
00725             cache.remove()
00726         return success, msg
00727 
00728     def _get_local_timestamp(self):
00729         """ Returns the string that can be used by the TIME substitution.
00730 
00731         @return: str with a timestamp in it
00732         """
00733 
00734         now = time.time()
00735         # default: UTC
00736         zone = "Z"
00737         u = self.request.user
00738 
00739         # setup the timezone
00740         if u.valid and u.tz_offset:
00741             tz = u.tz_offset
00742             # round to minutes
00743             tz -= tz % 60
00744             minutes = tz / 60
00745             hours = minutes / 60
00746             minutes -= hours * 60
00747 
00748             # construct the offset
00749             zone = "%+0.2d%02d" % (hours, minutes)
00750             # correct the time by the offset we've found
00751             now += tz
00752 
00753         return time.strftime("%Y-%m-%dT%H:%M:%S", timefuncs.tmtuple(now)) + zone
00754 
00755     def _expand_variables(self, text):
00756         """ Expand @VARIABLE@ in `text`and return the expanded text.
00757 
00758         @param text: current text of wikipage
00759         @rtype: string
00760         @return: new text of wikipage, variables replaced
00761         """
00762         # TODO: Allow addition of variables via wikiconfig or a global wiki dict.
00763         request = self.request
00764         now = self._get_local_timestamp()
00765         u = request.user
00766         obfuscated_email_address = encodeSpamSafeEmail(u.email)
00767         signature = u.signature()
00768         variables = {
00769             'PAGE': self.page_name,
00770             'TIME': "<<DateTime(%s)>>" % now,
00771             'DATE': "<<Date(%s)>>" % now,
00772             'ME': u.name,
00773             'USERNAME': signature,
00774             'USER': "-- %s" % signature,
00775             'SIG': "-- %s <<DateTime(%s)>>" % (signature, now),
00776             'EMAIL': "<<MailTo(%s)>>" % (obfuscated_email_address)
00777         }
00778 
00779         if u.valid and u.name:
00780             if u.email:
00781                 variables['MAILTO'] = "<<MailTo(%s)>>" % u.email
00782             # Users can define their own variables via
00783             # UserHomepage/MyDict, which override the default variables.
00784             userDictPage = u.name + "/MyDict"
00785             if userDictPage in request.dicts:
00786                 variables.update(request.dicts[userDictPage])
00787 
00788         for name in variables:
00789             text = text.replace('@%s@' % name, variables[name])
00790         return text
00791 
00792     def normalizeText(self, text, **kw):
00793         """ Normalize text
00794 
00795         Make sure text uses '\n' line endings, and has a trailing
00796         newline. Strip whitespace on end of lines if needed.
00797 
00798         You should normalize any text you enter into a page, for
00799         example, when getting new text from the editor, or when setting
00800         new text manually.
00801 
00802         @param text: text to normalize (unicode)
00803         @keyword stripspaces: if 1, strip spaces from text
00804         @rtype: unicode
00805         @return: normalized text
00806         """
00807         if text:
00808             lines = text.splitlines()
00809             # Strip trailing spaces if needed
00810             if kw.get('stripspaces', 0):
00811                 lines = [line.rstrip() for line in lines]
00812             # Add final newline if not present, better for diffs (does
00813             # not include former last line when just adding text to
00814             # bottom; idea by CliffordAdams)
00815             if not lines[-1] == u'':
00816                 # '' will make newline after join
00817                 lines.append(u'')
00818 
00819             text = u'\n'.join(lines)
00820         return text
00821 
00822     def _save_draft(self, text, rev, **kw):
00823         """ Save an editor backup to the drafts cache arena.
00824 
00825         @param text: draft text of the page
00826                      (if None, the draft gets removed from the cache)
00827         @param rev: the revision of the page this draft is based on
00828         @param kw: no keyword args used currently
00829         """
00830         request = self.request
00831         if not request.user.valid or not self.do_editor_backup:
00832             return None
00833 
00834         arena = 'drafts'
00835         key = request.user.id
00836         cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
00837         if cache.exists():
00838             cache_data = cache.content()
00839         else:
00840             cache_data = {}
00841         pagename = self.page_name
00842         if text is None:
00843             try:
00844                 del cache_data[pagename]
00845             except:
00846                 pass
00847         else:
00848             timestamp = int(time.time())
00849             cache_data[pagename] = (timestamp, rev, text)
00850         cache.update(cache_data)
00851 
00852     def _load_draft(self):
00853         """ Get a draft from the drafts cache arena.
00854 
00855         @rtype: unicode
00856         @return: draft text or None
00857         """
00858         request = self.request
00859         if not request.user.valid:
00860             return None
00861 
00862         arena = 'drafts'
00863         key = request.user.id
00864         cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
00865         pagename = self.page_name
00866         try:
00867             cache_data = cache.content()
00868             return cache_data.get(pagename)
00869         except caching.CacheError:
00870             return None
00871 
00872     def copy_underlay_page(self):
00873         # renamed from copypage to avoid conflicts with copyPage
00874         """ Copy a page from underlay directory to page directory """
00875         src = self.getPagePath(use_underlay=1, check_create=0)
00876         dst = self.getPagePath(use_underlay=0, check_create=0)
00877         if src and dst and src != dst and os.path.exists(src):
00878             try:
00879                 os.rmdir(dst) # simply remove empty dst dirs
00880                 # XXX in fact, we should better remove anything we regard as an
00881                 # empty page, maybe also if there is also an edit-lock or
00882                 # empty cache. revisions subdir...
00883             except:
00884                 pass
00885             if not os.path.exists(dst):
00886                 filesys.copytree(src, dst)
00887                 self.reset() # reinit stuff
00888 
00889     def _write_file(self, text, action='SAVE', comment=u'', extra=u'', deleted=False):
00890         """ Write the text to the page file (and make a backup of old page).
00891 
00892         @param text: text to save for this page
00893         @param deleted: if True, then don't write page content (used by deletePage)
00894         @rtype: int
00895         @return: mtime_usec of new page
00896         """
00897         request = self.request
00898         _ = self._
00899         was_deprecated = self.pi.get('deprecated', False)
00900 
00901         self.copy_underlay_page()
00902 
00903         # remember conflict state
00904         self.setConflict(wikiutil.containsConflictMarker(text))
00905 
00906         # Write always on the standard directory, never change the
00907         # underlay directory copy!
00908         pagedir = self.getPagePath(use_underlay=0, check_create=0)
00909 
00910         revdir = os.path.join(pagedir, 'revisions')
00911         cfn = os.path.join(pagedir, 'current')
00912         clfn = os.path.join(pagedir, 'current-locked')
00913         cltfn = os.path.join(pagedir, 'current-locked.tmp')
00914 
00915         # !!! these log objects MUST be created outside the locked area !!!
00916 
00917         # The local log should be the standard edit log, not the
00918         # underlay copy log!
00919         pagelog = self.getPagePath('edit-log', use_underlay=0, isfile=1)
00920         llog = editlog.EditLog(request, filename=pagelog,
00921                                uid_override=self.uid_override)
00922         # Open the global log
00923         glog = editlog.EditLog(request, uid_override=self.uid_override)
00924 
00925         if not os.path.exists(pagedir): # new page, create and init pagedir
00926             os.mkdir(pagedir)
00927         if not os.path.exists(revdir):
00928             os.mkdir(revdir)
00929             f = file(cfn, 'w')
00930             f.write('%08d\n' % 0)
00931             f.close()
00932 
00933         got_lock = False
00934         retry = 0
00935 
00936         try:
00937             while not got_lock and retry < 100:
00938                 retry += 1
00939                 try:
00940                     filesys.rename(cfn, clfn)
00941                     got_lock = True
00942                 except OSError, err:
00943                     got_lock = False
00944                     if err.errno == 2: # there was no 'current' file
00945                         time.sleep(0.1)
00946                     else:
00947                         raise self.CouldNotLock, _("Page could not get locked. Unexpected error (errno=%d).") % err.errno
00948 
00949             if not got_lock:
00950                 raise self.CouldNotLock, _("Page could not get locked. Missing 'current' file?")
00951 
00952             # increment rev number of current(-locked) page
00953             f = file(clfn)
00954             revstr = f.read()
00955             f.close()
00956             try:
00957                 rev = int(revstr)
00958             except ValueError, err:
00959                 raise self.SaveError, _("Unable to determine current page revision from the 'current' file. The page %s is damaged and cannot be edited right now.") % self.page_name
00960 
00961             if not was_deprecated:
00962                 if self.do_revision_backup or rev == 0:
00963                     rev += 1
00964             revstr = '%08d' % rev
00965             # write the current page rev to a temporary file
00966             try:
00967                 f = file(cltfn, 'w')
00968                 f.write(revstr+'\n')
00969                 f.close()
00970             except IOError, err:
00971                 try:
00972                     os.remove(cltfn)
00973                 except:
00974                     pass # we don't care for errors in the os.remove
00975                 # throw a nicer exception
00976                 if err.errno == errno.ENOSPC:
00977                     raise self.SaveError, _("Cannot save page %s, no storage space left.") % self.page_name
00978                 else:
00979                     raise self.SaveError, _("An I/O error occurred while saving page %s (errno=%d)") % (self.page_name, err.errno)
00980             # atomically put it in place (except on windows)
00981             else:
00982                 filesys.rename(cltfn, clfn)
00983 
00984             if not deleted:
00985                 # save to page file
00986                 pagefile = os.path.join(revdir, revstr)
00987                 f = codecs.open(pagefile, 'wb', config.charset)
00988                 # Write the file using text/* mime type
00989                 f.write(self.encodeTextMimeType(text))
00990                 f.close()
00991                 mtime_usecs = wikiutil.timestamp2version(os.path.getmtime(pagefile))
00992                 # set in-memory content
00993                 self.set_raw_body(text)
00994             else:
00995                 mtime_usecs = wikiutil.timestamp2version(time.time())
00996                 # set in-memory content
00997                 self.set_raw_body(None)
00998 
00999             # reset page object
01000             self.reset()
01001 
01002             # write the editlog entry
01003             # for now simply make 2 logs, better would be some multilog stuff maybe
01004             if self.do_revision_backup:
01005                 # do not globally log edits with no revision backup
01006                 # if somebody edits a deprecated page, log it in global log, but not local log
01007                 glog.add(request, mtime_usecs, rev, action, self.page_name, None, extra, comment)
01008             if not was_deprecated and self.do_revision_backup:
01009                 # if we did not create a new revision number, do not locally log it
01010                 llog.add(request, mtime_usecs, rev, action, self.page_name, None, extra, comment)
01011         finally:
01012             if got_lock:
01013                 filesys.rename(clfn, cfn)
01014 
01015         # add event log entry
01016         elog = eventlog.EventLog(request)
01017         elog.add(request, 'SAVEPAGE', {'pagename': self.page_name}, 1, mtime_usecs)
01018 
01019         return mtime_usecs, rev
01020 
01021     def saveText(self, newtext, rev, **kw):
01022         """ Save new text for a page.
01023 
01024         @param newtext: text to save for this page
01025         @param rev: revision of the page
01026         @keyword trivial: trivial edit (default: 0)
01027         @keyword extra: extra info field (e.g. for SAVE/REVERT with revno)
01028         @keyword comment: comment field (when preview is true)
01029         @keyword action: action for editlog (default: SAVE)
01030         @keyword index: needs indexing, not already handled (default: 1)
01031         @keyword deleted: if True, then don't save page content (used by DeletePage, default: False)
01032         @keyword notify: if False (default: True), don't send a PageChangedEvent
01033         @rtype: unicode
01034         @return: error msg
01035         """
01036         request = self.request
01037         _ = self._
01038         self._save_draft(newtext, rev, **kw)
01039         action = kw.get('action', 'SAVE')
01040         deleted = kw.get('deleted', False)
01041         notify = kw.get('notify', True)
01042 
01043         #!!! need to check if we still retain the lock here
01044         #!!! rev check is not enough since internal operations use "0"
01045 
01046         # expand variables, unless it's a template or form page
01047         if not wikiutil.isTemplatePage(request, self.page_name):
01048             newtext = self._expand_variables(newtext)
01049 
01050         msg = ""
01051         if not request.user.may.save(self, newtext, rev, **kw):
01052             msg = _('You are not allowed to edit this page!')
01053             raise self.AccessDenied, msg
01054         elif not self.isWritable():
01055             msg = _('Page is immutable!')
01056             raise self.Immutable, msg
01057         elif not newtext:
01058             msg = _('You cannot save empty pages.')
01059             raise self.EmptyPage, msg
01060         elif rev != 0 and rev != self.current_rev():
01061             # check if we already saved that page
01062             other = False
01063             pagelog = self.getPagePath('edit-log', use_underlay=0, isfile=1)
01064             next_line = None
01065             for line in editlog.EditLog(request, pagelog).reverse():
01066                 if int(line.rev) == int(rev):
01067                     break
01068                 if not line.is_from_current_user(request):
01069                     other = True
01070                 next_line = line
01071             if next_line and next_line.is_from_current_user(request):
01072                 saved_page = Page(request, self.page_name, rev=int(next_line.rev))
01073                 if newtext == saved_page.get_raw_body():
01074                     msg = _("You already saved this page!")
01075                     return msg
01076                 else:
01077                     msg = _("You already edited this page! Please do not use the back button.")
01078                     raise self.EditConflict, msg
01079 
01080                 msg = _("""Someone else saved this page while you were editing!
01081 Please review the page and save then. Do not save this page as it is!""")
01082 
01083             raise self.EditConflict, msg
01084         elif newtext == self.get_raw_body():
01085             msg = _('You did not change the page content, not saved!')
01086             self.lock.release()
01087             raise self.Unchanged, msg
01088         else:
01089             from MoinMoin.security import parseACL
01090             # Get current ACL and compare to new ACL from newtext. If
01091             # they are not the sames, the user must have admin
01092             # rights. This is a good place to update acl cache - instead
01093             # of wating for next request.
01094             acl = self.getACL(request)
01095             if (not request.user.may.admin(self.page_name) and
01096                 parseACL(request, newtext).acl != acl.acl and
01097                 action != "SAVE/REVERT"):
01098                 msg = _("You can't change ACLs on this page since you have no admin rights on it!")
01099                 raise self.NoAdmin, msg
01100 
01101         presave = PagePreSaveEvent(request, self, newtext)
01102         results = send_event(presave)
01103 
01104         for result in results:
01105             if isinstance(result, Abort):
01106                 # XXX: this should return a list of messages to the sorrounding context
01107                 # XXX: rather than dumbly concatenate them. Fix in the future.
01108                 msg = msg + result.reason
01109 
01110         # save only if no error occurred (msg is empty) and no abort has been requested
01111         if not msg:
01112             # set success msg
01113             msg = _("Thank you for your changes. Your attention to detail is appreciated.")
01114 
01115             # determine action for edit log
01116             if action == 'SAVE' and not self.exists():
01117                 action = 'SAVENEW'
01118             comment = kw.get('comment', u'')
01119             extra = kw.get('extra', u'')
01120             trivial = kw.get('trivial', 0)
01121             # write the page file
01122             mtime_usecs, rev = self._write_file(newtext, action, comment, extra, deleted=deleted)
01123             self._save_draft(None, None) # everything fine, kill the draft for this page
01124 
01125             if notify:
01126                 # send notifications
01127                 from MoinMoin import events
01128 
01129                 if trivial:
01130                     e = events.TrivialPageChangedEvent(self.request, self, comment)
01131                 else:
01132                     e = events.PageChangedEvent(self.request, self, comment)
01133                 results = events.send_event(e)
01134 
01135                 recipients = set()
01136                 for result in results:
01137                     if isinstance(result, notification.Success):
01138                         recipients.update(result.recipients)
01139 
01140                         if recipients:
01141                             info = _("Notifications sent to:")
01142                             msg = msg + "<p>%s %s</p>" % (info, ", ".join(recipients))
01143 
01144             # Update page trail with the page we just saved.
01145             # This is needed for NewPage macro with backto because it does not
01146             # send the page we just saved.
01147             request.user.addTrail(self)
01148 
01149         # remove lock (forcibly if we were allowed to break it by the UI)
01150         # !!! this is a little fishy, since the lock owner might not notice
01151         # we broke his lock ==> but revision checking during preview will
01152         self.lock.release(force=not msg) # XXX does "not msg" make any sense?
01153 
01154         return msg
01155 
01156 
01157 class PageLock:
01158     """ PageLock - Lock pages """
01159     # TODO: race conditions throughout, need to lock file during queries & update
01160     def __init__(self, pageobj):
01161         """
01162         """
01163         self.pageobj = pageobj
01164         self.page_name = pageobj.page_name
01165         request = pageobj.request
01166         self.request = request
01167         self._ = self.request.getText
01168         self.cfg = self.request.cfg
01169 
01170         # current time and user for later checks
01171         self.now = int(time.time())
01172         self.uid = request.user.valid and request.user.id or request.remote_addr
01173 
01174         # get details of the locking preference, i.e. warning or lock, and timeout
01175         self.locktype = None
01176         self.timeout = 10 * 60 # default timeout in minutes
01177 
01178         if self.cfg.edit_locking:
01179             lockinfo = self.cfg.edit_locking.split()
01180             if 1 <= len(lockinfo) <= 2:
01181                 self.locktype = lockinfo[0].lower()
01182                 if len(lockinfo) > 1:
01183                     try:
01184                         self.timeout = int(lockinfo[1]) * 60
01185                     except ValueError:
01186                         pass
01187 
01188 
01189     def acquire(self):
01190         """ Begin an edit lock depending on the mode chosen in the config.
01191 
01192         @rtype: tuple
01193         @return: tuple is returned containing 2 values:
01194               * a bool indicating successful acquiry
01195               * a string giving a reason for failure or an informational msg
01196         """
01197         if not self.locktype:
01198             # we are not using edit locking, so always succeed
01199             return 1, ''
01200 
01201         _ = self._
01202         #!!! race conditions, need to lock file during queries & update
01203         self._readLockFile()
01204         bumptime = self.request.user.getFormattedDateTime(self.now + self.timeout)
01205         timestamp = self.request.user.getFormattedDateTime(self.timestamp)
01206         owner = self.owner_html
01207         secs_valid = self.timestamp + self.timeout - self.now
01208 
01209         # do we own the lock, or is it stale?
01210         if self.owner is None or self.uid == self.owner or secs_valid < 0:
01211             # create or bump the lock
01212             self._writeLockFile()
01213 
01214             msg = []
01215             if self.owner is not None and -10800 < secs_valid < 0:
01216                 mins_ago = secs_valid / -60
01217                 msg.append(_(
01218                     "The lock of %(owner)s timed out %(mins_ago)d minute(s) ago,"
01219                     " and you were granted the lock for this page."
01220                     ) % {'owner': owner, 'mins_ago': mins_ago})
01221 
01222             if self.locktype == 'lock':
01223                 msg.append(_(
01224                     "Other users will be ''blocked'' from editing this page until %(bumptime)s.",
01225                     wiki=True) % {'bumptime': bumptime})
01226             else:
01227                 msg.append(_(
01228                     "Other users will be ''warned'' until %(bumptime)s that you are editing this page.",
01229                     wiki=True) % {'bumptime': bumptime})
01230             msg.append(_(
01231                 "Use the Preview button to extend the locking period."
01232                 ))
01233             result = 1, '\n'.join(msg)
01234         else:
01235             mins_valid = (secs_valid+59) / 60
01236             if self.locktype == 'lock':
01237                 # lout out user
01238                 result = 0, _(
01239                     "This page is currently ''locked'' for editing by %(owner)s until %(timestamp)s,"
01240                     " i.e. for %(mins_valid)d minute(s).",
01241                     wiki=True) % {'owner': owner, 'timestamp': timestamp, 'mins_valid': mins_valid}
01242             else:
01243                 # warn user about existing lock
01244 
01245                 result = 1, _(
01246 """This page was opened for editing or last previewed at %(timestamp)s by %(owner)s.<<BR>>
01247 '''You should ''refrain from editing'' this page for at least another %(mins_valid)d minute(s),
01248 to avoid editing conflicts.'''<<BR>>
01249 To leave the editor, press the Cancel button.""", wiki=True) % {
01250                     'timestamp': timestamp, 'owner': owner, 'mins_valid': mins_valid}
01251 
01252         return result
01253 
01254 
01255     def release(self, force=0):
01256         """ Release lock, if we own it.
01257 
01258         @param force: if 1, unconditionally release the lock.
01259         """
01260         if self.locktype:
01261             # check that we own the lock in order to delete it
01262             #!!! race conditions, need to lock file during queries & update
01263             self._readLockFile()
01264             if force or self.uid == self.owner:
01265                 self._deleteLockFile()
01266 
01267 
01268     def _filename(self):
01269         """ Get path and filename for edit-lock file. """
01270         return self.pageobj.getPagePath('edit-lock', isfile=1)
01271 
01272 
01273     def _readLockFile(self):
01274         """ Load lock info if not yet loaded. """
01275         _ = self._
01276         self.owner = None
01277         self.owner_html = wikiutil.escape(_("<unknown>"))
01278         self.timestamp = 0
01279 
01280         if self.locktype:
01281             try:
01282                 entry = editlog.EditLog(self.request, filename=self._filename()).next()
01283             except StopIteration:
01284                 entry = None
01285 
01286             if entry:
01287                 self.owner = entry.userid or entry.addr
01288                 self.owner_html = entry.getEditor(self.request)
01289                 self.timestamp = wikiutil.version2timestamp(entry.ed_time_usecs)
01290 
01291 
01292     def _writeLockFile(self):
01293         """ Write new lock file. """
01294         self._deleteLockFile()
01295         try:
01296             editlog.EditLog(self.request, filename=self._filename()).add(
01297                self.request, wikiutil.timestamp2version(self.now), 0, "LOCK", self.page_name)
01298         except IOError:
01299             pass
01300 
01301     def _deleteLockFile(self):
01302         """ Delete the lock file unconditionally. """
01303         try:
01304             os.remove(self._filename())
01305         except OSError:
01306             pass
01307