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