diff options
Diffstat (limited to 'Mailman/Cgi/options.py')
| -rw-r--r-- | Mailman/Cgi/options.py | 540 |
1 files changed, 506 insertions, 34 deletions
diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py index fa8d452ff..23cf71bd3 100644 --- a/Mailman/Cgi/options.py +++ b/Mailman/Cgi/options.py @@ -14,18 +14,11 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""Produce user options form, from options.html template. - -Takes listname/userid in PATH_INFO, expecting an `obscured' userid. Depending -on the Utils.{O,Uno}bscureEmail utilities tolerance, will work fine with an -unobscured ids as well. - -""" - -# We don't need to lock in this script, because we're never going to change -# data. +"""Produce and handle the member options.""" import os +import cgi +import signal from Mailman import mm_cfg from Mailman import Utils @@ -36,6 +29,7 @@ from Mailman.htmlformat import * from Mailman.Logging.Syslog import syslog SLASH = '/' +SETLANGUAGE = -1 # Set up i18n _ = i18n._ @@ -49,8 +43,8 @@ def main(): parts = Utils.GetPathPieces() if not parts or len(parts) < 2: - doc.AddItem(Header(2, _("Error"))) - doc.AddItem(Bold(_("Invalid options to CGI script."))) + doc.AddItem(Header(2, _('Error'))) + doc.AddItem(Bold(_('Invalid options to CGI script.'))) print doc.Format() return @@ -74,13 +68,11 @@ def main(): # Sanity check the user user = Utils.UnobscureEmail(SLASH.join(parts[1:])) user = Utils.LCDomain(user) - if not mlist.members.has_key(user) and \ - not mlist.digest_members.has_key(user): - # then + if not mlist.IsMember(user): doc.AddItem(Header(2, _("Error"))) doc.AddItem(Bold(_('%(listname)s: No such member %(user)s.'))) doc.AddItem(mlist.GetMailmanFooter()) - print doc.Format(bgcolor='#ffffff') + print doc.Format() return # Find the case preserved email address (the one the user subscribed with) @@ -88,12 +80,6 @@ def main(): cpuser = mlist.GetUserSubscribedAddress(lcuser) if lcuser == cpuser: cpuser = None - if mlist.obscure_addresses: - presentable_user = Utils.ObscureEmail(user, for_text=1) - if cpuser is not None: - cpuser = Utils.ObscureEmail(cpuser, for_text=1) - else: - presentable_user = user # And now we know the user making the request, so set things up for the # user's preferred language. @@ -101,8 +87,334 @@ def main(): doc.set_language(userlang) i18n.set_language(userlang) + # Are we processing an unsubscription request from the login screen? + cgidata = cgi.FieldStorage() + if cgidata.has_key('login-unsub'): + # Because they can't supply a password for unsubscribing, we'll need + # to do the confirmation dance. + mlist.ConfirmUnsubscription(user, userlang) + add_error_message( + doc, + _('The confirmation email has been sent.'), + tag='') + loginpage(mlist, doc, user, cgidata) + print doc.Format() + return + + # Are we processing a password reminder from the login screen? + if cgidata.has_key('login-remind'): + mlist.MailUserPassword(user) + add_error_message( + doc, + _('A reminder of your password has been emailed to you.'), + tag='') + loginpage(mlist, doc, user, cgidata) + print doc.Format() + return + + # Authenticate, possibly using the password supplied in the login page + password = cgidata.getvalue('password', '').strip() + + if not mlist.WebAuthenticate((mm_cfg.AuthUser, + mm_cfg.AuthListAdmin, + mm_cfg.AuthSiteAdmin), + password, user): + # Not authenticated, so throw up the login page again. If they tried + # to authenticate via cgi (instead of cookie), then print an error + # message. + if cgidata.has_key('login'): + add_error_message(doc, _('Authentication failed.'), tag='Error: ') + + loginpage(mlist, doc, user, cgidata) + print doc.Format() + return + + # From here on out, the user is okay to view and modify their membership + # options. The first set of checks does not require the list to be + # locked. + + if cgidata.has_key('logout'): + print mlist.ZapCookie(mm_cfg.AuthUser, user) + loginpage(mlist, doc, user, cgidata) + print doc.Format() + return + + if cgidata.has_key('emailpw'): + mlist.MailUserPassword(user) + options_page( + mlist, doc, user, cpuser, userlang, + _('A reminder of your password has been emailed to you.')) + print doc.Format() + return + + if cgidata.has_key('othersubs'): + hostname = mlist.host_name + title = _('List subscriptions for %(user)s on %(hostname)s') + doc.SetTitle(title) + doc.AddItem(Header(2, title)) + doc.AddItem(_('''Click on a link to visit your options page for the + requested mailing list.''')) + + # Troll through all the mailing lists that match host_name and see if + # the user is a member. If so, add it to the list. + onlists = [] + for gmlist in lists_of_member(mlist.host_name, user): + url = gmlist.GetOptionsURL(user) + link = Link(url, gmlist.real_name) + onlists.append((gmlist.real_name, link)) + onlists.sort() + items = OrderedList(*[link for name, link in onlists]) + doc.AddItem(items) + print doc.Format() + return + + if cgidata.has_key('change-of-address'): + newaddr = cgidata.getvalue('new-address') + confirmaddr = cgidata.getvalue('confirm-address') + if not newaddr or not confirmaddr: + options_page(mlist, doc, user, cpuser, userlang, + _('Addresses may not be blank')) + print doc.Format() + return + if newaddr <> confirmaddr: + options_page(mlist, doc, user, cpuser, userlang, + _('Addresses did not match!')) + print doc.Format() + return + + # See if the user wants to change their email address globally + globally = cgidata.getvalue('changeaddr-globally') + + # Standard sigterm handler. + def sigterm_handler(signum, frame, mlist=mlist): + mlist.Unlock() + sys.exit(0) + + # Register the pending change after the list is locked + msg = _('A confirmation message has been sent to %(newaddr)s') + mlist.Lock() + try: + try: + signal.signal(signal.SIGTERM, sigterm_handler) + mlist.ChangeMemberAddress(user, newaddr, globally) + mlist.Save() + finally: + mlist.Unlock() + except Errors.MMBadEmailError: + msg = _('Bad email address provided') + except Errors.MMHostileAddress: + msg = _('Illegal email address provided') + except Errors.MMAlreadyAMember: + msg = _('%(newaddr)s is already a member of the list.') + + options_page(mlist, doc, user, cpuser, userlang, msg) + print doc.Format() + return + + if cgidata.has_key('changepw'): + newpw = cgidata.getvalue('newpw') + confirmpw = cgidata.getvalue('confpw') + if not newpw or not confirmpw: + options_page(mlist, doc, user, cpuser, userlang, + _('Passwords may not be blank')) + print doc.Format() + return + if newpw <> confirmpw: + options_page(mlist, doc, user, cpuser, userlang, + _('Passwords did not match!')) + print doc.Format() + return + + # See if the user wants to change their passwords globally + if cgidata.getvalue('pw-globally'): + mlists = lists_of_member(mlist.host_name, user) + else: + mlists = [mlist] + + for gmlist in mlists: + change_password(gmlist, user, newpw, confirmpw) + + # Regenerate the cookie so a re-authorization isn't necessary + print mlist.MakeCookie(mm_cfg.AuthUser, user) + options_page(mlist, doc, user, cpuser, userlang, + _('Password successfully changed.')) + print doc.Format() + return + + if cgidata.has_key('unsub'): + # Was the confirming check box turned on? + if not cgidata.getvalue('unsubconfirm'): + options_page( + mlist, doc, user, cpuser, userlang, + _('''You must confirm your unsubscription request by turning + on the checkbox below the <em>Unsubscribe</em> button. You + have not been unsubscribed!''')) + print doc.Format() + return + + # Standard signal handler + def sigterm_handler(signum, frame, mlist=mlist): + mlist.Unlock() + sys.exit(0) + + # Okay, zap them. Leave them sitting at the list's listinfo page. We + # must own the list lock, and we want to make sure the user (BAW: and + # list admin?) is informed of the removal. + mlist.Lock() + try: + signal.signal(signal.SIGTERM, sigterm_handler) + mlist.DeleteMember(user, _('via the member options page'), + admin_notif=1, userack=1) + mlist.Save() + finally: + mlist.Unlock() + # Now throw up some results page, with appropriate links. We can't + # drop them back into their options page, because that's gone now! + fqdn_listname = mlist.GetListEmail() + owneraddr = mlist.GetOwnerEmail() + url = mlist.GetScriptURL('listinfo', absolute=1) + + title = _('Unsubscription results') + doc.SetTitle(title) + doc.AddItem(Header(2, title)) + doc.AddItem(_("""You have been successfully unsubscribed from the + mailing list %(fqdn_listname)s. If you were receiving digest + deliveries you may get one more digest. If you have any questions + about your unsubscription, please contact the list owners at + %(owneraddr)s.""")) + doc.AddItem(mlist.GetMailmanFooter()) + print doc.Format() + return + + if cgidata.has_key('options-submit'): + # Digest action flags + digestwarn = 0 + cantdigest = 0 + mustdigest = 0 + + newvals = [] + # First figure out which options have changed. The item names come + # from FormatOptionButton() in HTMLFormatter.py + for item, flag in (('digest', mm_cfg.Digests), + ('mime', mm_cfg.DisableMime), + ('dontreceive', mm_cfg.DontReceiveOwnPosts), + ('ackposts', mm_cfg.AcknowledgePosts), + ('disablemail', mm_cfg.DisableDelivery), + ('conceal', mm_cfg.ConcealSubscription), + ('remind', mm_cfg.SuppressPasswordReminder), + ): + try: + newval = int(cgidata.getvalue(item)) + except (TypeError, ValueError): + newval = None + + # Skip this option if there was a problem or it wasn't changed + if newval is None or newval == mlist.GetUserOption(user, flag): + continue + + newvals.append((flag, newval)) + + # The user language is handled a little differently + userlang = cgidata.getvalue('language') + if userlang not in mlist.GetAvailableLanguages(): + newvals.append((SETLANGUAGE, mlist.preferred_language)) + else: + newvals.append((SETLANGUAGE, userlang)) + + # The standard sigterm handler (see above) + def sigterm_handler(signum, frame, mlist=mlist): + mlist.Unlock() + sys.exit(0) + + # Now, lock the list and perform the changes + mlist.Lock() + try: + signal.signal(signal.SIGTERM, sigterm_handler) + # `values' is a tuple of flags and the web values + for flag, newval in newvals: + # Handle language settings differently + if flag == SETLANGUAGE: + mlist.SetPreferredLanguage(user, newval) + continue + + mlist.SetUserOption(user, flag, newval, save_list=0) + + # Digests also need another setting, which is a bit bogus, as + # SetUserOption() should be taught about this special step. + if flag == mm_cfg.Digests: + try: + mlist.SetUserDigest(user, newval) + if newval == 0: + digestwarn = 1 + except (Errors.MMAlreadyDigested, + Errors.MMAlreadyUndigested): + pass + except Errors.MMCantDigestError: + cantdigest = 1 + except Errors.MMMustDigestError: + mustdigest = 1 + # All done + mlist.Save() + finally: + mlist.Unlock() + + # The enable/disable option and the password remind option may have + # their global flags sets. + global_enable = None + if cgidata.getvalue('deliver-globally'): + # Yes, this is inefficient, but the list is so small it shouldn't + # make much of a difference. + for flag, newval in newvals: + if flag == mm_cfg.DisableDelivery: + global_enable = newval + break + + global_remind = None + if cgidata.getvalue('remind-globally'): + for flag, newval in newvals: + if flag == mm_cfg.SuppressPasswordReminder: + global_remind = newval + break + + if global_enable is not None or global_remind is not None: + for gmlist in lists_of_member(mlist.host_name, user): + global_options(gmlist, user, global_enable, global_remind) + + # Now print the results + if cantdigest: + msg = _('''The list administrator has disabled digest delivery for + this list, so your delivery option has not been set. However your + other options have been set successfully.''') + elif mustdigest: + msg = _('''The list administrator has disabled non-digest delivery + for this list, so your delivery option has not been set. However + your other options have been set successfully.''') + else: + msg = _('You have successfully set your options.') + + if digestwarn: + msg += _('You may get one last digest.') + + options_page(mlist, doc, user, cpuser, userlang, msg) + print doc.Format() + return + + options_page(mlist, doc, user, cpuser, userlang) + print doc.Format() + + + +def options_page(mlist, doc, user, cpuser, userlang, message=''): + if mlist.obscure_addresses: + presentable_user = Utils.ObscureEmail(user, for_text=1) + if cpuser is not None: + cpuser = Utils.ObscureEmail(cpuser, for_text=1) + else: + presentable_user = user + # Do replacements replacements = mlist.GetStandardReplacements(userlang) + replacements['<mm-results>'] = Bold(FontSize('+1', message)).Format() replacements['<mm-digest-radio-button>'] = mlist.FormatOptionButton( mm_cfg.Digests, 1, user) replacements['<mm-undigest-radio-button>'] = mlist.FormatOptionButton( @@ -132,30 +444,50 @@ def main(): mlist.FormatOptionButton(mm_cfg.ConcealSubscription, 0, user)) replacements['<mm-hide-subscription-button>'] = mlist.FormatOptionButton( mm_cfg.ConcealSubscription, 1, user) - replacements['<mm-digest-submit>'] = mlist.FormatButton( - 'setdigest', _('Submit My Changes')) replacements['<mm-unsubscribe-button>'] = ( - mlist.FormatButton('unsub', _('Unsubscribe'))) - replacements['<mm-digest-pw-box>'] = mlist.FormatSecureBox('digpw') - replacements['<mm-unsub-pw-box>'] = mlist.FormatSecureBox('upw') - replacements['<mm-old-pw-box>'] = mlist.FormatSecureBox('opw') + mlist.FormatButton('unsub', _('Unsubscribe')) + '<br>' + + CheckBox('unsubconfirm', 1, checked=0).Format() + + '<em>Yes, I really want to unsubscribe</em>') replacements['<mm-new-pass-box>'] = mlist.FormatSecureBox('newpw') replacements['<mm-confirm-pass-box>'] = mlist.FormatSecureBox('confpw') - replacements['<mm-other-subscriptions-pw-box>'] = ( - mlist.FormatSecureBox('othersubspw')) + replacements['<mm-change-pass-button>'] = ( + mlist.FormatButton('changepw', _("Change My Password"))) replacements['<mm-other-subscriptions-submit>'] = ( mlist.FormatButton('othersubs', _('List my other subscriptions'))) - replacements['<mm-change-pass-button>'] = ( - mlist.FormatButton('changepw', _("Change My Password"))) replacements['<mm-form-start>'] = ( - mlist.FormatFormStart('handle_opts', user)) + mlist.FormatFormStart('options', user)) replacements['<mm-user>'] = user replacements['<mm-presentable-user>'] = presentable_user replacements['<mm-email-my-pw>'] = mlist.FormatButton( 'emailpw', (_('Email My Password To Me'))) replacements['<mm-umbrella-notice>'] = ( mlist.FormatUmbrellaNotice(user, _("password"))) + replacements['<mm-logout-button>'] = ( + mlist.FormatButton('logout', _('Log out'))) + replacements['<mm-options-submit-button>'] = mlist.FormatButton( + 'options-submit', _('Submit My Changes')) + replacements['<mm-global-pw-changes-button>'] = ( + CheckBox('pw-globally', 1, checked=0).Format()) + replacements['<mm-global-deliver-button>'] = ( + CheckBox('deliver-globally', 1, checked=0).Format()) + replacements['<mm-global-remind-button>'] = ( + CheckBox('remind-globally', 1, checked=0).Format()) + + days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1)) + if days > 1: + units = _('days') + else: + units = _('day') + replacements['<mm-pending-days>'] = _('%(days)d %(units)s') + + replacements['<mm-new-address-box>'] = mlist.FormatBox('new-address') + replacements['<mm-confirm-address-box>'] = mlist.FormatBox( + 'confirm-address') + replacements['<mm-change-address-button>'] = mlist.FormatButton( + 'change-of-address', _('Change My Address')) + replacements['<mm-global-change-of-address>'] = CheckBox( + 'changeaddr-globally', 1, checked=0).Format() if cpuser is not None: replacements['<mm-case-preserved-user>'] = _(''' @@ -165,4 +497,144 @@ You are subscribed to this list with the case-preserved address replacements['<mm-case-preserved-user>'] = '' doc.AddItem(mlist.ParseTags('options.html', replacements, userlang)) - print doc.Format() + + + +def loginpage(mlist, doc, user, cgidata): + realname = mlist.real_name + obuser = Utils.ObscureEmail(user) + # Set up the login page + form = Form('%s/%s' % (mlist.GetScriptURL('options'), obuser)) + table = Table(width='100%', border=0, cellspacing=4, cellpadding=5) + # Set up the title + title = _('%(realname)s list: member options for user %(user)s') + doc.SetTitle(title) + table.AddRow([Center(Header(2, title))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, + bgcolor=mm_cfg.WEB_HEADERCOLOR) + # Preamble + table.AddRow([_("""In order to change your membership option, you must + first log in by giving your membership password in the section below. If + you don't remember your membership password, you can have it emailed to + you by clicking on the button below. If you just want to unsubscribe from + this list, click on the <em>Unsubscribe</em> button and a confirmation + message will be sent to you. + + <p><strong><em>Important:</em></strong> From this point on, you must have + cookies enabled in your browser, otherwise none of your changes will take + effect. + + <p>Session cookies are used in Mailman's membership options interface so + that you don't need to re-authenticate with every operation. This cookie + will expire automatically when you exit your browser, or you can + explicitly expire the cookie by hitting the <em>Logout</em> link (which + you'll see once you successfully log in). + """)]) + # Password and login button + ptable = Table(width='50%', border=0, cellspacing=4, cellpadding=5) + ptable.AddRow([Label(_('Password:')), + PasswordBox('password', size=20)]) + ptable.AddRow([Center(SubmitButton('login', _('Log in')))]) + ptable.AddCellInfo(ptable.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Center(ptable)]) + # Unsubscribe section, but only if the user didn't just unsubscribe + if not cgidata.has_key('login-unsub'): + table.AddRow([Center(Header(2, _('Unsubscribe')))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, + bgcolor=mm_cfg.WEB_HEADERCOLOR) + + table.AddRow([_("""By clicking on the <em>Unsubscribe</em> button, a + confirmation message will be emailed to you. This message will have a + link that you should click on to complete the removal process (you can + also confirm by email; see the instructions in the confirmation + message).""")]) + + table.AddRow([Center(SubmitButton('login-unsub', _('Unsubscribe')))]) + # Password reminder section, but only if the user didn't just request a + # password reminder + if not cgidata.has_key('login-remind'): + table.AddRow([Center(Header(2, _('Password reminder')))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, + bgcolor=mm_cfg.WEB_HEADERCOLOR) + + table.AddRow([_("""By clicking on the <em>Remind</em> button, your + password will be emailed to you.""")]) + + table.AddRow([Center(SubmitButton('login-remind', _('Remind')))]) + # Finish up glomming together the login page + form.AddItem(table) + doc.AddItem(form) + doc.AddItem(mlist.GetMailmanFooter()) + + + +def add_error_message(doc, errmsg, tag='Warning: ', *args): + doc.AddItem(Header(3, Bold(FontAttr( + _(tag), color="#ff0000", size="+2")).Format() + + Italic(errmsg % args).Format())) + + + +def lists_of_member(hostname, user): + onlists = [] + for listname in Utils.list_names(): + mlist = MailList.MailList(listname, lock=0) + if mlist.host_name <> hostname: + continue + if not mlist.IsMember(user): + continue + onlists.append(mlist) + return onlists + + + +def change_password(mlist, user, newpw, confirmpw): + # This operation requires the list lock, so let's set up the signal + # handling so the list lock will get released when the user hits the + # browser stop button. + def sigterm_handler(signum, frame, mlist=mlist): + # Make sure the list gets unlocked... + mlist.Unlock() + # ...and ensure we exit, otherwise race conditions could cause us to + # enter MailList.Save() while we're in the unlocked state, and that + # could be bad! + sys.exit(0) + + # Must own the list lock! + mlist.Lock() + try: + # Install the emergency shutdown signal handler + signal.signal(signal.SIGTERM, sigterm_handler) + # change the user's password + mlist.ChangeUserPassword(user, newpw, confirmpw) + mlist.Save() + finally: + mlist.Unlock() + + + +def global_options(mlist, user, global_enable, global_remind): + def sigterm_handler(signum, frame, mlist=mlist): + # Make sure the list gets unlocked... + mlist.Unlock() + # ...and ensure we exit, otherwise race conditions could cause us to + # enter MailList.Save() while we're in the unlocked state, and that + # could be bad! + sys.exit(0) + + # Must own the list lock! + mlist.Lock() + try: + # Install the emergency shutdown signal handler + signal.signal(signal.SIGTERM, sigterm_handler) + + if global_enable is not None: + mlist.SetUserOption(user, mm_cfg.DisableDelivery, global_enable) + + if global_remind is not None: + mlist.SetUserOption(user, mm_cfg.SuppressPasswordReminder, + global_remind) + + mlist.Save() + finally: + mlist.Unlock() |
