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