]> piware.de Git - bin.git/blob - editmoin
remove bzrdc, use debcommit now
[bin.git] / editmoin
1 #!/usr/bin/env python
2 """
3 Copyright (c) 2002-2006  Gustavo Niemeyer <gustavo@niemeyer.net>
4
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.
8
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".
11
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. ;-)
16
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
18 """
19
20 __author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
21 __version__ = "1.8"
22 __license__ = "GPL"
23
24 import tempfile
25 import textwrap
26 import sys, os
27 import urllib
28 import shutil
29 import md5
30 import re
31
32
33 USAGE = "Usage: editmoin [-t <template page>] <moin page URL>\n"
34
35 IDFILENAME = os.path.expanduser("~/.moin_ids")
36 ALIASFILENAME = os.path.expanduser("~/.moin_aliases")
37
38
39 BODYRE = re.compile('<textarea.*?name="savetext".*?>(.*)</textarea>',
40                     re.M|re.DOTALL)
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>',
45                         re.M|re.DOTALL)
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."
56
57
58 marker = object()
59
60
61 class Error(Exception): pass
62
63
64 class MoinFile:
65     multi_selection = ["notify", "add_category"]
66     def __init__(self, filename, id):
67         self.filename = filename
68         self.id = id
69         self.data = open(filename).read()
70         self.body = self._get_data(BODYRE, "body")
71
72         try:
73             self.datestamp = self._get_data(DATESTAMPRE, "datestamp")
74         except Error:
75             self.datestamp = None
76
77         try:
78             self.notify = self._get_data(NOTIFYRE, "notify")
79             self.comment = "None"
80         except Error:
81             self.notify = None
82             if COMMENTRE.search(self.data):
83                 self.comment = "None"
84             else:
85                 self.comment = None
86
87         self.categories = self._get_data_findall(CATEGORYRE, "category", [])
88         self.add_category = None
89
90         match = STATUSRE.search(self.data)
91         if match:
92             self.status = strip_html(match.group(1))
93         else:
94             self.status = None
95
96         self.rev = self._get_data(REVRE, "rev", None)
97         self.ticket = self._get_data(TICKETRE, "ticket", None)
98
99     def _get_data(self, pattern, info, default=marker):
100         match = pattern.search(self.data)
101         if not match:
102             if default is not marker:
103                 return default
104             message = get_message(self.data)
105             if message:
106                 print message
107             raise Error, info+" information not found"
108         else:
109             return match.group(1)
110
111     def _get_data_findall(self, pattern, info, default=marker):
112         groups = pattern.findall(self.data)
113         if not groups:
114             if default is not marker:
115                 return default
116             raise Error, info+" information not found"
117         return groups
118
119     def _get_selection(self, str):
120         for selected, option in SELECTIONRE.findall(str):
121             if selected.strip():
122                 return option.strip()
123         return None
124
125     def _unescape(self, data):
126         data = data.replace("&lt;", "<")
127         data = data.replace("&gt;", ">")
128         data = data.replace("&amp;", "&")
129         return data
130
131     def has_cancel(self):
132         return (CANCELRE.search(self.data) is not None)
133
134     def has_editor(self):
135         return (EDITORRE.search(self.data) is not None)
136
137     def write_raw(self):
138         filename = tempfile.mktemp(".moin")
139         file = open(filename, "w")
140         if not self.id:
141             file.write("@@ WARNING! You're NOT logged in!\n")
142         else:
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="@           ")
149             for line in lines:
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))
156         if self.categories:
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))
161         file.close()
162         return filename
163
164     def read_raw(self, filename):
165         file = open(filename)
166         lines = []
167         data = file.readline()
168         while data != "\n":
169             if data[0] != "@":
170                 break
171             if len(data) < 2:
172                 pass
173             elif data[1] == "@":
174                 lines.append(data[2:].strip())
175             else:
176                 lines[-1] += " "
177                 lines[-1] += data[2:].strip()
178             data = file.readline()
179         self.body = data+file.read()
180         file.close()
181         for line in lines:
182             sep = line.find(":")   
183             if sep != -1:
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))
188                 else:
189                     setattr(self, attr, value)
190  
191 def get_message(data):
192     match = MESSAGERE3.search(data)
193     if not match:
194         # Check for moin < 1.3.5 (not sure the precise version it changed).
195         match = MESSAGERE2.search(data)
196     if not match:
197         # Check for moin <= 0.9.
198         match = MESSAGERE1.search(data)
199     if match:
200         return strip_html(match.group(1))
201     return None 
202
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]
209     data = data.strip()
210     return data
211
212 def get_id(moinurl):
213     if os.path.isfile(IDFILENAME):
214         file = open(IDFILENAME)
215         for line in file.readlines():
216             line = line.strip()
217             if line and line[0] != "#":
218                 tokens = line.split()
219                 if len(tokens) > 1:
220                     url, id = tokens[:2]
221                 else:
222                     url, id = tokens[0], None
223                 if moinurl.startswith(url):
224                     return id
225     return None
226
227 def translate_shortcut(moinurl):
228     if "://" in moinurl:
229         return moinurl
230     if "/" in moinurl:
231         shortcut, pathinfo = moinurl.split("/", 1)
232     else:
233         shortcut, pathinfo = moinurl, ""
234     if os.path.isfile(ALIASFILENAME):
235         file = open(ALIASFILENAME)
236         try:
237             for line in file.readlines():
238                 line = line.strip()
239                 if line and line[0] != "#":
240                     alias, value = line.split(None, 1)
241                     if pathinfo:
242                         value = "%s/%s" % (value, pathinfo)
243                     if shortcut == alias:
244                         if "://" in value:
245                             return value
246                         if "/" in value:
247                             shortcut, pathinfo = value.split("/", 1)
248                         else:
249                             shortcut, pathinfo = value, ""
250         finally:
251             file.close()
252     if os.path.isfile(IDFILENAME):
253         file = open(IDFILENAME)
254         try:
255             for line in file.readlines():
256                 line = line.strip()
257                 if line and line[0] != "#":
258                     url = line.split()[0]
259                     if shortcut in url:
260                         if pathinfo:
261                             return "%s/%s" % (url, pathinfo)
262                         else:
263                             return url
264         finally:
265             file.close()
266     raise Error, "no suitable url found for shortcut '%s'" % shortcut
267
268
269 def get_urlopener(moinurl, id=None):
270     urlopener = urllib.URLopener()
271     proxy = os.environ.get("http_proxy")
272     if proxy:
273         urlopener.proxies.update({"http": proxy})
274     if id:
275         # moinmoin < 1.6
276         urlopener.addheader("Cookie", "MOIN_ID=\"%s\"" % id)
277         # moinmoin >= 1.6
278         urlopener.addheader("Cookie", "MOIN_SESSION=\"%s\"" % id)
279     return urlopener
280
281 def fetchfile(urlopener, url, id, template):
282     geturl = url+"?action=edit"
283     if template:
284         geturl += "&template=" + urllib.quote(template)
285     filename, headers = urlopener.retrieve(geturl)
286     return MoinFile(filename, id)
287
288 def editfile(moinfile):
289     edited = 0
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"))
296         edited = 1
297         moinfile.read_raw(filename)
298     os.unlink(filename)
299     return edited
300
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)
306     else:
307         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
312     else:
313         data += "&action=savepage"  # Moin < 1.5
314     if moinfile.datestamp:
315         data += "&datestamp=" + moinfile.datestamp
316     if moinfile.rev:
317         data += "&rev=" + moinfile.rev
318     if moinfile.ticket:
319         data += "&ticket=" + moinfile.ticket
320     if moinfile.notify == "Yes":
321         data += "&notify=1"
322     if moinfile.add_category and moinfile.add_category != "None":
323         data += "&category=" + urllib.quote(moinfile.add_category)
324     url = urlopener.open(url, data)
325     answer = url.read()
326     url.close()
327     message = get_message(answer)
328     if message is None:
329         print answer
330         raise Error, "data submitted, but message information not found"
331     else:
332         print message
333
334 def sendcancel(urlopener, url, moinfile):
335     if not moinfile.has_cancel():
336         return
337     data = "button_cancel=Cancel"
338     if moinfile.has_editor():
339         data += "&action=edit&savetext=dummy"  # Moin >= 1.5
340     else:
341         data += "&action=savepage"             # Moin < 1.5
342     if moinfile.datestamp:
343         data += "&datestamp=" + moinfile.datestamp
344     if moinfile.rev:
345         data += "&rev=" + moinfile.rev
346     if moinfile.ticket:
347         data += "&ticket=" + moinfile.ticket
348     url = urlopener.open(url, data)
349     answer = url.read()
350     url.close()
351     message = get_message(answer)
352     if not message:
353         print answer
354         raise Error, "cancel submitted, but message information not found"
355     else:
356         print message
357
358 def main():
359     argv = sys.argv[1:]
360     template = None
361     if len(argv) > 2 and argv[0] == "-t":
362         template = argv[1]
363         argv = argv[2:]
364     if len(argv) != 1 or argv[0] in ("-h", "--help"):
365         sys.stderr.write(USAGE)
366         sys.exit(1)
367     try:
368         url = translate_shortcut(argv[0])
369         id = get_id(url)
370         urlopener = get_urlopener(url, id)
371         moinfile = fetchfile(urlopener, url, id, template)
372         try:
373             if editfile(moinfile):
374                 sendfile(urlopener, url, moinfile)
375             else:
376                 sendcancel(urlopener, url, moinfile)
377         finally:
378             os.unlink(moinfile.filename)
379     except (IOError, OSError, Error), e:
380         sys.stderr.write("error: %s\n" % str(e))
381         sys.exit(1)
382
383 if __name__ == "__main__":
384     main()
385
386 # vim:et:ts=4:sw=4