1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - PostMessage Action 4 5 @copyright: 2012 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.PageEditor import PageEditor 10 from MoinMoin.log import getLogger 11 from MoinSupport import * 12 from email.parser import Parser 13 from subprocess import Popen, PIPE 14 15 try: 16 from cStringIO import StringIO 17 except ImportError: 18 from StringIO import StringIO 19 20 Dependencies = ['pages'] 21 22 class PostMessage: 23 24 "A posted message handler." 25 26 def __init__(self, pagename, request): 27 28 """ 29 On the page with the given 'pagename', use the given 'request' when 30 reading posted messages, modifying the Wiki. 31 """ 32 33 self.pagename = pagename 34 self.request = request 35 self.page = Page(request, pagename) 36 37 def do_action(self): 38 request = self.request 39 content_length = getHeader(request, "Content-Length", "HTTP") 40 if content_length: 41 content_length = int(content_length) 42 43 # Get the message. 44 45 self.handle_message(StringIO(request.read(content_length))) 46 47 def handle_message(self, message_text): 48 49 "Handle the given 'message_text'." 50 51 request = self.request 52 message = Parser().parse(message_text) 53 mimetype = message.get_content_type() 54 encoding = message.get_content_charset() 55 56 # Detect PGP/GPG-encoded payloads. 57 # See: http://tools.ietf.org/html/rfc3156 58 59 if mimetype == "multipart/encrypted" and \ 60 message.get_param("protocol") == "application/pgp-encrypted": 61 62 try: 63 declaration, part = message.get_payload() 64 except ValueError: 65 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 66 request.write("There must be a declaration and a content part for signed uploads.") 67 return 68 69 # Verify the message format. 70 71 if part.get_content_type() != "application/octet-stream": 72 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 73 request.write("Encrypted data must be provided as application/octet-stream.") 74 return 75 76 # Locate the keyring. 77 78 homedir = getattr(request.cfg, "postmessage_gpg_homedir") 79 if not homedir: 80 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 81 request.write("Encrypted data cannot currently be understood. Please notify the site administrator.") 82 return 83 84 # Decrypt the message text. 85 86 cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"], 87 stdin=PIPE, stdout=PIPE, stderr=PIPE) 88 89 cmd.stdin.write(part.get_payload()) 90 cmd.stdin.close() 91 92 errors = cmd.stderr.read() 93 if errors: 94 getLogger(__name__).warning(errors) 95 96 # Handle the embedded message. 97 98 try: 99 message_text = cmd.stdout.read() 100 101 # With a zero return code, accept the message. 102 103 if not cmd.wait(): 104 self.handle_plaintext_message(StringIO(message_text)) 105 106 # Otherwise, reject the unverified message. 107 108 else: 109 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 110 request.write("The message could not be verified.") 111 112 finally: 113 cmd.stdout.close() 114 cmd.stderr.close() 115 116 # Reject unsigned payloads. 117 118 else: 119 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 120 request.write("Only PGP/GPG-signed payloads are supported.") 121 122 def handle_plaintext_message(self, message_text): 123 124 "Handle the given 'message_text'." 125 126 request = self.request 127 message = Parser().parse(message_text) 128 129 # Handle a single part. 130 131 if not message.is_multipart(): 132 self.handle_message_parts([message], to_replace(message)) 133 134 # Handle multiple parts. 135 136 # This can be a collection of updates, with each update potentially being a 137 # collection of alternative representations. 138 139 elif is_collection(message): 140 for part in message.get_payload(): 141 if part.is_multipart(): 142 self.handle_message_parts(part.get_payload(), to_replace(part)) 143 else: 144 self.handle_message_parts([part], to_replace(part)) 145 146 # Or it can be a collection of alternative representations for a single 147 # update. 148 149 else: 150 self.handle_message_parts(message.get_payload(), to_replace(message)) 151 152 # Default output. 153 154 writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content") 155 156 def handle_message_parts(self, parts, replace): 157 158 """ 159 Handle the given message 'parts', replacing the page content if 160 'replace' is set to a true value. 161 """ 162 163 # NOTE: Should either choose preferred content types or somehow retain them 164 # NOTE: all but present one at a time. 165 166 body = [] 167 168 for part in parts: 169 mimetype = part.get_content_type() 170 encoding = part.get_content_charset() 171 if mimetype == "text/moin": 172 body.append(part.get_payload()) 173 if replace: 174 break 175 176 if not replace: 177 body.append(self.page.get_raw_body()) 178 179 page_editor = PageEditor(self.request, self.pagename) 180 page_editor.saveText("\n\n".join(body), 0) 181 182 def is_collection(message): 183 return message.get("Update-Type") == "collection" 184 185 def to_replace(message): 186 return message.get("Update-Action") == "replace" 187 188 # Action function. 189 190 def execute(pagename, request): 191 PostMessage(pagename, request).do_action() 192 193 # vim: tabstop=4 expandtab shiftwidth=4