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 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 queue = form.get("queue") 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 "queue_label" : escape(_("Queue message for sending")), 102 "queue_checked" : queue and 'checked="checked" ' or "", 103 } 104 105 # Prepare the output HTML. 106 107 html = ''' 108 <table> 109 <tr> 110 <td class="label"><label>%(recipient_label)s</label></td> 111 <td> 112 <select name="recipient"> 113 %(recipients_list)s 114 </select> 115 </td> 116 </tr> 117 <tr> 118 <td class="label"><label>%(format_label)s</label></td> 119 <td> 120 <input name="format" type="text" value="%(format)s" size="20" /> 121 </td> 122 </tr> 123 <tr> 124 <td class="label"><label>%(message_label)s</label></td> 125 <td> 126 <textarea name="message" cols="60" rows="10">%(message_default)s</textarea> 127 </td> 128 </tr> 129 <tr> 130 <td></td> 131 <td class="buttons"> 132 <input name="preview" type="submit" value="%(preview_label)s" /> 133 </td> 134 </tr> 135 <tr> 136 <td></td> 137 <td class="moinmessage-preview"> 138 %(preview_output)s 139 </td> 140 </tr> 141 <tr> 142 <td class="label"><label>%(queue_label)s</label></td> 143 <td> 144 <input name="queue" type="checkbox" value="true" %(queue_checked)s/> 145 </td> 146 </tr> 147 <tr> 148 <td></td> 149 <td class="buttons"> 150 %(buttons_html)s 151 </td> 152 </tr> 153 </table>''' % d 154 155 return html 156 157 def do_action(self): 158 159 "Attempt to send the message." 160 161 _ = self._ 162 request = self.request 163 form = self.get_form() 164 165 text = form.get("message", [None])[0] 166 recipient = form.get("recipient", [None])[0] 167 format = form.get("format", ["wiki"])[0] 168 queue = form.get("queue") 169 170 if not text: 171 return 0, _("A message must be given.") 172 173 if not recipient: 174 return 0, _("A recipient must be given.") 175 176 homedir = self.get_homedir() 177 if not homedir: 178 return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") 179 180 gpg = GPG(homedir) 181 182 # Construct a message from the request. 183 184 message = Message() 185 186 container = MIMEMultipart("related") 187 container["Update-Action"] = "store" 188 container["To"] = recipient 189 190 # Add the message body and any attachments. 191 192 parser_cls = getParserClass(request, format) 193 194 # Determine whether alternative output types are produced and, if so, 195 # bundle them in a multipart/alternative part. 196 197 output_types = getParserOutputTypes(parser_cls) 198 199 if len(output_types) > 1: 200 alternatives = MIMEMultipart("alternative") 201 container.attach(alternatives) 202 else: 203 alternatives = container 204 205 # Produce each of the representations. 206 207 for output_type in output_types: 208 209 # HTML must be processed to identify attachments. 210 211 if output_type == "text/html": 212 fmt = OutgoingHTMLFormatter(request) 213 fmt.setPage(request.page) 214 body = formatText(text, request, fmt, inhibit_p=False, parser_cls=parser_cls) 215 else: 216 body = formatTextForOutputType(text, request, parser_cls, output_type) 217 218 maintype, subtype = output_type.split("/", 1) 219 if maintype == "text": 220 part = MIMEText(body.encode("utf-8"), subtype, "utf-8") 221 else: 222 part = MIMEBase(maintype, subtype) 223 part.set_payload(body) 224 225 alternatives.attach(part) 226 227 # Produce any identified attachments. 228 229 for pos, (path, filename) in enumerate(fmt.attachments): 230 231 # Obtain the attachment content. 232 233 f = open(path, "rb") 234 try: 235 body = f.read() 236 finally: 237 f.close() 238 239 # Determine the attachment type. 240 241 mimetype = MimeType(filename=filename) 242 243 # NOTE: Support a limited set of explicit part types for now. 244 245 if mimetype.major == "image": 246 part = MIMEImage(body, mimetype.minor, **mimetype.params) 247 elif mimetype.major == "text": 248 part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) 249 else: 250 part = MIMEApplication(body, mimetype.minor, **mimetype.params) 251 252 # Label the attachment and include it in the message. 253 254 part["Content-ID"] = "attachment%d" % pos 255 container.attach(part) 256 257 message.add_update(container) 258 259 # Get the sender details for signing messages. 260 # This is not the same as the details for authenticating users in the 261 # PostMessage action since the fingerprints refer to public keys. 262 263 signing_users = self.get_signing_users() 264 signer = signing_users and signing_users.get(request.user.name) 265 266 # Get the recipient details. 267 268 recipients = self.get_recipients() 269 if not recipients: 270 return 0, _("No recipients page is defined for MoinMessage.") 271 272 recipient_details = recipients.get(recipient) 273 if not recipient_details: 274 return 0, _("The specified recipient is not present in the list of known contacts.") 275 276 parameters = parseDictEntry(recipient_details, ("type", "location", "fingerprint",)) 277 278 if not parameters.has_key("type"): 279 return 0, _("The recipient details are missing a destination type.") 280 281 if not parameters.has_key("location"): 282 return 0, _("The recipient details are missing a location for sent messages.") 283 284 if parameters.get("type") == "url" and not parameters.has_key("fingerprint"): 285 return 0, _("The recipient details are missing a fingerprint for sending messages.") 286 287 # Sign, encrypt and send the message. 288 289 message = message.get_payload() 290 type = parameters["type"] 291 292 if not queue and type == "url": 293 try: 294 if signer: 295 message = gpg.signMessage(message, signer) 296 297 message = gpg.encryptMessage(message, parameters["fingerprint"]) 298 sendMessage(message, parameters["location"]) 299 300 except MoinMessageError, exc: 301 return 0, "%s: %s" % (_("The message could not be prepared and sent"), exc) 302 303 # Or queue the message on the specified page. 304 305 elif type == "page": 306 page = Page(request, parameters["location"]) 307 outbox = ItemStore(page, "messages", "message-locks") 308 outbox.append(message.as_string()) 309 310 # Or queue the message in a special outbox. 311 312 else: 313 outbox = ItemStore(request.page, "outgoing-messages", "outgoing-message-locks") 314 outbox.append(message.as_string()) 315 316 return 1, _("Message sent!") 317 318 def get_homedir(self): 319 320 "Locate the GPG home directory." 321 322 return getattr(self.request.cfg, "moinmessage_gpg_homedir") 323 324 def get_recipients(self): 325 326 """ 327 Return the recipients dictionary by first obtaining the page in which it 328 is stored. This page may either be a subpage of the user's home page, if 329 stored on this wiki, or it may be relative to the site root. 330 331 The name of the subpage is defined by the configuration setting 332 'moinmessage_gpg_recipients_page', which if absent is set to 333 "MoinMessageRecipientsDict". 334 """ 335 336 request = self.request 337 338 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 339 homedetails = getInterwikiHomePage(request) 340 341 if homedetails: 342 homewiki, homepage = homedetails 343 if homewiki == "Self": 344 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 345 if recipients: 346 return recipients 347 348 return getWikiDict(subpage, request) 349 350 def get_signing_users(self): 351 return getWikiDict( 352 getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 353 self.request) 354 355 # Special message formatters. 356 357 def unquoteWikinameURL(url, charset=config.charset): 358 359 """ 360 The inverse of wikiutil.quoteWikinameURL, returning the page name referenced 361 by the given 'url', with the page name assumed to be encoded using the given 362 'charset' (or default charset if omitted). 363 """ 364 365 return unicode(urllib.unquote(url), encoding=charset) 366 367 def getAttachmentFromURL(url, request): 368 369 """ 370 Return a (full path, attachment filename) tuple for the attachment 371 referenced by the given 'url', using the 'request' to interpret the 372 structure of 'url'. 373 374 If 'url' does not refer to an attachment on this wiki, None is returned. 375 """ 376 377 # Detect static resources. 378 379 htdocs_dir = get_htdocs(request) 380 381 if htdocs_dir: 382 prefix = request.cfg.url_prefix_static 383 384 # Normalise the 385 386 if not prefix.endswith("/"): 387 prefix += "/" 388 389 if url.startswith(prefix): 390 filename = url[len(prefix):] 391 392 # Obtain the resource path. 393 394 path = abspath(join(htdocs_dir, filename)) 395 396 if exists(path): 397 return path, taintfilename(filename) 398 399 # Detect attachments and other resources. 400 401 script = request.getScriptname() 402 403 # Normalise the URL. 404 405 if not script.endswith("/"): 406 script += "/" 407 408 # Reject URLs outside the wiki. 409 410 if not url.startswith(script): 411 return None 412 413 path = url[len(script):].lstrip("/") 414 try: 415 qpagename, qs = path.split("?", 1) 416 except ValueError: 417 qpagename = path 418 qs = None 419 420 pagename = unquoteWikinameURL(qpagename) 421 qs = qs and parseQueryString(qs) or {} 422 423 filename = qs.get("target") or qs.get("drawing") 424 filename = taintfilename(filename) 425 426 # Obtain the attachment path. 427 428 path = AttachFile.getFilename(request, pagename, filename) 429 return path, filename 430 431 class OutgoingHTMLFormatter(text_html.Formatter): 432 433 """ 434 Handle outgoing HTML content by identifying attachments and rewriting their 435 locations. References to bundled attachments are done using RFC 2111: 436 437 https://tools.ietf.org/html/rfc2111 438 439 Messages employing references between parts are meant to comply with RFC 440 2387: 441 442 https://tools.ietf.org/html/rfc2387 443 """ 444 445 def __init__(self, request, **kw): 446 text_html.Formatter.__init__(self, request, **kw) 447 self.attachments = [] 448 449 def add_attachment(self, location): 450 details = getAttachmentFromURL(location, self.request) 451 if details: 452 pos = len(self.attachments) 453 self.attachments.append(details) 454 return "cid:attachment%d" % pos 455 else: 456 return None 457 458 def image(self, src=None, **kw): 459 src = src or kw.get("src") 460 ref = src and self.add_attachment(src) 461 return text_html.Formatter.image(self, ref or src, **kw) 462 463 def transclusion(self, on, **kw): 464 if on: 465 data = kw.get("data") 466 kw["data"] = data and self.add_attachment(data) 467 return text_html.Formatter.transclusion(self, on, **kw) 468 469 # Action function. 470 471 def execute(pagename, request): 472 SendMessage(pagename, request).render() 473 474 # vim: tabstop=4 expandtab shiftwidth=4