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