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