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