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 # vim: tabstop=4 expandtab shiftwidth=4