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