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