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, ("fingerprint",)) 276 277 if not parameters.has_key("page") and not parameters.has_key("url"): 278 return 0, _("The recipient details are missing a location for sent messages.") 279 280 if parameters.has_key("url") and not parameters.has_key("fingerprint"): 281 return 0, _("The recipient details are missing a fingerprint for sending messages.") 282 283 # Sign, encrypt and send the message. 284 285 message = message.get_payload() 286 287 if not queue and parameters.has_key("url"): 288 try: 289 if signer: 290 message = gpg.signMessage(message, signer) 291 292 message = gpg.encryptMessage(message, parameters["fingerprint"]) 293 sendMessage(message, parameters["url"]) 294 295 except MoinMessageError, exc: 296 return 0, "%s: %s" % (_("The message could not be prepared and sent"), exc) 297 298 # Or queue the message on the specified page. 299 300 elif parameters.has_key("page"): 301 page = Page(request, parameters["page"]) 302 outbox = ItemStore(page, "messages", "message-locks") 303 outbox.append(message.as_string()) 304 305 # Or queue the message in a special outbox. 306 307 else: 308 outbox = ItemStore(request.page, "outgoing-messages", "outgoing-message-locks") 309 outbox.append(message.as_string()) 310 311 return 1, _("Message sent!") 312 313 def get_homedir(self): 314 315 "Locate the GPG home directory." 316 317 return getattr(self.request.cfg, "moinmessage_gpg_homedir") 318 319 def get_recipients(self): 320 321 """ 322 Return the recipients dictionary by first obtaining the page in which it 323 is stored. This page may either be a subpage of the user's home page, if 324 stored on this wiki, or it may be relative to the site root. 325 326 The name of the subpage is defined by the configuration setting 327 'moinmessage_gpg_recipients_page', which if absent is set to 328 "MoinMessageRecipientsDict". 329 """ 330 331 request = self.request 332 333 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 334 homedetails = getInterwikiHomePage(request) 335 336 if homedetails: 337 homewiki, homepage = homedetails 338 if homewiki == "Self": 339 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 340 if recipients: 341 return recipients 342 343 return getWikiDict(subpage, request) 344 345 def get_signing_users(self): 346 return getWikiDict( 347 getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 348 self.request) 349 350 # Special message formatters. 351 352 def unquoteWikinameURL(url, charset=config.charset): 353 354 """ 355 The inverse of wikiutil.quoteWikinameURL, returning the page name referenced 356 by the given 'url', with the page name assumed to be encoded using the given 357 'charset' (or default charset if omitted). 358 """ 359 360 return unicode(urllib.unquote(url), encoding=charset) 361 362 def getAttachmentFromURL(url, request): 363 364 """ 365 Return a (full path, attachment filename) tuple for the attachment 366 referenced by the given 'url', using the 'request' to interpret the 367 structure of 'url'. 368 369 If 'url' does not refer to an attachment on this wiki, None is returned. 370 """ 371 372 # Detect static resources. 373 374 htdocs_dir = get_htdocs(request) 375 376 if htdocs_dir: 377 prefix = request.cfg.url_prefix_static 378 379 # Normalise the 380 381 if not prefix.endswith("/"): 382 prefix += "/" 383 384 if url.startswith(prefix): 385 filename = url[len(prefix):] 386 387 # Obtain the resource path. 388 389 path = abspath(join(htdocs_dir, filename)) 390 391 if exists(path): 392 return path, taintfilename(filename) 393 394 # Detect attachments and other resources. 395 396 script = request.getScriptname() 397 398 # Normalise the URL. 399 400 if not script.endswith("/"): 401 script += "/" 402 403 # Reject URLs outside the wiki. 404 405 if not url.startswith(script): 406 return None 407 408 path = url[len(script):].lstrip("/") 409 try: 410 qpagename, qs = path.split("?", 1) 411 except ValueError: 412 qpagename = path 413 qs = None 414 415 pagename = unquoteWikinameURL(qpagename) 416 qs = qs and parseQueryString(qs) or {} 417 418 filename = qs.get("target") or qs.get("drawing") 419 filename = taintfilename(filename) 420 421 # Obtain the attachment path. 422 423 path = AttachFile.getFilename(request, pagename, filename) 424 return path, filename 425 426 class OutgoingHTMLFormatter(text_html.Formatter): 427 428 """ 429 Handle outgoing HTML content by identifying attachments and rewriting their 430 locations. References to bundled attachments are done using RFC 2111: 431 432 https://tools.ietf.org/html/rfc2111 433 434 Messages employing references between parts are meant to comply with RFC 435 2387: 436 437 https://tools.ietf.org/html/rfc2387 438 """ 439 440 def __init__(self, request, **kw): 441 text_html.Formatter.__init__(self, request, **kw) 442 self.attachments = [] 443 444 def add_attachment(self, location): 445 details = getAttachmentFromURL(location, self.request) 446 if details: 447 pos = len(self.attachments) 448 self.attachments.append(details) 449 return "cid:attachment%d" % pos 450 else: 451 return None 452 453 def image(self, src=None, **kw): 454 src = src or kw.get("src") 455 ref = src and self.add_attachment(src) 456 return text_html.Formatter.image(self, ref or src, **kw) 457 458 def transclusion(self, on, **kw): 459 if on: 460 data = kw.get("data") 461 kw["data"] = data and self.add_attachment(data) 462 return text_html.Formatter.transclusion(self, on, **kw) 463 464 # Action function. 465 466 def execute(pagename, request): 467 SendMessage(pagename, request).render() 468 469 # vim: tabstop=4 expandtab shiftwidth=4