1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - SendMessage Action 4 5 @copyright: 2012, 2013, 2014 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 as_string 16 from MoinMessageSupport import get_signing_users, get_recipients, get_relays, \ 17 get_recipient_details, MoinMessageRecipientError 18 from MoinSupport import * 19 from ItemSupport import ItemStore 20 from MoinMoin.wikiutil import escape, MimeType, parseQueryString, \ 21 taintfilename 22 23 from email.mime.base import MIMEBase 24 from email.mime.image import MIMEImage 25 from email.mime.multipart import MIMEMultipart 26 from email.mime.text import MIMEText 27 from os.path import abspath, exists, join 28 import urllib 29 30 try: 31 from MoinMoin.web import static 32 htdocs = abspath(join(static.__file__, "htdocs")) 33 except ImportError: 34 htdocs = None 35 36 Dependencies = [] 37 38 def get_htdocs(request): 39 40 "Use the 'request' to find the htdocs directory." 41 42 global htdocs 43 44 if not htdocs: 45 htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None) 46 if htdocs_in_cfg and exists(htdocs_in_cfg): 47 htdocs = htdocs_in_cfg 48 return htdocs 49 htdocs_in_data = abspath(join(request.cfg.data_dir, "../htdocs")) 50 if exists(htdocs_in_data): 51 htdocs = htdocs_in_data 52 return htdocs 53 54 return htdocs 55 56 class SendMessage(ActionBase, ActionSupport): 57 58 "An action that can send a message to another site." 59 60 def get_form_html(self, buttons_html): 61 62 "Present an interface for message sending." 63 64 _ = self._ 65 request = self.request 66 form = self.get_form() 67 68 message = form.get("message", [""])[0] 69 recipient = form.get("recipient", [""])[0] 70 format = form.get("format", ["wiki"])[0] 71 preview = form.get("preview") 72 action = form.get("send-action", ["send"])[0] 73 74 # Get a list of potential recipients. 75 76 recipients = get_recipients(request, fetching=False) 77 78 # Prepare the recipients list, selecting the specified recipients. 79 80 recipients_list = [] 81 82 if recipients: 83 recipients_list += self.get_option_list(recipient, recipients) or [] 84 85 recipients_list.sort() 86 87 # Prepare any preview. 88 89 parser_cls = getParserClass(request, format) 90 request.formatter.setPage(self.page) 91 preview_output = preview and formatText(message, request, request.formatter, inhibit_p=False, parser_cls=parser_cls) or "" 92 93 # Fill in the fields and labels. 94 95 d = { 96 "buttons_html" : buttons_html, 97 "format_label" : escape(_("Message format")), 98 "format" : escattr(format), 99 "recipient_label" : escape(_("Recipient")), 100 "recipients_list" : "\n".join(recipients_list), 101 "message_label" : escape(_("Message text")), 102 "message_default" : escape(message), 103 "preview_label" : escattr(_("Preview message")), 104 "preview_output" : preview_output, 105 "send_label" : escape(_("Send message immediately")), 106 "send_selected" : self._get_selected("send", action), 107 "queue_label" : escape(_("Queue message for sending")), 108 "queue_selected" : self._get_selected("queue", action), 109 } 110 111 # Prepare the output HTML. 112 113 html = ''' 114 <table> 115 <tr> 116 <td class="label"><label>%(recipient_label)s</label></td> 117 <td> 118 <select name="recipient"> 119 %(recipients_list)s 120 </select> 121 </td> 122 </tr> 123 <tr> 124 <td class="label"><label>%(format_label)s</label></td> 125 <td> 126 <input name="format" type="text" value="%(format)s" size="20" /> 127 </td> 128 </tr> 129 <tr> 130 <td class="label"><label>%(message_label)s</label></td> 131 <td> 132 <textarea name="message" cols="60" rows="10">%(message_default)s</textarea> 133 </td> 134 </tr> 135 <tr> 136 <td></td> 137 <td class="buttons"> 138 <input name="preview" type="submit" value="%(preview_label)s" /> 139 </td> 140 </tr> 141 <tr> 142 <td></td> 143 <td class="moinmessage-preview"> 144 %(preview_output)s 145 </td> 146 </tr> 147 <tr> 148 <td></td> 149 <td> 150 <select name="send-action"> 151 <option value="send" %(send_selected)s>%(send_label)s</option> 152 <option value="queue" %(queue_selected)s>%(queue_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 181 if not text: 182 return 0, _("A message must be given.") 183 184 if not recipient: 185 return 0, _("A recipient must be given.") 186 187 homedir = self.get_homedir() 188 if not homedir: 189 return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") 190 191 gpg = GPG(homedir) 192 193 # Construct a message from the request. 194 195 message = Message() 196 197 container = MIMEMultipart("related") 198 container["Update-Action"] = "store" 199 container["To"] = recipient 200 201 # Add the message body and any attachments. 202 203 parser_cls = getParserClass(request, format) 204 205 # Determine whether alternative output types are produced and, if so, 206 # bundle them in a multipart/alternative part. 207 208 output_types = getParserOutputTypes(parser_cls) 209 210 if len(output_types) > 1: 211 alternatives = MIMEMultipart("alternative") 212 container.attach(alternatives) 213 else: 214 alternatives = container 215 216 # Produce each of the representations. 217 218 for output_type in output_types: 219 220 # HTML must be processed to identify attachments. 221 222 if output_type == "text/html": 223 fmt = OutgoingHTMLFormatter(request) 224 fmt.setPage(request.page) 225 body = formatText(text, request, fmt, inhibit_p=False, parser_cls=parser_cls) 226 else: 227 body = formatTextForOutputType(text, request, parser_cls, output_type) 228 229 maintype, subtype = output_type.split("/", 1) 230 if maintype == "text": 231 part = MIMEText(body.encode("utf-8"), subtype, "utf-8") 232 else: 233 part = MIMEBase(maintype, subtype) 234 part.set_payload(body) 235 236 alternatives.attach(part) 237 238 # Produce any identified attachments. 239 240 for pos, (path, filename) in enumerate(fmt.attachments): 241 242 # Obtain the attachment content. 243 244 f = open(path, "rb") 245 try: 246 body = f.read() 247 finally: 248 f.close() 249 250 # Determine the attachment type. 251 252 mimetype = MimeType(filename=filename) 253 254 # NOTE: Support a limited set of explicit part types for now. 255 256 if mimetype.major == "image": 257 part = MIMEImage(body, mimetype.minor, **mimetype.params) 258 elif mimetype.major == "text": 259 part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) 260 else: 261 part = MIMEApplication(body, mimetype.minor, **mimetype.params) 262 263 # Label the attachment and include it in the message. 264 265 part["Content-ID"] = "attachment%d" % pos 266 container.attach(part) 267 268 message.add_update(container) 269 270 # Get the sender details for signing messages. 271 # This is not the same as the details for authenticating users in the 272 # PostMessage action since the fingerprints refer to public keys. 273 274 signing_users = get_signing_users(request) 275 signer = signing_users and signing_users.get(request.user.name) 276 277 # Get the recipient details. 278 279 try: 280 parameters = get_recipient_details(request, recipient) 281 except MoinMessageRecipientError, exc: 282 return 0, exc.message 283 284 type = parameters["type"] 285 location = parameters["location"] 286 287 # Obtain the actual location if a relay is specified. 288 289 if parameters["type"] == "relay": 290 relays = get_relays(request) 291 if not relays: 292 return 0, _("No relays are defined for MoinMessage, but one is specified for the recipient.") 293 if not relays.has_key(location): 294 return 0, _("The relay specified for the recipient is not defined.") 295 296 location = relays[location] 297 298 # Sign, encrypt and send the message. 299 300 message = message.get_payload() 301 302 if not queue and type in ("url", "relay"): 303 try: 304 if signer: 305 message = gpg.signMessage(message, signer) 306 307 message = gpg.encryptMessage(message, parameters["fingerprint"]) 308 309 # Send relayed messages with an extra signature. 310 311 if type == "relay": 312 relaying_user = getattr(self.request.cfg, "moinmessage_gpg_relaying_user") 313 314 # Signing with the same identity if no special relaying user is 315 # defined. 316 317 if relaying_user: 318 signer = signing_users and signing_users.get(relaying_user) 319 320 timestamp(message) 321 message["Update-Action"] = "store" 322 message = gpg.signMessage(message, signer) 323 324 sendMessage(message, location) 325 326 except MoinMessageError, exc: 327 return 0, "%s: %s" % (_("The message could not be prepared and sent"), exc) 328 329 # Or queue the message on the specified page. 330 331 elif type == "page": 332 page = Page(request, location) 333 outbox = ItemStore(page, "messages", "message-locks") 334 outbox.append(as_string(message)) 335 336 # Or queue the message in a special outbox. 337 338 else: 339 outbox = ItemStore(request.page, "outgoing-messages", "outgoing-message-locks") 340 outbox.append(as_string(message)) 341 342 return 1, _("Message sent!") 343 344 def get_homedir(self): 345 346 "Locate the GPG home directory." 347 348 return getattr(self.request.cfg, "moinmessage_gpg_homedir") 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