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