###################################################################### # main ZWiki source file """ ZWiki README This zope product allows you to build wiki webs in zope. To install: - unpack ZWiki-x.x.x.tgz in your zope Products directory - restart zope - in the ZMI, add a ZWiki Web. Please understand the security issues noted on that form before publishing your wiki. For documentation and assistance, please see http://zwiki.org, http://zwiki.org/Discussion, etc. All feedback, bugreports and help appreciated. (c) 1999,2000,2001 Simon Michael for the zwiki community. This product is available under the GPL. All rights reserved, all disclaimers apply, etc. #>>> from Products.ZWiki.ZWikiPage import ZOPEMAJORVERSION, ZOPEMINORVERSION #>>> ZOPEMAJORVERSION #2 #>>> ZOPEMINORVERSION #4 """ __version__="$Revision: 1.1 $"[11:-2] #import os #os.environ['STUPID_LOG_FILE']= "debug.log" #from zLOG import LOG import os, sys, re, string from string import split,join,find,lower,rfind,atoi from types import * from urllib import quote, unquote # *url* quoting from wwml import translate_WMML from AccessControl import getSecurityManager import Acquisition from App.Common import absattr, rfc1123_date, aq_base import DocumentTemplate from DocumentTemplate.DT_Util import html_quote # *html* quoting import Globals from Globals import HTMLFile, MessageDialog from Products.MailHost.MailHost import MailBase from OFS.content_types import guess_content_type from OFS.Document import Document from OFS.DTMLDocument import DTMLDocument import OFS.Image import StructuredText from Defaults import DEFAULT_PAGE_TYPE, DISABLE_JAVASCRIPT, \ LARGE_FILE_SIZE, default_wiki_page, \ default_wiki_header, default_wiki_footer, default_editform, \ default_backlinks, default_subscribeform from Diff import ZWikiDiffMixin from Parents import ZWikiParentsMixin import Permissions from Regexps import intl_char_entities, urlchars, urlendchar, \ url, bracketedexpr, wikiname1, wikiname2, simplewikilink, \ wikilink, interwikilink, remotewikiurl, pagesubscribers, protected_line from SubscriberList import SubscriberList #from Products.ZCatalog.CatalogAwareness import CatalogAware from CatalogAwareness import CatalogAware # we'll use this later #v = context.getPhysicalRoot().Control_Panel.version_txt() v = open(os.path.join(SOFTWARE_HOME,'version.txt')).read() m = re.match(r'(?i)zope[^0-9]*([0-9]+)\.([0-9]+)',v) #ZOPEVERSION = float(v) # sad :( ZOPEMAJORVERSION, ZOPEMINORVERSION = int(m.group(1)),int(m.group(2)) ########################################################################### # CLASS ZwikiPage ########################################################################### #from Products.FLEjaagup.WebtopItem import WebtopItem class ZWikiPage(DTMLDocument, ZWikiParentsMixin, ZWikiDiffMixin, \ SubscriberList, CatalogAware): """ A ZWikiPage is a DTML Document which knows how to render itself in various wiki styles, and provides some utility methods to support wiki-building. RESPONSIBILITIES: - render itself - provide edit/create forms, backlinks, table of contents - accept edit/create requests, with authentication - store metadata such as permissions, time, last author, parents etc - manage subscriber lists for self & parent folder COLLABORATORS: Zope, Folder, Request, Response, WikiNesting, WWML, SubscriberList, ZWikiDiffMixin, ZWikiParentMixing """ ###################################################################### # VARIABLES ###################################################################### meta_type = "ZWiki Page" icon = "misc_/ZWiki/ZWikiPage_icon" page_type = DEFAULT_PAGE_TYPE last_editor = '' last_editor_ip = '' # properties visible in the ZMI # would rather append to the superclass' _properties here # DocumentTemplate.inheritedAttribute('_properties'),...) ? _properties=( {'id':'title', 'type': 'string', 'mode':'w'}, {'id':'page_type', 'type': 'string', 'mode': 'w'}, {'id':'last_editor', 'type': 'string', 'mode': 'r'}, {'id':'last_editor_ip', 'type': 'string', 'mode': 'r'}, #useful ? #{'id':'creator', 'type': 'string', 'mode': 'r'}, #{'id':'creator_ip', 'type': 'string', 'mode': 'r'}, #{'id':'creation_time', 'type': 'string', 'mode': 'r'}, ) \ + ZWikiParentsMixin._properties \ + SubscriberList._properties # permissions # Most of these we check in the code ourselves, because edit() # does a lot of different things. This also allows more helpful # authorization errors. __ac_permissions__=( (Permissions.Append, ()), (Permissions.ChangeType, ()), (Permissions.Change, ('PUT','manage_edit','manage_upload')), (Permissions.Delete, ()), ) \ + ZWikiParentsMixin.__ac_permissions__ \ # + SubscriberList.__ac_permissions__ ###################################################################### # METHOD CATEGORY: page rendering ###################################################################### def __call__(self, client=None, REQUEST={}, RESPONSE=None, **kw): """Render this zwiki page """ self.doLegacyFixups() #header = apply(self._renderHeaderOrFooter, # ('header',REQUEST,RESPONSE),kw) r = 'render_' + self.page_type if hasattr(self, r): method = getattr(self,r) else: method = self.render_plaintext body = apply(method,(self, REQUEST, RESPONSE), kw) #footer = apply(self._renderHeaderOrFooter, # ('footer',REQUEST,RESPONSE),kw) if RESPONSE is not None: RESPONSE.setHeader('Content-Type', 'text/html') #RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime)) #causes browser caching problems ? #return header + body + footer return body def _renderHeaderOrFooter(self, type, REQUEST, RESPONSE, **kw): """generate the header or footer for this zwiki page, unless it has been disabled in one way or another (clean up & document) """ # if REQUEST is not None: # if hasattr(self, 'standard_wiki_'+type): # return (apply(getattr(self,'standard_wiki_'+type).__call__, # (self, REQUEST, RESPONSE), kw)) # else: # if type is 'header': # return(default_wiki_header(self,REQUEST)) # else: # return(default_wiki_footer(self,REQUEST)) # else: # return '' # includes some old cruft trying to allow a zwikipage to work # in place of a a dtml method # actually a flag in REQUEST is not much use, try a keyword arg # why doesn't work yet ? if (REQUEST is not None and not (hasattr(REQUEST,'bare') or hasattr(REQUEST,'no'+type) or kw.has_key('bare') or kw.has_key('no'+type))): if hasattr(self, 'standard_wiki_'+type): if getattr(self,'standard_wiki_'+type).meta_type is "ZWiki Page": ##REQUEST['noheader']=1 #REQUEST.noheader=1 setattr(REQUEST,no+type,1) hdr = apply(getattr(self,'standard_wiki_'+type).__call__, (self, REQUEST, RESPONSE), kw) if getattr(self,'standard_wiki_'+type).meta_type is "ZWiki Page": hdr=unquote(hdr) hdr=re.sub(r'',r'',hdr) else: if type is 'header': hdr = default_wiki_header(self,REQUEST) else: hdr = default_wiki_footer(self,REQUEST) else: hdr = '' return hdr # built-in render methods; add new ones as dtml/python/external # methods. Their names correspond to the page_type attribute. # Special case: if the name contains "dtml", we will do DTML # validation after each edit. # I inlined all these for maximum control def render_structuredtextdtml(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # DTML + structured text + wiki links + HTML t = apply(DTMLDocument.__call__,(self, client, REQUEST, RESPONSE), kw) # XXX problem: an interesting DTML "feature". DTML # decapitates() initial lines that look like http headers, # moving them into the actual HTTP header. # (not-at-all-)quick workaround: keep a comment in front to hide them, # omit it everywhere else; see _set_text, read t = re.sub(protected_line, self._protect_line, t) t = self._structured_text(t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_structuredtext(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # structured text + wiki links + HTML t = str(self.read()) t = re.sub(protected_line, self._protect_line, t) t = self._structured_text(t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_structuredtextonly(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # structured text + wiki links # NB this one should be render_structuredtext and the above # should be render_structuredtexthtml t = html_quote(self.read()) t = re.sub(protected_line, self._protect_line, t) t = self._structured_text(t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_htmldtml(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # DTML + wiki links + HTML t = apply(DTMLDocument.__call__,(self, client, REQUEST, RESPONSE), kw) t = re.sub(protected_line, self._protect_line, t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_html(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # wiki links + HTML t = str(self.read()) t = re.sub(protected_line, self._protect_line, t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_plainhtml(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # HTML with no surprises t = str(self.read()) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_plainhtmldtml(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # DTML + HTML, nothing else t = apply(DTMLDocument.__call__,(self, client, REQUEST, RESPONSE), kw) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_classicwiki(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # classic wiki formatting + (inter)wiki links t = html_quote(self.read()) t = translate_WMML(t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(simplewikilink, thunk_substituter(self._wikilink_replace, t, 1), t) #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def render_plaintext(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # fixed-width plain text with no surprises t = "
\n" + html_quote(self.read()) + "\n
\n" #return t footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer # "just testing" def render_issuedtml(self, client=None, REQUEST={}, RESPONSE=None, **kw): header = apply(self._renderHeaderOrFooter, ('header',REQUEST,RESPONSE),kw) # render an "issue" page # this is an ordinary zwiki page with some extra attributes # (properties, attributes or embedded fields ?) # (if properties, does that imply a zwikipage superclass ?) # (set via in-page dtml, dtml method/pythonscript, product method ?) # which we render as form fields # embedded form for changing issue properties issueform = DocumentTemplate.HTML ("""
>
**Issue title:** << ^^ >>
**Category:** **Severity:** **Status:**

**Details:** (none)
""") #dtml-evaluate both form and main page # not sure about this first one t = apply(issueform.__call__,(self, REQUEST),kw) + \ apply(DTMLDocument.__call__,(self, client, REQUEST, RESPONSE), kw) #wikilinks t = re.sub(protected_line, self._protect_line, t) t = self._structured_text(t) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) footer = apply(self._renderHeaderOrFooter, ('footer',REQUEST,RESPONSE),kw) return header + t + footer def changeProperties(self, REQUEST=None, **kw): """ identical to manage_changeProperties, except redirects back to the current page """ if REQUEST is None: props={} else: props=REQUEST if kw: for name, value in kw.items(): props[name]=value propdict=self.propdict() for name, value in props.items(): if self.hasProperty(name): if not 'w' in propdict[name].get('mode', 'wd'): raise 'BadRequest', '%s cannot be changed' % name self._updateProperty(name, value) self.reindex_object() # don't forget this if REQUEST: REQUEST.RESPONSE.redirect(self.page_url()) def wikilink(self, text): """ utility method for wiki-linking an arbitrary text snippet >>> zc.TestPage.wikilink('http://a.b.c/d') 'http://a.b.c/d' >>> zc.TestPage.wikilink('mailto://a@b.c') 'mailto://a@b.c' >>> zc.TestPage.wikilink('nolink') 'nolink' >>> zc.TestPage.wikilink('TestPage')[-23:] '/TestPage">TestPage' >>> zc.TestPage.wikilink('TestPage1234')[-43:] '/TestPage/editform?page=TestPage1234">?' >>> zc.TestPage.wikilink('!TestPage') 'TestPage' >>> zc.TestPage.wikilink('[nolink]')[-37:] '/TestPage/editform?page=nolink">?' a problem with escaping remote wiki links was reported >>> zc.TestPage.edit(text='RemoteWikiURL: URL/') >>> zc.TestPage.wikilink('TestPage:REMOTEPAGE') # should give: 'TestPage:REMOTEPAGE' >>> zc.TestPage.wikilink('!TestPage:REMOTEPAGE') # should give: 'TestPage:REMOTEPAGE' """ # ' for font-lock # could modify the render_ methods to do this instead t = re.sub(protected_line, self._protect_line, text) t = re.sub(interwikilink, thunk_substituter(self._interwikilink_replace, t, 1), t) t = re.sub(wikilink, thunk_substituter(self._wikilink_replace, t, 1), t) return t def htmlquote(self, text): # expose this darn thing for dtml programmers once and for all! return html_quote(text) def _wikilink_replace(self, match, allowed=0, state=None, text=''): # tasty spaghetti regexps! better suggestions welcome ? """ Replace an occurrence of the wikilink regexp or one of the special [] constructs with a suitable hyperlink To be used as a re.sub repl function *and* get a proper value for literal context, 'allowed', etc, enclose this function with the value using 'thunk_substituter'. """ # In a literal? if state is not None: if within_literal(match.start(1), match.end(1)-1, state, text): return match.group(1) # matches beginning with ! should be left alone if re.match('^!',match.group(0)): return match.group(1) m = match.group(1) # if it's a bracketed expression, if re.match(bracketedexpr,m): # strip the enclosing []'s m = re.sub(bracketedexpr, r'\1', m) # extract a (non-url) path if there is one pathmatch = re.match(r'(([^/]*/)+)([^/]+)',m) if pathmatch: path,id = pathmatch.group(1), pathmatch.group(3) else: path,id = '',m # if it looks like an image link, inline it if guess_content_type(id)[0][0:5] == 'image': return '' % (path,id) # or if there was a path assume it's to some non-wiki # object and skip the usual existence checking for # simplicity. Could also attempt to navigate the path in # zodb to learn more about the destination if path: return '%s' % (path,id,id) # otherwise fall through to normal link processing # if it's an ordinary url, link to it if re.match(url,m): # except, if preceded by " or = it should probably be left alone if re.match('^["=]',m): # " return m else: return '%s' % (m, m) # a wikiname - if a page (or something) of this name exists, link to it elif hasattr(self.aq_parent, m): #return '%s' % (quote(m), m) # XXX make all wiki links absolute. this is a bit drastic!! # It's to make eg BookMarks more robust in editform & backlinks, # hopefully it won't break anything. I have no better ideas # right now so it's Do The Simplest Thing time return '%s' % (self.wiki_url(),quote(m), m) # otherwise, provide a "?" creation link else: # XXX see above #return '%s?' % (m, quote(self.id()), quote(m)) return '%s?' % (m, self.wiki_url(), quote(self.id()), quote(m)) def _interwikilink_replace(self, match, allowed=0, state=None, text=''): """ Replace an occurrence of interwikilink with a suitable hyperlink. To be used as a re.sub repl function *and* get a proper value for literal context, 'allowed', etc, enclose this function with the value using 'thunk_substituter'. """ # matches beginning with ! should be left alone #if re.match('^!',match.group(0)): return match.group(1) # NB this is a bit naughty, but: since we know this text will likely # be scanned with _wikilink_replace right after this pass, leave # the ! in place for it to find. Otherwise the localname will # get wiki-linked. if re.match('^!',match.group(0)): return match.group(0) localname = match.group('local') remotename = match.group('remote') # named groups come in handy here! # NB localname could be [bracketed] if re.match(bracketedexpr,localname): localname = re.sub(bracketedexpr, r'\1', localname) # look for a RemoteWikiURL definition if hasattr(self.aq_parent, localname): localpage = getattr(self.aq_parent,localname) # local page found - search for "RemoteWikiUrl: url" m = re.search(remotewikiurl, str(localpage)) if m is not None: remoteurl = html_unquote(m.group(1)) # NB: pages are # stored html-quoted # XXX eh ? they are ? # something's not # right # somewhere.. # I have lost my # grip on this # whole quoting # issue. # we have a valid inter-wiki link link = '%s:%s' % \ (remoteurl, remotename, localname, remotename) # protect it from any later wiki-izing passes return re.sub(wikilink, r'!\1', link) # otherwise, leave alone return match.group(0) def _protect_line(self, match): """protect an entire line from _wikilink_replace, by protecting all its wikilink occurrences """ return re.sub(wikilink, r'!\1', match.group(1)) def _structured_text(self, text): """ Render some text into html according to the structured text rules, massaging and tweaking slightly to work around stx issues and for our nefarious zwiki purposes. """ if ZOPEMAJORVERSION <= 2 and ZOPEMINORVERSION <= 3: # zope 2.2 - 2.3 # workarounds for classic structured text # strip trailing blank lines to avoid the famous last line # heading bug text = re.sub(r'(?m)\n[\n\s]*$', r'\n', text) # "A paragraph that begins with a sequence of sequences, where # each sequence is a sequence of digits or a sequence of # letters followed by a period, is treated as an ordered list # element." # This stx rule converts an initial single word plus period # into a numeric bullet. Don't understand why it exists. # Quickest workaround: prepend a harmless marker like # preserve indentation # now why did I need (?m) here ? seems backwards # hmm \s matches our line breaks # caution - our workaround will be visible eg in :: examples, # so we remove it after stx has done it's thing, below text = re.sub(r'(?m)^([ \t]*)([A-z]\w*\.)', r'\1\2', text) # "Sub-paragraphs of a paragraph that ends in the word example # or the word examples, or :: is treated as example code and # is output as is" # This fails if there is whitespace after the ::, so remove it # NB contrary to the docs "the word example or examples" is # not used, rightly I think text = re.sub(r'(?m)::[ \t]+$', r'::', text) # skip stx's [] footnote linking (by not calling # html_with_references) text = str(StructuredText.HTML(text,level=3)) # DTSTTCPW, DTSTTCPW text = re.sub(r'(?i)(<|<)!--STXKLUDGE--(>|>)', r'', text) else: # zope 2.4 - # interesting new workarounds for structured text NG # "Sub-paragraphs of a paragraph that ends in the word example # or the word examples, or :: is treated as example code and # is output as is" # This fails if there is whitespace after the ::, so remove it # still need this one text = re.sub(r'(?m)::[ \t]+$', r'::', text) # stxng is doing it's [] footnote linking but I can't # figure out how # so, another silly hack text = re.sub(r'(?m)\[',r'[',text) text = str(StructuredText.HTML(text,level=3)) text = re.sub(r'(<|<)!--STXKLUDGE--(>|>)', r'', text) return text ###################################################################### # METHOD CATEGORY: page editing & creation ###################################################################### def create(self, page, text=None, type=None, title='', REQUEST=None): """ Create a new wiki page and redirect there if appropriate; can upload a file at the same time. Normally edit() will call this for you. >>> # create a blank page >>> >>> zc.TestPage.create('TestPage1',text='') >>> assert hasattr(zc,'TestPage1') >>> zc.TestPage1.text() '' >>> >>> # create a classicwiki page with some text >>> >>> zc.TestPage.create('TestPage2',text='test page data',type='classicwiki') >>> assert hasattr(zc,'TestPage1') >>> zc.TestPage2.src() 'test page data' >>> zc.TestPage2.page_type 'classicwiki' >>> #>>> # add a file while creating a page #>>> # this capability broke - fix if ever needed #>>> # and the rest of this is tested in edit() I think #>>> #>>> import OFS.Image #>>> file = OFS.Image.Pdata('test file data') #>>> file.filename = 'test_file' #>>> zc.REQUEST.file = file #>>> zc.TestPage.create('TestPage3',text='test page data',REQUEST=zc.REQUEST) #>>> # the new file should exist #>>> assert hasattr(zc,'test_file') #>>> # with the right data #>>> str(zc['test_file']) #'test file data' #>>> # and a link should have been added to the new wiki page #>>> zc.TestPage3.src() #'test page data\\n\\ntest_file\\n' #>>> #>>> # ditto, with an image #>>> #>>> file.filename = 'test_image.gif' #>>> zc.REQUEST.file = file #>>> zc.TestPage1.create('TestPage4',text='test page data',REQUEST=zc.REQUEST) #>>> assert hasattr(zc,'test_image.gif') #>>> zc['test_image.gif'].content_type #'image/gif' #>>> zc.TestPage4.src() #'test page data\\n\\n\\n' #>>> #>>> # images should not be inlined if dontinline is set #>>> #>>> file.filename = 'test_image.JPG' #>>> zc.REQUEST.dontinline = 1 #>>> zc.TestPage1.create('TestPage5',text='',REQUEST=zc.REQUEST) #>>> zc.TestPage5.src() #'\\n\\ntest_image.JPG\\n' #>>> # cleanup #>>> zc.REQUEST.dontinline = None """ # do we have permission ? checkPermission = getSecurityManager().checkPermission if not checkPermission(Permissions.Add,self.aq_parent): raise 'Unauthorized', ( 'You are not authorized to add ZWiki Pages here.') # make a new (blank) page object, p = ZWikiPage(source_string='', __name__=page) p.title = title p.parents = [self.id()] p._set_last_editor(REQUEST) # inherit type from the previous (parent) page if not specified # or overridden via folder attribute, if hasattr(self.aq_parent,'standard_page_type'): p.page_type = self.aq_parent.standard_page_type elif type: p.page_type = type else: p.page_type = self.page_type # & situate it in the parent folder # NB newid might not be the same as page newid = self.aq_parent._setObject(page,p) # To help manage executable content, make sure the new page # acquires it's owner from the parent folder. p._deleteOwnershipAfterAdd() # choose initial page text and set it as edit() would, with # cleanups and dtml validation if text is not None: t = text elif hasattr(self.aq_parent, 'standard_wiki_page'): if callable(self.aq_parent.standard_wiki_page): t = self.aq_parent.standard_wiki_page(self,REQUEST) else: t = DocumentTemplate.HTML(self.aq_parent.standard_wiki_page) else: t = default_wiki_page(self,REQUEST) p._set_text(t,REQUEST) # if a file was submitted as well, handle that # we pass in aq_parent because the new-born p doesn't # have a proper acquisition context ? p._handleFileUpload(REQUEST, parent=self.aq_parent) # update catalog if present # DTMLDocumentExt indexed us after _setObject, but do it again # now that we have set the text # NB need the full wrapped object for this getattr(self.aq_parent,newid).index_object() # old stuff: #if hasattr(self, self.default_catalog): # url = join(split(self.url(),'/')[:-1],'/') + '/' + newid # !!?? # getattr(self, self.default_catalog).catalog_object(p, url) # redirect browser if needed if REQUEST is not None: u=REQUEST['URL2'] REQUEST.RESPONSE.redirect(u + '/' + quote(newid)) def append(self, text='', separator='\n\n', REQUEST=None): """ Appends some text to an existing zwiki page by calling edit; may result in mail notifications to subscribers. """ oldtext = self.read() text = str(text) if text: # cc comment to subscribers # temporary - # edit() is able to send mail notifications now, but in # this case I prefer to handle it myself so as to just # mail the comment text rather than a diff. # Done first because edit may not return. #try: # self.sendMailToSubscribers(text,REQUEST) #except: # pass # usability hack: scroll to bottom after adding a comment if REQUEST: REQUEST['URL1'] = REQUEST['URL1'] + '#bottom' #text = oldtext + separator #if comment_heading: # text = text + \ # '
\n&dtml-zwiki_username_or_ip;
' self.edit(text=oldtext+separator+text, REQUEST=REQUEST) def comment(self, text='', username='', time='', note=' (via web)', use_heading=0, REQUEST=None): """ A handy method, like append but - adds a standard comment heading with user name, time & note - allows those fields to be specified for some flexibility with mail-ins etc - some a little html prettification for web display; nb other parts try to strip this same html (sendMailSubscriber) As a convenience for standard_wiki_footer, unless use_heading is true we act exactly like append. Should think of something better here. >>> zc.TestPage.edit(text='test') >>> zc.TestPage.comment(text='comment',username='me',time='now') >>> zc.TestPage.read() 'test\\n\\ncomment' >>> zc.TestPage.comment(text='comment',username='me',time='now', ... use_heading=1) >>> zc.TestPage.read() 'test\\n\\ncomment\\n\\n
me, now (via web):
\\ncomment' """ if not use_heading: return self.append(text=text,REQUEST=REQUEST) # generate the comment heading if not username: username = self.zwiki_username_or_ip(REQUEST) if re.match(r'^[0-9\.\s]*$',username): # hide ip addresses username = '' if username: username = username + ', ' if not time: time = self.ZopeTime().strftime('%Y/%m/%d %H:%M %Z') username = html_quote(username) time = html_quote(time) note = html_quote(note) heading = '\n\n
%s%s%s:
\n' % (username,time,note) # italicize quoted text in replies text = re.sub(r'(?m)^>(.*)',r'
>\1',text) self.append(text,separator=heading,REQUEST=REQUEST) def edit(self, page=None, text=None, type=None, title='', timeStamp=None, REQUEST=None, subjectSuffix=''): # temp """ General-purpose method for editing & creating zwiki pages. Changes the text and/or markup type of this (or the specified) page, or creates the specified page if it does not exist. Other special features: - Usually called from a time-stamped web form; we use timeStamp to detect and warn when two people attempt to work on a page at the same time. This makes sense only if timeStamp came from an editform for the page we are actually changing. - The username (authenticated user or zwiki_username cookie) and ip address are saved in page's last_editor, last_editor_ip attributes if a change is made - If the text begins with "DeleteMe", move this page to the recycle_bin subfolder. - If file has been submitted in REQUEST, create a file or image object and link or inline it on the current page. - May also cause mail notifications to be sent to subscribers This code has become more complex to support late page creation, but the api should now be more general & powerful than it was. Doing all this stuff in one method simplifies the layer above I think. >>> # set a page's text to something >>> zc.TestPage.edit(text='something') >>> zc.TestPage.src() 'something' >>> >>> # set a page's text to the empty string >>> zc.TestPage.edit(text='') >>> zc.TestPage.src() '' >>> >>> # add a file to a page >>> import OFS.Image >>> file = OFS.Image.Pdata('test file data') >>> file.filename = 'edittestfile' >>> zc.REQUEST.file = file >>> zc.TestPage.edit(REQUEST=zc.REQUEST) >>> # the new file should exist >>> assert hasattr(zc,'edittestfile') >>> # with the right data >>> str(zc['edittestfile']) 'test file data' >>> # and a link should have been added to the page >>> zc.TestPage.src() '\\n\\nedittestfile\\n' >>> # cleanup - temporary I think, because of double testing >>> zc.manage_delObjects(['edittestfile']) >>> >>> # ditto, with an image >>> zc.REQUEST.file.filename = 'edittestimage.jpg' >>> zc.TestPage.edit(REQUEST=zc.REQUEST) >>> assert hasattr(zc,'edittestimage.jpg') >>> zc['edittestimage.jpg'].content_type 'image/jpeg' >>> zc.TestPage.src() '\\n\\nedittestfile\\n\\n\\n\\n' >>> # cleanup >>> zc.manage_delObjects(['edittestimage.jpg']) >>> >>> # images should not be inlined if dontinline is set >>> zc.REQUEST.file.filename = 'edittestimage.png' >>> zc.REQUEST.dontinline = 1 >>> zc.TestPage.edit(REQUEST=zc.REQUEST) >>> zc.TestPage.src() '\\n\\nedittestfile\\n\\n\\n\\n\\n\\nedittestimage.png\\n' >>> # cleanup >>> zc.manage_delObjects(['edittestimage.png']) >>> zc.REQUEST.dontinline = None >>> >>> # a file with blank filename should be ignored >>> zc.REQUEST.file.filename = '' >>> old = zc.TestPage.src() >>> zc.TestPage.edit(REQUEST=zc.REQUEST) >>> assert zc.TestPage.src() == old DeleteMe How should this work ? Do the simplest thing. When we see a first line beginning with "DeleteMe": - move to recycle_bin - redirect to first parent or default page create a test page >>> ZWiki.manage_addZWikiPage('TestPageA', file='not much') '' deleteme's not at the beginning shouldn't do anything >>> zc.TestPageA.edit(text=zc.TestPageA.src()+'\\nDeleteMe') >>> zc.TestPageA.src() 'not much\\nDeleteMe' XXX deleteme at the beginning will send it to recycle_bin/: >>> zc.TestPageA.edit(text='DeleteMe, with comments\\n'+zc.TestPageA.src()) >>> assert not hasattr(zc,'TestPageA') >>> zc.recycle_bin.TestPageA.src() 'not much\\nDeleteMe' Username stamping (old) problem: how do we manipulate the authenticated user and still run tests ? #if we are authenticated, our auth. name should be recorded #>>> zc.TestPage.username = '-' #>>> zc.REQUEST.REMOTE_ADDR = '1.2.3.4' #>>> zc.REQUEST.cookies['zwiki_username'] = 'TESTUSERNAME' #>>> zc.TestPage.edit(text=zc.TestPage.src()+'.',REQUEST=zc.REQUEST) #>>> assert zc.TestPage.username == zc.REQUEST.AUTHENTICATED_USER.getUserName() #if a cookie is not present, IP address should be used #>>> zc.REQUEST.REMOTE_ADDR = '1.2.3.4' #>>> zc.TestPage.edit(text=zc.TestPage.src()+'.',REQUEST=zc.REQUEST) #>>> zc.TestPage.username #'1.2.3.4' #NB username will not be stamped unless something is actually #changed #>>> zc.REQUEST.REMOTE_ADDR = '5.6.7.8' #>>> zc.TestPage.edit(text=zc.TestPage.src(), #... type=zc.TestPage.page_type,REQUEST=zc.REQUEST) #>>> zc.TestPage.username #'1.2.3.4' #if the zwiki_username cookie is present, it should be used #>>> zc.REQUEST.REMOTE_ADDR = '1.2.3.4' #>>> zc.REQUEST.cookies['zwiki_username'] = 'TESTUSERNAME' #>>> zc.TestPage.edit(text=zc.TestPage.src()+'.',REQUEST=zc.REQUEST) #>>> zc.TestPage.username #'TESTUSERNAME' #if the cookie is present but blank, IP address should be used #>>> zc.REQUEST.REMOTE_ADDR = '1.2.3.4' #>>> zc.REQUEST.cookies['zwiki_username'] = '' #>>> zc.TestPage.edit(text=zc.TestPage.src()+'.',REQUEST=zc.REQUEST) #>>> zc.TestPage.username #'1.2.3.4' """ #self._validateProxy(REQUEST) # XXX correct ? don't think so # do zwiki pages obey proxy roles ? # are we changing this page, another page, or creating a new one ? if page: page = unquote(page) if page is None: # changing the current page p = self elif hasattr(self.aq_parent, page): # changing another specified page p = getattr(self.aq_parent, page) else: # creating a new page return self.create(page,text,type,title,REQUEST) # ok, changing p. We may be doing several things here; # each of these handlers checks permissions and does the # necessary. Some of these can halt further processing. # todo: tie these in to mail notification, along with # other changes like reparenting if self.checkEditConflict(timeStamp, REQUEST): return self.editConflictDialog() if p._handleDeleteMe(text,REQUEST): return p._handleEditPageType(type,REQUEST) p._handleEditText(text,REQUEST,subjectSuffix) p._handleFileUpload(REQUEST) # update catalog if present # sometimes getting this here: #Error Type: NameError #Error Value: i #Traceback (innermost last): # File /usr/local/Zope-2.3.0-src/lib/python/ZPublisher/Publish.py, line 222, in publish_module # File /usr/local/Zope-2.3.0-src/lib/python/ZPublisher/Publish.py, line 187, in publish # File /usr/local/Zope-2.3.0-src/lib/python/Zope/__init__.py, line 221, in zpublisher_exception_hook # (Object: SiteTracker) # File /usr/local/Zope-2.3.0-src/lib/python/ZPublisher/Publish.py, line 171, in publish # File /usr/local/Zope-2.3.0-src/lib/python/ZPublisher/mapply.py, line 160, in mapply # (Object: edit) # File /usr/local/Zope-2.3.0-src/lib/python/ZPublisher/Publish.py, line 112, in call_object # (Object: edit) # File /home/simon/Products/ZWiki/ZWikiPage.py, line 877, in edit # (Object: SiteTracker) # File /home/simon/Products/DTMLDocumentExt/__init__.py, line 86, in index_object # (Object: SiteTracker) # File /usr/local/Zope-2.3.0-src/lib/python/Products/ZCatalog/ZCatalog.py, line 408, in catalog_object # (Object: Traversable) # File /usr/local/Zope-2.3.0-src/lib/python/Products/ZCatalog/Catalog.py, line 382, in catalogObject # File /usr/local/Zope-2.3.0-src/lib/python/SearchIndex/UnTextIndex.py, line 387, in index_object # File /usr/local/Zope-2.3.0-src/lib/python/SearchIndex/UnTextIndex.py, line 332, in removeForwardEntry #NameError: (see above) try: p.index_object() except: # log the problem try: p.reindex_object() except: # log the problem pass # redirect browser if needed if REQUEST is not None: try: u=p.DestinationURL() # what could we do with this ? except: u=REQUEST['URL1'] REQUEST.RESPONSE.redirect(u) def _handleEditPageType(self,type,REQUEST=None): # is the new page type valid and different ? if (type is not None and type != self.page_type): # do we have permission ? checkPermission = getSecurityManager().checkPermission if not checkPermission(Permissions.ChangeType,self): raise 'Unauthorized', ( 'You are not authorized to change this ZWiki Page\'s type.') # change it self.page_type = type self._set_last_editor(REQUEST) def _handleEditText(self,text,REQUEST=None, subjectSuffix=''): # is the new text valid and different ? if (text is not None and self._text_cleanups(text) != self.read()): # do we have permission ? checkPermission = getSecurityManager().checkPermission if (not (checkPermission(Permissions.Change, self) or (checkPermission(Permissions.Append, self) and find(self._text_cleanups(text),self.read()) == 0))): raise 'Unauthorized', ( 'You are not authorized to edit this ZWiki Page.') # change it oldtext = self.read() self._set_text(text,REQUEST) self._set_last_editor(REQUEST) # cc the diff to subscribers # don't let mail stuff disrupt the edit try: self.sendMailToSubscribers( self.textDiff(a=oldtext,b=self.read()), REQUEST, "") except: # XXX log this error pass def _handleDeleteMe(self,text,REQUEST=None): # is this the DeleteMe special case (text beginning with "DeleteMe") ? if text and re.match('(?m)^DeleteMe', text): # do we have permission ? checkPermission = getSecurityManager().checkPermission if (not (checkPermission(Permissions.Change, self) or (checkPermission(Permissions.Append, self) and find(self._text_cleanups(text),self.read()) == 0))): raise 'Unauthorized', ( 'You are not authorized to edit this ZWiki Page.') if not checkPermission(Permissions.Delete, self): raise 'Unauthorized', ( 'You are not authorized to delete this ZWiki Page.') # delete the page self._delete(REQUEST) # redirect to first parent or front page if appropriate if REQUEST: try: destpage=f.recycle_bin[id].parents[0] assert(hasattr(f,destpage)) except: destpage='' REQUEST.RESPONSE.redirect( self.wiki_url()+'/'+quote(destpage)) #XXX sys.stderr.write('zwiki: WHY ARE WE HERE! RETURN 1 ANYWAY!' +'\n') #SKWM return 1 else: # set return flag to halt further edit handling return 1 def _delete(self, REQUEST=None): """ move this page to the recycle_bin subfolder, creating it if necessary. """ # create recycle_bin folder if needed f = self.aq_parent if not hasattr(f,'recycle_bin'): f.manage_addFolder('recycle_bin', 'deleted wiki pages') # & move page there id=self.id() # XXX use getId() when available cb=f.manage_cutObjects(id) # update catalog if present # DTMLDocumentExt will catalog the new location.. # just hack around it for now save = DTMLDocument.manage_afterAdd DTMLDocument.manage_afterAdd = lambda self,item,container: None f.recycle_bin.manage_pasteObjects(cb) DTMLDocument.manage_afterAdd = save def _handleFileUpload(self,REQUEST,parent=None): # is there a file upload ? if (REQUEST and hasattr(REQUEST,'file') and hasattr(REQUEST.file,'filename') and REQUEST.file.filename): # XXX do something # figure out the upload destination if hasattr(self,'uploads'): uploaddir = self.uploads else: uploaddir = parent or self.aq_parent # see create() # do we have permission ? checkPermission = getSecurityManager().checkPermission if not (checkPermission(Permissions.Upload,uploaddir)):# or #checkPermission(Permissions.UploadSmallFiles, # self.aq_parent)): raise 'Unauthorized', ( 'You are not authorized to upload files here.') if not (checkPermission(Permissions.Change, self) or checkPermission(Permissions.Append, self)): raise 'Unauthorized', ( 'You are not authorized to add a link on this ZWiki Page.') # can we check file's size ? #if (len(REQUEST.file) > LARGE_FILE_SIZE and # not checkPermission(Permissions.Upload, # uploaddir)): # raise 'Unauthorized', ( # 'You are not authorized to add files larger than ' + \ # LARGE_FILE_SIZE + ' here.') # create the File or Image object file_id, content_type, size = \ self._createFileOrImage(REQUEST.file, title=REQUEST.get('title', ''), REQUEST=REQUEST, parent=parent) if file_id: # and link it on the page self._addFileLink(file_id, content_type, size, REQUEST) # update catalog if present self.index_object() else: # failed to create - give up (what about an error) pass def _createFileOrImage(self,file,title='',REQUEST=None,parent=None): # based on WikiForNow which was based on # OFS/Image.py:File:manage_addFile """ Add a new File or Image object, depending on file's filename suffix. Returns a tuple containing the new id, content type & size, or (None,None,None). >>> import OFS.Image >>> file = OFS.Image.Pdata('test file data') >>> >>> # our test page/folder should initially have no uploads attr. >>> self = zc.TestPage >>> #assert not hasattr(self,'uploads') >>> >>> # calling with an unnamed file should do nothing much >>> zc.TestPage._createFileOrImage(file) (None, None, None) >>> >>> # ditto for a blank filename >>> file.filename = '' >>> zc.TestPage._createFileOrImage(file) (None, None, None) >>> >>> # here, a file object of unknown type should be created >>> name = 'testfile' >>> file.filename = name >>> id, content_type,size = zc.TestPage._createFileOrImage(file) >>> str(zc[id]) 'test file data' >>> content_type 'application/octet-stream' >>> # was 'text/x-unknown-content-type' >>> size 14 >>> >>> # a text file >>> name = 'testfile.txt' >>> file.filename = name >>> id, content_type,size = zc.TestPage._createFileOrImage(file) >>> str(zc[id]) 'test file data' >>> content_type 'text/plain' >>> >>> # an image >>> name = 'testfile.gif' >>> file.filename = name >>> id, content_type,size = zc.TestPage._createFileOrImage(file) >>> str(zc[id])[-47:] # evaluating an Image gives its html tag '/testfile.gif" alt="testfile.gif" border="0" />' >>> content_type 'image/gif' """ #' for font-lock # set id & title from filename title=str(title) id, title = OFS.Image.cookId('', title, file) if not id: return None, None, None # find out where to store files - in the 'uploads' # folder if defined, otherwise the wiki folder # NB a page might override this with a property if hasattr(self,'uploads'): folder = self.uploads else: folder = parent or self.aq_parent # see create() # First, we create the file or image object without data: # XXX SM handle existing object ? if guess_content_type(file.filename)[0][0:5] == 'image': folder._setObject(id, OFS.Image.Image(id,title,'')) else: folder._setObject(id, OFS.Image.File(id,title,'')) # Now we "upload" the data. By doing this in two steps, we # can use a database trick to make the upload more efficient. folder._getOb(id).manage_upload(file) return id, folder._getOb(id).content_type, folder._getOb(id).getSize() def _addFileLink(self, file_id, content_type, size, REQUEST): """ Add a link to the specified file at the end of this page. If the file is an image and not too big, inline it instead. """ if hasattr(self,'uploads'): filepath = 'uploads/' else: filepath = '' if content_type[0:5] == 'image' and \ not (hasattr(REQUEST,'dontinline') and REQUEST.dontinline) and \ size <= LARGE_FILE_SIZE : linktxt = '\n\n\n' % (filepath,file_id) else: linktxt = '\n\n%s\n' % (filepath,file_id,file_id) self._set_text(self.src()+linktxt,REQUEST) self._set_last_editor(REQUEST) def _set_text(self, text='', REQUEST=None): """change the page text, with cleanups and perhaps DTML validation """ t = self._text_cleanups(text) # for DTML page types, execute the DTML to catch problems - # zope will magically roll back this whole transaction and the # user will get an appropriate error if re.search(r'(?i)dtml',self.page_type): # antidecapitationkludge - see render_structuredtextdtml # this has been fixed by 2.4 I think if ZOPEMAJORVERSION <= 2 and ZOPEMINORVERSION <= 3: t = '\n\n' + t else: pass self.munge(t) # XXX problem: dtml in a newly-created page will give unauthorized # a commit doesn't seem to help #get_transaction().commit() # so I'm going to skip dtml validation in this case # a person entering dtml in a new page knows what they're doing # and should be able to go to /editform if needed # XXX bug - now we don't do any dtml validation at all? #DTMLDocument.__call__(self,self,REQUEST,REQUEST.RESPONSE) else: self.raw = t def _text_cleanups(self, t): """do some cleanup of a page's new text """ # strip any browser-appended ^M's t = re.sub('\r\n', '\n', t) # convert international characters to HTML entities for safekeeping for c,e in intl_char_entities: t = re.sub(c, e, t) # here's the place to strip out any disallowed html/scripting elements if DISABLE_JAVASCRIPT: # XXX needs work # don't match #t = re.sub(r'(?i)<([^d>]*script[^>]*)>',r'<disabled \1>',t) t = re.sub(r'(?i)<([^>\w]*script[^>]*)>',r'<disabled \1>',t) return t def _set_last_editor(self, REQUEST=None): if REQUEST: self.last_editor_ip = REQUEST.REMOTE_ADDR self.last_editor = self.zwiki_username_or_ip(REQUEST) else: # this has been fiddled with before # if we have no REQUEST, at least update last editor self.last_editor_ip = '' self.last_editor = '' ###################################################################### # METHOD CATEGORY: edit conflict checking ###################################################################### # how can we be smarter about edit conflicts, eg in the common # case where you back up in your browser, edit and click the # change button again ? Ignore the timestamp if our username & IP # match the last editor's ? # # XXX should we consider the append form in this ? # # prior musings which may be a load of codswallop by now: # What if # - we are behind a proxy so all ip's are the same ? # - several people use the same cookie-based username ? # - people use the same cookie-name as an existing member name ? # - noone is using usernames ? # in the proxy case, we'll usually get a username mismatch as long # as we exclude "anonymous" and ''. But this would require # anonymous users to configure a username or experience the old # strict behaviour, which is a drag. By default we'll assume no # proxy and let these users by on the strength of their ip. # # so 4 strategies so far: # # 0. no conflict checking # # 1. strict - based only on timestamp. (The behaviour of previous # releases. Safest but obstructs backtrack & re-edit). # # 2. semi-careful - include username & ip address in the # "timestamp". If your (non-anonymous) username and ip match, a # time mismatch will be ignored. (If a proxy is present there # will be no conflict checking amongst users with the same member- # or cookie-name. Anonymous users will experience strict checking # until they configure a username.) # # 3. relaxed - if your username & ip match, a time mismatch will # be ignored. (The new default. If a proxy is present there will # be no conflict checking amongst anonymous users). # # Started making this an option but it's too much hassle def timeStamp(self): return str(self._p_mtime) def checkEditConflict(self, timeStamp, REQUEST): username = self.zwiki_username_or_ip() if (timeStamp is not None and timeStamp != self.timeStamp() and (not hasattr(self,'last_editor') or not hasattr(self,'last_editor_ip') or username != self.last_editor or REQUEST.REMOTE_ADDR != self.last_editor_ip)): # XXX how can we do something more friendly here #raise 'EditingConflict', ( # '''Someone has edited this page since you loaded the # page for editing.

# Try editing the page again. # ''') return 1 else: return 0 ###################################################################### # METHOD CATEGORY: ftp/PUT/webdav handling ###################################################################### def manage_FTPget(self): "Get source for FTP download" return "Wiki-Safetybelt: %s\n\n%s" % ( self.timeStamp(), self.read()) def PUT(self, REQUEST, RESPONSE): # should this do what _set_text() does ? """Handle HTTP PUT requests.""" self.dav__init(REQUEST, RESPONSE) body=REQUEST.get('BODY', '') self._validateProxy(REQUEST) m=re.match(r'Wiki-Safetybelt: ([0-9]+[.][0-9]+)[\n][\n]?', body) if m: body = body[m.span()[1]:] if self.checkEditConflict(m.group(1), REQUEST): RESPONSE.setStatus(404) #XXX what should this be return RESPONSE self.munge(body) self._set_last_editor(REQUEST) RESPONSE.setStatus(204) return RESPONSE ###################################################################### # METHOD CATEGORY: wiki-mail integration ###################################################################### # see also SubscriberList mixin def sendMailToSubscribers(self, text, REQUEST, subjectSuffix=''): """ If a mailhost and mail_from property have been configured and there are subscribers to this page, email text to them """ recipients = self.allSubscribers() if recipients: self.sendMailTo(recipients,text,REQUEST,subjectSuffix) def sendMailTo(self, recipients, text, REQUEST, subjectSuffix=''): """ If a mailhost and mail_from property have been configured, email text to recipients """ # ugLy temp hack # strip out the message heading typically prepended on *Discussion pages mailouttext = re.sub(r'(?s)(


)?(.*?)
\n',r'',text) mailouttext = re.sub(r'(?m)^
>(.*?)',r'>\1',mailouttext) mailouttext = html_unquote(mailouttext) # from SendMailTag.py if hasattr(self, 'MailHost'): mhost=self.MailHost if hasattr(self.aq_parent,'mail_from'): # send message - make this configurable # the \ is required mhost.send("""\ From: %s (%s) To: %s Subject: %s %s %s --- forwarded from %s """ % (self.aq_parent.mail_from, self.zwiki_username_or_ip(REQUEST), recipients, self.id(), subjectSuffix, mailouttext, self.page_url() )) ###################################################################### # METHOD CATEGORY: forms & dialogs ###################################################################### def editform(self, REQUEST=None, page=None, text=None, action='Change'): """ Display a form which will edit or create the specified page. For new pages, initial text may be specified. May be overridden by a DTML method of the same name. """ # what are we going to do ? set up page, text & action accordingly if page is None: # no page specified - editing the current page page = self.id() text = self.read() elif hasattr(self.aq_parent, page): # editing a different page text = getattr(self.aq_parent,page).read() else: # editing a brand-new page action = 'Create' # supply default text if needed if text is None: if hasattr(self.aq_parent, 'standard_wiki_page'): if callable(self.aq_parent.standard_wiki_page): text = self.aq_parent.standard_wiki_page(self,REQUEST) else: text = DocumentTemplate.HTML( self.aq_parent.standard_wiki_page) else: text = default_wiki_page(self,REQUEST) # display the edit form - a dtml method or the builtin default # NB we redefine id as a convenience, so that one header can work # for pages and editforms # XXX can we simplify this/make dtml more version independent ? # no way to restrict /editform currently if hasattr(self.aq_parent, 'editform'): return self.aq_parent.editform(self,REQUEST, page=page,text=text,action=action, id=page,oldid=self.id()) else: return default_editform(self,REQUEST, page=page,text=text,action=action, id=page,oldid=self.id()) def editConflictDialog(self): """ web page displayed in edit conflict situations. """ return MessageDialog( title='Edit conflict!', message=""" Edit conflict!

Someone else has saved this page while you were editing. To resolve the conflict, do this:

  1. Click your browser's back button
  2. Copy your recent edits to the clipboard
  3. Click your browser's refresh button
  4. Paste in your edits again, being mindful of the latest changes
  5. Click the Change button again.
or,

To discard your recent edit and start again, click OK. """, action=self.page_url()+'/editform') def backlinks(self, REQUEST=None): """ Display a default backlinks page. May be overridden by a DTML method of the same name. """ if hasattr(self.aq_parent, 'backlinks'): return self.aq_parent.backlinks(self,REQUEST) else: return default_backlinks(self,REQUEST) def subscribeform(self, REQUEST=None): """ Display a default mail subscription form. May be overridden by a DTML method of the same name. """ if hasattr(self.aq_parent, 'subscribeform'): return self.aq_parent.subscribeform(self,REQUEST) else: return default_subscribeform(self,REQUEST) ###################################################################### # METHOD CATEGORY: misc ###################################################################### zwiki_username_or_ip__roles__ = None def zwiki_username_or_ip(self, REQUEST=None): """ search REQUEST for an authenticated member or a zwiki_username cookie XXX added REQUEST arg at one point when sending mail notification in append() was troublesome - still needed ? """ if not REQUEST and hasattr(self,'REQUEST'): REQUEST = self.REQUEST username = None if REQUEST: user = REQUEST.AUTHENTICATED_USER username = user.getUserName() if not username or str(user.acl_users._nobody) == username: if hasattr(REQUEST, 'cookies') and \ REQUEST.cookies.has_key('zwiki_username') and \ REQUEST.cookies['zwiki_username']: username = REQUEST.cookies['zwiki_username'] else: username = REQUEST.REMOTE_ADDR return username or '' text__roles__ = None def text(self, REQUEST=None, RESPONSE=None): # see also backwards compatibility section # why permission-free ? """ return this page's raw text (a permission-free version of document_src) also fiddle the mime type for web browsing misc tests: ### ensure we don't lose first lines to DTML's decapitate() >>> zc.TestPage._set_text(r'first: line\\n\\nsecond line\\n') >>> zc.TestPage.text() 'first: line\\\\n\\\\nsecond line\\\\n' >>> zc.TestPage.text() 'first: line\\\\n\\\\nsecond line\\\\n' ### ensure none of these reveal the antidecapkludge >>> zc.TestPage.edit(type='htmldtml') >>> zc.TestPage._set_text('test text') >>> zc.TestPage.text() 'test text' >>> zc.TestPage.read() 'test text' >>> zc.TestPage.__str__() 'test text' """ if RESPONSE is not None: RESPONSE.setHeader('Content-Type', 'text/plain') #RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime)) return self.read() # antidecapitationkludge - see render_structuredtextdtml _old_read = DTMLDocument.read def read(self): return re.sub('\n\n?','', self._old_read()) def __repr__(self): return ("<%s %s at 0x%s>" % (self.__class__.__name__, `self.id()`, hex(id(self))[2:])) # methods for reliable dtml access to page & wiki url # these have been troublesome; they need to work with & without # virtual hosting. Tests needed. # Keep old versions around to help with debugging - see # http://zwiki.org/VirtualHostingSummary def page_url(self): """return the url path for this wiki page""" return self.page_url7() def wiki_url(self): """return the base url path for this wiki""" return self.wiki_url7() def page_url1(self): """return the url path for the current wiki page""" o = self url = [] while hasattr(o,'id'): url.insert(0,absattr(o.id)) o = getattr(o,'aq_parent', None) return quote('/' + join(url[1:],'/')) def wiki_url1(self): """return the base url path for the current wiki""" # this code is buried somewhere in cvs return '?' def page_url2(self): """return the url path for the current wiki page""" return quote('/' + self.absolute_url(relative=1)) def wiki_url2(self): """return the base url path for the current wiki""" return quote('/' + self.aq_inner.aq_parent.absolute_url(relative=1)) def page_url3(self): """return the url path for the current wiki page""" return self.REQUEST['BASE1'] + '/' + self.absolute_url(relative=1) def wiki_url3(self): """return the base url path for the current wiki""" return self.REQUEST['BASE1'] + '/' + self.aq_inner.aq_parent.absolute_url(relative=1) def page_url4(self): """return the url path for the current wiki page""" return self.absolute_url(relative=0) def wiki_url4(self): """return the base url path for the current wiki""" return self.aq_inner.aq_parent.absolute_url(relative=0) def page_url5(self): """return the url path for the current wiki page""" return self.absolute_url() def wiki_url5(self): """return the base url path for the current wiki""" return self.aq_inner.aq_parent.absolute_url() def page_url6(self): """return the url path for the current wiki page""" return '/' + self.absolute_url(relative=1) def wiki_url6(self): """return the base url path for the current wiki""" return '/' + self.aq_inner._my_folder().absolute_url(relative=1) def _my_folder(self): """Obtain parent folder, avoiding potential acquisition recursion.""" got = parent = self.aq_parent # Handle bizarred degenerate case - darnit, i've forgotten now where # i've seen this occur! while (got and hasattr(got, 'aq_parent') and got.aq_parent and (got is not got.aq_parent) and (aq_base(got) is aq_base(got.aq_parent))): parent = got got = got.aq_parent return parent def page_url7(self): """return the url path for the current wiki page""" return self.wiki_url() + '/' + quote(self.id()) def wiki_url7(self): """return the base url path for the current wiki""" return self.aq_inner.aq_parent.absolute_url() ###################################################################### # METHOD CATEGORY: backwards compatibility ###################################################################### def doLegacyFixups(self): """upgrade old zwikipage objects on the fly """ # can probably be simplified. # would this be better done offline by a script ? # Note that the objects don't get very far unpickling, some # by-hand adjustment via command-line interaction is necessary # to get them over the transition, sigh. --ken # not sure what this means --SM # Confused about what happens in the zodb when class # definitions change. I now think that all instances in the # zodb conform to the new class shape immediately on # refresh/restart. True ? Not sure what exactly happens to # deleted properties/attributes - discarded ? No I think they # lurk around. # add parents property if (not hasattr(self, 'parents')) or self.parents is None: self.parents = [] # username -> last_editor/last_editor_ip if not hasattr(self, 'last_editor'): self.last_editor = '' if not hasattr(self, 'last_editor_ip'): self.last_editor_ip = '' if (hasattr(self, 'username') and type(self.username) is StringType): if re.match(r'[0-9\.]+',self.username): self.last_editor_ip = self.username else: self.last_editor = self.username delattr(self,'username') self.last_editor_ip = '' self.last_editor = 'UpGrade' get_transaction().commit() # a bit confusing - here's what I believe I'm doing: # when we encounter a zwikipage with a username string # attribute (left over from the old username property), we # copy the info to the appropriate last_editor* property # and delete the old attribute. Once it's gone, the # username method below is revealed and allows old dtml to # work as before - handy. Also, do a commit to update the # page's timestamp immediately, otherwise the append form # will get displayed with an out of date timestamp. # upgrade page_type if not hasattr(self, 'page_type'): self.page_type = DEFAULT_PAGE_TYPE elif self.page_type == 'Structured Text': self.page_type = 'structuredtext' elif self.page_type == 'structuredtext_dtml': self.page_type = 'structuredtextdtml' elif self.page_type[0:4] == 'HTML': self.page_type = 'html' elif self.page_type == 'html_dtml': self.page_type = 'htmldtml' elif self.page_type == 'Classic Wiki': self.page_type = 'classicwiki' elif self.page_type == 'Plain Text': self.page_type = 'plaintext' # API fixups to help keep legacy DTML working def username(self): # backwards compatibility for dtml which tries to display the old # username property (doLegacyFixups may not have been called yet) for p in self._properties: if p['id'] == 'username': return username if hasattr(self,'last_editor') and self.last_editor: return self.last_editor if hasattr(self,'last_editor_ip') and self.last_editor_ip: return self.last_editor_ip return '' # actually the above is useful to keep around as last_editor_or_ip = username last_editor_or_ip__roles__ = None # text() was src() # revert to that name ? or use document_src() ? src = text src__roles__ = None # these were renamed # do these have __roles__=None just to get them published in # the absence of a docstring ? wiki_page_url = page_url wiki_page_url__roles__ = None wiki_base_url = wiki_url wiki_base_url__roles__ = None editTimestamp = timeStamp editTimestamp__roles__ = None checkEditTimeStamp = checkEditConflict checkEditTimeStamp__roles__ = None ###################################################################### # FUNCTION CATEGORY: rendering helper functions ###################################################################### def thunk_substituter(func, text, allowed): """Return a function which takes one arg and passes it with other args to passed-in func. thunk_substituter passes in the value of it's parameter, 'allowed', and a dictionary {'lastend': int, 'inpre': bool, 'intag': bool}. This is for use in a re.sub situation, to get the 'allowed' parameter and the state dict into the callback. (The technical term really is "thunk". Honest.-)""" state = {'lastend': 0, 'inpre': 0, 'incode': 0, 'intag': 0} return lambda arg, func=func, allowed=allowed, text=text, state=state: ( func(arg, allowed, state, text)) def within_literal(upto, after, state, text, rfind=rfind, lower=lower): """Check text from state['lastend'] to upto for literal context: - Within an enclosing '

' preformatted region '
' - Within an enclosing '' code fragment '' - Within a tag '<' body '>' We also update the state dict accordingly.""" # XXX This breaks on badly nested angle brackets and
, etc.
    lastend, inpre, intag = state['lastend'], state['inpre'], state['intag']
    lastend = state['lastend']
    inpre, incode, intag = state['inpre'], state['incode'], state['intag']
    newintag = newincode = newinpre = 0
    text = lower(text)

    # Check whether '
' is currently (possibly, still) prevailing.
    opening = rfind(text, '
', lastend, upto)
    if (opening != -1) or inpre:
        if opening != -1: opening = opening + 4
        else: opening = lastend
        if -1 == rfind(text, '
', opening, upto): newinpre = 1 state['inpre'] = newinpre # Check whether '' is currently (possibly, still) prevailing. opening = rfind(text, '', lastend, upto) if (opening != -1) or incode: if opening != -1: opening = opening + 5 # We must already be incode, start at beginning of this segment: else: opening = lastend if -1 == rfind(text, '', opening, upto): newincode = 1 state['incode'] = newincode # Determine whether we're (possibly, still) within a tag. opening = rfind(text, '<', lastend, upto) if (opening != -1) or intag: # May also be intag - either way, we skip past last : if opening != -1: opening = opening + 1 # We must already be intag, start at beginning of this segment: else: opening = lastend if -1 == rfind(text, '>', opening, upto): newintag = 1 state['intag'] = newintag state['lastend'] = after return newinpre or newincode or newintag # exact reverse of DT_Util.html_quote def html_unquote(v, name='(Unknown name)', md={}, character_entities=( (('&'), '&'), (('<'), '<' ), (('>'), '>' ), (('<'), '\213' ), (('>'), '\233' ), (('"'), '"'))): #" text=str(v) for re,name in character_entities: if find(text, re) >= 0: text=join(split(text,re),name) return text ###################################################################### # FUNCTION CATEGORY: page creation (ZMI) ###################################################################### # ZMI page creation form manage_addZWikiPageForm = HTMLFile('dtml/zwikiPageAdd', globals()) def manage_addZWikiPage(self, id, title='', file='', REQUEST=None, submit=None): """Add a ZWiki Page object with the contents of file. If 'file' is empty, default text is used. """ # refactor with create # don't bother with create's default text and dtml shenanigans right now text = file if type(text) is not StringType: text=text.read() if not text: text = '' # if hasattr(self, 'standard_wiki_page'): # if callable(self.aq_parent.standard_wiki_page): # text = self.standard_wiki_page(self,REQUEST) # else: # text = DocumentTemplate.HTML(self.standard_wiki_page) # else: # text = default_wiki_page ob=ZWikiPage(source_string=text, __name__=id) ob.page_type = DEFAULT_PAGE_TYPE ob.title=title id=self._setObject(id, ob) # 2.2-specific: the new page object is owned by the current # authenticated user, if any; not desirable for executable content. # Remove any such ownership so that the page will acquire it's # owner from the parent folder. ob._deleteOwnershipAfterAdd() #XXX or _owner=UnownableOwner ? # redirect browser if needed if REQUEST is not None: try: u=self.DestinationURL() except: u=REQUEST['URL1'] if submit==" Add and Edit ": u="%s/%s" % (u,quote(id)) REQUEST.RESPONSE.redirect(u+'/manage_main') return '' # ensure that doctest will test all ZWikiPage's private methods # eg _createFileOrImage # but, causes public methods to be tested twice ? # and worse, it's twice within one invocation of setup/teardown ? #__test__ = { "ZWikiPage": ZWikiPage, }