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, is_signed, is_encrypted 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 # Detect PGP/GPG-encoded payloads. 61 # See: http://tools.ietf.org/html/rfc3156 62 63 if is_signed(message): 64 self.handle_signed_message(message) 65 elif is_encrypted(message): 66 self.handle_encrypted_message(message) 67 68 # Reject unsigned and unencrypted payloads. 69 70 else: 71 request = self.request 72 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 73 request.write("Only PGP/GPG-signed payloads are supported.") 74 75 def handle_encrypted_message(self, message): 76 77 "Handle the given encrypted 'message'." 78 79 request = self.request 80 81 homedir = self.get_homedir() 82 if not homedir: 83 return 84 85 gpg = GPG(homedir) 86 87 try: 88 text = gpg.decryptMessage(message) 89 90 # Reject messages without a declaration. 91 92 except MoinMessageMissingPart: 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 # Reject messages without appropriate content. 98 99 except MoinMessageBadContent: 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 # Reject any unencryptable message. 105 106 except MoinMessageError: 107 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 108 request.write("The message could not be decrypted.") 109 return 110 111 # Log non-fatal errors. 112 113 if gpg.errors: 114 getLogger(__name__).warning(gpg.errors) 115 116 # Handle the embedded message which may itself be a signed message. 117 118 self.handle_message_text(text) 119 120 def handle_signed_message(self, message): 121 122 "Handle the given signed 'message'." 123 124 request = self.request 125 126 homedir = self.get_homedir() 127 if not homedir: 128 return 129 130 gpg = GPG(homedir) 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 fingerprint, identity, content = gpg.verifyMessage(message) 139 140 # Reject messages without a declaration. 141 142 except MoinMessageMissingPart: 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 # Reject messages without appropriate content. 148 149 except MoinMessageBadContent: 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 # Reject any unverified message. 155 156 except MoinMessageError: 157 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 158 request.write("The message could not be verified.") 159 return 160 161 # Log non-fatal errors. 162 163 if gpg.errors: 164 getLogger(__name__).warning(gpg.errors) 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 # Handle the embedded message. 186 187 self.handle_message_content(content) 188 189 # Restore any user identity. 190 191 finally: 192 if old_user: 193 request.user = old_user 194 195 def handle_message_content(self, content): 196 197 "Handle the given message 'content'." 198 199 request = self.request 200 201 # Interpret the content as one or more updates. 202 203 message = Message() 204 message.handle_message(content) 205 206 # Test any date against the page or message store. 207 208 if message.date: 209 store_date = time.gmtime(self.store.mtime()) 210 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 211 last_date = max(store_date, page_date) 212 213 # Reject messages older than the page date. 214 215 if message.date < last_date: 216 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 217 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 218 return 219 220 # Reject messages without dates if so configured. 221 222 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 223 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 224 request.write("The message has no date information.") 225 return 226 227 # Handle the message as an object. 228 229 self.handle_message_object(message) 230 231 def get_homedir(self): 232 233 "Locate the GPG home directory." 234 235 homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir") 236 if not homedir: 237 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 238 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 239 return homedir 240 241 # vim: tabstop=4 expandtab shiftwidth=4