1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - PostMessage Action 4 5 @copyright: 2012 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.PageEditor import PageEditor 10 from MoinMoin.log import getLogger 11 from MoinSupport import * 12 from email.parser import Parser 13 from subprocess import Popen, PIPE 14 from tempfile import mkstemp 15 import os 16 17 try: 18 from cStringIO import StringIO 19 except ImportError: 20 from StringIO import StringIO 21 22 Dependencies = ['pages'] 23 24 class PostMessage: 25 26 "A posted message handler." 27 28 def __init__(self, pagename, request): 29 30 """ 31 On the page with the given 'pagename', use the given 'request' when 32 reading posted messages, modifying the Wiki. 33 """ 34 35 self.pagename = pagename 36 self.request = request 37 self.page = Page(request, pagename) 38 39 def do_action(self): 40 request = self.request 41 content_length = getHeader(request, "Content-Length", "HTTP") 42 if content_length: 43 content_length = int(content_length) 44 45 # Get the message. 46 47 self.handle_message(StringIO(request.read(content_length))) 48 49 def handle_message(self, message_text): 50 51 "Handle the given 'message_text'." 52 53 request = self.request 54 message = Parser().parse(message_text) 55 mimetype = message.get_content_type() 56 encoding = message.get_content_charset() 57 58 # Detect PGP/GPG-encoded payloads. 59 # See: http://tools.ietf.org/html/rfc3156 60 61 # NOTE: RFC 3156 states that signed messages should employ a detached 62 # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures 63 # NOTE: instead of "BEGIN PGP SIGNATURE". 64 # NOTE: The "micalg" parameter is currently not supported. 65 66 if mimetype == "multipart/signed" and \ 67 message.get_param("protocol") == "application/pgp-signature": 68 69 try: 70 content, signature = message.get_payload() 71 except ValueError: 72 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 73 request.write("There must be a content part and a signature for signed uploads.") 74 return 75 76 # Verify the message format. 77 78 if signature.get_content_type() != "application/pgp-signature": 79 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 80 request.write("Signature data must be provided in the second part as application/pgp-signature.") 81 return 82 83 # Locate the keyring. 84 85 homedir = getattr(request.cfg, "postmessage_gpg_homedir") 86 if not homedir: 87 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 88 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 89 return 90 91 # Write the detached signature and content to files. 92 93 signature_fd, signature_filename = mkstemp() 94 content_fd, content_filename = mkstemp() 95 try: 96 signature_fp = os.fdopen(signature_fd, "w") 97 content_fp = os.fdopen(content_fd, "w") 98 try: 99 signature_fp.write(signature.get_payload()) 100 content_fp.write(content.as_string()) 101 finally: 102 signature_fp.close() 103 content_fp.close() 104 105 # Verify the message text. 106 107 cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename], 108 stdout=PIPE, stderr=PIPE) 109 110 errors = cmd.stderr.read() 111 if errors: 112 getLogger(__name__).warning(errors) 113 114 # Handle the embedded message. 115 116 try: 117 # With a zero return code, accept the message. 118 119 if not cmd.wait(): 120 self.handle_parsed_message(content) 121 122 # Otherwise, reject the unverified message. 123 124 else: 125 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 126 request.write("The message could not be verified.") 127 128 finally: 129 cmd.stdout.close() 130 cmd.stderr.close() 131 132 finally: 133 os.remove(signature_filename) 134 os.remove(content_filename) 135 136 # Reject unsigned payloads. 137 138 else: 139 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 140 request.write("Only PGP/GPG-signed payloads are supported.") 141 142 def handle_plaintext_message(self, message_text): 143 144 "Handle the given 'message_text'." 145 146 message = Parser().parse(message_text) 147 self.handle_parsed_message(message) 148 149 def handle_parsed_message(self, message): 150 151 "Handle the given 'message_text'." 152 153 request = self.request 154 155 # Handle a single part. 156 157 if not message.is_multipart(): 158 self.handle_message_parts([message], to_replace(message)) 159 160 # Handle multiple parts. 161 162 # This can be a collection of updates, with each update potentially being a 163 # collection of alternative representations. 164 165 elif is_collection(message): 166 for part in message.get_payload(): 167 if part.is_multipart(): 168 self.handle_message_parts(part.get_payload(), to_replace(part)) 169 else: 170 self.handle_message_parts([part], to_replace(part)) 171 172 # Or it can be a collection of alternative representations for a single 173 # update. 174 175 else: 176 self.handle_message_parts(message.get_payload(), to_replace(message)) 177 178 # Default output. 179 180 writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content") 181 182 def handle_message_parts(self, parts, replace): 183 184 """ 185 Handle the given message 'parts', replacing the page content if 186 'replace' is set to a true value. 187 """ 188 189 # NOTE: Should either choose preferred content types or somehow retain them 190 # NOTE: all but present one at a time. 191 192 body = [] 193 194 for part in parts: 195 mimetype = part.get_content_type() 196 encoding = part.get_content_charset() 197 if mimetype == "text/moin": 198 body.append(part.get_payload()) 199 if replace: 200 break 201 202 if not replace: 203 body.append(self.page.get_raw_body()) 204 205 page_editor = PageEditor(self.request, self.pagename) 206 page_editor.saveText("\n\n".join(body), 0) 207 208 def is_collection(message): 209 return message.get("Update-Type") == "collection" 210 211 def to_replace(message): 212 return message.get("Update-Action") == "replace" 213 214 # Action function. 215 216 def execute(pagename, request): 217 PostMessage(pagename, request).do_action() 218 219 # vim: tabstop=4 expandtab shiftwidth=4