#!/usr/local/bin/python """Process and produce the list-administration options forms. To run stand-alone for debugging, set env var PATH_INFO to name of list and, optionally, options category.""" __version__ = "$Revision: 383 $" import sys sys.path.append('/home/mailman/mailman/modules') import os, cgi, string, crypt, types import mm_utils, maillist, mm_cfg, mm_err from htmlformat import * try: sys.stderr = mm_utils.StampedLogger("error", label = 'admin', manual_reprime=1, nofail=0) except IOError: pass # Oh well - SOL on redirect, errors show thru. CATEGORIES = [('general', "General Options"), ('members', "Membership Management"), ('privacy', "Privacy Options"), ('nondigest', "Regular-member (non-digest) Options"), ('digest', "Digest-member Options"), ('bounce', "Bounce Options"), ('archive', "Archival Options")] def main(): """Process and produce list options form. CGI input indicates that we're returning from submission of some new settings, which is processed before producing the new version.""" global list_name # For encompassing try/except. doc = Document() path = os.environ['PATH_INFO'] list_info = mm_utils.GetPathPieces(path) if len(list_info) < 1: doc.AddItem(Header(2, "Invalid options to CGI script.")) print doc.Format(bgcolor="#ffffff") sys.exit(0) list_name = string.lower(list_info[0]) list = maillist.MailList(list_name) try: if not (list and list._ready): doc.AddItem(Header(3, "%s: No such list" % list_name)) print doc.Format(bgcolor="#ffffff") sys.exit(0) if len(list_info) == 1: category = 'general' category_suffix = '' else: category = list_info[1] category_suffix = category if category not in map(lambda x: x[0], CATEGORIES): category = 'general' cgi_data = cgi.FieldStorage() if len(cgi_data.keys()): if cgi_data.has_key('VARHELP'): FormatOptionHelp(doc, cgi_data['VARHELP'].value, list) print doc.Format(bgcolor="#ffffff") return if not cgi_data.has_key('adminpw'): AddErrorMessage(doc, 'Error: You must supply the admin password to' ' change options.') else: try: list.ConfirmAdminPassword(cgi_data['adminpw'].value) ChangeOptions(list, category, cgi_data, doc) # Yuck. This shouldn't need to be here. if not list.digestable and not list.nondigestable: list.nondigestable = 1 except mm_err.MMBadPasswordError: AddErrorMessage(doc, 'Error: Incorrect admin password.') if not list.digestable and len(list.digest_members): AddErrorMessage(doc, 'Warning: you have digest members,' ' but digests are turned off.' ' Those people will not receive mail.') if not list.nondigestable and len(list.members): AddErrorMessage(doc, 'Warning: you have list members,' ' but non-digestified mail is turned' ' off. They will receive mail until' ' you fix this problem.') if len(cgi_data.keys()): if (cgi_data.has_key('bounce_matching_headers')): try: pairs = list.parse_matching_header_opt() except mm_err.MMBadConfigError, line: AddErrorMessage(doc, 'Warning: bad matching-header line' ' (does it have the colon?)', line) FormatConfiguration(doc, list, category, category_suffix) print doc.Format(bgcolor="#ffffff") finally: list.Unlock() # Form Production: def FormatConfiguration(doc, list, category, category_suffix): """Produce the overall doc, *except* any processing error messages.""" for k, v in CATEGORIES: if k == category: label = v doc.SetTitle('%s Administration' % list.real_name) doc.AddItem(Center(Header(2, ('%s Maillist Configuration - %s Section' % (list.real_name, label))))) doc.AddItem('
') links_table = Table(valign="top") links_table.AddRow([Center(Bold("Configuration Categories")), Center(Bold("Other Administrative Activities"))]) other_links = UnorderedList() link = Link(list.GetScriptURL('admindb'), 'Tend to pending administrative requests.') other_links.AddItem(link) link = Link(list.GetScriptURL('listinfo'), 'Go to the general list information page.') other_links.AddItem(link) link = Link(list.GetScriptURL('edithtml'), 'Edit the HTML for the public list pages.') other_links.AddItem(link) these_links = UnorderedList() url = list.GetScriptURL('admin') for k, v in CATEGORIES: if k == category: these_links.AddItem(" => " + v + " <= ") else: these_links.AddItem(Link(os.path.join(url, k), v)) links_table.AddRow([these_links, other_links]) links_table.AddRowInfo(max(links_table.GetCurrentRowIndex(), 0), valign="top") doc.AddItem(links_table) doc.AddItem('
') if category_suffix: form = Form(os.path.join(list.GetScriptURL('admin'), category)) else: form = Form(list.GetScriptURL('admin')) doc.AddItem(form) form.AddItem("Make your changes, below, and then submit it all at the" " bottom. (You can also change your password there," " as well.)

") form.AddItem(FormatOptionsSection(category, list)) form.AddItem(Center(FormatPasswordStuff())) form.AddItem(list.GetMailmanFooter()) def FormatOptionsSection(category, list): """Produce the category-specific options table.""" if category == 'members': # Special case for members section. return FormatMembershipOptions(list) options = GetConfigOptions(list, category) big_table = Table(cellspacing=3, cellpadding=4) # Get and portray the text label for the category. for k, v in CATEGORIES: if k == category: label = v big_table.AddRow([Center(Header(2, label))]) big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor="#99ccff") def ColHeader(big_table = big_table): big_table.AddRow([Center(Bold('Description')), Center(Bold('Value'))]) big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0, width="15%") big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 1, width="85%") did_col_header = 0 for item in options: if type(item) == types.StringType: # The very first banner option (string in an options list) is # treated as a general description, while any others are # treated as section headers - centered and italicized... if did_col_header: item = "

" + item + "
" big_table.AddRow([item]) big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0, colspan=2) if not did_col_header: # Do col header after very first string descr, if any... ColHeader() did_col_header = 1 else: if not did_col_header: # ... but do col header before anything else. ColHeader() did_col_header = 1 big_table.AddRow(GetGuiItem(item, category, list)) big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 1, bgcolor="#cccccc") big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0, bgcolor="#cccccc") big_table.AddRow(['
']) big_table.AddCellInfo(big_table.GetCurrentRowIndex(), 0, colspan=2) return big_table def FormatOptionHelp(doc, varref, list): item = bad = None reflist = string.split(varref, '/') if len(reflist) == 2: category, varname = reflist options = GetConfigOptions(list, category) for i in options: if i and i[0] == varname: item = i break if not item: bad = ("Option %s/%s not found. %s" % (category, varname, os.environ['PATH_INFO'])) if len(item) < 6: bad = "Option %s has no extended help." % varname if bad: AddErrorMessage(doc, bad) header = Table(width="100%") legend = ('%s Maillist Configuration Help
%s Option' % (list.real_name, varname)) header.AddRow([Center(Header(3, legend))]) header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor="#99ccff") doc.SetTitle("Mailman %s List Option Help" % varname) doc.AddItem(header) doc.AddItem("%s (%s): %s

" % (varname, category, item[4])) doc.AddItem("%s

" % item[5]) doc.AddItem("Current value:") valbox = Table(border=1, cellpadding=2) val = getattr(list, varname) if type(val) == types.StringType: val = Preformatted(val) valbox.AddRow([val]) doc.AddItem(Center(valbox)) def GetGuiItem(table_entry, category, list): """Return the contents for a table row representing an options item. Elements of the table_entry list are: 0 option-var name 1 type 2 entry size 3 ?dependancies? 4 Brief description 5 OPTIONAL description elaboration """ if len(table_entry) == 5: elaboration = None varname, kind, params, dependancies, descr = table_entry elif len(table_entry) == 6: varname, kind, params, dependancies, descr, elaboration = table_entry else: list.LogMsg("error", "admin: Badly formed options entry:\n %s", table_entry) return Italic("") if kind == mm_cfg.Radio or kind == mm_cfg.Toggle: gui_part = RadioButtonArray(varname, params, getattr(list, varname)) elif (kind == mm_cfg.String or kind == mm_cfg.Email or kind == mm_cfg.Host or kind == mm_cfg.Number): gui_part = TextBox(varname, getattr(list, varname), params) elif kind == mm_cfg.Text: if params: r, c = params else: r, c = None, None val = getattr(list, varname) if not val: val = '' gui_part = TextArea(varname, val, r, c) elif kind == mm_cfg.EmailList: if params: r, c = params else: r, c = None, None res = string.join(getattr(list, varname), '\n') gui_part = TextArea(varname, res, r, c, wrap='off') descr = '

' + descr if elaboration: ref = '?VARHELP=' + category + "/" + varname descr = Container(descr, Link(ref, " (Details)", target="MMHelp"), "
") else: descr = descr + "" return [descr, gui_part] def FormatMembershipOptions(list): container = Container() header = Table(width="100%") header.AddRow([Center(Header(2, "Membership Management"))]) header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor="#99ccff") header.AddRow([Bold("Subscribe and Unsubscribe Members")]) header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor ="#FFF0D0") container.AddItem(header) container.AddItem('

Mass Subscribe Members

') container.AddItem('

Enter one email address per line

') container.AddItem(TextArea(name='subscribees', rows=20,cols=60,wrap=None)) container.AddItem('

To Unsubscribe Members...

') container.AddItem(""" To unsubscribe members you must use your admin password in place of the user's password on the user's edit-options page. Visit their edit-options page (via the roster page) and do the unsubscribe procedure, providing the admin password instead of the user's password.

(Note that you can, alternately, set the subscriber's no-delivery option to inhibit delivery of their messages, if you want to only temporarily disable their delivery.)

""" % list.GetScriptURL('roster')) return container def FormatPasswordStuff(): password_submit = Table(width="100%", bgcolor="#99cccc", border=0, cellspacing=0, cellpadding=2) password_submit.AddRow([Center(Bold('To Submit Your Changes'))]) password_submit.AddCellInfo(password_submit.GetCurrentRowIndex(), 0, colspan=2) password_submit.AddRow(["Enter the Administrator Password:", PasswordBox('adminpw')]) password_submit.AddRow([Center("and..."), Center(Bold(SubmitButton('submit', 'Submit Changes')))]) change_pw_table = Table(width="100%", bgcolor="#cccccc", border=0, cellspacing=0, cellpadding=2) change_pw_table.AddRow([Bold(Center('To Change Your Password'))]) change_pw_table.AddCellInfo(change_pw_table.GetCurrentRowIndex(), 0, colspan=2) change_pw_table.AddRow(['Enter your new password:', PasswordBox('newpw')]) change_pw_table.AddRow(['And also confirm it:', PasswordBox('confirmpw')]) change_pw_table.AddRow([Center('... and then submit your changes, above.')]) change_pw_table.AddCellInfo(change_pw_table.GetCurrentRowIndex(), 0, colspan=2) password_stuff = Table() password_stuff.AddRow([password_submit]) password_stuff.AddRow([change_pw_table]) return password_stuff # XXX klm - looks like turn_on_moderation is orphaned. turn_on_moderation = 0 # Options processing def GetValidValue(list, prop, my_type, val, dependant): if my_type == mm_cfg.Radio or my_type == mm_cfg.Toggle: if type(val) <> types.IntType: try: # XXX Security!? val = eval(val) except: pass # Don't know what to do here... return val elif my_type == mm_cfg.String or my_type == mm_cfg.Text: return val elif my_type == mm_cfg.Email: try: valid = mm_utils.ValidEmail(val) if valid: return val except: pass # Revert to the old value. return getattr(list, prop) elif my_type == mm_cfg.EmailList: def SafeValidAddr(addr): import mm_utils try: valid = mm_utils.ValidEmail(addr) if valid: return 1 else: return 0 except: return 0 val = filter(SafeValidAddr, map(string.strip, string.split(val, '\n'))) if dependant and len(val): # Wait till we've set everything to turn it on, # as we don't want to clobber our special case. # XXX klm - looks like turn_on_moderation is orphaned? turn_on_moderation = 1 return val elif my_type == mm_cfg.Host: return val ## ## This code is sendmail dependant, so we'll just live w/o ## the error checking for now. ## ## # Shouldn't have to read in the whole file. ## file = open('/etc/sendmail.cf', 'r') ## lines = string.split(file.read(), '\n') ## file.close() ## def ConfirmCWEntry(item): ## return item[0:2] == 'Cw' ## lines = filter(ConfirmCWEntry, lines) ## if not len(lines): ## # Revert to the old value. ## return getattr(list, prop) ## for line in lines: ## if string.lower(string.strip(line[2:])) == string.lower(val): ## return val ## return getattr(list, prop) elif my_type == mm_cfg.Number: try: num = eval(val) if num < 0: return getattr(list, prop) return num except: return getattr(list, prop) else: # Should never get here... return val def ChangeOptions(list, category, cgi_info, document): dirty = 0 if category != 'members': opt_list = GetConfigOptions(list, category) for item in opt_list: if len(item) < 5: continue property, kind, args, deps, desc = (item[0], item[1], item[2], item[3], item[4]) if not cgi_info.has_key(property): if (kind <> mm_cfg.Text and kind <> mm_cfg.String and kind <> mm_cfg.EmailList): continue else: val = '' else: val = cgi_info[property].value value = GetValidValue(list, property, kind, val, deps) if getattr(list, property) != value: setattr(list, property, value) dirty = 1 if cgi_info.has_key('subscribees'): name_text = cgi_info['subscribees'].value names = string.split(name_text, '\r\n') for new_name in names: try: #FIXME: The admin needs to be able to specify subscribe options list.AddMember(new_name, (mm_utils.GetRandomSeed() + mm_utils.GetRandomSeed())) dirty = 1 #FIXME: Give some sort of an indication of which names didn't work, # and why they didn't work... except: pass if cgi_info.has_key('newpw'): if cgi_info.has_key('confirmpw'): new = cgi_info['newpw'].value confirm = cgi_info['confirmpw'].value if new == confirm: list.password = crypt.crypt(new, mm_utils.GetRandomSeed()) dirty = 1 else: m = 'Error: Passwords did not match.' document.AddItem( Header(3, Italic(FontAttr(m, color="ff5060")))) else: m = 'Error: You must type in your new password twice.' document.AddItem( Header(3, Italic(FontAttr(m, color="ff5060")))) if dirty: list.Save() def AddErrorMessage(doc, errmsg, *args): doc.AddItem(Header(3, Italic(FontAttr(errmsg % args, color="#ff66cc")))) def error_page(errmsg, *args): print apply(error_page_doc, (errmsg,) + args).Format() def error_page_doc(errmsg, *args): """Produce a simple error-message page on stdout and exit.""" doc = Document() doc.AddItem(Header(2, "Error")) doc.AddItem(Bold(errmsg % args)) return doc _config_info = None def GetConfigOptions(list, category): global _config_info if _config_info == None: _config_info = list.GetConfigInfo() return _config_info[category] if __name__ == "__main__": try: main() except KeyboardInterrupt: print "Interrupted!" raise SystemExit, 0 except mm_err.MMUnknownListError, msg: error_page("%s: %s", list_name, msg) raise SystemExit, 0 except: print "Content-type: text/html\n" print "

We're sorry, we hit a bug!

\n" print "If you would like to help us identify the problem, please " print "email a copy of this page to the webmaster for this site" print 'with a description of what happened. Thanks!' print "\n
"
	try:
	    import traceback
	    sys.stderr = sys.stdout
	    traceback.print_exc()
	except:
	    print "[failed to get traceback]"
	print "\n\n
"