1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - SendMessage Action 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.action import ActionBase, AttachFile 10 from MoinMoin.formatter import text_html 11 from MoinMoin.log import getLogger 12 from MoinMoin.Page import Page 13 from MoinMoin import config 14 from MoinMessage import GPG, MoinMessageError, Message, sendMessage, timestamp 15 from MoinMessageSupport import get_signing_users, get_recipients, get_relays, \ 16 get_recipient_details, MoinMessageRecipientError 17 from MoinSupport import * 18 from ItemSupport import ItemStore 19 from MoinMoin.wikiutil import escape, MimeType, parseQueryString, \ 20 taintfilename 21 22 from email.mime.base import MIMEBase 23 from email.mime.image import MIMEImage 24 from email.mime.multipart import MIMEMultipart 25 from email.mime.text import MIMEText 26 from os.path import abspath, exists, join 27 import urllib 28 29 try: 30 from MoinMoin.web import static 31 htdocs = abspath(join(static.__file__, "htdocs")) 32 except ImportError: 33 htdocs = None 34 35 Dependencies = [] 36 37 def get_htdocs(request): 38 39 "Use the 'request' to find the htdocs directory." 40 41 global htdocs 42 43 if not htdocs: 44 htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None) 45 if htdocs_in_cfg and exists(htdocs_in_cfg): 46 htdocs = htdocs_in_cfg 47 return htdocs 48 htdocs_in_data = abspath(join(request.cfg.data_dir, "../htdocs")) 49 if exists(htdocs_in_data): 50 htdocs = htdocs_in_data 51 return htdocs 52 53 return htdocs 54 55 class SendMessage(ActionBase, ActionSupport): 56 57 "An action that can send a message to another site." 58 59 def get_form_html(self, buttons_html): 60 61 "Present an interface for message sending." 62 63 _ = self._ 64 request = self.request 65 form = self.get_form() 66 67 message = form.get("message", [""])[0] 68 recipient = form.get("recipient", [""])[0] 69 format = form.get("format", ["wiki"])[0] 70 preview = form.get("preview") 71 action = form.get("send-action", ["send"])[0] 72 73 # Get a list of potential recipients. 74 75 recipients = get_recipients(request, fetching=False) 76 77 # Prepare the recipients list, selecting the specified recipients. 78 79 recipients_list = [] 80 81 if recipients: 82 recipients_list += self.get_option_list(recipient, recipients) or [] 83 84 recipients_list.sort() 85 86 # Prepare any preview. 87 88 parser_cls = getParserClass(request, format) 89 request.formatter.setPage(self.page) 90 preview_output = preview and formatText(message, request, request.formatter, inhibit_p=False, parser_cls=parser_cls) or "" 91 92 # Fill in the fields and labels. 93 94 d = { 95 "buttons_html" : buttons_html, 96 "format_label" : escape(_("Message format")), 97 "format" : escattr(format), 98 "recipient_label" : escape(_("Recipient")), 99 "recipients_list" : "\n".join(recipients_list), 100 "message_label" : escape(_("Message text")), 101 "message_default" : escape(message), 102 "preview_label" : escattr(_("Preview message")), 103 "preview_output" : preview_output, 104 "send_label" : escape(_("Send message immediately")), 105 "send_selected" : self._get_selected("send", action), 106 "queue_label" : escape(_("Queue message for sending")), 107 "queue_selected" : self._get_selected("queue", action), 108 } 109 110 # Prepare the output HTML. 111 112 html = ''' 113 <table> 114 <tr> 115 <td class="label"><label>%(recipient_label)s</label></td> 116 <td> 117 <select name="recipient"> 118 %(recipients_list)s 119 </select> 120 </td> 121 </tr> 122 <tr> 123 <td class="label"><label>%(format_label)s</label></td> 124 <td> 125 <input name="format" type="text" value="%(format)s" size="20" /> 126 </td> 127 </tr> 128 <tr> 129 <td class="label"><label>%(message_label)s</label></td> 130 <td> 131 <textarea name="message" cols="60" rows="10">%(message_default)s</textarea> 132 </td> 133 </tr> 134 <tr> 135 <td></td> 136 <td class="buttons"> 137 <input name="preview" type="submit" value="%(preview_label)s" /> 138 </td> 139 </tr> 140 <tr> 141 <td></td> 142 <td class="moinmessage-preview"> 143 %(preview_output)s 144 </td> 145 </tr> 146 <tr> 147 <td></td> 148 <td> 149 <select name="send-action"> 150 <option value="send" %(send_selected)s>%(send_label)s</option> 151 <option value="queue" %(queue_selected)s>%(queue_label)s</option> 152 </select> 153 </td> 154 </tr> 155 <tr> 156 <td></td> 157 <td class="buttons"> 158 %(buttons_html)s 159 </td> 160 </tr> 161 </table>''' % d 162 163 return html 164 165 def do_action(self): 166 167 "Attempt to send the message." 168 169 _ = self._ 170 request = self.request 171 form = self.get_form() 172 173 text = form.get("message", [None])[0] 174 recipient = form.get("recipient", [None])[0] 175 format = form.get("format", ["wiki"])[0] 176 action = form.get("send-action", ["send"])[0] 177 178 queue = action == "queue" 179 180 if not text: 181 return 0, _("A message must be given.") 182 183 if not recipient: 184 return 0, _("A recipient must be given.") 185 186 homedir = self.get_homedir() 187 if not homedir: 188 return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") 189 190 gpg = GPG(homedir) 191 192 # Construct a message from the request. 193 194 message = Message() 195 196 container = MIMEMultipart("related") 197 container["Update-Action"] = "store" 198 container["To"] = recipient 199 200 # Add the message body and any attachments. 201 202 parser_cls = getParserClass(request, format) 203 204 # Determine whether alternative output types are produced and, if so, 205 # bundle them in a multipart/alternative part. 206 207 output_types = getParserOutputTypes(parser_cls) 208 209 if len(output_types) > 1: 210 alternatives = MIMEMultipart("alternative") 211 container.attach(alternatives) 212 else: 213 alternatives = container 214 215 # Produce each of the representations. 216 217 for output_type in output_types: 218 219 # HTML must be processed to identify attachments. 220 221 if output_type == "text/html": 222 fmt = OutgoingHTMLFormatter(request) 223 fmt.setPage(request.page) 224 body = formatText(text, request, fmt, inhibit_p=False, parser_cls=parser_cls) 225 else: 226 body = formatTextForOutputType(text, request, parser_cls, output_type) 227 228 maintype, subtype = output_type.split("/", 1) 229 if maintype == "text": 230 part = MIMEText(body.encode("utf-8"), subtype, "utf-8") 231 else: 232 part = MIMEBase(maintype, subtype) 233 part.set_payload(body) 234 235 alternatives.attach(part) 236 237 # Produce any identified attachments. 238 239 for pos, (path, filename) in enumerate(fmt.attachments): 240 241 # Obtain the attachment content. 242 243 f = open(path, "rb") 244 try: 245 body = f.read() 246 finally: 247 f.close() 248 249 # Determine the attachment type. 250 251 mimetype = MimeType(filename=filename) 252 253 # NOTE: Support a limited set of explicit part types for now. 254 255 if mimetype.major == "image": 256 part = MIMEImage(body, mimetype.minor, **mimetype.params) 257 elif mimetype.major == "text": 258 part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) 259 else: 260 part = MIMEApplication(body, mimetype.minor, **mimetype.params) 261 262 # Label the attachment and include it in the message. 263 264 part["Content-ID"] = "attachment%d" % pos 265 container.attach(part) 266 267 message.add_update(container) 268 269 # Get the sender details for signing messages. 270 # This is not the same as the details for authenticating users in the 271 # PostMessage action since the fingerprints refer to public keys. 272 273 signing_users = get_signing_users(request) 274 signer = signing_users and signing_users.get(request.user.name) 275 276 # Get the recipient details. 277 278 try: 279 parameters = get_recipient_details(request, recipient) 280 except MoinMessageRecipientError, exc: 281 return 0, exc.message 282 283 type = parameters["type"] 284 location = parameters["location"] 285 286 # Obtain the actual location if a relay is specified. 287 288 if parameters["type"] == "relay": 289 relays = get_relays(request) 290 if not relays: 291 return 0, _("No relays are defined for MoinMessage, but one is specified for the recipient.") 292 if not relays.has_key(location): 293 return 0, _("The relay specified for the recipient is not defined.") 294 295 location = relays[location] 296 297 # Sign, encrypt and send the message. 298 299 message = message.get_payload() 300 301 if not queue and type in ("url", "relay"): 302 try: 303 if signer: 304 message = gpg.signMessage(message, signer) 305 306 message = gpg.encryptMessage(message, parameters["fingerprint"]) 307 308 # Send relayed messages with an extra signature. 309 310 if type == "relay": 311 relaying_user = getattr(self.request.cfg, "moinmessage_gpg_relaying_user") 312 313 # Signing with the same identity if no special relaying user is 314 # defined. 315 316 if relaying_user: 317 signer = signing_users and signing_users.get(relaying_user) 318 319 timestamp(message) 320 message["Update-Action"] = "store" 321 message = gpg.signMessage(message, signer) 322 323 sendMessage(message, location) 324 325 except MoinMessageError, exc: 326 return 0, "%s: %s" % (_("The message could not be prepared and sent"), exc) 327 328 # Or queue the message on the specified page. 329 330 elif type == "page": 331 page = Page(request, location) 332 outbox = ItemStore(page, "messages", "message-locks") 333 outbox.append(message.as_string()) 334 335 # Or queue the message in a special outbox. 336 337 else: 338 outbox = ItemStore(request.page, "outgoing-messages", "outgoing-message-locks") 339 outbox.append(message.as_string()) 340 341 return 1, _("Message sent!") 342 343 def get_homedir(self): 344 345 "Locate the GPG home directory." 346 347 return getattr(self.request.cfg, "moinmessage_gpg_homedir") 348 349 # Special message formatters. 350 351 def unquoteWikinameURL(url, charset=config.charset): 352 353 """ 354 The inverse of wikiutil.quoteWikinameURL, returning the page name referenced 355 by the given 'url', with the page name assumed to be encoded using the given 356 'charset' (or default charset if omitted). 357 """ 358 359 return unicode(urllib.unquote(url), encoding=charset) 360 361 def getAttachmentFromURL(url, request): 362 363 """ 364 Return a (full path, attachment filename) tuple for the attachment 365 referenced by the given 'url', using the 'request' to interpret the 366 structure of 'url'. 367 368 If 'url' does not refer to an attachment on this wiki, None is returned. 369 """ 370 371 # Detect static resources. 372 373 htdocs_dir = get_htdocs(request) 374 375 if htdocs_dir: 376 prefix = request.cfg.url_prefix_static 377 378 # Normalise the 379 380 if not prefix.endswith("/"): 381 prefix += "/" 382 383 if url.startswith(prefix): 384 filename = url[len(prefix):] 385 386 # Obtain the resource path. 387 388 path = abspath(join(htdocs_dir, filename)) 389 390 if exists(path): 391 return path, taintfilename(filename) 392 393 # Detect attachments and other resources. 394 395 script = request.getScriptname() 396 397 # Normalise the URL. 398 399 if not script.endswith("/"): 400 script += "/" 401 402 # Reject URLs outside the wiki. 403 404 if not url.startswith(script): 405 return None 406 407 path = url[len(script):].lstrip("/") 408 try: 409 qpagename, qs = path.split("?", 1) 410 except ValueError: 411 qpagename = path 412 qs = None 413 414 pagename = unquoteWikinameURL(qpagename) 415 qs = qs and parseQueryString(qs) or {} 416 417 filename = qs.get("target") or qs.get("drawing") 418 filename = taintfilename(filename) 419 420 # Obtain the attachment path. 421 422 path = AttachFile.getFilename(request, pagename, filename) 423 return path, filename 424 425 class OutgoingHTMLFormatter(text_html.Formatter): 426 427 """ 428 Handle outgoing HTML content by identifying attachments and rewriting their 429 locations. References to bundled attachments are done using RFC 2111: 430 431 https://tools.ietf.org/html/rfc2111 432 433 Messages employing references between parts are meant to comply with RFC 434 2387: 435 436 https://tools.ietf.org/html/rfc2387 437 """ 438 439 def __init__(self, request, **kw): 440 text_html.Formatter.__init__(self, request, **kw) 441 self.attachments = [] 442 443 def add_attachment(self, location): 444 details = getAttachmentFromURL(location, self.request) 445 if details: 446 pos = len(self.attachments) 447 self.attachments.append(details) 448 return "cid:attachment%d" % pos 449 else: 450 return None 451 452 def image(self, src=None, **kw): 453 src = src or kw.get("src") 454 ref = src and self.add_attachment(src) 455 return text_html.Formatter.image(self, ref or src, **kw) 456 457 def transclusion(self, on, **kw): 458 if on: 459 data = kw.get("data") 460 kw["data"] = data and self.add_attachment(data) 461 return text_html.Formatter.transclusion(self, on, **kw) 462 463 # Action function. 464 465 def execute(pagename, request): 466 SendMessage(pagename, request).render() 467 468 # vim: tabstop=4 expandtab shiftwidth=4