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