3 Copyright (c) 2002-2006 Gustavo Niemeyer <gustavo@niemeyer.net>
5 This program allows you to edit moin (see http://moin.sourceforge.net)
6 pages with your preferred editor. The default editor is vi. If you want
7 to use any other, just set the EDITOR environment variable.
9 To define your moin id used when logging in in a specifc moin, edit a
10 file named ~/.moin_ids and include lines like "http://moin.url/etc myid".
12 WARNING: This program expects information to be in a very specific
13 format. It will break if this format changes, so there are
14 no warranties of working at all. All I can say is that it
15 worked for me, at least once. ;-)
17 Tested moin versions: 0.9, 0.11, 1.0, 1.1, 1.3.5, 1.5, 1.5.1, 1.5.4, 1.5.5, 1.6
20 __author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
33 USAGE = "Usage: editmoin [-t <template page>] <moin page URL>\n"
35 IDFILENAME = os.path.expanduser("~/.moin_ids")
36 ALIASFILENAME = os.path.expanduser("~/.moin_aliases")
39 BODYRE = re.compile('<textarea.*?name="savetext".*?>(.*)</textarea>',
41 DATESTAMPRE = re.compile('<input.*?name="datestamp".*?value="(.*?)".*?>')
42 NOTIFYRE = re.compile('<input.*?name="notify".*?value="(.*?)".*?>')
43 COMMENTRE = re.compile('<input.*?name="comment".*>')
44 MESSAGERE1 = re.compile('^</table>(.*?)<a.*?>Clear message</a>',
46 MESSAGERE2 = re.compile('<div class="message">(.*?)</div>', re.M|re.DOTALL)
47 MESSAGERE3 = re.compile('<div id="message">\s*<p>(.*?)</p>', re.M|re.DOTALL)
48 STATUSRE = re.compile('<p class="status">(.*?)</p>', re.M|re.DOTALL)
49 CANCELRE = re.compile('<input.*?type="submit" name="button_cancel" value="(.*?)">')
50 EDITORRE = re.compile('<input.*?type="hidden" name="editor" value="text">')
51 TICKETRE = re.compile('<input.*?type="hidden" name="ticket" value="(.*?)">')
52 REVRE = re.compile('<input.*?type="hidden" name="rev" value="(.*?)">')
53 CATEGORYRE = re.compile('<option value="(Category\w+?)">')
54 SELECTIONRE = re.compile("\(([^)]*)\)\s*([^(]*)")
55 EXTENDMSG = "Use the Preview button to extend the locking period."
61 class Error(Exception): pass
65 multi_selection = ["notify", "add_category"]
66 def __init__(self, filename, id):
67 self.filename = filename
69 self.data = open(filename).read()
70 self.body = self._get_data(BODYRE, "body")
73 self.datestamp = self._get_data(DATESTAMPRE, "datestamp")
78 self.notify = self._get_data(NOTIFYRE, "notify")
82 if COMMENTRE.search(self.data):
87 self.categories = self._get_data_findall(CATEGORYRE, "category", [])
88 self.add_category = None
90 match = STATUSRE.search(self.data)
92 self.status = strip_html(match.group(1))
96 self.rev = self._get_data(REVRE, "rev", None)
97 self.ticket = self._get_data(TICKETRE, "ticket", None)
99 def _get_data(self, pattern, info, default=marker):
100 match = pattern.search(self.data)
102 if default is not marker:
104 message = get_message(self.data)
107 raise Error, info+" information not found"
109 return match.group(1)
111 def _get_data_findall(self, pattern, info, default=marker):
112 groups = pattern.findall(self.data)
114 if default is not marker:
116 raise Error, info+" information not found"
119 def _get_selection(self, str):
120 for selected, option in SELECTIONRE.findall(str):
122 return option.strip()
125 def _unescape(self, data):
126 data = data.replace("<", "<")
127 data = data.replace(">", ">")
128 data = data.replace("&", "&")
131 def has_cancel(self):
132 return (CANCELRE.search(self.data) is not None)
134 def has_editor(self):
135 return (EDITORRE.search(self.data) is not None)
138 filename = tempfile.mktemp(".moin")
139 file = open(filename, "w")
141 file.write("@@ WARNING! You're NOT logged in!\n")
143 file.write("@@ Using ID %s.\n" % self.id)
144 if self.status is not None:
145 text = self.status.replace(EXTENDMSG, "").strip()
146 lines = textwrap.wrap(text, 70,
147 initial_indent="@@ Message: ",
148 subsequent_indent="@ ")
150 file.write(line+"\n")
151 if self.comment is not None:
152 file.write("@@ Comment: %s\n" % self.comment)
153 if self.notify is not None:
154 yes, no = (self.notify and ("x", " ") or (" ", "x"))
155 file.write("@@ Notify: (%s) Yes (%s) No\n" % (yes, no))
157 file.write("@@ Add category: (x) None\n")
158 for category in self.categories:
159 file.write("@ ( ) %s\n" % category)
160 file.write(self._unescape(self.body))
164 def read_raw(self, filename):
165 file = open(filename)
167 data = file.readline()
174 lines.append(data[2:].strip())
177 lines[-1] += data[2:].strip()
178 data = file.readline()
179 self.body = data+file.read()
184 attr = line[:sep].lower().replace(' ', '_')
185 value = line[sep+1:].strip()
186 if attr in self.multi_selection:
187 setattr(self, attr, self._get_selection(value))
189 setattr(self, attr, value)
191 def get_message(data):
192 match = MESSAGERE3.search(data)
194 # Check for moin < 1.3.5 (not sure the precise version it changed).
195 match = MESSAGERE2.search(data)
197 # Check for moin <= 0.9.
198 match = MESSAGERE1.search(data)
200 return strip_html(match.group(1))
203 def strip_html(data):
204 data = re.subn("\n", " ", data)[0]
205 data = re.subn("<p>|<br>", "\n", data)[0]
206 data = re.subn("<.*?>", "", data)[0]
207 data = re.subn("Clear data", "", data)[0]
208 data = re.subn("[ \t]+", " ", data)[0]
213 if os.path.isfile(IDFILENAME):
214 file = open(IDFILENAME)
215 for line in file.readlines():
217 if line and line[0] != "#":
218 tokens = line.split()
222 url, id = tokens[0], None
223 if moinurl.startswith(url):
227 def translate_shortcut(moinurl):
231 shortcut, pathinfo = moinurl.split("/", 1)
233 shortcut, pathinfo = moinurl, ""
234 if os.path.isfile(ALIASFILENAME):
235 file = open(ALIASFILENAME)
237 for line in file.readlines():
239 if line and line[0] != "#":
240 alias, value = line.split(None, 1)
242 value = "%s/%s" % (value, pathinfo)
243 if shortcut == alias:
247 shortcut, pathinfo = value.split("/", 1)
249 shortcut, pathinfo = value, ""
252 if os.path.isfile(IDFILENAME):
253 file = open(IDFILENAME)
255 for line in file.readlines():
257 if line and line[0] != "#":
258 url = line.split()[0]
261 return "%s/%s" % (url, pathinfo)
266 raise Error, "no suitable url found for shortcut '%s'" % shortcut
269 def get_urlopener(moinurl, id=None):
270 urlopener = urllib.URLopener()
271 proxy = os.environ.get("http_proxy")
273 urlopener.proxies.update({"http": proxy})
276 urlopener.addheader("Cookie", "MOIN_ID=\"%s\"" % id)
278 urlopener.addheader("Cookie", "MOIN_SESSION=\"%s\"" % id)
281 def fetchfile(urlopener, url, id, template):
282 geturl = url+"?action=edit"
284 geturl += "&template=" + urllib.quote(template)
285 filename, headers = urlopener.retrieve(geturl)
286 return MoinFile(filename, id)
288 def editfile(moinfile):
290 filename = moinfile.write_raw()
291 editor = os.environ.get("EDITOR", "vi")
292 digest = md5.md5(open(filename).read()).digest()
293 os.system("%s %s" % (editor, filename))
294 if digest != md5.md5(open(filename).read()).digest():
295 shutil.copyfile(filename, os.path.expanduser("~/.moin_lastedit"))
297 moinfile.read_raw(filename)
301 def sendfile(urlopener, url, moinfile):
302 if moinfile.comment is not None:
303 comment = "&comment="
304 if moinfile.comment.lower() != "none":
305 comment += urllib.quote(moinfile.comment)
308 data = "button_save=1&savetext=%s%s" \
309 % (urllib.quote(moinfile.body), comment)
310 if moinfile.has_editor():
311 data += "&action=edit" # Moin >= 1.5
313 data += "&action=savepage" # Moin < 1.5
314 if moinfile.datestamp:
315 data += "&datestamp=" + moinfile.datestamp
317 data += "&rev=" + moinfile.rev
319 data += "&ticket=" + moinfile.ticket
320 if moinfile.notify == "Yes":
322 if moinfile.add_category and moinfile.add_category != "None":
323 data += "&category=" + urllib.quote(moinfile.add_category)
324 url = urlopener.open(url, data)
327 message = get_message(answer)
330 raise Error, "data submitted, but message information not found"
334 def sendcancel(urlopener, url, moinfile):
335 if not moinfile.has_cancel():
337 data = "button_cancel=Cancel"
338 if moinfile.has_editor():
339 data += "&action=edit&savetext=dummy" # Moin >= 1.5
341 data += "&action=savepage" # Moin < 1.5
342 if moinfile.datestamp:
343 data += "&datestamp=" + moinfile.datestamp
345 data += "&rev=" + moinfile.rev
347 data += "&ticket=" + moinfile.ticket
348 url = urlopener.open(url, data)
351 message = get_message(answer)
354 raise Error, "cancel submitted, but message information not found"
361 if len(argv) > 2 and argv[0] == "-t":
364 if len(argv) != 1 or argv[0] in ("-h", "--help"):
365 sys.stderr.write(USAGE)
368 url = translate_shortcut(argv[0])
370 urlopener = get_urlopener(url, id)
371 moinfile = fetchfile(urlopener, url, id, template)
373 if editfile(moinfile):
374 sendfile(urlopener, url, moinfile)
376 sendcancel(urlopener, url, moinfile)
378 os.unlink(moinfile.filename)
379 except (IOError, OSError, Error), e:
380 sys.stderr.write("error: %s\n" % str(e))
383 if __name__ == "__main__":