1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinMessageSupport library 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.log import getLogger 11 from MoinMoin.user import User 12 from MoinMoin import wikiutil 13 from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders 14 from MoinMessage import GPG, Message, MoinMessageError 15 from email.parser import Parser 16 import time 17 18 try: 19 from cStringIO import StringIO 20 except ImportError: 21 from StringIO import StringIO 22 23 Dependencies = ['pages'] 24 25 class MoinMessageAction: 26 27 "Common message handling support for actions." 28 29 def __init__(self, pagename, request): 30 31 """ 32 On the page with the given 'pagename', use the given 'request' when 33 reading posted messages, modifying the Wiki. 34 """ 35 36 self.pagename = pagename 37 self.request = request 38 self.page = Page(request, pagename) 39 self.store = ItemStore(self.page, "messages", "message-locks") 40 41 def do_action(self): 42 request = self.request 43 content_length = getHeader(request, "Content-Length", "HTTP") 44 if content_length: 45 content_length = int(content_length) 46 47 self.handle_message_text(request.read(content_length)) 48 49 def handle_message_text(self, message_text): 50 51 "Handle the given 'message_text'." 52 53 message = Parser().parse(StringIO(message_text)) 54 self.handle_message(message) 55 56 def handle_message(self, message): 57 58 "Handle the given 'message'." 59 60 request = self.request 61 mimetype = message.get_content_type() 62 encoding = message.get_content_charset() 63 64 # Detect PGP/GPG-encoded payloads. 65 # See: http://tools.ietf.org/html/rfc3156 66 67 if mimetype == "multipart/signed" and \ 68 message.get_param("protocol") == "application/pgp-signature": 69 70 self.handle_signed_message(message) 71 72 elif mimetype == "multipart/encrypted" and \ 73 message.get_param("protocol") == "application/pgp-encrypted": 74 75 self.handle_encrypted_message(message) 76 77 # Reject unsigned payloads. 78 79 else: 80 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 81 request.write("Only PGP/GPG-signed payloads are supported.") 82 83 def handle_encrypted_message(self, message): 84 85 "Handle the given encrypted 'message'." 86 87 request = self.request 88 89 try: 90 declaration, content = message.get_payload() 91 except ValueError: 92 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 93 request.write("There must be a declaration and a content part for encrypted uploads.") 94 return 95 96 # Verify the message format. 97 98 if content.get_content_type() != "application/octet-stream": 99 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 100 request.write("Encrypted data must be provided as application/octet-stream.") 101 return 102 103 homedir = self.get_homedir() 104 if not homedir: 105 return 106 107 gpg = GPG(homedir) 108 109 # Get the decrypted message text. 110 111 try: 112 text = gpg.decryptMessage(content.get_payload()) 113 114 # Log non-fatal errors. 115 116 if gpg.errors: 117 getLogger(__name__).warning(gpg.errors) 118 119 # Handle the embedded message. 120 121 self.handle_message_text(text) 122 123 # Otherwise, reject the unverified message. 124 125 except MoinMessageError: 126 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 127 request.write("The message could not be decrypted.") 128 129 def handle_signed_message(self, message): 130 131 "Handle the given signed 'message'." 132 133 request = self.request 134 135 # NOTE: RFC 3156 states that signed messages should employ a detached 136 # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures 137 # NOTE: instead of "BEGIN PGP SIGNATURE". 138 # NOTE: The "micalg" parameter is currently not supported. 139 140 try: 141 content, signature = message.get_payload() 142 except ValueError: 143 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 144 request.write("There must be a content part and a signature for signed uploads.") 145 return 146 147 # Verify the message format. 148 149 if signature.get_content_type() != "application/pgp-signature": 150 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 151 request.write("Signature data must be provided in the second part as application/pgp-signature.") 152 return 153 154 homedir = self.get_homedir() 155 if not homedir: 156 return 157 158 gpg = GPG(homedir) 159 160 # Verify the message. 161 162 try: 163 fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string()) 164 165 # Map the fingerprint to a Wiki user. 166 167 old_user = None 168 request = self.request 169 170 try: 171 if fingerprint: 172 gpg_users = getWikiDict( 173 getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), 174 request 175 ) 176 177 # With a user mapping and a fingerprint corresponding to a known 178 # user, temporarily switch user in order to make the edit. 179 180 if gpg_users and gpg_users.has_key(fingerprint): 181 old_user = request.user 182 request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint]) 183 184 # Log non-fatal errors. 185 186 if gpg.errors: 187 getLogger(__name__).warning(gpg.errors) 188 189 # Handle the embedded message. 190 191 self.handle_message_content(content) 192 193 # Restore any user identity. 194 195 finally: 196 if old_user: 197 request.user = old_user 198 199 # Otherwise, reject the unverified message. 200 201 except MoinMessageError: 202 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 203 request.write("The message could not be verified.") 204 205 def handle_message_content(self, content): 206 207 "Handle the given message 'content'." 208 209 request = self.request 210 211 # Interpret the content as one or more updates. 212 213 message = Message() 214 message.handle_message(content) 215 216 # Test any date against the page or message store. 217 218 if message.date: 219 store_date = time.gmtime(self.store.mtime()) 220 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 221 last_date = max(store_date, page_date) 222 223 # Reject messages older than the page date. 224 225 if message.date < last_date: 226 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 227 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 228 return 229 230 # Reject messages without dates if so configured. 231 232 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 233 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 234 request.write("The message has no date information.") 235 return 236 237 # Handle the message as an object. 238 239 self.handle_message_object(message) 240 241 def get_homedir(self): 242 243 "Locate the GPG home directory." 244 245 homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir") 246 if not homedir: 247 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 248 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 249 return homedir 250 251 # vim: tabstop=4 expandtab shiftwidth=4