paul@29 | 1 | # -*- coding: iso-8859-1 -*- |
paul@29 | 2 | """ |
paul@29 | 3 | MoinMoin - MoinMessageSupport library |
paul@29 | 4 | |
paul@98 | 5 | @copyright: 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk> |
paul@29 | 6 | @license: GNU GPL (v2 or later), see COPYING.txt for details. |
paul@29 | 7 | """ |
paul@29 | 8 | |
paul@122 | 9 | from MoinMoin import config, user |
paul@29 | 10 | from MoinMoin.Page import Page |
paul@120 | 11 | from MoinMoin.action import AttachFile |
paul@120 | 12 | from MoinMoin.formatter import text_html |
paul@29 | 13 | from MoinMoin.log import getLogger |
paul@29 | 14 | from MoinMoin.user import User |
paul@120 | 15 | from MoinMoin.wikiutil import parseQueryString, taintfilename, \ |
paul@120 | 16 | version2timestamp, getInterwikiHomePage |
paul@120 | 17 | |
paul@36 | 18 | from MoinMessage import GPG, Message, MoinMessageError, \ |
paul@56 | 19 | MoinMessageMissingPart, MoinMessageBadContent, \ |
paul@105 | 20 | is_signed, is_encrypted, getContentAndSignature |
paul@120 | 21 | from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \ |
paul@120 | 22 | parseDictEntry, getStaticContentDirectory |
paul@120 | 23 | from ItemSupport import ItemStore |
paul@120 | 24 | from TokenSupport import getIdentifiers |
paul@120 | 25 | |
paul@29 | 26 | from email.parser import Parser |
paul@120 | 27 | from os.path import abspath, exists, join |
paul@30 | 28 | import time |
paul@120 | 29 | import urllib |
paul@29 | 30 | |
paul@117 | 31 | RECIPIENT_PARAMETERS = ("type", "location", "fingerprint") |
paul@29 | 32 | |
paul@29 | 33 | class MoinMessageAction: |
paul@29 | 34 | |
paul@29 | 35 | "Common message handling support for actions." |
paul@29 | 36 | |
paul@29 | 37 | def __init__(self, pagename, request): |
paul@29 | 38 | |
paul@29 | 39 | """ |
paul@29 | 40 | On the page with the given 'pagename', use the given 'request' when |
paul@29 | 41 | reading posted messages, modifying the Wiki. |
paul@29 | 42 | """ |
paul@29 | 43 | |
paul@29 | 44 | self.pagename = pagename |
paul@29 | 45 | self.request = request |
paul@29 | 46 | self.page = Page(request, pagename) |
paul@126 | 47 | self.message = None |
paul@125 | 48 | self.new_user = None |
paul@83 | 49 | |
paul@83 | 50 | def init_store(self): |
paul@29 | 51 | self.store = ItemStore(self.page, "messages", "message-locks") |
paul@29 | 52 | |
paul@125 | 53 | def do_as_user(self, new_user, fn, args): |
paul@125 | 54 | |
paul@125 | 55 | "As 'new_user', perform 'fn' using the given 'args'." |
paul@125 | 56 | |
paul@125 | 57 | request = self.request |
paul@125 | 58 | |
paul@125 | 59 | # Switch to the relaying user if necessary. |
paul@125 | 60 | |
paul@125 | 61 | if new_user: |
paul@125 | 62 | user = request.user |
paul@125 | 63 | request.user = get_user(request, new_user) or user |
paul@125 | 64 | |
paul@125 | 65 | # Handle the content. |
paul@125 | 66 | |
paul@125 | 67 | try: |
paul@125 | 68 | fn(*args) |
paul@125 | 69 | finally: |
paul@125 | 70 | if new_user: |
paul@125 | 71 | request.user = user |
paul@125 | 72 | |
paul@29 | 73 | def do_action(self): |
paul@29 | 74 | request = self.request |
paul@29 | 75 | content_length = getHeader(request, "Content-Length", "HTTP") |
paul@29 | 76 | if content_length: |
paul@29 | 77 | content_length = int(content_length) |
paul@29 | 78 | |
paul@29 | 79 | self.handle_message_text(request.read(content_length)) |
paul@29 | 80 | |
paul@29 | 81 | def handle_message_text(self, message_text): |
paul@29 | 82 | |
paul@29 | 83 | "Handle the given 'message_text'." |
paul@29 | 84 | |
paul@83 | 85 | request = self.request |
paul@91 | 86 | message = Parser().parsestr(message_text) |
paul@83 | 87 | |
paul@83 | 88 | # Detect any indicated recipient and change the target page, if |
paul@83 | 89 | # appropriate. |
paul@83 | 90 | |
paul@122 | 91 | new_user = None |
paul@122 | 92 | |
paul@83 | 93 | if message.has_key("To"): |
paul@83 | 94 | try: |
paul@83 | 95 | parameters = get_recipient_details(request, message["To"], main=True) |
paul@83 | 96 | except MoinMessageRecipientError, exc: |
paul@120 | 97 | |
paul@120 | 98 | # Reject missing recipients if being strict and not relying only |
paul@120 | 99 | # on signatures and user actions. |
paul@120 | 100 | |
paul@120 | 101 | if getattr(request, "moinmessage_reject_missing_global_recipients", False): |
paul@120 | 102 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@120 | 103 | request.write("The recipient indicated in the message is not known to this site. " |
paul@120 | 104 | "Details: %s" % exc.message) |
paul@120 | 105 | return |
paul@83 | 106 | else: |
paul@122 | 107 | # Recipients with pages can have their messages redirected to |
paul@122 | 108 | # those pages. |
paul@122 | 109 | |
paul@83 | 110 | if parameters["type"] == "page": |
paul@83 | 111 | self.page = Page(request, parameters["location"]) |
paul@83 | 112 | |
paul@122 | 113 | # Recipients with URLs can have their messages forwarded. |
paul@122 | 114 | |
paul@122 | 115 | # Recipients accessible via relays have their messages |
paul@122 | 116 | # forwarded. |
paul@122 | 117 | |
paul@122 | 118 | elif parameters["type"] in ("url", "relay"): |
paul@122 | 119 | |
paul@122 | 120 | # Get the relaying user page and select it. |
paul@122 | 121 | |
paul@122 | 122 | relaying_user = getattr(self.request.cfg, "moinmessage_gpg_relaying_user") |
paul@122 | 123 | relaying_page = relaying_user and get_local_homepage(request, relaying_user) |
paul@122 | 124 | |
paul@122 | 125 | if not relaying_page: |
paul@122 | 126 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@122 | 127 | request.write("This site is not able to forward the message to the recipient.") |
paul@122 | 128 | return |
paul@122 | 129 | |
paul@122 | 130 | self.page = Page(request, relaying_page) |
paul@122 | 131 | new_user = relaying_user |
paul@122 | 132 | |
paul@122 | 133 | # The stored messages should then be processed |
paul@122 | 134 | # asynchronously. |
paul@122 | 135 | |
paul@122 | 136 | # NOTE: An action should be able to process stored messages, |
paul@122 | 137 | # NOTE: by invoking code that is also used asynchronously. |
paul@122 | 138 | |
paul@126 | 139 | # Remember the original message for later processing. |
paul@126 | 140 | |
paul@126 | 141 | self.message = message |
paul@125 | 142 | self.new_user = new_user |
paul@83 | 143 | |
paul@83 | 144 | # Handle the parsed message. |
paul@83 | 145 | |
paul@125 | 146 | self.init_store() |
paul@125 | 147 | self.handle_message(message) |
paul@29 | 148 | |
paul@29 | 149 | def handle_message(self, message): |
paul@29 | 150 | |
paul@29 | 151 | "Handle the given 'message'." |
paul@29 | 152 | |
paul@29 | 153 | # Detect PGP/GPG-encoded payloads. |
paul@29 | 154 | # See: http://tools.ietf.org/html/rfc3156 |
paul@29 | 155 | |
paul@64 | 156 | # Signed payloads are checked and then passed on for further processing |
paul@64 | 157 | # elsewhere. Verification is the last step in this base implementation, |
paul@64 | 158 | # even if an encrypted-then-signed payload is involved. |
paul@64 | 159 | |
paul@33 | 160 | if is_signed(message): |
paul@29 | 161 | self.handle_signed_message(message) |
paul@64 | 162 | |
paul@64 | 163 | # Encrypted payloads are decrypted and then sent back into this method |
paul@64 | 164 | # for signature checking as described above. Thus, signed-then-encrypted |
paul@64 | 165 | # payloads are first decrypted and then verified. |
paul@64 | 166 | |
paul@33 | 167 | elif is_encrypted(message): |
paul@29 | 168 | self.handle_encrypted_message(message) |
paul@29 | 169 | |
paul@33 | 170 | # Reject unsigned and unencrypted payloads. |
paul@29 | 171 | |
paul@29 | 172 | else: |
paul@33 | 173 | request = self.request |
paul@29 | 174 | writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") |
paul@29 | 175 | request.write("Only PGP/GPG-signed payloads are supported.") |
paul@29 | 176 | |
paul@29 | 177 | def handle_encrypted_message(self, message): |
paul@29 | 178 | |
paul@29 | 179 | "Handle the given encrypted 'message'." |
paul@29 | 180 | |
paul@29 | 181 | request = self.request |
paul@29 | 182 | |
paul@29 | 183 | homedir = self.get_homedir() |
paul@29 | 184 | if not homedir: |
paul@84 | 185 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@84 | 186 | request.write("This site is not configured for this request.") |
paul@29 | 187 | return |
paul@29 | 188 | |
paul@29 | 189 | gpg = GPG(homedir) |
paul@29 | 190 | |
paul@33 | 191 | try: |
paul@33 | 192 | text = gpg.decryptMessage(message) |
paul@29 | 193 | |
paul@33 | 194 | # Reject messages without a declaration. |
paul@29 | 195 | |
paul@33 | 196 | except MoinMessageMissingPart: |
paul@33 | 197 | writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") |
paul@33 | 198 | request.write("There must be a declaration and a content part for encrypted uploads.") |
paul@33 | 199 | return |
paul@33 | 200 | |
paul@33 | 201 | # Reject messages without appropriate content. |
paul@29 | 202 | |
paul@33 | 203 | except MoinMessageBadContent: |
paul@33 | 204 | writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") |
paul@33 | 205 | request.write("Encrypted data must be provided as application/octet-stream.") |
paul@33 | 206 | return |
paul@29 | 207 | |
paul@120 | 208 | # Reject any undecryptable message. |
paul@29 | 209 | |
paul@29 | 210 | except MoinMessageError: |
paul@29 | 211 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@29 | 212 | request.write("The message could not be decrypted.") |
paul@33 | 213 | return |
paul@33 | 214 | |
paul@33 | 215 | # Log non-fatal errors. |
paul@33 | 216 | |
paul@33 | 217 | if gpg.errors: |
paul@33 | 218 | getLogger(__name__).warning(gpg.errors) |
paul@33 | 219 | |
paul@33 | 220 | # Handle the embedded message which may itself be a signed message. |
paul@33 | 221 | |
paul@33 | 222 | self.handle_message_text(text) |
paul@29 | 223 | |
paul@29 | 224 | def handle_signed_message(self, message): |
paul@29 | 225 | |
paul@29 | 226 | "Handle the given signed 'message'." |
paul@29 | 227 | |
paul@29 | 228 | request = self.request |
paul@29 | 229 | |
paul@36 | 230 | # Accept any message whose sender was authenticated by the PGP method. |
paul@33 | 231 | |
paul@125 | 232 | if request.user and request.user.valid and request.user.auth_method == "pgp": |
paul@33 | 233 | |
paul@36 | 234 | # Handle the embedded message. |
paul@29 | 235 | |
paul@36 | 236 | content, signature = getContentAndSignature(message) |
paul@105 | 237 | self.handle_message_content(content) |
paul@29 | 238 | |
paul@33 | 239 | # Reject any unverified message. |
paul@29 | 240 | |
paul@36 | 241 | else: |
paul@29 | 242 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@39 | 243 | request.write("The message could not be verified. " |
paul@39 | 244 | "Maybe this site is not performing authentication using PGP signatures.") |
paul@29 | 245 | |
paul@29 | 246 | def handle_message_content(self, content): |
paul@29 | 247 | |
paul@29 | 248 | "Handle the given message 'content'." |
paul@29 | 249 | |
paul@30 | 250 | request = self.request |
paul@30 | 251 | |
paul@30 | 252 | # Interpret the content as one or more updates. |
paul@30 | 253 | |
paul@30 | 254 | message = Message() |
paul@30 | 255 | message.handle_message(content) |
paul@30 | 256 | |
paul@123 | 257 | # Test any date against the page or message store (if not empty). |
paul@30 | 258 | |
paul@30 | 259 | if message.date: |
paul@30 | 260 | store_date = time.gmtime(self.store.mtime()) |
paul@120 | 261 | page_date = time.gmtime(version2timestamp(self.page.mtime_usecs())) |
paul@123 | 262 | |
paul@123 | 263 | if len(self.store) > 0: |
paul@123 | 264 | last_date = max(store_date, page_date) |
paul@123 | 265 | else: |
paul@123 | 266 | last_date = page_date |
paul@30 | 267 | |
paul@30 | 268 | # Reject messages older than the page date. |
paul@30 | 269 | |
paul@103 | 270 | if message.date.to_utc().as_tuple() < last_date: |
paul@30 | 271 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@30 | 272 | request.write("The message is too old: %s versus %s." % (message.date, last_date)) |
paul@30 | 273 | return |
paul@30 | 274 | |
paul@30 | 275 | # Reject messages without dates if so configured. |
paul@30 | 276 | |
paul@30 | 277 | elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): |
paul@30 | 278 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@30 | 279 | request.write("The message has no date information.") |
paul@30 | 280 | return |
paul@30 | 281 | |
paul@30 | 282 | # Handle the message as an object. |
paul@30 | 283 | |
paul@30 | 284 | self.handle_message_object(message) |
paul@29 | 285 | |
paul@29 | 286 | def get_homedir(self): |
paul@29 | 287 | |
paul@29 | 288 | "Locate the GPG home directory." |
paul@29 | 289 | |
paul@34 | 290 | request = self.request |
paul@34 | 291 | homedir = get_homedir(self.request) |
paul@34 | 292 | |
paul@29 | 293 | if not homedir: |
paul@29 | 294 | writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") |
paul@29 | 295 | request.write("Encoded data cannot currently be understood. Please notify the site administrator.") |
paul@34 | 296 | |
paul@29 | 297 | return homedir |
paul@29 | 298 | |
paul@85 | 299 | def can_perform_action(self, action): |
paul@85 | 300 | |
paul@85 | 301 | """ |
paul@85 | 302 | Determine whether the user in the request has the necessary privileges |
paul@85 | 303 | to change the current page using a message requesting the given |
paul@85 | 304 | 'action'. |
paul@85 | 305 | """ |
paul@85 | 306 | |
paul@85 | 307 | for identifier in get_update_actions_for_user(self.request): |
paul@85 | 308 | |
paul@85 | 309 | # Expect "action:pagename", rejecting ill-formed identifiers. |
paul@85 | 310 | |
paul@85 | 311 | details = identifier.split(":", 1) |
paul@85 | 312 | if len(details) != 2: |
paul@85 | 313 | continue |
paul@85 | 314 | |
paul@85 | 315 | # If the action and page name match, return success. |
paul@85 | 316 | |
paul@85 | 317 | permitted, pagename = details |
paul@85 | 318 | if permitted.lower() == action.lower() and pagename == self.page.page_name: |
paul@85 | 319 | return True |
paul@85 | 320 | |
paul@85 | 321 | return False |
paul@85 | 322 | |
paul@85 | 323 | # More specific errors. |
paul@85 | 324 | |
paul@83 | 325 | class MoinMessageRecipientError(MoinMessageError): |
paul@83 | 326 | pass |
paul@83 | 327 | |
paul@83 | 328 | class MoinMessageNoRecipients(MoinMessageRecipientError): |
paul@83 | 329 | pass |
paul@83 | 330 | |
paul@83 | 331 | class MoinMessageUnknownRecipient(MoinMessageRecipientError): |
paul@83 | 332 | pass |
paul@83 | 333 | |
paul@83 | 334 | class MoinMessageBadRecipient(MoinMessageRecipientError): |
paul@83 | 335 | pass |
paul@83 | 336 | |
paul@85 | 337 | # Utility functions. |
paul@85 | 338 | |
paul@122 | 339 | def get_user(request, username): |
paul@122 | 340 | |
paul@122 | 341 | "Return the user having the given 'username'." |
paul@122 | 342 | |
paul@122 | 343 | uid = user.getUserId(request, username) |
paul@122 | 344 | |
paul@122 | 345 | # If the user does not exist, just return None. |
paul@122 | 346 | |
paul@122 | 347 | if not uid: |
paul@122 | 348 | return None |
paul@122 | 349 | |
paul@122 | 350 | # Otherwise, return the requested user. |
paul@122 | 351 | |
paul@122 | 352 | return user.User(request, uid) |
paul@122 | 353 | |
paul@34 | 354 | def get_homedir(request): |
paul@34 | 355 | |
paul@34 | 356 | "Locate the GPG home directory." |
paul@34 | 357 | |
paul@34 | 358 | return getattr(request.cfg, "moinmessage_gpg_homedir") |
paul@34 | 359 | |
paul@60 | 360 | def get_signing_users(request): |
paul@60 | 361 | |
paul@60 | 362 | "Return a dictionary mapping usernames to signing keys." |
paul@60 | 363 | |
paul@60 | 364 | return getWikiDict( |
paul@60 | 365 | getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), |
paul@60 | 366 | request) |
paul@60 | 367 | |
paul@68 | 368 | def get_relays(request): |
paul@68 | 369 | |
paul@68 | 370 | "Return a dictionary mapping relays to URLs." |
paul@68 | 371 | |
paul@68 | 372 | return getWikiDict( |
paul@68 | 373 | getattr(request.cfg, "moinmessage_gpg_relays_page", "MoinMessageRelayDict"), |
paul@68 | 374 | request) |
paul@68 | 375 | |
paul@84 | 376 | def get_recipients(request, main=False, sending=True, fetching=True): |
paul@60 | 377 | |
paul@60 | 378 | """ |
paul@60 | 379 | Return the recipients dictionary by first obtaining the page in which it |
paul@60 | 380 | is stored. This page may either be a subpage of the user's home page, if |
paul@60 | 381 | stored on this wiki, or it may be relative to the site root. |
paul@60 | 382 | |
paul@83 | 383 | When 'main' is specified and set to a true value, only a dictionary under |
paul@83 | 384 | the site root is consulted. |
paul@83 | 385 | |
paul@84 | 386 | When 'sending' or 'fetching' is specified and set to a false value, any |
paul@84 | 387 | recipients of the indicated type will be excluded from the result of this |
paul@84 | 388 | function. |
paul@84 | 389 | |
paul@60 | 390 | The name of the subpage is defined by the configuration setting |
paul@60 | 391 | 'moinmessage_gpg_recipients_page', which if absent is set to |
paul@60 | 392 | "MoinMessageRecipientsDict". |
paul@60 | 393 | """ |
paul@60 | 394 | |
paul@60 | 395 | subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") |
paul@83 | 396 | |
paul@83 | 397 | if not main: |
paul@122 | 398 | homepage = get_local_homepage(request, None) |
paul@122 | 399 | recipients = getWikiDict("%s/%s" % (homepage, subpage), request) |
paul@122 | 400 | if recipients: |
paul@122 | 401 | return filter_recipients(recipients, sending, fetching) |
paul@60 | 402 | |
paul@84 | 403 | return filter_recipients(getWikiDict(subpage, request), sending, fetching) |
paul@60 | 404 | |
paul@122 | 405 | def get_local_homepage(request, username): |
paul@122 | 406 | |
paul@122 | 407 | """ |
paul@122 | 408 | Using the 'request', return the homepage for the given 'username' or None if |
paul@122 | 409 | no local homepage exists for the user. If 'username' is None, the current |
paul@122 | 410 | user's homepage is located. |
paul@122 | 411 | """ |
paul@122 | 412 | |
paul@122 | 413 | homedetails = getInterwikiHomePage(request, username) |
paul@122 | 414 | |
paul@122 | 415 | if homedetails: |
paul@122 | 416 | homewiki, homepage = homedetails |
paul@122 | 417 | if homewiki == "Self": |
paul@122 | 418 | return homepage |
paul@122 | 419 | |
paul@122 | 420 | return None |
paul@122 | 421 | |
paul@80 | 422 | def get_username_for_fingerprint(request, fingerprint): |
paul@80 | 423 | |
paul@80 | 424 | """ |
paul@80 | 425 | Using the 'request', return the username corresponding to the given key |
paul@80 | 426 | 'fingerprint' or None if no correspondence is present in the mapping page. |
paul@80 | 427 | """ |
paul@80 | 428 | |
paul@85 | 429 | # Since this function must be able to work before any user has been |
paul@85 | 430 | # identified, the wikidict operation uses superuser privileges. |
paul@85 | 431 | |
paul@80 | 432 | gpg_users = getWikiDict( |
paul@80 | 433 | getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), |
paul@80 | 434 | request, |
paul@85 | 435 | superuser=True |
paul@80 | 436 | ) |
paul@80 | 437 | |
paul@80 | 438 | if gpg_users and gpg_users.has_key(fingerprint): |
paul@80 | 439 | return gpg_users[fingerprint] |
paul@80 | 440 | else: |
paul@80 | 441 | return None |
paul@80 | 442 | |
paul@85 | 443 | def get_update_actions_for_user(request): |
paul@85 | 444 | |
paul@85 | 445 | """ |
paul@85 | 446 | For the user associated with the 'request', return the permitted actions for |
paul@117 | 447 | the user in the form of a list of "action:pagename" identifiers. |
paul@85 | 448 | """ |
paul@85 | 449 | |
paul@85 | 450 | if not request.user or not request.user.valid: |
paul@85 | 451 | return [] |
paul@85 | 452 | |
paul@85 | 453 | actions = getWikiDict( |
paul@85 | 454 | getattr(request.cfg, "moinmessage_user_actions_page", "MoinMessageUserActionsDict"), |
paul@85 | 455 | request |
paul@85 | 456 | ) |
paul@85 | 457 | |
paul@85 | 458 | username = request.user.name |
paul@85 | 459 | |
paul@85 | 460 | if actions and actions.has_key(username): |
paul@85 | 461 | return getIdentifiers(actions[username]) |
paul@85 | 462 | else: |
paul@85 | 463 | return [] |
paul@85 | 464 | |
paul@84 | 465 | def get_recipient_details(request, recipient, main=False, fetching=False): |
paul@83 | 466 | |
paul@83 | 467 | """ |
paul@83 | 468 | Using the 'request', return a dictionary of details for the specified |
paul@83 | 469 | 'recipient'. If no details exist, raise a MoinMessageRecipientError |
paul@83 | 470 | exception. |
paul@83 | 471 | |
paul@83 | 472 | When 'main' is specified and set to a true value, only the recipients |
paul@83 | 473 | dictionary under the site root is consulted. |
paul@84 | 474 | |
paul@84 | 475 | When 'fetching' is specified and set to a true value, the recipient need |
paul@84 | 476 | not have a "type" or "location" defined, but it must have a "fingerprint" |
paul@84 | 477 | defined. |
paul@83 | 478 | """ |
paul@83 | 479 | |
paul@83 | 480 | _ = request.getText |
paul@83 | 481 | |
paul@83 | 482 | recipients = get_recipients(request, main) |
paul@83 | 483 | if not recipients: |
paul@83 | 484 | raise MoinMessageNoRecipients, _("No recipients page is defined for MoinMessage.") |
paul@83 | 485 | |
paul@83 | 486 | recipient_details = recipients.get(recipient) |
paul@83 | 487 | if not recipient_details: |
paul@83 | 488 | raise MoinMessageUnknownRecipient, _("The specified recipient is not present in the list of known contacts.") |
paul@83 | 489 | |
paul@117 | 490 | parameters = parseDictEntry(recipient_details, RECIPIENT_PARAMETERS) |
paul@83 | 491 | |
paul@84 | 492 | type = parameters.get("type") |
paul@84 | 493 | location = parameters.get("location") |
paul@84 | 494 | fingerprint = parameters.get("fingerprint") |
paul@84 | 495 | |
paul@84 | 496 | if type in (None, "none") and not fetching: |
paul@83 | 497 | raise MoinMessageBadRecipient, _("The recipient details are missing a destination type.") |
paul@83 | 498 | |
paul@84 | 499 | if type not in (None, "none") and not location: |
paul@83 | 500 | raise MoinMessageBadRecipient, _("The recipient details are missing a location for sent messages.") |
paul@83 | 501 | |
paul@84 | 502 | if type in ("url", "relay", None, "none") and not fingerprint: |
paul@83 | 503 | raise MoinMessageBadRecipient, _("The recipient details are missing a fingerprint for sending messages.") |
paul@83 | 504 | |
paul@83 | 505 | return parameters |
paul@83 | 506 | |
paul@84 | 507 | def filter_recipients(recipients, sending, fetching): |
paul@84 | 508 | |
paul@84 | 509 | """ |
paul@84 | 510 | Return a copy of the given 'recipients' dictionary retaining all entries |
paul@84 | 511 | that apply to the given 'sending' and 'fetching' criteria. |
paul@84 | 512 | """ |
paul@84 | 513 | |
paul@84 | 514 | result = {} |
paul@117 | 515 | for recipient, details in recipients.items(): |
paul@117 | 516 | parameters = parseDictEntry(details, RECIPIENT_PARAMETERS) |
paul@117 | 517 | |
paul@84 | 518 | if not fetching and parameters.get("type") in (None, "none"): |
paul@84 | 519 | continue |
paul@84 | 520 | if not sending and not parameters.get("fingerprint"): |
paul@84 | 521 | continue |
paul@117 | 522 | |
paul@117 | 523 | result[recipient] = details |
paul@117 | 524 | |
paul@84 | 525 | return result |
paul@84 | 526 | |
paul@120 | 527 | # Access to static Moin content. |
paul@120 | 528 | |
paul@120 | 529 | htdocs = None |
paul@120 | 530 | |
paul@120 | 531 | def get_htdocs(request): |
paul@120 | 532 | |
paul@120 | 533 | "Use the 'request' to find the htdocs directory." |
paul@120 | 534 | |
paul@120 | 535 | global htdocs |
paul@120 | 536 | htdocs = getStaticContentDirectory(request) |
paul@120 | 537 | |
paul@120 | 538 | if not htdocs: |
paul@120 | 539 | htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None) |
paul@120 | 540 | if htdocs_in_cfg and exists(htdocs_in_cfg): |
paul@120 | 541 | htdocs = htdocs_in_cfg |
paul@120 | 542 | return htdocs |
paul@120 | 543 | |
paul@120 | 544 | return htdocs |
paul@120 | 545 | |
paul@120 | 546 | # Special message formatters. |
paul@120 | 547 | |
paul@120 | 548 | def unquoteWikinameURL(url, charset=config.charset): |
paul@120 | 549 | |
paul@120 | 550 | """ |
paul@120 | 551 | The inverse of wikiutil.quoteWikinameURL, returning the page name referenced |
paul@120 | 552 | by the given 'url', with the page name assumed to be encoded using the given |
paul@120 | 553 | 'charset' (or default charset if omitted). |
paul@120 | 554 | """ |
paul@120 | 555 | |
paul@120 | 556 | return unicode(urllib.unquote(url), encoding=charset) |
paul@120 | 557 | |
paul@120 | 558 | def getAttachmentFromURL(url, request): |
paul@120 | 559 | |
paul@120 | 560 | """ |
paul@120 | 561 | Return a (full path, attachment filename) tuple for the attachment |
paul@120 | 562 | referenced by the given 'url', using the 'request' to interpret the |
paul@120 | 563 | structure of 'url'. |
paul@120 | 564 | |
paul@120 | 565 | If 'url' does not refer to an attachment on this wiki, None is returned. |
paul@120 | 566 | """ |
paul@120 | 567 | |
paul@120 | 568 | # Detect static resources. |
paul@120 | 569 | |
paul@120 | 570 | htdocs_dir = get_htdocs(request) |
paul@120 | 571 | |
paul@120 | 572 | if htdocs_dir: |
paul@120 | 573 | prefix = request.cfg.url_prefix_static |
paul@120 | 574 | |
paul@120 | 575 | # Normalise the |
paul@120 | 576 | |
paul@120 | 577 | if not prefix.endswith("/"): |
paul@120 | 578 | prefix += "/" |
paul@120 | 579 | |
paul@120 | 580 | if url.startswith(prefix): |
paul@120 | 581 | filename = url[len(prefix):] |
paul@120 | 582 | |
paul@120 | 583 | # Obtain the resource path. |
paul@120 | 584 | |
paul@120 | 585 | path = abspath(join(htdocs_dir, filename)) |
paul@120 | 586 | |
paul@120 | 587 | if exists(path): |
paul@120 | 588 | return path, taintfilename(filename) |
paul@120 | 589 | |
paul@120 | 590 | # Detect attachments and other resources. |
paul@120 | 591 | |
paul@120 | 592 | script = request.getScriptname() |
paul@120 | 593 | |
paul@120 | 594 | # Normalise the URL. |
paul@120 | 595 | |
paul@120 | 596 | if not script.endswith("/"): |
paul@120 | 597 | script += "/" |
paul@120 | 598 | |
paul@120 | 599 | # Reject URLs outside the wiki. |
paul@120 | 600 | |
paul@120 | 601 | if not url.startswith(script): |
paul@120 | 602 | return None |
paul@120 | 603 | |
paul@120 | 604 | path = url[len(script):].lstrip("/") |
paul@120 | 605 | try: |
paul@120 | 606 | qpagename, qs = path.split("?", 1) |
paul@120 | 607 | except ValueError: |
paul@120 | 608 | qpagename = path |
paul@120 | 609 | qs = None |
paul@120 | 610 | |
paul@120 | 611 | pagename = unquoteWikinameURL(qpagename) |
paul@120 | 612 | qs = qs and parseQueryString(qs) or {} |
paul@120 | 613 | |
paul@120 | 614 | filename = qs.get("target") or qs.get("drawing") |
paul@120 | 615 | filename = taintfilename(filename) |
paul@120 | 616 | |
paul@120 | 617 | # Obtain the attachment path. |
paul@120 | 618 | |
paul@120 | 619 | path = AttachFile.getFilename(request, pagename, filename) |
paul@120 | 620 | return path, filename |
paul@120 | 621 | |
paul@120 | 622 | class OutgoingHTMLFormatter(text_html.Formatter): |
paul@120 | 623 | |
paul@120 | 624 | """ |
paul@120 | 625 | Handle outgoing HTML content by identifying attachments and rewriting their |
paul@120 | 626 | locations. References to bundled attachments are done using RFC 2111: |
paul@120 | 627 | |
paul@120 | 628 | https://tools.ietf.org/html/rfc2111 |
paul@120 | 629 | |
paul@120 | 630 | Messages employing references between parts are meant to comply with RFC |
paul@120 | 631 | 2387: |
paul@120 | 632 | |
paul@120 | 633 | https://tools.ietf.org/html/rfc2387 |
paul@120 | 634 | """ |
paul@120 | 635 | |
paul@120 | 636 | def __init__(self, request, **kw): |
paul@120 | 637 | text_html.Formatter.__init__(self, request, **kw) |
paul@120 | 638 | self.attachments = [] |
paul@120 | 639 | |
paul@120 | 640 | def add_attachment(self, location): |
paul@120 | 641 | details = getAttachmentFromURL(location, self.request) |
paul@120 | 642 | if details: |
paul@120 | 643 | pos = len(self.attachments) |
paul@120 | 644 | self.attachments.append(details) |
paul@120 | 645 | return "cid:attachment%d" % pos |
paul@120 | 646 | else: |
paul@120 | 647 | return None |
paul@120 | 648 | |
paul@120 | 649 | def image(self, src=None, **kw): |
paul@120 | 650 | src = src or kw.get("src") |
paul@120 | 651 | ref = src and self.add_attachment(src) |
paul@120 | 652 | return text_html.Formatter.image(self, ref or src, **kw) |
paul@120 | 653 | |
paul@120 | 654 | def transclusion(self, on, **kw): |
paul@120 | 655 | if on: |
paul@120 | 656 | data = kw.get("data") |
paul@120 | 657 | kw["data"] = data and self.add_attachment(data) |
paul@120 | 658 | return text_html.Formatter.transclusion(self, on, **kw) |
paul@120 | 659 | |
paul@29 | 660 | # vim: tabstop=4 expandtab shiftwidth=4 |