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