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 MoinMessage import GPG, MoinMessageError 13 from email.parser import Parser 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 self.handle_message_text(request.read(content_length)) 44 45 def handle_message_text(self, message_text): 46 47 "Handle the given 'message_text'." 48 49 message = Parser().parse(StringIO(message_text)) 50 self.handle_message(message) 51 52 def handle_message(self, message): 53 54 "Handle the given 'message'." 55 56 request = self.request 57 mimetype = message.get_content_type() 58 encoding = message.get_content_charset() 59 60 # Detect PGP/GPG-encoded payloads. 61 # See: http://tools.ietf.org/html/rfc3156 62 63 if mimetype == "multipart/signed" and \ 64 message.get_param("protocol") == "application/pgp-signature": 65 66 self.handle_signed_message(message) 67 68 elif mimetype == "multipart/encrypted" and \ 69 message.get_param("protocol") == "application/pgp-encrypted": 70 71 self.handle_encrypted_message(message) 72 73 # Reject unsigned payloads. 74 75 else: 76 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 77 request.write("Only PGP/GPG-signed payloads are supported.") 78 79 def handle_encrypted_message(self, message): 80 81 "Handle the given encrypted 'message'." 82 83 request = self.request 84 85 try: 86 declaration, content = message.get_payload() 87 except ValueError: 88 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 89 request.write("There must be a declaration and a content part for encrypted uploads.") 90 return 91 92 # Verify the message format. 93 94 if content.get_content_type() != "application/octet-stream": 95 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 96 request.write("Encrypted data must be provided as application/octet-stream.") 97 return 98 99 homedir = self.get_homedir() 100 if not homedir: 101 return 102 103 gpg = GPG(homedir) 104 105 # Get the decrypted message text. 106 107 try: 108 text = gpg.decryptMessage(content.get_payload()) 109 110 # Log non-fatal errors. 111 112 if gpg.errors: 113 getLogger(__name__).warning(gpg.errors) 114 115 # Handle the embedded message. 116 117 self.handle_message_text(text) 118 119 # Otherwise, reject the unverified message. 120 121 except MoinMessageError: 122 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 123 request.write("The message could not be decrypted.") 124 125 def handle_signed_message(self, message): 126 127 "Handle the given signed 'message'." 128 129 request = self.request 130 131 # NOTE: RFC 3156 states that signed messages should employ a detached 132 # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures 133 # NOTE: instead of "BEGIN PGP SIGNATURE". 134 # NOTE: The "micalg" parameter is currently not supported. 135 136 try: 137 content, signature = message.get_payload() 138 except ValueError: 139 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 140 request.write("There must be a content part and a signature for signed uploads.") 141 return 142 143 # Verify the message format. 144 145 if signature.get_content_type() != "application/pgp-signature": 146 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 147 request.write("Signature data must be provided in the second part as application/pgp-signature.") 148 return 149 150 homedir = self.get_homedir() 151 if not homedir: 152 return 153 154 gpg = GPG(homedir) 155 156 # Verify the message. 157 158 try: 159 gpg.verifyMessage(signature.get_payload(), content.as_string()) 160 161 # Log non-fatal errors. 162 163 if gpg.errors: 164 getLogger(__name__).warning(gpg.errors) 165 166 # Handle the embedded message. 167 168 self.handle_message_content(content) 169 170 # Otherwise, reject the unverified message. 171 172 except MoinMessageError: 173 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 174 request.write("The message could not be verified.") 175 176 def handle_message_content(self, message): 177 178 "Handle the given 'message'." 179 180 request = self.request 181 182 # Handle a single part. 183 184 if not message.is_multipart(): 185 self.handle_message_parts([message], to_replace(message)) 186 187 # Handle multiple parts. 188 189 # This can be a collection of updates, with each update potentially being a 190 # collection of alternative representations. 191 192 elif is_collection(message): 193 for part in message.get_payload(): 194 if part.is_multipart(): 195 self.handle_message_parts(part.get_payload(), to_replace(part)) 196 else: 197 self.handle_message_parts([part], to_replace(part)) 198 199 # Or it can be a collection of alternative representations for a single 200 # update. 201 202 else: 203 self.handle_message_parts(message.get_payload(), to_replace(message)) 204 205 # Default output. 206 207 writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content") 208 209 def handle_message_parts(self, parts, replace): 210 211 """ 212 Handle the given message 'parts', replacing the page content if 213 'replace' is set to a true value. 214 """ 215 216 # NOTE: Should either choose preferred content types or somehow retain them 217 # NOTE: all but present one at a time. 218 219 body = [] 220 221 for part in parts: 222 mimetype = part.get_content_type() 223 encoding = part.get_content_charset() 224 if mimetype == "text/moin": 225 body.append(part.get_payload()) 226 if replace: 227 break 228 229 if not replace: 230 body.append(self.page.get_raw_body()) 231 232 page_editor = PageEditor(self.request, self.pagename) 233 page_editor.saveText("\n\n".join(body), 0) 234 235 def get_homedir(self): 236 237 "Locate the GPG home directory." 238 239 homedir = getattr(self.request.cfg, "postmessage_gpg_homedir") 240 if not homedir: 241 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 242 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 243 return homedir 244 245 def is_collection(message): 246 return message.get("Update-Type") == "collection" 247 248 def to_replace(message): 249 return message.get("Update-Action") == "replace" 250 251 # Action function. 252 253 def execute(pagename, request): 254 PostMessage(pagename, request).do_action() 255 256 # vim: tabstop=4 expandtab shiftwidth=4