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, \ 14 writeHeaders 15 from MoinMessage import GPG, Message, MoinMessageError, \ 16 MoinMessageMissingPart, MoinMessageBadContent, \ 17 is_signed, is_encrypted, getContentAndSignature 18 from email.parser import Parser 19 import time 20 21 try: 22 from cStringIO import StringIO 23 except ImportError: 24 from StringIO import StringIO 25 26 Dependencies = ['pages'] 27 28 class MoinMessageAction: 29 30 "Common message handling support for actions." 31 32 def __init__(self, pagename, request): 33 34 """ 35 On the page with the given 'pagename', use the given 'request' when 36 reading posted messages, modifying the Wiki. 37 """ 38 39 self.pagename = pagename 40 self.request = request 41 self.page = Page(request, pagename) 42 self.store = ItemStore(self.page, "messages", "message-locks") 43 44 def do_action(self): 45 request = self.request 46 content_length = getHeader(request, "Content-Length", "HTTP") 47 if content_length: 48 content_length = int(content_length) 49 50 self.handle_message_text(request.read(content_length)) 51 52 def handle_message_text(self, message_text): 53 54 "Handle the given 'message_text'." 55 56 message = Parser().parse(StringIO(message_text)) 57 self.handle_message(message) 58 59 def handle_message(self, message): 60 61 "Handle the given 'message'." 62 63 # Detect PGP/GPG-encoded payloads. 64 # See: http://tools.ietf.org/html/rfc3156 65 66 if is_signed(message): 67 self.handle_signed_message(message) 68 elif is_encrypted(message): 69 self.handle_encrypted_message(message) 70 71 # Reject unsigned and unencrypted payloads. 72 73 else: 74 request = self.request 75 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 76 request.write("Only PGP/GPG-signed payloads are supported.") 77 78 def handle_encrypted_message(self, message): 79 80 "Handle the given encrypted 'message'." 81 82 request = self.request 83 84 homedir = self.get_homedir() 85 if not homedir: 86 return 87 88 gpg = GPG(homedir) 89 90 try: 91 text = gpg.decryptMessage(message) 92 93 # Reject messages without a declaration. 94 95 except MoinMessageMissingPart: 96 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 97 request.write("There must be a declaration and a content part for encrypted uploads.") 98 return 99 100 # Reject messages without appropriate content. 101 102 except MoinMessageBadContent: 103 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 104 request.write("Encrypted data must be provided as application/octet-stream.") 105 return 106 107 # Reject any unencryptable message. 108 109 except MoinMessageError: 110 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 111 request.write("The message could not be decrypted.") 112 return 113 114 # Log non-fatal errors. 115 116 if gpg.errors: 117 getLogger(__name__).warning(gpg.errors) 118 119 # Handle the embedded message which may itself be a signed message. 120 121 self.handle_message_text(text) 122 123 def handle_signed_message(self, message): 124 125 "Handle the given signed 'message'." 126 127 request = self.request 128 129 # Accept any message whose sender was authenticated by the PGP method. 130 131 if request.user and request.user.valid and request.user.auth_method == "pgp": 132 133 # Handle the embedded message. 134 135 content, signature = getContentAndSignature(message) 136 self.handle_message_content(content) 137 138 # Reject any unverified message. 139 140 else: 141 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 142 request.write("The message could not be verified. " 143 "Maybe this site is not performing authentication using PGP signatures.") 144 145 def handle_message_content(self, content): 146 147 "Handle the given message 'content'." 148 149 request = self.request 150 151 # Interpret the content as one or more updates. 152 153 message = Message() 154 message.handle_message(content) 155 156 # Test any date against the page or message store. 157 158 if message.date: 159 store_date = time.gmtime(self.store.mtime()) 160 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 161 last_date = max(store_date, page_date) 162 163 # Reject messages older than the page date. 164 165 if message.date < last_date: 166 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 167 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 168 return 169 170 # Reject messages without dates if so configured. 171 172 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 173 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 174 request.write("The message has no date information.") 175 return 176 177 # Handle the message as an object. 178 179 self.handle_message_object(message) 180 181 def get_homedir(self): 182 183 "Locate the GPG home directory." 184 185 request = self.request 186 homedir = get_homedir(self.request) 187 188 if not homedir: 189 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 190 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 191 192 return homedir 193 194 def get_homedir(request): 195 196 "Locate the GPG home directory." 197 198 return getattr(request.cfg, "moinmessage_gpg_homedir") 199 200 def get_signing_users(request): 201 202 "Return a dictionary mapping usernames to signing keys." 203 204 return getWikiDict( 205 getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 206 request) 207 208 def get_recipients(request): 209 210 """ 211 Return the recipients dictionary by first obtaining the page in which it 212 is stored. This page may either be a subpage of the user's home page, if 213 stored on this wiki, or it may be relative to the site root. 214 215 The name of the subpage is defined by the configuration setting 216 'moinmessage_gpg_recipients_page', which if absent is set to 217 "MoinMessageRecipientsDict". 218 """ 219 220 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 221 homedetails = wikiutil.getInterwikiHomePage(request) 222 223 if homedetails: 224 homewiki, homepage = homedetails 225 if homewiki == "Self": 226 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 227 if recipients: 228 return recipients 229 230 return getWikiDict(subpage, request) 231 232 # vim: tabstop=4 expandtab shiftwidth=4