diff options
| author | viega | 1998-06-14 00:48:39 +0000 |
|---|---|---|
| committer | viega | 1998-06-14 00:48:39 +0000 |
| commit | 05cf09209ce15e0801379f8ab5566b43d3d3c9a6 (patch) | |
| tree | 5f6e820f91c4ed28e617de5adc1ebef39769246f | |
| parent | 7f9a9be4291b1c350251535788436d1f2a301e7b (diff) | |
| download | mailman-05cf09209ce15e0801379f8ab5566b43d3d3c9a6.tar.gz mailman-05cf09209ce15e0801379f8ab5566b43d3d3c9a6.tar.zst mailman-05cf09209ce15e0801379f8ab5566b43d3d3c9a6.zip | |
| -rw-r--r-- | Mailman/Cgi/__init__.py | 0 | ||||
| -rw-r--r-- | Mailman/Cgi/admin.py | 835 | ||||
| -rw-r--r-- | Mailman/Cgi/admindb.py | 232 | ||||
| -rw-r--r-- | Mailman/Cgi/archives.py | 86 | ||||
| -rw-r--r-- | Mailman/Cgi/edithtml.py | 167 | ||||
| -rw-r--r-- | Mailman/Cgi/handle_opts.py | 207 | ||||
| -rw-r--r-- | Mailman/Cgi/listinfo.py | 178 | ||||
| -rw-r--r-- | Mailman/Cgi/options.py | 125 | ||||
| -rw-r--r-- | Mailman/Cgi/private.py | 228 | ||||
| -rw-r--r-- | Mailman/Cgi/roster.py | 110 | ||||
| -rw-r--r-- | Mailman/Cgi/subscribe.py | 188 | ||||
| -rw-r--r-- | modules/Cgi/__init__.py | 0 | ||||
| -rwxr-xr-x | modules/Cgi/admin.py | 835 | ||||
| -rwxr-xr-x | modules/Cgi/admindb.py | 232 | ||||
| -rwxr-xr-x | modules/Cgi/archives.py | 86 | ||||
| -rwxr-xr-x | modules/Cgi/edithtml.py | 167 | ||||
| -rwxr-xr-x | modules/Cgi/handle_opts.py | 207 | ||||
| -rwxr-xr-x | modules/Cgi/listinfo.py | 178 | ||||
| -rwxr-xr-x | modules/Cgi/options.py | 125 | ||||
| -rwxr-xr-x | modules/Cgi/private.py | 228 | ||||
| -rwxr-xr-x | modules/Cgi/roster.py | 110 | ||||
| -rwxr-xr-x | modules/Cgi/subscribe.py | 188 |
22 files changed, 4712 insertions, 0 deletions
diff --git a/Mailman/Cgi/__init__.py b/Mailman/Cgi/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/Mailman/Cgi/__init__.py diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py new file mode 100644 index 000000000..c9155c87e --- /dev/null +++ b/Mailman/Cgi/admin.py @@ -0,0 +1,835 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""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: 734 $" + +import sys +import os, cgi, string, crypt, types, time +import paths # path hacking +import mm_utils, maillist, mm_cfg, mm_err, mm_mailcmd, Cookie +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"), + ('gateway', "Mail-News and News-Mail gateways")] + + +LOGIN_PAGE = """ +<html> +<head> + <title>%(listname)s Administrative Authentication</title> +</head> +<body bgcolor="#ffffff"> +<FORM METHOD=POST ACTION="%(path)s"> +%(message)s + <TABLE WIDTH="100%%" BORDER="0" CELLSPACING="4" CELLPADDING="5"> + <TR> + <TD COLSPAN="2" WIDTH="100%%" BGCOLOR="#99CCFF" ALIGN="CENTER"> + <B><FONT COLOR="#000000" SIZE="+1">%(listname)s Administrative + Authentication</FONT></B> + </TD> + </TR> + <tr> + <TD> <div ALIGN="Right"> List Administrative Password: </div> </TD> + <TD> <INPUT TYPE=password NAME=adminpw SIZE=30></TD> + </tr> + <tr> + <td colspan=2 align=middle> <INPUT TYPE=SUBMIT name="request_login"> + </td> + </tr> + </TABLE> +</FORM> +""" + +# " <- icky emacs font-lock bug workaround + +SECRET="monty" + +def isAuthenticated(list, password=None, SECRET="SECRET"): + import base64, md5 + if password is not None: # explicit login + try: + list.ConfirmAdminPassword(password) + except mm_err.MMBadPasswordError: + AddErrorMessage(doc, 'Error: Incorrect admin password.') + return 0 + except: + print "Content-type: text/html\n" + + print "<p><h3>We're sorry, we hit a bug!</h3>\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<PRE>" + try: + import traceback + sys.stderr = sys.stdout + traceback.print_exc() + except: + print "[failed to get traceback]" + print "\n\n</PRE>" + + token = md5.new(SECRET + list_name + SECRET).digest() + token = base64.encodestring(token) + token = string.strip(token) + c = Cookie.Cookie() + cookie_key = list_name + "-admin" + c[cookie_key] = token + c[cookie_key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE + print c # Output the cookie + return 1 + if os.environ.has_key('HTTP_COOKIE'): + c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) + if c.has_key(list_name + "-admin"): + try: + inp = base64.decodestring(c[list_name + "-admin"].value) + check = md5.new(SECRET+list_name+SECRET).digest() + except Error: # the decodestring may return incorrect padding? + raise 'Decode failed' + return 0 + if inp == check: + return 1 + else: + return 0 + return 0 + + +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, list_info, doc + doc = Document() + + try: + path = os.environ['PATH_INFO'] + except KeyError: + path = "" + list_info = mm_utils.GetPathPieces(path) + # How many ../'s we need to get back to http://host/mailman + + if len(list_info) == 0: + FormatAdminOverview() + return + + list_name = string.lower(list_info[0]) + + try: + lst = maillist.MailList(list_name) + except mm_err.MMUnknownListError: + lst = None + try: + if not (lst and lst._ready): + FormatAdminOverview(error="List <em>%s</em> not found." + % list_name) + return + + 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' + global cgi_data + cgi_data = cgi.FieldStorage() + is_auth = 0 + if cgi_data.has_key("adminpw"): + is_auth = isAuthenticated(lst, cgi_data["adminpw"].value) + message = FontAttr("Sorry, wrong password. Try again.", + color="ff5060", size="+1").Format() + else: + is_auth = isAuthenticated(lst) + message = "" + if not is_auth: + print "Content-type: text/html\n\n" + print LOGIN_PAGE % ({"listname": list_name, + "path": os.environ.get("REQUEST_URI", "/mailman/admin"), + "message": message}) + return + + if len(cgi_data.keys()): + if cgi_data.has_key('VARHELP'): + FormatOptionHelp(doc, cgi_data['VARHELP'].value, lst) + print doc.Format(bgcolor="#ffffff") + return + if (cgi_data.has_key('bounce_matching_headers')): + try: + pairs = lst.parse_matching_header_opt() + except mm_err.MMBadConfigError, line: + AddErrorMessage(doc, + 'Warning: bad matching-header line' + ' (does it have the colon?)<ul> %s </ul>', + line) + + if not lst.digestable and len(lst.digest_members): + AddErrorMessage(doc, + 'Warning: you have digest members,' + ' but digests are turned off.' + ' Those people will not receive mail.') + if not lst.nondigestable and len(lst.members): + AddErrorMessage(doc, + 'Warning: you have lst members,' + ' but non-digestified mail is turned' + ' off. They will receive mail until' + ' you fix this problem.') + if len(cgi_data.keys()): + ChangeOptions(lst, category, cgi_data, doc) + FormatConfiguration(doc, lst, category, category_suffix) + print doc.Format(bgcolor="#ffffff") + + finally: + if lst: + lst.Unlock() + +# Form Production: + +def FormatAdminOverview(error=None): + "Present a general welcome and itemize the (public) lists." + doc = Document() + legend = "%s maillists - Admin Links" % mm_cfg.DEFAULT_HOST_NAME + doc.SetTitle(legend) + + table = Table(border=0, width="100%") + table.AddRow([Center(Header(2, legend))]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + + advertised = [] + names = mm_utils.list_names() + names.sort() + for n in names: + l = maillist.MailList(n, lock=0) + if l.advertised: advertised.append(l) + + if error: + greeting = FontAttr(error, color="ff5060", size="+1") + else: + greeting = "Welcome!" + + if not advertised: + welcome_items = (greeting, + "<p>" + " There currently are no publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + ) + else: + + welcome_items = ( + greeting, + "<p>" + " Below is the collection of publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + (' Click on a list name to visit the configuration pages' + ' for that list.' + ) + ) + + welcome_items = (welcome_items + + (" To visit the administrators configuration page for" + " an unadvertised list, open a URL similar to this" + + + (" one, but with a '/' and the %slist name appended.<p>" + % ((error and "the right ") or "")) + + + " General list information can be found at ", + Link(os.path.join('../'* mm_utils.GetNestingLevel(), + "listinfo/"), "the maillist overview page"), + "." + "<p>(Send questions and comments to ", + Link("mailto:%s" % mm_cfg.MAILMAN_OWNER, + mm_cfg.MAILMAN_OWNER), + ".)<p>" + ) + ) + + table.AddRow([apply(Container, welcome_items)]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) + + if advertised: + table.AddRow([Italic("List"), Italic("Description")]) + for l in advertised: + table.AddRow([Link(l.GetRelativeScriptURL('admin'), + Bold(l.real_name)),l.description]) + + doc.AddItem(table) + + print doc.Format(bgcolor="#ffffff") + +def FormatConfiguration(doc, lst, 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' % lst.real_name) + doc.AddItem(Center(Header(2, ('%s Maillist Configuration - %s Section' + % (lst.real_name, label))))) + doc.AddItem('<hr>') + + links_table = Table(valign="top") + + links_table.AddRow([Center(Bold("Configuration Categories")), + Center(Bold("Other Administrative Activities"))]) + other_links = UnorderedList() + link = Link(lst.GetRelativeScriptURL('admindb'), + 'Tend to pending administrative requests.') + other_links.AddItem(link) + link = Link(lst.GetRelativeScriptURL('listinfo'), + 'Go to the general list information page.') + other_links.AddItem(link) + link = Link(lst.GetRelativeScriptURL('edithtml'), + 'Edit the HTML for the public list pages.') + other_links.AddItem(link) + + these_links = UnorderedList() + for k, v in CATEGORIES: + if k == category: + these_links.AddItem("<b> => " + v + " <= </b>") + else: + these_links.AddItem(Link("%s/%s" % + (lst.GetRelativeScriptURL('admin'),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('<hr>') + if category_suffix: + form = Form("%s/%s" % (lst.GetRelativeScriptURL('admin'), + category_suffix)) + else: + form = Form(lst.GetRelativeScriptURL('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.)<p>") + + form.AddItem(FormatOptionsSection(category, lst)) + + form.AddItem(Center(FormatPasswordStuff())) + + form.AddItem(lst.GetMailmanFooter()) + +def FormatOptionsSection(category, lst): + """Produce the category-specific options table.""" + if category == 'members': + # Special case for members section. + return FormatMembershipOptions(lst) + + options = GetConfigOptions(lst, 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 = "<center><i>" + item + "</i></center>" + 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 + AddOptionsTableItem(big_table, item, category, lst) + big_table.AddRow(['<br>']) + big_table.AddCellInfo(big_table.GetCurrentRowIndex(), 0, colspan=2) + return big_table + +def AddOptionsTableItem(table, item, category, lst, nodetails=0): + """Add a row to an options table with the item description and value.""" + try: + got = GetItemCharacteristics(item) + varname, kind, params, dependancies, descr, elaboration = got + except ValueError, msg: + lst.LogMsg("error", "admin: %s", msg) + return Italic("<malformed option>") + descr = GetItemGuiDescr(lst, category, varname, descr, + elaboration, nodetails) + val = GetItemGuiValue(lst, kind, varname, params) + table.AddRow([descr, val]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1, + bgcolor="#cccccc") + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + bgcolor="#cccccc") + +def FormatOptionHelp(doc, varref, lst): + item = bad = None + reflist = string.split(varref, '/') + if len(reflist) == 2: + category, varname = reflist + options = GetConfigOptions(lst, 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'])) + else: + try: + got = GetItemCharacteristics(item) + varname, kind, params, dependancies, descr, elaboration = got + except ValueError, msg: + bad = msg + if not bad and not elaboration: + bad = "Option %s has no extended help." % varname + if bad: + AddErrorMessage(doc, bad) + return + + header = Table(width="100%") + legend = ('%s Maillist Configuration Help<br><em>%s</em> Option' + % (lst.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("<b>%s</b> (%s): %s<p>" % (varname, category, item[4])) + doc.AddItem("%s<p>" % item[5]) + + form = Form(os.path.join(lst.GetRelativeScriptURL('admin'), category)) + valtab = Table(cellspacing=3, cellpadding=4) + AddOptionsTableItem(valtab, item, category, lst, nodetails=1) + form.AddItem(valtab) + # XXX I don't think we want to be able to set options from two places, + # since they'll go out of sync. + #form.AddItem(Center(FormatPasswordStuff())) + doc.AddItem(Center(form)) + +def GetItemCharacteristics(table_entry): + """Break out the components of an item description from its table entry: + 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: + raise ValueError, ("Badly formed options entry:\n %s" + % table_entry) + return (varname, kind, params, dependancies, descr, elaboration) + +def GetItemGuiValue(lst, kind, varname, params): + """Return a representation of an item's settings.""" + if kind == mm_cfg.Radio or kind == mm_cfg.Toggle: + return RadioButtonArray(varname, params, getattr(lst, varname)) + elif (kind == mm_cfg.String or kind == mm_cfg.Email or + kind == mm_cfg.Host or kind == mm_cfg.Number): + return TextBox(varname, getattr(lst, varname), params) + elif kind == mm_cfg.Text: + if params: + r, c = params + else: + r, c = None, None + val = getattr(lst, varname) + if not val: + val = '' + return TextArea(varname, val, r, c) + elif kind == mm_cfg.EmailList: + if params: + r, c = params + else: + r, c = None, None + res = string.join(getattr(lst, varname), '\n') + return TextArea(varname, res, r, c, wrap='off') + +def GetItemGuiDescr(lst, category, varname, descr, elaboration, nodetails): + """Return a representation of an item's description, with link to + elaboration if any.""" + descr = '<div ALIGN="right">' + descr + if not nodetails and elaboration: + ref = "../" * (mm_utils.GetNestingLevel()-1) + list_name + "/" + ref = ref + '?VARHELP=' + category + "/" + varname + descr = Container(descr, + Link(ref, " (Details)", target="MMHelp"), + "</div>") + else: + descr = descr + "</div>" + return descr + +def FormatMembershipOptions(lst): + container = Container() + header = Table(width="100%") + header.AddRow([Center(Header(2, "Membership Management"))]) + header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + container.AddItem(header) + user_table = Table(width="90%") + user_table.AddRow([Center(Header(4, "Membership List"))]) + user_table.AddCellInfo(user_table.GetCurrentRowIndex(), + user_table.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) + + members = {} + digests = {} + for member in lst.members: + members[member] = 1 + for member in lst.digest_members: + digests[member] = 1 + all = lst.members + lst.digest_members + if len(all) > mm_cfg.ADMIN_MEMBER_CHUNKSIZE: + chunks = mm_utils.chunkify(all) + if not cgi_data.has_key("chunk"): + chunk = 0 + else: + chunk = string.atoi(cgi_data["chunk"].value) + all = chunks[chunk] + footer = ("<p><em>To View other sections, " + "click on the appropriate range listed below</em>") + chunk_indices = range(len(chunks)) + chunk_indices.remove(chunk) + buttons = [] + pi = os.environ["PATH_INFO"] + for ci in chunk_indices: + start, end = chunks[ci][0], chunks[ci][-1] + buttons.append("<a href=/mailman/admin%s?chunk=%d> from %s to %s </a>" % \ + ( pi, ci, start, end)) + buttons = apply(UnorderedList, tuple(buttons)) + footer = footer + buttons.Format() + "<p>" + else: + all.sort() + footer = "<p>" + for member in all: + cells = [member + "<input type=hidden name=user value=%s>" % (member), + "subscribed " +CheckBox(member + "_subscribed", "on", 1).Format(), + ] + if members.get(member): + cells.append("digest " + CheckBox(member + "_digest", "off", 0).Format()) + else: + cells.append("digest " + CheckBox(member + "_digest", "on", 1).Format()) + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if lst.GetUserOption(member, mm_mailcmd.option_info[opt]): + value = "on" + checked = 1 + else: + value = "off" + checked = 0 + box = CheckBox("%s_%s" % (member, opt), value, checked) + cells.append("%s %s" % (opt, box.Format())) + user_table.AddRow(cells) + container.AddItem(Center(user_table)) + container.AddItem(footer) + t = Table(width="90%") + t.AddRow([Center(Header(4, "Mass Subscribe Members"))]) + t.AddCellInfo(t.GetCurrentRowIndex(), + t.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) + container.AddItem(Center(t)) + container.AddItem(Center(TextArea(name='subscribees', rows=10,cols=60,wrap=None))) + container.AddItem(Center("<em> Enter One address per line</em><p>")) + return container + +def FormatPasswordStuff(): + submit = Table(bgcolor="#99ccff", + border=0, cellspacing=0, cellpadding=2, width="100%") + submit.AddRow([Bold(SubmitButton('submit', 'Submit Your Changes'))]) + submit.AddCellInfo(submit.GetCurrentRowIndex(), 0, align="middle") + change_pw_table = Table(bgcolor="#99cccc", border=0, + cellspacing=0, cellpadding=2, width="90%") + change_pw_table.AddRow([Bold(Center('To Change The Administrator Password')), + '<div ALIGN="right"> Enter the new password: </div>', + PasswordBox('newpw'),]) + change_pw_table.AddCellInfo(0, 0, align="middle", colspan=2) + change_pw_table.AddRow(['<div ALIGN="right"> Enter the current password </div>', + PasswordBox('adminpw'), + '<div ALIGN="right">Again to confirm it:</div>', + PasswordBox('confirmpw')]) + password_stuff = Container() + password_stuff.AddItem(change_pw_table) + password_stuff.AddItem("<p>") + password_stuff.AddItem(submit) + return password_stuff + +# XXX klm - looks like turn_on_moderation is orphaned. +turn_on_moderation = 0 + +# Options processing + +def GetValidValue(lst, 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(lst, 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(lst, prop) + return num + except: + return getattr(lst, prop) + else: + # Should never get here... + return val + + +def ChangeOptions(lst, category, cgi_info, document): + dirty = 0 + confirmed = 0 + if cgi_info.has_key('newpw'): + if cgi_info.has_key('confirmpw'): + if cgi_info.has_key('adminpw'): + try: + lst.ConfirmAdminPassword(cgi_info['adminpw'].value) + confirmed = 1 + except mm_err.MMBadPasswordError: + m = "Error: incorrect administrator password" + document.AddItem(Header(3, Italic(FontAttr(m, color="ff5060")))) + confirmed = 0 + if confirmed: + new = cgi_info['newpw'].value + confirm = cgi_info['confirmpw'].value + if new == confirm: + lst.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")))) + # + # for some reason, the login page mangles important values for the list + # such as .real_name so we only process these changes if the category + # is not "members" and the request is not from the login page + # -scott 19980515 + # + if category != 'members' and not cgi_info.has_key("request_login") and\ + len(cgi_info.keys()) > 1: + opt_list = GetConfigOptions(lst, 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(lst, property, kind, val, deps) + if getattr(lst, property) != value: + setattr(lst, property, value) + dirty = 1 + # + # mass subscription processing for members category + # + if cgi_info.has_key('subscribees'): + name_text = cgi_info['subscribees'].value + name_text = string.replace(name_text, '\r', '') + names = string.split(name_text, '\n') + if '' in names: + names.remove('') + subscribe_success = [] + subscribe_errors = [] + for new_name in map(string.strip,names): + digest = 0 + if not lst.digestable: + digest = 0 + if not lst.nondigestable: + digest = 1 + try: + lst.ApprovedAddMember(new_name, (mm_utils.GetRandomSeed() + + mm_utils.GetRandomSeed()), digest) + subscribe_success.append(new_name) + except mm_err.MMAlreadyAMember: + subscribe_errors.append((new_name, 'Already a member')) + + except mm_err.MMBadEmailError: + subscribe_errors.append((new_name, "Bad/Invalid email address")) + except mm_err.MMHostileAddress: + subscribe_errors.append((new_name, "Hostile Address (illegal characters)")) + if subscribe_success: + document.AddItem(Header(5, "Successfully Subscribed:")) + document.AddItem(apply(UnorderedList, tuple((subscribe_success)))) + document.AddItem("<p>") + if subscribe_errors: + document.AddItem(Header(5, "Error Subscribing:")) + items = map(lambda x: "%s -- %s" % (x[0], x[1]), subscribe_errors) + document.AddItem(apply(UnorderedList, tuple((items)))) + document.AddItem("<p>") + # + # do the user options for members category + # + if cgi_info.has_key('user'): + user = cgi_info["user"] + if type(user) is type([]): + users = [] + for ui in range(len(user)): + users.append(user[ui].value) + else: + users = [user.value] + for user in users: + if not cgi_info.has_key('%s_subscribed' % (user)): + lst.DeleteMember(user) + dirty = 1 + continue + if not cgi_info.has_key("%s_digest" % (user)): + if user in lst.digest_members: + list.digest_members.remove(user) + dirty = 1 + if user not in lst.members: + lst.members.append(user) + dirty = 1 + else: + if user not in lst.digest_members: + lst.digest_members.append(user) + dirty = 1 + if user in lst.members: + lst.members.remove(user) + dirty = 1 + + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if cgi_info.has_key("%s_%s" % (user, opt)): + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 1) + dirty = 1 + else: + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 0) + dirty = 1 + + + if dirty: + lst.Save() + +def AddErrorMessage(doc, errmsg, *args): + doc.AddItem(Header(3, Italic(FontAttr(errmsg % args, + color="#ff66cc")))) + + +_config_info = None +def GetConfigOptions(lst, category): + global _config_info + if _config_info == None: + _config_info = lst.GetConfigInfo() + return _config_info[category] + diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py new file mode 100644 index 000000000..415184d3b --- /dev/null +++ b/Mailman/Cgi/admindb.py @@ -0,0 +1,232 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce and process the pending-approval items for a list.""" + +import sys +import os, cgi, string, crypt, types +import mm_utils, maillist, mm_err, htmlformat + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + + +if len(list_info) < 1: + doc.SetTitle("Admindb Error") + doc.AddItem(htmlformat.Header(2, "Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name) +except: + msg = "%s: No such list." % list_name + doc.SetTitle("Admindb Error - %s" % msg) + doc.AddItem(htmlformat.Header(2, msg)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +if not list._ready: + msg = "%s: No such list." % list_name + doc.SetTitle("Admindb Error - %s" % msg) + doc.AddItem(htmlformat.Header(2, msg)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +# Note, these 2 functions use i only to count the number of times to +# go around. We always operate on the first element of the list +# because we're going to delete the element after we operate on it. + +def SubscribeAll(): + for i in range(len(list.requests['add_member'])): + comment_key = 'comment-%d' % list.requests['add_member'][0][0] + if form.has_key(comment_key): + list.HandleRequest(('add_member', 0), 1, form[comment_key].value) + else: + list.HandleRequest(('add_member', 0), 1) + +def SubscribeNone(): + for i in range(len(list.requests['add_member'])): + comment_key = 'comment-%d' % list.requests['add_member'][0][0] + if form.has_key(comment_key): + list.HandleRequest(('add_member', 0), 0, form[comment_key].value) + else: + list.HandleRequest(('add_member', 0), 0) + +def PrintHeader(str, error=0): + if error: + it = htmlformat.FontAttr(str, color="ff5060") + else: + it = str + doc.AddItem(htmlformat.Header(3, htmlformat.Italic(it))) + doc.AddItem('<hr>') + +def HandleRequests(doc): + if not form.has_key('adminpw'): + PrintHeader('You need to supply the admin password ' + 'to answer requests.', error=1) + return + try: + list.ConfirmAdminPassword(form['adminpw'].value) + except: + PrintHeader('Incorrect admin password.', error=1) + return + ignore_subscribes = 0 + if form.has_key('subscribe_all'): + ignore_subscribes = 1 + SubscribeAll() + elif form.has_key('subscribe_none'): + ignore_subscribes = 1 + SubscribeNone() + for k in form.keys(): + try: + # XXX Security?! + v = eval(form[k].value) + request_id = eval(k) + except: # For stuff like adminpw + continue + if type(request_id) <> types.IntType: + continue + try: + request = list.GetRequest(request_id) + except mm_err.MMBadRequestId: + continue # You've already changed the database. No biggie. + if ignore_subscribes and request[0] == 'add_member': + # We already handled this request. + continue + comment_key = 'comment-%d' % request_id + if form.has_key(comment_key): + list.HandleRequest(request, v, form[comment_key].value) + else: + list.HandleRequest(request, v) + list.Save() + PrintHeader('Database Updated...') + + +def PrintAddMemberRequest(val, table): + table.AddRow([ + val[3], + htmlformat.RadioButtonArray(val[0], ("Refuse", "Subscribe")), + htmlformat.TextBox("comment-%d" % val[0], size=50) + ]) + +def PrintPostRequest(val, form): + t = htmlformat.Table(cellspacing=10) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Post held because: ')), + val[3]]) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Action to take on this post:')), + htmlformat.RadioButtonArray(val[0], ("Approve", "Reject", + "Discard (eg, spam)")), + htmlformat.SubmitButton('submit', 'Submit All Data') + ]) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('If you reject this post, ' + 'explain (optional):')), + htmlformat.TextBox("comment-%d" % val[0], size=50)]) + + cur_row = t.GetCurrentRowIndex() + cur_col = t.GetCurrentCellIndex() + t.AddCellInfo(cur_row, cur_col, colspan=3) + + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Contents:'))]) + form.AddItem(t) + form.AddItem(htmlformat.Preformatted(val[2][1])) + form.AddItem('<p>') + + +def PrintRequests(doc): + # The only types of requests we know about are add_member and post. + # Anything else that might have gotten in here somehow we'll just + # ignore (This should never happen unless someone is hacking at + # the code). + + doc.AddItem(htmlformat.Header(2, "Administrative requests for " + "'%s' mailing list" % list.real_name)) + doc.AddItem(htmlformat.FontSize("+1", htmlformat.Link( + list.GetRelativeScriptURL('admin'), htmlformat.Italic( + 'View or edit the list configuration information')))) + doc.AddItem('<p><hr>') + if not list.RequestsPending(): + doc.AddItem(htmlformat.Header(3,'There are no pending requests.')) + doc.AddItem(list.GetMailmanFooter()) + return + form = htmlformat.Form(list.GetRelativeScriptURL('admindb')) + doc.AddItem(form) + form.AddItem('Admin password: ') + form.AddItem(htmlformat.PasswordBox('adminpw')) + form.AddItem('<p>') + if list.requests.has_key('add_member'): +## form.AddItem('<hr>') +## t = htmlformat.Table(cellspacing=10) +## t.AddRow([ +## htmlformat.SubmitButton('submit', 'Submit All Data'), +## htmlformat.SubmitButton('subscribe_all', 'Subscribe Everybody'), +## htmlformat.SubmitButton('subscribe_none', 'Refuse Everybody') +## ]) +## form.AddItem(t) + form.AddItem('<hr>') + form.AddItem(htmlformat.Center( + htmlformat.Header(2, 'Subscription Requests'))) + t = htmlformat.Table(border=2) + t.AddRow([ + htmlformat.Bold('Email'), + htmlformat.Bold('Descision'), + htmlformat.Bold('Reasoning for subscription refusal (optional)')]) + for request in list.requests['add_member']: + PrintAddMemberRequest(request, t) + + form.AddItem(t) + t = htmlformat.Table(cellspacing=10) + t.AddRow([ + htmlformat.SubmitButton('submit', 'Submit All Data'), + htmlformat.SubmitButton('subscribe_all', 'Subscribe Everybody'), + htmlformat.SubmitButton('subscribe_none', 'Refuse Everybody') + ]) + form.AddItem(t) + + # Print submitit buttons... + if list.requests.has_key('post'): + for request in list.requests['post']: + form.AddItem('<hr>') + form.AddItem(htmlformat.Center(htmlformat.Header(2, "Held Message"))) + PrintPostRequest(request, form) + doc.AddItem(list.GetMailmanFooter()) + +try: + form = cgi.FieldStorage() + if len(form.keys()): + doc.SetTitle("%s Admindb Results" % list.real_name) + HandleRequests(doc) + else: + doc.SetTitle("%s Admindb" % list.real_name) + PrintRequests(doc) + text = doc.Format(bgcolor="#ffffff") + print text + sys.stdout.flush() +finally: + list.Unlock() diff --git a/Mailman/Cgi/archives.py b/Mailman/Cgi/archives.py new file mode 100644 index 000000000..2b0ed0fa9 --- /dev/null +++ b/Mailman/Cgi/archives.py @@ -0,0 +1,86 @@ +#! /usr/bin/env/python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# This script is being deprecated, in favor hookups for an external archiver. + +# We don't need to lock in this script, because we're never going to change +# data. + +import sys +import os, types, posix, string +import mm_utils, maillist, htmlformat + +print "Content-type: text/html" +print + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 1: + print "<h2>Invalid options to CGI script.</h2>" + sys.exit(0) + +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name) +except: + print "<h2>%s: No such list.</h2>" % list_name + sys.exit(0) + +if not list._ready: + print "<h2>%s: No such list.</h2>" % list_name + sys.exit(0) + +def GetArchiveList(list): + archive_list = htmlformat.UnorderedList() + + def ArchiveFilter(str): + if str[:7] <> 'volume_': + return 0 + try: + x = eval(str[7:]) + if type(x) <> types.IntType: + return 0 + if x < 1: + return 0 + return 1 + except: + return 0 + try: + dir_listing = filter(ArchiveFilter, os.listdir(list.archive_directory)) + except posix.error: + return "<h3><em>No archives are currently available.</em></h3>" + if not len(dir_listing): + return "<h3><em>No archives are currently available.</em></h3>" + for dir in dir_listing: + link = htmlformat.Link(os.path.join(list._base_archive_url, dir), + "Volume %s" % dir[7:]) + archive_list.AddItem(link) + + return archive_list.Format() + + + + +replacements = list.GetStandardReplacements() +replacements['<mm-archive-list>'] = GetArchiveList(list) + +# Just doing print list.ParseTags(...) calls ParseTags twice??? +text = list.ParseTags('archives.html', replacements) +print text diff --git a/Mailman/Cgi/edithtml.py b/Mailman/Cgi/edithtml.py new file mode 100644 index 000000000..faf35e715 --- /dev/null +++ b/Mailman/Cgi/edithtml.py @@ -0,0 +1,167 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Script which implements admin editing of the list's html templates.""" + +import sys +import os, cgi, string, crypt, types +import mm_utils, maillist, mm_cfg +import htmlformat + +#Editable templates. We should also be able to edit the archive index, which +#currently isn't a working template, but will be soon. + +template_data = (('listinfo.html', 'General list information page'), + ('subscribe.html', 'Subscribe results page'), + ('options.html', 'User specific options page'), + ('handle_opts.html', 'Changing user options results page'), + ('archives.html', 'Archives index page') + ) + + +def InitDocument(): + return htmlformat.HeadlessDocument() + +doc = InitDocument() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 1: + doc.AddItem(htmlformat.Header(2, "Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name, lock=0) +except: + doc.AddItem(htmlformat.Header(2, "%s : No such list" % list_name)) + print doc.Format() + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "%s : No such list" % list_name)) + print doc.Format() + sys.exit(0) + + +if len(list_info) > 1: + template_name = list_info[1] + for (template, info) in template_data: + if template == template_name: + template_info = info + doc.SetTitle('%s -- Edit html for %s' % + (list.real_name, template_info)) + break + else: + doc.SetTitle('Edit HTML : Error') + doc.AddItem(htmlformat.Header(2, "%s: Invalid template" % template_name)) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) +else: + doc.SetTitle('%s -- HTML Page Editing' % list.real_name) + doc.AddItem(htmlformat.Header(1, '%s -- HTML Page Editing' % list.real_name)) + doc.AddItem(htmlformat.Header(2, 'Select page to edit:')) + template_list = htmlformat.UnorderedList() + for (template, info) in template_data: + l = htmlformat.Link(os.path.join(list.GetRelativeScriptURL('edithtml'), + template), info) + + template_list.AddItem(l) + doc.AddItem(htmlformat.FontSize("+2", template_list)) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + + +def FormatHTML(doc): + doc.AddItem(htmlformat.Header(1,'%s:' % list.real_name)) + doc.AddItem(htmlformat.Header(1, template_info)) + + doc.AddItem('<hr>') + + link = htmlformat.Link(list.GetRelativeScriptURL('admin'), + 'View or edit the list configuration information.') + doc.AddItem(htmlformat.FontSize("+1", link)) + doc.AddItem('<p>') + + doc.AddItem('<hr>') + + form = htmlformat.Form(os.path.join(list.GetRelativeScriptURL('edithtml'), + template_name)) + doc.AddItem(form) + + password_table = htmlformat.Table() + password_table.AddRow(['Enter the admin password to edit html:', + htmlformat.PasswordBox('adminpw')]) + password_table.AddRow(['When you are done making changes...', + htmlformat.SubmitButton('submit', 'Submit Changes')]) + + form.AddItem(password_table) + + text = mm_utils.QuoteHyperChars(list.SnarfHTMLTemplate(template_name)) + form.AddItem(htmlformat.TextArea('html_code', text, rows=40, cols=75)) + +def ChangeHTML(list, cgi_info, template_name, doc): + if not cgi_info.has_key('html_code'): + doc.AddItem(htmlformat.Header(3,"Can't have empty html page.")) + doc.AddItem(htmlformat.Header(3,"HTML Unchanged.")) + doc.AddItem('<hr>') + return + code = cgi_info['html_code'].value + f = open(os.path.join(list._template_dir, template_name), 'w') + f.write(code) + f.close() + doc.AddItem(htmlformat.Header(3, 'HTML successfully updated.')) + doc.AddItem('<hr>') + +try: + cgi_data = cgi.FieldStorage() + if len(cgi_data.keys()): + if not cgi_data.has_key('adminpw'): + m = 'Error: You must supply the admin password to edit html.' + doc.AddItem(htmlformat.Header(3, + htmlformat.Italic( + htmlformat.FontAttr( + m, color="ff5060")))) + doc.AddItem('<hr>') + else: + try: + list.ConfirmAdminPassword(cgi_data['adminpw'].value) + ChangeHTML(list, cgi_data, template_name, doc) + except: + m = 'Error: Incorrect admin password.' + doc.AddItem(htmlformat.Header(3, + htmlformat.Italic( + htmlformat.FontAttr( + m, color="ff5060")))) + doc.AddItem('<hr>') + + + + FormatHTML(doc) + +finally: + try: + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + except: + pass diff --git a/Mailman/Cgi/handle_opts.py b/Mailman/Cgi/handle_opts.py new file mode 100644 index 000000000..7b5747f4f --- /dev/null +++ b/Mailman/Cgi/handle_opts.py @@ -0,0 +1,207 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Process input to user options form.""" + +import sys +import os, cgi, string +import mm_utils, maillist, mm_err, mm_cfg, htmlformat + + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +list_name = string.lower(list_info[0]) +user = list_info[1] + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +try: + list = maillist.MailList(list_name) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name)) + print doc.Format(bgcolor="#ffffff") + list.Unlock() + sys.exit(0) + + +def PrintResults(results): + replacements = list.GetStandardReplacements() + replacements['<mm-results>'] = results + replacements['<mm-operation>'] = operation + output = list.ParseTags('handle_opts.html', replacements) + + doc.AddItem(output) + print doc.Format(bgcolor="#ffffff") + list.Unlock() + sys.exit(0) + +form = cgi.FieldStorage() + +error = 0 +operation = "" + +if string.lower(user) not in list.members + list.digest_members: + PrintResults("%s not a member!<p>" % user) + +if form.has_key("unsub"): + operation = "Unsubscribe" + if not form.has_key("upw"): + PrintResults("You must give your password to unsubscribe.<p>") + else: + try: + pw = form["upw"].value + if list.ConfirmUserPassword(user, pw): + list.DeleteMember(user, "web cmd") + except mm_err.MMListNotReady: + PrintResults("List is not functional.") + except mm_err.MMNoSuchUserError: + PrintResults("You seem to already be not a member.<p>") + except mm_err.MMBadUserError: + PrintResults("Your account has gone awry - " + "please contact the list administrator!<p>") + except mm_err.MMBadPasswordError: + PrintResults("That password was incorrect.<p>") +# except: +# PrintResults('''An unknown error occured. <p> +#Please send mail to <a href=%s>%s</a> explaining +#exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER, +# mm_cfg.MAILMAN_OWNER)) + + PrintResults("You have been unsubscribed.<p>") +elif form.has_key("emailpw"): + try: + list.MailUserPassword(user) + PrintResults("A reminder of your password " + "has been emailed to you.<p>") + except mm_err.MMBadUserError: + PrintResults("Your password entry has not been found. The list " + "manager is being notified.<p>") + +elif form.has_key("changepw"): + if (form.has_key('opw') + and form.has_key('newpw') + and form.has_key('confpw')): + try: + list.ConfirmUserPassword(user, form['opw'].value) + list.ChangeUserPassword(user, + form['newpw'].value, form['confpw'].value) + except mm_err.MMListNotReady: + PrintResults("The list is currently not funcitonal.") + except mm_err.MMNotAMemberError: + PrintResults("You seem to no longer be a list member.") + except mm_err.MMBadPasswordError: + PrintResults("The old password you supplied was incorrect.") + except mm_err.MMPasswordsMustMatch: + PrintResults("Passwords must match.") + except: + PrintResults('''An unknown error occured. <p> +Please send mail to <a href=%s>%s</a> explaining +exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER, mm_cfg.MAILMAN_OWNER)) + + + PrintResults("Your password has been changed.") + else: + PrintResults("You must supply your old password," + " and your new password twice.") + +else: + # If keys don't exist, set them to whatever they were. (essentially a noop) + if form.has_key("digest"): + digest_value = eval(form["digest"].value) + else: + digest_value = list.GetUserOption(user, mm_cfg.Digests) + if form.has_key("mime"): + mime = eval(form["mime"].value) + else: + mime = list.GetUserOption(user, mm_cfg.DisableMime) + if form.has_key("dontreceive"): + dont_receive = eval(form["dontreceive"].value) + else: + dont_receive = list.GetUserOption(user, mm_cfg.DontReceiveOwnPosts) + if form.has_key("ackposts"): + ack_posts = eval(form["ackposts"].value) + else: + ack_posts = list.GetUserOption(user, mm_cfg.AcknowlegePosts) + if form.has_key("disablemail"): + disable_mail = eval(form["disablemail"].value) + else: + disable_mail = list.GetUserOption(user, mm_cfg.DisableDelivery) + if form.has_key("conceal"): + conceal = eval(form["conceal"].value) + else: + conceal = list.GetUserOption(user, mm_cfg.ConcealSubscription) + + if not form.has_key("digpw"): + PrintResults("You must supply a password to change options.") + try: + list.ConfirmUserPassword(user, form['digpw'].value) + except mm_err.MMAlreadyDigested: + pass + except mm_err.MMAlreadyUndigested: + pass + except mm_err.MMMustDigestError: + PrintResults("List only accepts digest members.") + except mm_err.MMCantDigestError: + PrintResults("List doesn't accept digest members.") + except mm_err.MMNotAMemberError: + PrintResults("%s isn't subscribed to this list." % mail.GetSender()) + except mm_err.MMListNotReady: + PrintResults("List is not functional.") + except mm_err.MMNoSuchUserError: + PrintResults("%s is not subscribed to this list." %mail.GetSender()) + except mm_err.MMBadPasswordError: + PrintResults("You gave the wrong password.") + except: + PrintResults('''An unknown error occured. <p> +Please send mail to <a href=%s>%s</a> explaining +exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER)) + + + list.SetUserOption(user, mm_cfg.DisableDelivery, disable_mail) + list.SetUserOption(user, mm_cfg.DontReceiveOwnPosts, dont_receive) + list.SetUserOption(user, mm_cfg.AcknowlegePosts, ack_posts) + list.SetUserOption(user, mm_cfg.DisableMime, mime) + try: + list.SetUserDigest(user, digest_value) + except (mm_err.MMAlreadyDigested, mm_err.MMAlreadyUndigested): + pass + list.SetUserOption(user, mm_cfg.ConcealSubscription, conceal) + PrintResults("You have successfully set your options.") + + +list.Unlock()
\ No newline at end of file diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py new file mode 100644 index 000000000..b1aae2d92 --- /dev/null +++ b/Mailman/Cgi/listinfo.py @@ -0,0 +1,178 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce listinfo page, primary web entry-point to maillists. +""" + +# No lock needed in this script, because we don't change data. + +import sys +import os, string +from regsub import gsub +import mm_utils, maillist, mm_cfg +from htmlformat import * + +def main(): + try: + path = os.environ['PATH_INFO'] + except KeyError: + path = "" + + list_info = mm_utils.GetPathPieces(path) + + if len(list_info) == 0: + FormatListinfoOverview() + return + + list_name = string.lower(list_info[0]) + + try: + list = maillist.MailList(list_name, lock=0) + except: + list = None + + if not (list and list._ready): + FormatListinfoOverview(error="List <em>%s</em> not found." % list_name) + return + + FormatListListinfo(list) + +def FormatListinfoOverview(error=None): + "Present a general welcome and itemize the (public) lists for this host." + doc = Document() + legend = "%s maillists" % mm_cfg.DEFAULT_HOST_NAME + doc.SetTitle(legend) + + table = Table(border=0, width="100%") + table.AddRow([Center(Header(2, legend))]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + + advertised = [] + names = mm_utils.list_names() + names.sort() + + # XXX We need a portable way to determine the host by which we are being + # visited! An absolute URL would do... + if os.environ.has_key('HTTP_HOST'): + http_host = os.environ['HTTP_HOST'] + else: + http_host = None + + for n in names: + l = maillist.MailList(n, lock = 0) + if l.advertised: + if (http_host + and (string.find(http_host, l.host_name) == -1 + and string.find(l.host_name, http_host) == -1)): + # List is for different identity for this host - skip it. + continue + else: + advertised.append(l) + + if error: + greeting = FontAttr(error, color="ff5060", size="+1") + else: + greeting = "Welcome!" + + if not advertised: + welcome_items = (greeting, + "<p>" + " There currently are no publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + ) + else: + + welcome_items = ( + greeting, + "<p>" + " Below is the collection of publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + (' Click on a list name to visit the info page' + ' for that list. There you can learn more about the list,' + ' subscribe to it, or find the roster of current subscribers.'), + ) + + welcome_items = (welcome_items + + (" To visit the info page for an unadvertised list," + " open a URL similar to this one, but with a '/' and" + + + (" the %slist name appended." + % ((error and "right ") or "")) + + + '<p> List administrators, you can visit ', + Link(os.path.join('../' * mm_utils.GetNestingLevel(), + 'admin/'), "the list admin overview page"), + " to find the management interface for your list." + "<p>(Send questions or comments to ", + Link("mailto:%s" % mm_cfg.MAILMAN_OWNER, + mm_cfg.MAILMAN_OWNER), + ".)<p>")) + + table.AddRow([apply(Container, welcome_items)]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) + + if advertised: + table.AddRow([Italic("List"), Italic("Description")]) + for l in advertised: + table.AddRow([Link(l.GetRelativeScriptURL('listinfo'), + Bold(l.real_name)), l.description]) + + doc.AddItem(table) + + print doc.Format(bgcolor="#ffffff") + +def FormatListListinfo(list): + "Expand the listinfo template against the list's settings, and print." + + doc = HeadlessDocument() + + replacements = list.GetStandardReplacements() + + if not list.digestable or not list.nondigestable: + replacements['<mm-digest-radio-button>'] = "" + replacements['<mm-undigest-radio-button>'] = "" + else: + replacements['<mm-digest-radio-button>'] = list.FormatDigestButton() + replacements['<mm-undigest-radio-button>'] = \ + list.FormatUndigestButton() + replacements['<mm-plain-digests-button>'] = list.FormatPlainDigestsButton() + replacements['<mm-mime-digests-button>'] = list.FormatMimeDigestsButton() + replacements['<mm-subscribe-box>'] = list.FormatBox('email') + replacements['<mm-subscribe-button>'] = list.FormatButton('email-button', + text='Subscribe') + replacements['<mm-new-password-box>'] = list.FormatSecureBox('pw') + replacements['<mm-confirm-password>'] = list.FormatSecureBox('pw-conf') + replacements['<mm-subscribe-form-start>'] = \ + list.FormatFormStart('subscribe') + replacements['<mm-roster-form-start>'] = list.FormatFormStart('roster') + replacements['<mm-editing-options>'] = list.FormatEditingOption() + replacements['<mm-info-button>'] = SubmitButton('UserOptions', + 'Edit Options').Format() + replacements['<mm-roster-option>'] = list.FormatRosterOptionForUser() + + # Do the expansion. + doc.AddItem(list.ParseTags('listinfo.html', replacements)) + + print doc.Format() + + +if __name__ == "__main__": + main() diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py new file mode 100644 index 000000000..07664bbb3 --- /dev/null +++ b/Mailman/Cgi/options.py @@ -0,0 +1,125 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# 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 list options.html template. + +Takes listname/userid in PATH_INFO, expecting an `obscured' userid. Depending +on the mm_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. + +import sys +import os, string +import mm_utils, maillist, htmlformat, mm_cfg + +doc = htmlformat.HeadlessDocument() + +try: + path = os.environ['PATH_INFO'] +except KeyError: + path = "" +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +list_name = string.lower(list_info[0]) +user = mm_utils.UnobscureEmail(list_info[1]) + +try: + list = maillist.MailList(list_name, lock=0) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + +if string.lower(user) not in list.members + list.digest_members: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such member %s." + % (list_name, `user`))) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + +# Re-obscure the user's address for the page banner if obscure_addresses set. +if list.obscure_addresses: + presentable_user = mm_utils.ObscureEmail(user, for_text=1) +else: + presentable_user = user + +replacements = list.GetStandardReplacements() +replacements['<mm-digest-radio-button>'] = list.FormatOptionButton( + mm_cfg.Digests, 1, user) +replacements['<mm-undigest-radio-button>'] = list.FormatOptionButton( + mm_cfg.Digests, 0, user) +replacements['<mm-plain-digests-button>'] = list.FormatOptionButton( + mm_cfg.DisableMime, 1, user) +replacements['<mm-mime-digests-button>'] = list.FormatOptionButton( + mm_cfg.DisableMime, 0, user) +replacements['<mm-delivery-enable-button>'] = list.FormatOptionButton( + mm_cfg.DisableDelivery, 0, user) +replacements['<mm-delivery-disable-button>'] = list.FormatOptionButton( + mm_cfg.DisableDelivery, 1, user) +replacements['<mm-disabled-notice>'] = list.FormatDisabledNotice(user) +replacements['<mm-dont-ack-posts-button>'] = list.FormatOptionButton( + mm_cfg.AcknowlegePosts, 0, user) +replacements['<mm-ack-posts-button>'] = list.FormatOptionButton( + mm_cfg.AcknowlegePosts, 1, user) +replacements['<mm-receive-own-mail-button>'] = list.FormatOptionButton( + mm_cfg.DontReceiveOwnPosts, 0, user) +replacements['<mm-dont-receive-own-mail-button>'] = list.FormatOptionButton( + mm_cfg.DontReceiveOwnPosts, 1, user) +replacements['<mm-public-subscription-button>'] = list.FormatOptionButton( + mm_cfg.ConcealSubscription, 0, user) +replacements['<mm-hide-subscription-button>'] = list.FormatOptionButton( + mm_cfg.ConcealSubscription, 1, user) + +replacements['<mm-digest-submit>'] = list.FormatButton('setdigest', + 'Submit My Changes') +replacements['<mm-unsubscribe-button>'] = list.FormatButton('unsub', 'Unsubscribe') +replacements['<mm-digest-pw-box>'] = list.FormatSecureBox('digpw') +replacements['<mm-unsub-pw-box>'] = list.FormatSecureBox('upw') +replacements['<mm-old-pw-box>'] = list.FormatSecureBox('opw') +replacements['<mm-new-pass-box>'] = list.FormatSecureBox('newpw') +replacements['<mm-confirm-pass-box>'] = list.FormatSecureBox('confpw') +replacements['<mm-change-pass-button>'] = list.FormatButton('changepw', + "Change My Password") +replacements['<mm-form-start>'] = list.FormatFormStart('handle_opts', user) +replacements['<mm-user>'] = user +replacements['<mm-presentable-user>'] = presentable_user +replacements['<mm-email-my-pw>'] = list.FormatButton('emailpw', + 'Email My Password To Me') + + +doc.AddItem(list.ParseTags('options.html', replacements)) +print doc.Format() + diff --git a/Mailman/Cgi/private.py b/Mailman/Cgi/private.py new file mode 100644 index 000000000..4a06a7270 --- /dev/null +++ b/Mailman/Cgi/private.py @@ -0,0 +1,228 @@ +#! /usr/bin/env python -u +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Provide a password-interface wrapper around a hierarchy of web pages. + +Currently this is organized to obtain passwords for mailman maillist +subscribers. + + - Set the ROOT variable to point to the root of your archives private + hierarchy. The script will look there for the private archive files. + - Put the ../misc/Cookie.py script in ../../cgi-bin (where the wrapper + executables are). +""" + +import sys, os, string, re +import maillist, mm_err, mm_utils +import Cookie + +ROOT = "/local/pipermail/private/" +SECRET = "secret" # XXX used for hashing + +PAGE = ''' +<html> +<head> + <title>%(listname)s Private Archives Authentication</title> +</head> +<body> +<FORM METHOD=POST ACTION="%(basepath)s/%(path)s"> + <TABLE WIDTH="100%" BORDER="0" CELLSPACING="4" CELLPADDING="5"> + <TR> + <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#99CCFF" ALIGN="CENTER"> + <B><FONT COLOR="#000000" SIZE="+1">%(listname)s Private Archives + Authentication</FONT></B> + </TD> + </TR> + <tr> + <td COLSPAN="2"> <P>%(message)s </td> + <tr> + </tr> + <TD> <div ALIGN="Right">Address: </div></TD> + <TD> <INPUT TYPE=TEXT NAME=username SIZE=30> </TD> + <tr> + </tr> + <TD> <div ALIGN="Right"> Password: </div> </TD> + <TD> <INPUT TYPE=password NAME=password SIZE=30></TD> + <tr> + </tr> + <td></td> + <td> <INPUT TYPE=SUBMIT> + </td> + </tr> + </TABLE> +</FORM> +''' + +login_attempted = 0 +_list = None + +name_pat = re.compile(r""" +(?: / (?: \d{4} q \d\. )? # Match "/", and, optionally, 1998q1." + ( [^/]* ) /? # The SIG name + /[^/]*$ # The trailing 12345.html portion +) | (?: + / ( [^/.]* ) # Match matrix-sig + (?:\.html)? # Optionally match .html + /? # Optionally match a trailing slash + $ # Must match to end of string + +) +""", re.VERBOSE) + +def getListName(path): + match = name_pat.search(path) + if match is None: return + if match.group(1): return match.group(1) + if match.group(2): return match.group(2) + raise ValueError, "Can't identify SIG name" + + +def GetListobj(list_name): + """Return an unlocked instance of the named maillist, if found.""" + global _list + if _list: + return _list + try: + _list = maillist.MailList(list_name, lock=0) + except mm_err.MMUnknownListError: + _list = None + return None + return _list + +def isAuthenticated(list_name): + if os.environ.has_key('HTTP_COOKIE'): + c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) + if c.has_key(list_name): + # The user has a token like 'c++-sig=AE23446AB...'; verify + # that it's correct. + token = c[list_name].value + import base64, md5 + if base64.decodestring(token) != md5.new(SECRET + + list_name + + SECRET).digest(): + return 0 + return 1 + + # No corresponding cookie. OK, then check for username, password + # CGI variables + import cgi + v = cgi.FieldStorage() + username = password = None + if v.has_key('username'): + username = v['username'] + if type(username) == type([]): username = username[0] + username = username.value + if v.has_key('password'): + password = v['password'] + if type(password) == type([]): password = password[0] + password = password.value + + if username is None or password is None: return 0 + + # Record that this is a login attempt, so if it fails the form can + # be displayed with an appropriate message. + global login_attempted + login_attempted=1 + + listobj = GetListobj(list_name) + if not listobj: + print '\n<P>A list named,', repr(list_name), "was not found." + return 0 + + try: + listobj.ConfirmUserPassword( username, password) + except (mm_err.MMBadUserError, mm_err.MMBadPasswordError): + return 1 + + import base64, md5 + token = md5.new(SECRET + list_name + SECRET).digest() + token = base64.encodestring(token) + token = string.strip(token) + c = Cookie.Cookie() + c[list_name] = token + print c # Output the cookie + return 1 + + +def true_path(path): + "Ensure that the path is safe by removing .." + path = string.split(path, '/') + for i in range(len(path)): + if path[i] == ".": path[i] = "" # ./ is just redundant + elif path[i] == "..": + # Remove any .. components + path[i] = "" + j=i-1 + while j>0 and path[j] == "": j=j-1 + path[j] = "" + + path = filter(None, path) + return string.join(path, '/') + +def processPage(page): + """Change any URLs that start with ../ to work properly when output from + /cgi-bin/private""" + # Escape any % signs not followed by ( + page = re.sub('%([^(])', r'%%\1', page) + + # Convert references like HREF="../doc" to just /doc. + page = re.sub('([\'="])../', r'\1/', page) + + return page + +def main(): + print 'Content-type: text/html\n' + path = os.environ.get('PATH_INFO', "/index.html") + true_filename = os.path.join(ROOT, true_path(path) ) + list_name = getListName(path) + + if os.path.isdir(true_filename): + true_filename = true_filename + '/index.html' + + if not isAuthenticated(list_name): + # Output the password form + page = processPage( PAGE ) + + listobj = GetListobj(list_name) + if login_attempted: + message = ("Your email address or password were incorrect." + " Please try again.") + else: + message = ("Please enter your %s subscription email address" + " and password." % listobj.real_name) + while path and path[0] == '/': path=path[1:] # Remove leading /'s + basepath = os.path.split(listobj.GetBaseArchiveURL())[0] + listname = listobj.real_name + print '\n\n', page % vars() + sys.exit(0) + + print '\n\n' + # Authorization confirmed... output the desired file + try: + f = open(true_filename, 'r') + except IOError: + print "<H3>Archive File Not Found</H3>" + print "No file", path + else: + while (1): + data = f.read(16384) + if data == "": break + sys.stdout.write(data) + f.close() + + diff --git a/Mailman/Cgi/roster.py b/Mailman/Cgi/roster.py new file mode 100644 index 000000000..b150df68d --- /dev/null +++ b/Mailman/Cgi/roster.py @@ -0,0 +1,110 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce subscriber roster, using listinfo form data, roster.html template. + +Takes listname in PATH_INFO.""" + + +# We don't need to lock in this script, because we're never going to change +# data. + +import sys +import os, string +import cgi +import mm_utils, maillist, htmlformat, mm_cfg, mm_err + +def main(): + doc = htmlformat.HeadlessDocument() + form = cgi.FieldStorage() + list = get_list() + + bad = "" + # These nested conditionals constituted a cascading authentication + # check, yielding a + if not list.private_roster: + # No privacy. + bad = "" + else: + auth_req = ("%s subscriber list requires authentication." + % list.real_name) + if not form.has_key("roster-pw"): + bad = auth_req + else: + pw = form['roster-pw'].value + # Just the admin password is sufficient - check it early. + if not list.ValidAdminPassword(pw): + if not form.has_key('roster-email'): + # No admin password and no user id, nogo. + bad = auth_req + else: + id = form['roster-email'].value + if list.private_roster == 1: + # Private list - members visible. + try: + list.ConfirmUserPassword(id, pw) + except (mm_err.MMBadUserError, + mm_err.MMBadPasswordError): + bad = ("%s subscriber authentication failed." + % list.real_name) + else: + # Anonymous list - admin-only visible + # - and we already tried admin password, above. + bad = ("%s admin authentication failed." + % list.real_name) + if bad: + doc = error_page_doc(bad) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + + replacements = list.GetStandardReplacements() + + doc.AddItem(list.ParseTags('roster.html', replacements)) + print doc.Format() + +def get_list(): + "Return list or bail out with error page." + + list_info = mm_utils.GetPathPieces(os.environ['PATH_INFO']) + if len(list_info) != 1: + error_page("Invalid options to CGI script.") + sys.exit(0) + list_name = string.lower(list_info[0]) + try: + list = maillist.MailList(list_name, lock = 0) + except mm_err.MMUnknownListError: + error_page("%s: No such list.", list_name) + sys.exit(0) + if not list._ready: + error_page("%s: No such list.", list_name) + sys.exit(0) + return list + +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. + + Optional arg justreturn means just return the doc, don't print it.""" + doc = htmlformat.Document() + doc.SetTitle("Error") + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold(errmsg % args)) + return doc diff --git a/Mailman/Cgi/subscribe.py b/Mailman/Cgi/subscribe.py new file mode 100644 index 000000000..aa69b959f --- /dev/null +++ b/Mailman/Cgi/subscribe.py @@ -0,0 +1,188 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Process listinfo form submission, ie subscriptions or roster requests.""" + +import sys +import os, cgi, string +from regsub import gsub +import mm_utils, maillist, mm_err, mm_message, mm_cfg, mm_pending, htmlformat + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) +list_name = string.lower(list_info[0]) + +if len(list_info) < 1: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +try: + list = maillist.MailList(list_name) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + list.Unlock() + sys.exit(0) + +form = cgi.FieldStorage() + +error = 0 +results = '' + +def call_script(which, pathinfo): + "A little bit of a hack to call one of the scripts..." + os.environ['PATH_INFO'] = string.join(pathinfo, '/') + file = os.path.join(mm_cfg.SCRIPTS_DIR, which) + list.Unlock() + execfile(file) + sys.exit(0) + +####### +# Preliminaries done, actual processing of the form input below. + +if (form.has_key("UserOptions") + or (form.has_key("info") and not form.has_key("email"))): + # Go to user options section. + if not form.has_key("info"): + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("You must supply your email address.")) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + list.Unlock() + sys.exit(0) + addr = form['info'].value + member = list.FindUser(addr) + if not list.FindUser(addr): + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s has no subscribed addr <i>%s</i>." + % (list.real_name, addr))) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + list.Unlock() + sys.exit(0) + call_script('options', [list._internal_name, member]) +if not form.has_key("email"): + error = 1 + results = results + "You must supply a valid email address.<br>" +else: + email = form["email"].value +if not form.has_key("pw") or not form.has_key("pw-conf"): + error = 1 + results = results + "You must supply a valid password, and confirm it.<br>" +else: + pw = form["pw"].value + pwc = form["pw-conf"].value + +if not error and (pw <> pwc): + error = 1 + results = results + "Your passwords did not match.<br>" + +if form.has_key("digest"): + digest = eval(form["digest"].value) + +if not list.digestable: + digest = 0 +elif not list.nondigestable: + digest = 1 + + +def PrintResults(): + replacements = list.GetStandardReplacements() + replacements['<mm-results>'] = results + output = list.ParseTags('subscribe.html', replacements) + + doc.AddItem(output) + print doc.Format() + list.Unlock() + sys.exit(0) + +if error: + PrintResults() + +else: + try: + results = results + ("Confirmation from your email address is " + "required, to prevent anyone from covertly " + "subscribing you. Instructions are being " + "sent to you at %s." % email) + if os.environ.has_key('REMOTE_HOST'): + remote = os.environ['REMOTE_HOST'] + elif os.environ.has_key('REMOTE_ADDR'): + remote = os.environ['REMOTE_ADDR'] + else: + remote = "." + if digest: + digesting = " digest" + else: + digesting = "" + cookie = mm_pending.gencookie() + mm_pending.add2pending(email, pw, digest, cookie) + list.SendTextToUser(subject = "%s -- confirmation of subscription -- request %d" % \ + (list.real_name, cookie), + recipient = email, + sender = list.GetRequestEmail(), + text = mm_pending.VERIFY_FMT % ({"email": email, + "listaddress": list.GetListEmail(), + "listname": list.real_name, + "cookie": cookie, + "requestor": remote, + "request_addr": list.GetRequestEmail()}), + + add_headers = ["Reply-to: %s" + % list.GetRequestEmail(), + "Errors-To: %s" + % list.GetAdminEmail()]) + except mm_err.MMBadEmailError: + results = results + ("Mailman won't accept the given email " + "address as a valid address. (Does it " + "have an @ in it???)<p>") + except mm_err.MMListNotReady: + results = results + ("The list is not fully functional, and " + "can not accept subscription requests.<p>") +# +# deprecating this, it might be useful if we decide to +# allow approved based subscriptions without confirmation +# +## except mm_err.MMNeedApproval, x: +## results = results + ("Subscription was <em>deferred</em> " +## "because:<br> %s<p>Your request must " +## "be approved by the list admin. " +## "You will receive email informing you " +## "of the moderator's descision when they " +## "get to your request.<p>" % x) + except mm_err.MMHostileAddress: + results = results + ("Your subscription is not allowed because " + "the email address you gave is insecure.<p>") + except mm_err.MMAlreadyAMember: + results = results + "You are already subscribed!<p>" + + +PrintResults() +list.Unlock()
\ No newline at end of file diff --git a/modules/Cgi/__init__.py b/modules/Cgi/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/modules/Cgi/__init__.py diff --git a/modules/Cgi/admin.py b/modules/Cgi/admin.py new file mode 100755 index 000000000..c9155c87e --- /dev/null +++ b/modules/Cgi/admin.py @@ -0,0 +1,835 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""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: 734 $" + +import sys +import os, cgi, string, crypt, types, time +import paths # path hacking +import mm_utils, maillist, mm_cfg, mm_err, mm_mailcmd, Cookie +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"), + ('gateway', "Mail-News and News-Mail gateways")] + + +LOGIN_PAGE = """ +<html> +<head> + <title>%(listname)s Administrative Authentication</title> +</head> +<body bgcolor="#ffffff"> +<FORM METHOD=POST ACTION="%(path)s"> +%(message)s + <TABLE WIDTH="100%%" BORDER="0" CELLSPACING="4" CELLPADDING="5"> + <TR> + <TD COLSPAN="2" WIDTH="100%%" BGCOLOR="#99CCFF" ALIGN="CENTER"> + <B><FONT COLOR="#000000" SIZE="+1">%(listname)s Administrative + Authentication</FONT></B> + </TD> + </TR> + <tr> + <TD> <div ALIGN="Right"> List Administrative Password: </div> </TD> + <TD> <INPUT TYPE=password NAME=adminpw SIZE=30></TD> + </tr> + <tr> + <td colspan=2 align=middle> <INPUT TYPE=SUBMIT name="request_login"> + </td> + </tr> + </TABLE> +</FORM> +""" + +# " <- icky emacs font-lock bug workaround + +SECRET="monty" + +def isAuthenticated(list, password=None, SECRET="SECRET"): + import base64, md5 + if password is not None: # explicit login + try: + list.ConfirmAdminPassword(password) + except mm_err.MMBadPasswordError: + AddErrorMessage(doc, 'Error: Incorrect admin password.') + return 0 + except: + print "Content-type: text/html\n" + + print "<p><h3>We're sorry, we hit a bug!</h3>\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<PRE>" + try: + import traceback + sys.stderr = sys.stdout + traceback.print_exc() + except: + print "[failed to get traceback]" + print "\n\n</PRE>" + + token = md5.new(SECRET + list_name + SECRET).digest() + token = base64.encodestring(token) + token = string.strip(token) + c = Cookie.Cookie() + cookie_key = list_name + "-admin" + c[cookie_key] = token + c[cookie_key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE + print c # Output the cookie + return 1 + if os.environ.has_key('HTTP_COOKIE'): + c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) + if c.has_key(list_name + "-admin"): + try: + inp = base64.decodestring(c[list_name + "-admin"].value) + check = md5.new(SECRET+list_name+SECRET).digest() + except Error: # the decodestring may return incorrect padding? + raise 'Decode failed' + return 0 + if inp == check: + return 1 + else: + return 0 + return 0 + + +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, list_info, doc + doc = Document() + + try: + path = os.environ['PATH_INFO'] + except KeyError: + path = "" + list_info = mm_utils.GetPathPieces(path) + # How many ../'s we need to get back to http://host/mailman + + if len(list_info) == 0: + FormatAdminOverview() + return + + list_name = string.lower(list_info[0]) + + try: + lst = maillist.MailList(list_name) + except mm_err.MMUnknownListError: + lst = None + try: + if not (lst and lst._ready): + FormatAdminOverview(error="List <em>%s</em> not found." + % list_name) + return + + 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' + global cgi_data + cgi_data = cgi.FieldStorage() + is_auth = 0 + if cgi_data.has_key("adminpw"): + is_auth = isAuthenticated(lst, cgi_data["adminpw"].value) + message = FontAttr("Sorry, wrong password. Try again.", + color="ff5060", size="+1").Format() + else: + is_auth = isAuthenticated(lst) + message = "" + if not is_auth: + print "Content-type: text/html\n\n" + print LOGIN_PAGE % ({"listname": list_name, + "path": os.environ.get("REQUEST_URI", "/mailman/admin"), + "message": message}) + return + + if len(cgi_data.keys()): + if cgi_data.has_key('VARHELP'): + FormatOptionHelp(doc, cgi_data['VARHELP'].value, lst) + print doc.Format(bgcolor="#ffffff") + return + if (cgi_data.has_key('bounce_matching_headers')): + try: + pairs = lst.parse_matching_header_opt() + except mm_err.MMBadConfigError, line: + AddErrorMessage(doc, + 'Warning: bad matching-header line' + ' (does it have the colon?)<ul> %s </ul>', + line) + + if not lst.digestable and len(lst.digest_members): + AddErrorMessage(doc, + 'Warning: you have digest members,' + ' but digests are turned off.' + ' Those people will not receive mail.') + if not lst.nondigestable and len(lst.members): + AddErrorMessage(doc, + 'Warning: you have lst members,' + ' but non-digestified mail is turned' + ' off. They will receive mail until' + ' you fix this problem.') + if len(cgi_data.keys()): + ChangeOptions(lst, category, cgi_data, doc) + FormatConfiguration(doc, lst, category, category_suffix) + print doc.Format(bgcolor="#ffffff") + + finally: + if lst: + lst.Unlock() + +# Form Production: + +def FormatAdminOverview(error=None): + "Present a general welcome and itemize the (public) lists." + doc = Document() + legend = "%s maillists - Admin Links" % mm_cfg.DEFAULT_HOST_NAME + doc.SetTitle(legend) + + table = Table(border=0, width="100%") + table.AddRow([Center(Header(2, legend))]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + + advertised = [] + names = mm_utils.list_names() + names.sort() + for n in names: + l = maillist.MailList(n, lock=0) + if l.advertised: advertised.append(l) + + if error: + greeting = FontAttr(error, color="ff5060", size="+1") + else: + greeting = "Welcome!" + + if not advertised: + welcome_items = (greeting, + "<p>" + " There currently are no publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + ) + else: + + welcome_items = ( + greeting, + "<p>" + " Below is the collection of publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + (' Click on a list name to visit the configuration pages' + ' for that list.' + ) + ) + + welcome_items = (welcome_items + + (" To visit the administrators configuration page for" + " an unadvertised list, open a URL similar to this" + + + (" one, but with a '/' and the %slist name appended.<p>" + % ((error and "the right ") or "")) + + + " General list information can be found at ", + Link(os.path.join('../'* mm_utils.GetNestingLevel(), + "listinfo/"), "the maillist overview page"), + "." + "<p>(Send questions and comments to ", + Link("mailto:%s" % mm_cfg.MAILMAN_OWNER, + mm_cfg.MAILMAN_OWNER), + ".)<p>" + ) + ) + + table.AddRow([apply(Container, welcome_items)]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) + + if advertised: + table.AddRow([Italic("List"), Italic("Description")]) + for l in advertised: + table.AddRow([Link(l.GetRelativeScriptURL('admin'), + Bold(l.real_name)),l.description]) + + doc.AddItem(table) + + print doc.Format(bgcolor="#ffffff") + +def FormatConfiguration(doc, lst, 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' % lst.real_name) + doc.AddItem(Center(Header(2, ('%s Maillist Configuration - %s Section' + % (lst.real_name, label))))) + doc.AddItem('<hr>') + + links_table = Table(valign="top") + + links_table.AddRow([Center(Bold("Configuration Categories")), + Center(Bold("Other Administrative Activities"))]) + other_links = UnorderedList() + link = Link(lst.GetRelativeScriptURL('admindb'), + 'Tend to pending administrative requests.') + other_links.AddItem(link) + link = Link(lst.GetRelativeScriptURL('listinfo'), + 'Go to the general list information page.') + other_links.AddItem(link) + link = Link(lst.GetRelativeScriptURL('edithtml'), + 'Edit the HTML for the public list pages.') + other_links.AddItem(link) + + these_links = UnorderedList() + for k, v in CATEGORIES: + if k == category: + these_links.AddItem("<b> => " + v + " <= </b>") + else: + these_links.AddItem(Link("%s/%s" % + (lst.GetRelativeScriptURL('admin'),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('<hr>') + if category_suffix: + form = Form("%s/%s" % (lst.GetRelativeScriptURL('admin'), + category_suffix)) + else: + form = Form(lst.GetRelativeScriptURL('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.)<p>") + + form.AddItem(FormatOptionsSection(category, lst)) + + form.AddItem(Center(FormatPasswordStuff())) + + form.AddItem(lst.GetMailmanFooter()) + +def FormatOptionsSection(category, lst): + """Produce the category-specific options table.""" + if category == 'members': + # Special case for members section. + return FormatMembershipOptions(lst) + + options = GetConfigOptions(lst, 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 = "<center><i>" + item + "</i></center>" + 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 + AddOptionsTableItem(big_table, item, category, lst) + big_table.AddRow(['<br>']) + big_table.AddCellInfo(big_table.GetCurrentRowIndex(), 0, colspan=2) + return big_table + +def AddOptionsTableItem(table, item, category, lst, nodetails=0): + """Add a row to an options table with the item description and value.""" + try: + got = GetItemCharacteristics(item) + varname, kind, params, dependancies, descr, elaboration = got + except ValueError, msg: + lst.LogMsg("error", "admin: %s", msg) + return Italic("<malformed option>") + descr = GetItemGuiDescr(lst, category, varname, descr, + elaboration, nodetails) + val = GetItemGuiValue(lst, kind, varname, params) + table.AddRow([descr, val]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1, + bgcolor="#cccccc") + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + bgcolor="#cccccc") + +def FormatOptionHelp(doc, varref, lst): + item = bad = None + reflist = string.split(varref, '/') + if len(reflist) == 2: + category, varname = reflist + options = GetConfigOptions(lst, 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'])) + else: + try: + got = GetItemCharacteristics(item) + varname, kind, params, dependancies, descr, elaboration = got + except ValueError, msg: + bad = msg + if not bad and not elaboration: + bad = "Option %s has no extended help." % varname + if bad: + AddErrorMessage(doc, bad) + return + + header = Table(width="100%") + legend = ('%s Maillist Configuration Help<br><em>%s</em> Option' + % (lst.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("<b>%s</b> (%s): %s<p>" % (varname, category, item[4])) + doc.AddItem("%s<p>" % item[5]) + + form = Form(os.path.join(lst.GetRelativeScriptURL('admin'), category)) + valtab = Table(cellspacing=3, cellpadding=4) + AddOptionsTableItem(valtab, item, category, lst, nodetails=1) + form.AddItem(valtab) + # XXX I don't think we want to be able to set options from two places, + # since they'll go out of sync. + #form.AddItem(Center(FormatPasswordStuff())) + doc.AddItem(Center(form)) + +def GetItemCharacteristics(table_entry): + """Break out the components of an item description from its table entry: + 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: + raise ValueError, ("Badly formed options entry:\n %s" + % table_entry) + return (varname, kind, params, dependancies, descr, elaboration) + +def GetItemGuiValue(lst, kind, varname, params): + """Return a representation of an item's settings.""" + if kind == mm_cfg.Radio or kind == mm_cfg.Toggle: + return RadioButtonArray(varname, params, getattr(lst, varname)) + elif (kind == mm_cfg.String or kind == mm_cfg.Email or + kind == mm_cfg.Host or kind == mm_cfg.Number): + return TextBox(varname, getattr(lst, varname), params) + elif kind == mm_cfg.Text: + if params: + r, c = params + else: + r, c = None, None + val = getattr(lst, varname) + if not val: + val = '' + return TextArea(varname, val, r, c) + elif kind == mm_cfg.EmailList: + if params: + r, c = params + else: + r, c = None, None + res = string.join(getattr(lst, varname), '\n') + return TextArea(varname, res, r, c, wrap='off') + +def GetItemGuiDescr(lst, category, varname, descr, elaboration, nodetails): + """Return a representation of an item's description, with link to + elaboration if any.""" + descr = '<div ALIGN="right">' + descr + if not nodetails and elaboration: + ref = "../" * (mm_utils.GetNestingLevel()-1) + list_name + "/" + ref = ref + '?VARHELP=' + category + "/" + varname + descr = Container(descr, + Link(ref, " (Details)", target="MMHelp"), + "</div>") + else: + descr = descr + "</div>" + return descr + +def FormatMembershipOptions(lst): + container = Container() + header = Table(width="100%") + header.AddRow([Center(Header(2, "Membership Management"))]) + header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + container.AddItem(header) + user_table = Table(width="90%") + user_table.AddRow([Center(Header(4, "Membership List"))]) + user_table.AddCellInfo(user_table.GetCurrentRowIndex(), + user_table.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) + + members = {} + digests = {} + for member in lst.members: + members[member] = 1 + for member in lst.digest_members: + digests[member] = 1 + all = lst.members + lst.digest_members + if len(all) > mm_cfg.ADMIN_MEMBER_CHUNKSIZE: + chunks = mm_utils.chunkify(all) + if not cgi_data.has_key("chunk"): + chunk = 0 + else: + chunk = string.atoi(cgi_data["chunk"].value) + all = chunks[chunk] + footer = ("<p><em>To View other sections, " + "click on the appropriate range listed below</em>") + chunk_indices = range(len(chunks)) + chunk_indices.remove(chunk) + buttons = [] + pi = os.environ["PATH_INFO"] + for ci in chunk_indices: + start, end = chunks[ci][0], chunks[ci][-1] + buttons.append("<a href=/mailman/admin%s?chunk=%d> from %s to %s </a>" % \ + ( pi, ci, start, end)) + buttons = apply(UnorderedList, tuple(buttons)) + footer = footer + buttons.Format() + "<p>" + else: + all.sort() + footer = "<p>" + for member in all: + cells = [member + "<input type=hidden name=user value=%s>" % (member), + "subscribed " +CheckBox(member + "_subscribed", "on", 1).Format(), + ] + if members.get(member): + cells.append("digest " + CheckBox(member + "_digest", "off", 0).Format()) + else: + cells.append("digest " + CheckBox(member + "_digest", "on", 1).Format()) + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if lst.GetUserOption(member, mm_mailcmd.option_info[opt]): + value = "on" + checked = 1 + else: + value = "off" + checked = 0 + box = CheckBox("%s_%s" % (member, opt), value, checked) + cells.append("%s %s" % (opt, box.Format())) + user_table.AddRow(cells) + container.AddItem(Center(user_table)) + container.AddItem(footer) + t = Table(width="90%") + t.AddRow([Center(Header(4, "Mass Subscribe Members"))]) + t.AddCellInfo(t.GetCurrentRowIndex(), + t.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) + container.AddItem(Center(t)) + container.AddItem(Center(TextArea(name='subscribees', rows=10,cols=60,wrap=None))) + container.AddItem(Center("<em> Enter One address per line</em><p>")) + return container + +def FormatPasswordStuff(): + submit = Table(bgcolor="#99ccff", + border=0, cellspacing=0, cellpadding=2, width="100%") + submit.AddRow([Bold(SubmitButton('submit', 'Submit Your Changes'))]) + submit.AddCellInfo(submit.GetCurrentRowIndex(), 0, align="middle") + change_pw_table = Table(bgcolor="#99cccc", border=0, + cellspacing=0, cellpadding=2, width="90%") + change_pw_table.AddRow([Bold(Center('To Change The Administrator Password')), + '<div ALIGN="right"> Enter the new password: </div>', + PasswordBox('newpw'),]) + change_pw_table.AddCellInfo(0, 0, align="middle", colspan=2) + change_pw_table.AddRow(['<div ALIGN="right"> Enter the current password </div>', + PasswordBox('adminpw'), + '<div ALIGN="right">Again to confirm it:</div>', + PasswordBox('confirmpw')]) + password_stuff = Container() + password_stuff.AddItem(change_pw_table) + password_stuff.AddItem("<p>") + password_stuff.AddItem(submit) + return password_stuff + +# XXX klm - looks like turn_on_moderation is orphaned. +turn_on_moderation = 0 + +# Options processing + +def GetValidValue(lst, 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(lst, 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(lst, prop) + return num + except: + return getattr(lst, prop) + else: + # Should never get here... + return val + + +def ChangeOptions(lst, category, cgi_info, document): + dirty = 0 + confirmed = 0 + if cgi_info.has_key('newpw'): + if cgi_info.has_key('confirmpw'): + if cgi_info.has_key('adminpw'): + try: + lst.ConfirmAdminPassword(cgi_info['adminpw'].value) + confirmed = 1 + except mm_err.MMBadPasswordError: + m = "Error: incorrect administrator password" + document.AddItem(Header(3, Italic(FontAttr(m, color="ff5060")))) + confirmed = 0 + if confirmed: + new = cgi_info['newpw'].value + confirm = cgi_info['confirmpw'].value + if new == confirm: + lst.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")))) + # + # for some reason, the login page mangles important values for the list + # such as .real_name so we only process these changes if the category + # is not "members" and the request is not from the login page + # -scott 19980515 + # + if category != 'members' and not cgi_info.has_key("request_login") and\ + len(cgi_info.keys()) > 1: + opt_list = GetConfigOptions(lst, 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(lst, property, kind, val, deps) + if getattr(lst, property) != value: + setattr(lst, property, value) + dirty = 1 + # + # mass subscription processing for members category + # + if cgi_info.has_key('subscribees'): + name_text = cgi_info['subscribees'].value + name_text = string.replace(name_text, '\r', '') + names = string.split(name_text, '\n') + if '' in names: + names.remove('') + subscribe_success = [] + subscribe_errors = [] + for new_name in map(string.strip,names): + digest = 0 + if not lst.digestable: + digest = 0 + if not lst.nondigestable: + digest = 1 + try: + lst.ApprovedAddMember(new_name, (mm_utils.GetRandomSeed() + + mm_utils.GetRandomSeed()), digest) + subscribe_success.append(new_name) + except mm_err.MMAlreadyAMember: + subscribe_errors.append((new_name, 'Already a member')) + + except mm_err.MMBadEmailError: + subscribe_errors.append((new_name, "Bad/Invalid email address")) + except mm_err.MMHostileAddress: + subscribe_errors.append((new_name, "Hostile Address (illegal characters)")) + if subscribe_success: + document.AddItem(Header(5, "Successfully Subscribed:")) + document.AddItem(apply(UnorderedList, tuple((subscribe_success)))) + document.AddItem("<p>") + if subscribe_errors: + document.AddItem(Header(5, "Error Subscribing:")) + items = map(lambda x: "%s -- %s" % (x[0], x[1]), subscribe_errors) + document.AddItem(apply(UnorderedList, tuple((items)))) + document.AddItem("<p>") + # + # do the user options for members category + # + if cgi_info.has_key('user'): + user = cgi_info["user"] + if type(user) is type([]): + users = [] + for ui in range(len(user)): + users.append(user[ui].value) + else: + users = [user.value] + for user in users: + if not cgi_info.has_key('%s_subscribed' % (user)): + lst.DeleteMember(user) + dirty = 1 + continue + if not cgi_info.has_key("%s_digest" % (user)): + if user in lst.digest_members: + list.digest_members.remove(user) + dirty = 1 + if user not in lst.members: + lst.members.append(user) + dirty = 1 + else: + if user not in lst.digest_members: + lst.digest_members.append(user) + dirty = 1 + if user in lst.members: + lst.members.remove(user) + dirty = 1 + + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if cgi_info.has_key("%s_%s" % (user, opt)): + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 1) + dirty = 1 + else: + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 0) + dirty = 1 + + + if dirty: + lst.Save() + +def AddErrorMessage(doc, errmsg, *args): + doc.AddItem(Header(3, Italic(FontAttr(errmsg % args, + color="#ff66cc")))) + + +_config_info = None +def GetConfigOptions(lst, category): + global _config_info + if _config_info == None: + _config_info = lst.GetConfigInfo() + return _config_info[category] + diff --git a/modules/Cgi/admindb.py b/modules/Cgi/admindb.py new file mode 100755 index 000000000..415184d3b --- /dev/null +++ b/modules/Cgi/admindb.py @@ -0,0 +1,232 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce and process the pending-approval items for a list.""" + +import sys +import os, cgi, string, crypt, types +import mm_utils, maillist, mm_err, htmlformat + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + + +if len(list_info) < 1: + doc.SetTitle("Admindb Error") + doc.AddItem(htmlformat.Header(2, "Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name) +except: + msg = "%s: No such list." % list_name + doc.SetTitle("Admindb Error - %s" % msg) + doc.AddItem(htmlformat.Header(2, msg)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +if not list._ready: + msg = "%s: No such list." % list_name + doc.SetTitle("Admindb Error - %s" % msg) + doc.AddItem(htmlformat.Header(2, msg)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +# Note, these 2 functions use i only to count the number of times to +# go around. We always operate on the first element of the list +# because we're going to delete the element after we operate on it. + +def SubscribeAll(): + for i in range(len(list.requests['add_member'])): + comment_key = 'comment-%d' % list.requests['add_member'][0][0] + if form.has_key(comment_key): + list.HandleRequest(('add_member', 0), 1, form[comment_key].value) + else: + list.HandleRequest(('add_member', 0), 1) + +def SubscribeNone(): + for i in range(len(list.requests['add_member'])): + comment_key = 'comment-%d' % list.requests['add_member'][0][0] + if form.has_key(comment_key): + list.HandleRequest(('add_member', 0), 0, form[comment_key].value) + else: + list.HandleRequest(('add_member', 0), 0) + +def PrintHeader(str, error=0): + if error: + it = htmlformat.FontAttr(str, color="ff5060") + else: + it = str + doc.AddItem(htmlformat.Header(3, htmlformat.Italic(it))) + doc.AddItem('<hr>') + +def HandleRequests(doc): + if not form.has_key('adminpw'): + PrintHeader('You need to supply the admin password ' + 'to answer requests.', error=1) + return + try: + list.ConfirmAdminPassword(form['adminpw'].value) + except: + PrintHeader('Incorrect admin password.', error=1) + return + ignore_subscribes = 0 + if form.has_key('subscribe_all'): + ignore_subscribes = 1 + SubscribeAll() + elif form.has_key('subscribe_none'): + ignore_subscribes = 1 + SubscribeNone() + for k in form.keys(): + try: + # XXX Security?! + v = eval(form[k].value) + request_id = eval(k) + except: # For stuff like adminpw + continue + if type(request_id) <> types.IntType: + continue + try: + request = list.GetRequest(request_id) + except mm_err.MMBadRequestId: + continue # You've already changed the database. No biggie. + if ignore_subscribes and request[0] == 'add_member': + # We already handled this request. + continue + comment_key = 'comment-%d' % request_id + if form.has_key(comment_key): + list.HandleRequest(request, v, form[comment_key].value) + else: + list.HandleRequest(request, v) + list.Save() + PrintHeader('Database Updated...') + + +def PrintAddMemberRequest(val, table): + table.AddRow([ + val[3], + htmlformat.RadioButtonArray(val[0], ("Refuse", "Subscribe")), + htmlformat.TextBox("comment-%d" % val[0], size=50) + ]) + +def PrintPostRequest(val, form): + t = htmlformat.Table(cellspacing=10) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Post held because: ')), + val[3]]) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Action to take on this post:')), + htmlformat.RadioButtonArray(val[0], ("Approve", "Reject", + "Discard (eg, spam)")), + htmlformat.SubmitButton('submit', 'Submit All Data') + ]) + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('If you reject this post, ' + 'explain (optional):')), + htmlformat.TextBox("comment-%d" % val[0], size=50)]) + + cur_row = t.GetCurrentRowIndex() + cur_col = t.GetCurrentCellIndex() + t.AddCellInfo(cur_row, cur_col, colspan=3) + + t.AddRow([ + htmlformat.FontSize("+1", + htmlformat.Bold('Contents:'))]) + form.AddItem(t) + form.AddItem(htmlformat.Preformatted(val[2][1])) + form.AddItem('<p>') + + +def PrintRequests(doc): + # The only types of requests we know about are add_member and post. + # Anything else that might have gotten in here somehow we'll just + # ignore (This should never happen unless someone is hacking at + # the code). + + doc.AddItem(htmlformat.Header(2, "Administrative requests for " + "'%s' mailing list" % list.real_name)) + doc.AddItem(htmlformat.FontSize("+1", htmlformat.Link( + list.GetRelativeScriptURL('admin'), htmlformat.Italic( + 'View or edit the list configuration information')))) + doc.AddItem('<p><hr>') + if not list.RequestsPending(): + doc.AddItem(htmlformat.Header(3,'There are no pending requests.')) + doc.AddItem(list.GetMailmanFooter()) + return + form = htmlformat.Form(list.GetRelativeScriptURL('admindb')) + doc.AddItem(form) + form.AddItem('Admin password: ') + form.AddItem(htmlformat.PasswordBox('adminpw')) + form.AddItem('<p>') + if list.requests.has_key('add_member'): +## form.AddItem('<hr>') +## t = htmlformat.Table(cellspacing=10) +## t.AddRow([ +## htmlformat.SubmitButton('submit', 'Submit All Data'), +## htmlformat.SubmitButton('subscribe_all', 'Subscribe Everybody'), +## htmlformat.SubmitButton('subscribe_none', 'Refuse Everybody') +## ]) +## form.AddItem(t) + form.AddItem('<hr>') + form.AddItem(htmlformat.Center( + htmlformat.Header(2, 'Subscription Requests'))) + t = htmlformat.Table(border=2) + t.AddRow([ + htmlformat.Bold('Email'), + htmlformat.Bold('Descision'), + htmlformat.Bold('Reasoning for subscription refusal (optional)')]) + for request in list.requests['add_member']: + PrintAddMemberRequest(request, t) + + form.AddItem(t) + t = htmlformat.Table(cellspacing=10) + t.AddRow([ + htmlformat.SubmitButton('submit', 'Submit All Data'), + htmlformat.SubmitButton('subscribe_all', 'Subscribe Everybody'), + htmlformat.SubmitButton('subscribe_none', 'Refuse Everybody') + ]) + form.AddItem(t) + + # Print submitit buttons... + if list.requests.has_key('post'): + for request in list.requests['post']: + form.AddItem('<hr>') + form.AddItem(htmlformat.Center(htmlformat.Header(2, "Held Message"))) + PrintPostRequest(request, form) + doc.AddItem(list.GetMailmanFooter()) + +try: + form = cgi.FieldStorage() + if len(form.keys()): + doc.SetTitle("%s Admindb Results" % list.real_name) + HandleRequests(doc) + else: + doc.SetTitle("%s Admindb" % list.real_name) + PrintRequests(doc) + text = doc.Format(bgcolor="#ffffff") + print text + sys.stdout.flush() +finally: + list.Unlock() diff --git a/modules/Cgi/archives.py b/modules/Cgi/archives.py new file mode 100755 index 000000000..2b0ed0fa9 --- /dev/null +++ b/modules/Cgi/archives.py @@ -0,0 +1,86 @@ +#! /usr/bin/env/python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# This script is being deprecated, in favor hookups for an external archiver. + +# We don't need to lock in this script, because we're never going to change +# data. + +import sys +import os, types, posix, string +import mm_utils, maillist, htmlformat + +print "Content-type: text/html" +print + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 1: + print "<h2>Invalid options to CGI script.</h2>" + sys.exit(0) + +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name) +except: + print "<h2>%s: No such list.</h2>" % list_name + sys.exit(0) + +if not list._ready: + print "<h2>%s: No such list.</h2>" % list_name + sys.exit(0) + +def GetArchiveList(list): + archive_list = htmlformat.UnorderedList() + + def ArchiveFilter(str): + if str[:7] <> 'volume_': + return 0 + try: + x = eval(str[7:]) + if type(x) <> types.IntType: + return 0 + if x < 1: + return 0 + return 1 + except: + return 0 + try: + dir_listing = filter(ArchiveFilter, os.listdir(list.archive_directory)) + except posix.error: + return "<h3><em>No archives are currently available.</em></h3>" + if not len(dir_listing): + return "<h3><em>No archives are currently available.</em></h3>" + for dir in dir_listing: + link = htmlformat.Link(os.path.join(list._base_archive_url, dir), + "Volume %s" % dir[7:]) + archive_list.AddItem(link) + + return archive_list.Format() + + + + +replacements = list.GetStandardReplacements() +replacements['<mm-archive-list>'] = GetArchiveList(list) + +# Just doing print list.ParseTags(...) calls ParseTags twice??? +text = list.ParseTags('archives.html', replacements) +print text diff --git a/modules/Cgi/edithtml.py b/modules/Cgi/edithtml.py new file mode 100755 index 000000000..faf35e715 --- /dev/null +++ b/modules/Cgi/edithtml.py @@ -0,0 +1,167 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Script which implements admin editing of the list's html templates.""" + +import sys +import os, cgi, string, crypt, types +import mm_utils, maillist, mm_cfg +import htmlformat + +#Editable templates. We should also be able to edit the archive index, which +#currently isn't a working template, but will be soon. + +template_data = (('listinfo.html', 'General list information page'), + ('subscribe.html', 'Subscribe results page'), + ('options.html', 'User specific options page'), + ('handle_opts.html', 'Changing user options results page'), + ('archives.html', 'Archives index page') + ) + + +def InitDocument(): + return htmlformat.HeadlessDocument() + +doc = InitDocument() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 1: + doc.AddItem(htmlformat.Header(2, "Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +list_name = string.lower(list_info[0]) + +try: + list = maillist.MailList(list_name, lock=0) +except: + doc.AddItem(htmlformat.Header(2, "%s : No such list" % list_name)) + print doc.Format() + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "%s : No such list" % list_name)) + print doc.Format() + sys.exit(0) + + +if len(list_info) > 1: + template_name = list_info[1] + for (template, info) in template_data: + if template == template_name: + template_info = info + doc.SetTitle('%s -- Edit html for %s' % + (list.real_name, template_info)) + break + else: + doc.SetTitle('Edit HTML : Error') + doc.AddItem(htmlformat.Header(2, "%s: Invalid template" % template_name)) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) +else: + doc.SetTitle('%s -- HTML Page Editing' % list.real_name) + doc.AddItem(htmlformat.Header(1, '%s -- HTML Page Editing' % list.real_name)) + doc.AddItem(htmlformat.Header(2, 'Select page to edit:')) + template_list = htmlformat.UnorderedList() + for (template, info) in template_data: + l = htmlformat.Link(os.path.join(list.GetRelativeScriptURL('edithtml'), + template), info) + + template_list.AddItem(l) + doc.AddItem(htmlformat.FontSize("+2", template_list)) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + + +def FormatHTML(doc): + doc.AddItem(htmlformat.Header(1,'%s:' % list.real_name)) + doc.AddItem(htmlformat.Header(1, template_info)) + + doc.AddItem('<hr>') + + link = htmlformat.Link(list.GetRelativeScriptURL('admin'), + 'View or edit the list configuration information.') + doc.AddItem(htmlformat.FontSize("+1", link)) + doc.AddItem('<p>') + + doc.AddItem('<hr>') + + form = htmlformat.Form(os.path.join(list.GetRelativeScriptURL('edithtml'), + template_name)) + doc.AddItem(form) + + password_table = htmlformat.Table() + password_table.AddRow(['Enter the admin password to edit html:', + htmlformat.PasswordBox('adminpw')]) + password_table.AddRow(['When you are done making changes...', + htmlformat.SubmitButton('submit', 'Submit Changes')]) + + form.AddItem(password_table) + + text = mm_utils.QuoteHyperChars(list.SnarfHTMLTemplate(template_name)) + form.AddItem(htmlformat.TextArea('html_code', text, rows=40, cols=75)) + +def ChangeHTML(list, cgi_info, template_name, doc): + if not cgi_info.has_key('html_code'): + doc.AddItem(htmlformat.Header(3,"Can't have empty html page.")) + doc.AddItem(htmlformat.Header(3,"HTML Unchanged.")) + doc.AddItem('<hr>') + return + code = cgi_info['html_code'].value + f = open(os.path.join(list._template_dir, template_name), 'w') + f.write(code) + f.close() + doc.AddItem(htmlformat.Header(3, 'HTML successfully updated.')) + doc.AddItem('<hr>') + +try: + cgi_data = cgi.FieldStorage() + if len(cgi_data.keys()): + if not cgi_data.has_key('adminpw'): + m = 'Error: You must supply the admin password to edit html.' + doc.AddItem(htmlformat.Header(3, + htmlformat.Italic( + htmlformat.FontAttr( + m, color="ff5060")))) + doc.AddItem('<hr>') + else: + try: + list.ConfirmAdminPassword(cgi_data['adminpw'].value) + ChangeHTML(list, cgi_data, template_name, doc) + except: + m = 'Error: Incorrect admin password.' + doc.AddItem(htmlformat.Header(3, + htmlformat.Italic( + htmlformat.FontAttr( + m, color="ff5060")))) + doc.AddItem('<hr>') + + + + FormatHTML(doc) + +finally: + try: + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + except: + pass diff --git a/modules/Cgi/handle_opts.py b/modules/Cgi/handle_opts.py new file mode 100755 index 000000000..7b5747f4f --- /dev/null +++ b/modules/Cgi/handle_opts.py @@ -0,0 +1,207 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Process input to user options form.""" + +import sys +import os, cgi, string +import mm_utils, maillist, mm_err, mm_cfg, htmlformat + + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +list_name = string.lower(list_info[0]) +user = list_info[1] + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +try: + list = maillist.MailList(list_name) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name)) + print doc.Format(bgcolor="#ffffff") + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name)) + print doc.Format(bgcolor="#ffffff") + list.Unlock() + sys.exit(0) + + +def PrintResults(results): + replacements = list.GetStandardReplacements() + replacements['<mm-results>'] = results + replacements['<mm-operation>'] = operation + output = list.ParseTags('handle_opts.html', replacements) + + doc.AddItem(output) + print doc.Format(bgcolor="#ffffff") + list.Unlock() + sys.exit(0) + +form = cgi.FieldStorage() + +error = 0 +operation = "" + +if string.lower(user) not in list.members + list.digest_members: + PrintResults("%s not a member!<p>" % user) + +if form.has_key("unsub"): + operation = "Unsubscribe" + if not form.has_key("upw"): + PrintResults("You must give your password to unsubscribe.<p>") + else: + try: + pw = form["upw"].value + if list.ConfirmUserPassword(user, pw): + list.DeleteMember(user, "web cmd") + except mm_err.MMListNotReady: + PrintResults("List is not functional.") + except mm_err.MMNoSuchUserError: + PrintResults("You seem to already be not a member.<p>") + except mm_err.MMBadUserError: + PrintResults("Your account has gone awry - " + "please contact the list administrator!<p>") + except mm_err.MMBadPasswordError: + PrintResults("That password was incorrect.<p>") +# except: +# PrintResults('''An unknown error occured. <p> +#Please send mail to <a href=%s>%s</a> explaining +#exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER, +# mm_cfg.MAILMAN_OWNER)) + + PrintResults("You have been unsubscribed.<p>") +elif form.has_key("emailpw"): + try: + list.MailUserPassword(user) + PrintResults("A reminder of your password " + "has been emailed to you.<p>") + except mm_err.MMBadUserError: + PrintResults("Your password entry has not been found. The list " + "manager is being notified.<p>") + +elif form.has_key("changepw"): + if (form.has_key('opw') + and form.has_key('newpw') + and form.has_key('confpw')): + try: + list.ConfirmUserPassword(user, form['opw'].value) + list.ChangeUserPassword(user, + form['newpw'].value, form['confpw'].value) + except mm_err.MMListNotReady: + PrintResults("The list is currently not funcitonal.") + except mm_err.MMNotAMemberError: + PrintResults("You seem to no longer be a list member.") + except mm_err.MMBadPasswordError: + PrintResults("The old password you supplied was incorrect.") + except mm_err.MMPasswordsMustMatch: + PrintResults("Passwords must match.") + except: + PrintResults('''An unknown error occured. <p> +Please send mail to <a href=%s>%s</a> explaining +exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER, mm_cfg.MAILMAN_OWNER)) + + + PrintResults("Your password has been changed.") + else: + PrintResults("You must supply your old password," + " and your new password twice.") + +else: + # If keys don't exist, set them to whatever they were. (essentially a noop) + if form.has_key("digest"): + digest_value = eval(form["digest"].value) + else: + digest_value = list.GetUserOption(user, mm_cfg.Digests) + if form.has_key("mime"): + mime = eval(form["mime"].value) + else: + mime = list.GetUserOption(user, mm_cfg.DisableMime) + if form.has_key("dontreceive"): + dont_receive = eval(form["dontreceive"].value) + else: + dont_receive = list.GetUserOption(user, mm_cfg.DontReceiveOwnPosts) + if form.has_key("ackposts"): + ack_posts = eval(form["ackposts"].value) + else: + ack_posts = list.GetUserOption(user, mm_cfg.AcknowlegePosts) + if form.has_key("disablemail"): + disable_mail = eval(form["disablemail"].value) + else: + disable_mail = list.GetUserOption(user, mm_cfg.DisableDelivery) + if form.has_key("conceal"): + conceal = eval(form["conceal"].value) + else: + conceal = list.GetUserOption(user, mm_cfg.ConcealSubscription) + + if not form.has_key("digpw"): + PrintResults("You must supply a password to change options.") + try: + list.ConfirmUserPassword(user, form['digpw'].value) + except mm_err.MMAlreadyDigested: + pass + except mm_err.MMAlreadyUndigested: + pass + except mm_err.MMMustDigestError: + PrintResults("List only accepts digest members.") + except mm_err.MMCantDigestError: + PrintResults("List doesn't accept digest members.") + except mm_err.MMNotAMemberError: + PrintResults("%s isn't subscribed to this list." % mail.GetSender()) + except mm_err.MMListNotReady: + PrintResults("List is not functional.") + except mm_err.MMNoSuchUserError: + PrintResults("%s is not subscribed to this list." %mail.GetSender()) + except mm_err.MMBadPasswordError: + PrintResults("You gave the wrong password.") + except: + PrintResults('''An unknown error occured. <p> +Please send mail to <a href=%s>%s</a> explaining +exactly what you did to get this error.<p>''' % (mm_cfg.MAILMAN_OWNER)) + + + list.SetUserOption(user, mm_cfg.DisableDelivery, disable_mail) + list.SetUserOption(user, mm_cfg.DontReceiveOwnPosts, dont_receive) + list.SetUserOption(user, mm_cfg.AcknowlegePosts, ack_posts) + list.SetUserOption(user, mm_cfg.DisableMime, mime) + try: + list.SetUserDigest(user, digest_value) + except (mm_err.MMAlreadyDigested, mm_err.MMAlreadyUndigested): + pass + list.SetUserOption(user, mm_cfg.ConcealSubscription, conceal) + PrintResults("You have successfully set your options.") + + +list.Unlock()
\ No newline at end of file diff --git a/modules/Cgi/listinfo.py b/modules/Cgi/listinfo.py new file mode 100755 index 000000000..b1aae2d92 --- /dev/null +++ b/modules/Cgi/listinfo.py @@ -0,0 +1,178 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce listinfo page, primary web entry-point to maillists. +""" + +# No lock needed in this script, because we don't change data. + +import sys +import os, string +from regsub import gsub +import mm_utils, maillist, mm_cfg +from htmlformat import * + +def main(): + try: + path = os.environ['PATH_INFO'] + except KeyError: + path = "" + + list_info = mm_utils.GetPathPieces(path) + + if len(list_info) == 0: + FormatListinfoOverview() + return + + list_name = string.lower(list_info[0]) + + try: + list = maillist.MailList(list_name, lock=0) + except: + list = None + + if not (list and list._ready): + FormatListinfoOverview(error="List <em>%s</em> not found." % list_name) + return + + FormatListListinfo(list) + +def FormatListinfoOverview(error=None): + "Present a general welcome and itemize the (public) lists for this host." + doc = Document() + legend = "%s maillists" % mm_cfg.DEFAULT_HOST_NAME + doc.SetTitle(legend) + + table = Table(border=0, width="100%") + table.AddRow([Center(Header(2, legend))]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + colspan=2, bgcolor="#99ccff") + + advertised = [] + names = mm_utils.list_names() + names.sort() + + # XXX We need a portable way to determine the host by which we are being + # visited! An absolute URL would do... + if os.environ.has_key('HTTP_HOST'): + http_host = os.environ['HTTP_HOST'] + else: + http_host = None + + for n in names: + l = maillist.MailList(n, lock = 0) + if l.advertised: + if (http_host + and (string.find(http_host, l.host_name) == -1 + and string.find(l.host_name, http_host) == -1)): + # List is for different identity for this host - skip it. + continue + else: + advertised.append(l) + + if error: + greeting = FontAttr(error, color="ff5060", size="+1") + else: + greeting = "Welcome!" + + if not advertised: + welcome_items = (greeting, + "<p>" + " There currently are no publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + ) + else: + + welcome_items = ( + greeting, + "<p>" + " Below is the collection of publicly-advertised ", + Link(mm_cfg.MAILMAN_URL, "mailman"), + " maillists on %s." % mm_cfg.DEFAULT_HOST_NAME, + (' Click on a list name to visit the info page' + ' for that list. There you can learn more about the list,' + ' subscribe to it, or find the roster of current subscribers.'), + ) + + welcome_items = (welcome_items + + (" To visit the info page for an unadvertised list," + " open a URL similar to this one, but with a '/' and" + + + (" the %slist name appended." + % ((error and "right ") or "")) + + + '<p> List administrators, you can visit ', + Link(os.path.join('../' * mm_utils.GetNestingLevel(), + 'admin/'), "the list admin overview page"), + " to find the management interface for your list." + "<p>(Send questions or comments to ", + Link("mailto:%s" % mm_cfg.MAILMAN_OWNER, + mm_cfg.MAILMAN_OWNER), + ".)<p>")) + + table.AddRow([apply(Container, welcome_items)]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) + + if advertised: + table.AddRow([Italic("List"), Italic("Description")]) + for l in advertised: + table.AddRow([Link(l.GetRelativeScriptURL('listinfo'), + Bold(l.real_name)), l.description]) + + doc.AddItem(table) + + print doc.Format(bgcolor="#ffffff") + +def FormatListListinfo(list): + "Expand the listinfo template against the list's settings, and print." + + doc = HeadlessDocument() + + replacements = list.GetStandardReplacements() + + if not list.digestable or not list.nondigestable: + replacements['<mm-digest-radio-button>'] = "" + replacements['<mm-undigest-radio-button>'] = "" + else: + replacements['<mm-digest-radio-button>'] = list.FormatDigestButton() + replacements['<mm-undigest-radio-button>'] = \ + list.FormatUndigestButton() + replacements['<mm-plain-digests-button>'] = list.FormatPlainDigestsButton() + replacements['<mm-mime-digests-button>'] = list.FormatMimeDigestsButton() + replacements['<mm-subscribe-box>'] = list.FormatBox('email') + replacements['<mm-subscribe-button>'] = list.FormatButton('email-button', + text='Subscribe') + replacements['<mm-new-password-box>'] = list.FormatSecureBox('pw') + replacements['<mm-confirm-password>'] = list.FormatSecureBox('pw-conf') + replacements['<mm-subscribe-form-start>'] = \ + list.FormatFormStart('subscribe') + replacements['<mm-roster-form-start>'] = list.FormatFormStart('roster') + replacements['<mm-editing-options>'] = list.FormatEditingOption() + replacements['<mm-info-button>'] = SubmitButton('UserOptions', + 'Edit Options').Format() + replacements['<mm-roster-option>'] = list.FormatRosterOptionForUser() + + # Do the expansion. + doc.AddItem(list.ParseTags('listinfo.html', replacements)) + + print doc.Format() + + +if __name__ == "__main__": + main() diff --git a/modules/Cgi/options.py b/modules/Cgi/options.py new file mode 100755 index 000000000..07664bbb3 --- /dev/null +++ b/modules/Cgi/options.py @@ -0,0 +1,125 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# 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 list options.html template. + +Takes listname/userid in PATH_INFO, expecting an `obscured' userid. Depending +on the mm_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. + +import sys +import os, string +import mm_utils, maillist, htmlformat, mm_cfg + +doc = htmlformat.HeadlessDocument() + +try: + path = os.environ['PATH_INFO'] +except KeyError: + path = "" +list_info = mm_utils.GetPathPieces(path) + +if len(list_info) < 2: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +list_name = string.lower(list_info[0]) +user = mm_utils.UnobscureEmail(list_info[1]) + +try: + list = maillist.MailList(list_name, lock=0) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + +if string.lower(user) not in list.members + list.digest_members: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such member %s." + % (list_name, `user`))) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + +# Re-obscure the user's address for the page banner if obscure_addresses set. +if list.obscure_addresses: + presentable_user = mm_utils.ObscureEmail(user, for_text=1) +else: + presentable_user = user + +replacements = list.GetStandardReplacements() +replacements['<mm-digest-radio-button>'] = list.FormatOptionButton( + mm_cfg.Digests, 1, user) +replacements['<mm-undigest-radio-button>'] = list.FormatOptionButton( + mm_cfg.Digests, 0, user) +replacements['<mm-plain-digests-button>'] = list.FormatOptionButton( + mm_cfg.DisableMime, 1, user) +replacements['<mm-mime-digests-button>'] = list.FormatOptionButton( + mm_cfg.DisableMime, 0, user) +replacements['<mm-delivery-enable-button>'] = list.FormatOptionButton( + mm_cfg.DisableDelivery, 0, user) +replacements['<mm-delivery-disable-button>'] = list.FormatOptionButton( + mm_cfg.DisableDelivery, 1, user) +replacements['<mm-disabled-notice>'] = list.FormatDisabledNotice(user) +replacements['<mm-dont-ack-posts-button>'] = list.FormatOptionButton( + mm_cfg.AcknowlegePosts, 0, user) +replacements['<mm-ack-posts-button>'] = list.FormatOptionButton( + mm_cfg.AcknowlegePosts, 1, user) +replacements['<mm-receive-own-mail-button>'] = list.FormatOptionButton( + mm_cfg.DontReceiveOwnPosts, 0, user) +replacements['<mm-dont-receive-own-mail-button>'] = list.FormatOptionButton( + mm_cfg.DontReceiveOwnPosts, 1, user) +replacements['<mm-public-subscription-button>'] = list.FormatOptionButton( + mm_cfg.ConcealSubscription, 0, user) +replacements['<mm-hide-subscription-button>'] = list.FormatOptionButton( + mm_cfg.ConcealSubscription, 1, user) + +replacements['<mm-digest-submit>'] = list.FormatButton('setdigest', + 'Submit My Changes') +replacements['<mm-unsubscribe-button>'] = list.FormatButton('unsub', 'Unsubscribe') +replacements['<mm-digest-pw-box>'] = list.FormatSecureBox('digpw') +replacements['<mm-unsub-pw-box>'] = list.FormatSecureBox('upw') +replacements['<mm-old-pw-box>'] = list.FormatSecureBox('opw') +replacements['<mm-new-pass-box>'] = list.FormatSecureBox('newpw') +replacements['<mm-confirm-pass-box>'] = list.FormatSecureBox('confpw') +replacements['<mm-change-pass-button>'] = list.FormatButton('changepw', + "Change My Password") +replacements['<mm-form-start>'] = list.FormatFormStart('handle_opts', user) +replacements['<mm-user>'] = user +replacements['<mm-presentable-user>'] = presentable_user +replacements['<mm-email-my-pw>'] = list.FormatButton('emailpw', + 'Email My Password To Me') + + +doc.AddItem(list.ParseTags('options.html', replacements)) +print doc.Format() + diff --git a/modules/Cgi/private.py b/modules/Cgi/private.py new file mode 100755 index 000000000..4a06a7270 --- /dev/null +++ b/modules/Cgi/private.py @@ -0,0 +1,228 @@ +#! /usr/bin/env python -u +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Provide a password-interface wrapper around a hierarchy of web pages. + +Currently this is organized to obtain passwords for mailman maillist +subscribers. + + - Set the ROOT variable to point to the root of your archives private + hierarchy. The script will look there for the private archive files. + - Put the ../misc/Cookie.py script in ../../cgi-bin (where the wrapper + executables are). +""" + +import sys, os, string, re +import maillist, mm_err, mm_utils +import Cookie + +ROOT = "/local/pipermail/private/" +SECRET = "secret" # XXX used for hashing + +PAGE = ''' +<html> +<head> + <title>%(listname)s Private Archives Authentication</title> +</head> +<body> +<FORM METHOD=POST ACTION="%(basepath)s/%(path)s"> + <TABLE WIDTH="100%" BORDER="0" CELLSPACING="4" CELLPADDING="5"> + <TR> + <TD COLSPAN="2" WIDTH="100%" BGCOLOR="#99CCFF" ALIGN="CENTER"> + <B><FONT COLOR="#000000" SIZE="+1">%(listname)s Private Archives + Authentication</FONT></B> + </TD> + </TR> + <tr> + <td COLSPAN="2"> <P>%(message)s </td> + <tr> + </tr> + <TD> <div ALIGN="Right">Address: </div></TD> + <TD> <INPUT TYPE=TEXT NAME=username SIZE=30> </TD> + <tr> + </tr> + <TD> <div ALIGN="Right"> Password: </div> </TD> + <TD> <INPUT TYPE=password NAME=password SIZE=30></TD> + <tr> + </tr> + <td></td> + <td> <INPUT TYPE=SUBMIT> + </td> + </tr> + </TABLE> +</FORM> +''' + +login_attempted = 0 +_list = None + +name_pat = re.compile(r""" +(?: / (?: \d{4} q \d\. )? # Match "/", and, optionally, 1998q1." + ( [^/]* ) /? # The SIG name + /[^/]*$ # The trailing 12345.html portion +) | (?: + / ( [^/.]* ) # Match matrix-sig + (?:\.html)? # Optionally match .html + /? # Optionally match a trailing slash + $ # Must match to end of string + +) +""", re.VERBOSE) + +def getListName(path): + match = name_pat.search(path) + if match is None: return + if match.group(1): return match.group(1) + if match.group(2): return match.group(2) + raise ValueError, "Can't identify SIG name" + + +def GetListobj(list_name): + """Return an unlocked instance of the named maillist, if found.""" + global _list + if _list: + return _list + try: + _list = maillist.MailList(list_name, lock=0) + except mm_err.MMUnknownListError: + _list = None + return None + return _list + +def isAuthenticated(list_name): + if os.environ.has_key('HTTP_COOKIE'): + c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) + if c.has_key(list_name): + # The user has a token like 'c++-sig=AE23446AB...'; verify + # that it's correct. + token = c[list_name].value + import base64, md5 + if base64.decodestring(token) != md5.new(SECRET + + list_name + + SECRET).digest(): + return 0 + return 1 + + # No corresponding cookie. OK, then check for username, password + # CGI variables + import cgi + v = cgi.FieldStorage() + username = password = None + if v.has_key('username'): + username = v['username'] + if type(username) == type([]): username = username[0] + username = username.value + if v.has_key('password'): + password = v['password'] + if type(password) == type([]): password = password[0] + password = password.value + + if username is None or password is None: return 0 + + # Record that this is a login attempt, so if it fails the form can + # be displayed with an appropriate message. + global login_attempted + login_attempted=1 + + listobj = GetListobj(list_name) + if not listobj: + print '\n<P>A list named,', repr(list_name), "was not found." + return 0 + + try: + listobj.ConfirmUserPassword( username, password) + except (mm_err.MMBadUserError, mm_err.MMBadPasswordError): + return 1 + + import base64, md5 + token = md5.new(SECRET + list_name + SECRET).digest() + token = base64.encodestring(token) + token = string.strip(token) + c = Cookie.Cookie() + c[list_name] = token + print c # Output the cookie + return 1 + + +def true_path(path): + "Ensure that the path is safe by removing .." + path = string.split(path, '/') + for i in range(len(path)): + if path[i] == ".": path[i] = "" # ./ is just redundant + elif path[i] == "..": + # Remove any .. components + path[i] = "" + j=i-1 + while j>0 and path[j] == "": j=j-1 + path[j] = "" + + path = filter(None, path) + return string.join(path, '/') + +def processPage(page): + """Change any URLs that start with ../ to work properly when output from + /cgi-bin/private""" + # Escape any % signs not followed by ( + page = re.sub('%([^(])', r'%%\1', page) + + # Convert references like HREF="../doc" to just /doc. + page = re.sub('([\'="])../', r'\1/', page) + + return page + +def main(): + print 'Content-type: text/html\n' + path = os.environ.get('PATH_INFO', "/index.html") + true_filename = os.path.join(ROOT, true_path(path) ) + list_name = getListName(path) + + if os.path.isdir(true_filename): + true_filename = true_filename + '/index.html' + + if not isAuthenticated(list_name): + # Output the password form + page = processPage( PAGE ) + + listobj = GetListobj(list_name) + if login_attempted: + message = ("Your email address or password were incorrect." + " Please try again.") + else: + message = ("Please enter your %s subscription email address" + " and password." % listobj.real_name) + while path and path[0] == '/': path=path[1:] # Remove leading /'s + basepath = os.path.split(listobj.GetBaseArchiveURL())[0] + listname = listobj.real_name + print '\n\n', page % vars() + sys.exit(0) + + print '\n\n' + # Authorization confirmed... output the desired file + try: + f = open(true_filename, 'r') + except IOError: + print "<H3>Archive File Not Found</H3>" + print "No file", path + else: + while (1): + data = f.read(16384) + if data == "": break + sys.stdout.write(data) + f.close() + + diff --git a/modules/Cgi/roster.py b/modules/Cgi/roster.py new file mode 100755 index 000000000..b150df68d --- /dev/null +++ b/modules/Cgi/roster.py @@ -0,0 +1,110 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Produce subscriber roster, using listinfo form data, roster.html template. + +Takes listname in PATH_INFO.""" + + +# We don't need to lock in this script, because we're never going to change +# data. + +import sys +import os, string +import cgi +import mm_utils, maillist, htmlformat, mm_cfg, mm_err + +def main(): + doc = htmlformat.HeadlessDocument() + form = cgi.FieldStorage() + list = get_list() + + bad = "" + # These nested conditionals constituted a cascading authentication + # check, yielding a + if not list.private_roster: + # No privacy. + bad = "" + else: + auth_req = ("%s subscriber list requires authentication." + % list.real_name) + if not form.has_key("roster-pw"): + bad = auth_req + else: + pw = form['roster-pw'].value + # Just the admin password is sufficient - check it early. + if not list.ValidAdminPassword(pw): + if not form.has_key('roster-email'): + # No admin password and no user id, nogo. + bad = auth_req + else: + id = form['roster-email'].value + if list.private_roster == 1: + # Private list - members visible. + try: + list.ConfirmUserPassword(id, pw) + except (mm_err.MMBadUserError, + mm_err.MMBadPasswordError): + bad = ("%s subscriber authentication failed." + % list.real_name) + else: + # Anonymous list - admin-only visible + # - and we already tried admin password, above. + bad = ("%s admin authentication failed." + % list.real_name) + if bad: + doc = error_page_doc(bad) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + sys.exit(0) + + replacements = list.GetStandardReplacements() + + doc.AddItem(list.ParseTags('roster.html', replacements)) + print doc.Format() + +def get_list(): + "Return list or bail out with error page." + + list_info = mm_utils.GetPathPieces(os.environ['PATH_INFO']) + if len(list_info) != 1: + error_page("Invalid options to CGI script.") + sys.exit(0) + list_name = string.lower(list_info[0]) + try: + list = maillist.MailList(list_name, lock = 0) + except mm_err.MMUnknownListError: + error_page("%s: No such list.", list_name) + sys.exit(0) + if not list._ready: + error_page("%s: No such list.", list_name) + sys.exit(0) + return list + +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. + + Optional arg justreturn means just return the doc, don't print it.""" + doc = htmlformat.Document() + doc.SetTitle("Error") + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold(errmsg % args)) + return doc diff --git a/modules/Cgi/subscribe.py b/modules/Cgi/subscribe.py new file mode 100755 index 000000000..aa69b959f --- /dev/null +++ b/modules/Cgi/subscribe.py @@ -0,0 +1,188 @@ +#! /usr/bin/env python +# +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Process listinfo form submission, ie subscriptions or roster requests.""" + +import sys +import os, cgi, string +from regsub import gsub +import mm_utils, maillist, mm_err, mm_message, mm_cfg, mm_pending, htmlformat + +doc = htmlformat.Document() + +path = os.environ['PATH_INFO'] +list_info = mm_utils.GetPathPieces(path) +list_name = string.lower(list_info[0]) + +if len(list_info) < 1: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("Invalid options to CGI script.")) + print doc.Format() + sys.exit(0) + +try: + list = maillist.MailList(list_name) +except: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + sys.exit(0) + + +if not list._ready: + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s: No such list." % list_name )) + print doc.Format() + list.Unlock() + sys.exit(0) + +form = cgi.FieldStorage() + +error = 0 +results = '' + +def call_script(which, pathinfo): + "A little bit of a hack to call one of the scripts..." + os.environ['PATH_INFO'] = string.join(pathinfo, '/') + file = os.path.join(mm_cfg.SCRIPTS_DIR, which) + list.Unlock() + execfile(file) + sys.exit(0) + +####### +# Preliminaries done, actual processing of the form input below. + +if (form.has_key("UserOptions") + or (form.has_key("info") and not form.has_key("email"))): + # Go to user options section. + if not form.has_key("info"): + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("You must supply your email address.")) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + list.Unlock() + sys.exit(0) + addr = form['info'].value + member = list.FindUser(addr) + if not list.FindUser(addr): + doc.AddItem(htmlformat.Header(2, "Error")) + doc.AddItem(htmlformat.Bold("%s has no subscribed addr <i>%s</i>." + % (list.real_name, addr))) + doc.AddItem(list.GetMailmanFooter()) + print doc.Format() + list.Unlock() + sys.exit(0) + call_script('options', [list._internal_name, member]) +if not form.has_key("email"): + error = 1 + results = results + "You must supply a valid email address.<br>" +else: + email = form["email"].value +if not form.has_key("pw") or not form.has_key("pw-conf"): + error = 1 + results = results + "You must supply a valid password, and confirm it.<br>" +else: + pw = form["pw"].value + pwc = form["pw-conf"].value + +if not error and (pw <> pwc): + error = 1 + results = results + "Your passwords did not match.<br>" + +if form.has_key("digest"): + digest = eval(form["digest"].value) + +if not list.digestable: + digest = 0 +elif not list.nondigestable: + digest = 1 + + +def PrintResults(): + replacements = list.GetStandardReplacements() + replacements['<mm-results>'] = results + output = list.ParseTags('subscribe.html', replacements) + + doc.AddItem(output) + print doc.Format() + list.Unlock() + sys.exit(0) + +if error: + PrintResults() + +else: + try: + results = results + ("Confirmation from your email address is " + "required, to prevent anyone from covertly " + "subscribing you. Instructions are being " + "sent to you at %s." % email) + if os.environ.has_key('REMOTE_HOST'): + remote = os.environ['REMOTE_HOST'] + elif os.environ.has_key('REMOTE_ADDR'): + remote = os.environ['REMOTE_ADDR'] + else: + remote = "." + if digest: + digesting = " digest" + else: + digesting = "" + cookie = mm_pending.gencookie() + mm_pending.add2pending(email, pw, digest, cookie) + list.SendTextToUser(subject = "%s -- confirmation of subscription -- request %d" % \ + (list.real_name, cookie), + recipient = email, + sender = list.GetRequestEmail(), + text = mm_pending.VERIFY_FMT % ({"email": email, + "listaddress": list.GetListEmail(), + "listname": list.real_name, + "cookie": cookie, + "requestor": remote, + "request_addr": list.GetRequestEmail()}), + + add_headers = ["Reply-to: %s" + % list.GetRequestEmail(), + "Errors-To: %s" + % list.GetAdminEmail()]) + except mm_err.MMBadEmailError: + results = results + ("Mailman won't accept the given email " + "address as a valid address. (Does it " + "have an @ in it???)<p>") + except mm_err.MMListNotReady: + results = results + ("The list is not fully functional, and " + "can not accept subscription requests.<p>") +# +# deprecating this, it might be useful if we decide to +# allow approved based subscriptions without confirmation +# +## except mm_err.MMNeedApproval, x: +## results = results + ("Subscription was <em>deferred</em> " +## "because:<br> %s<p>Your request must " +## "be approved by the list admin. " +## "You will receive email informing you " +## "of the moderator's descision when they " +## "get to your request.<p>" % x) + except mm_err.MMHostileAddress: + results = results + ("Your subscription is not allowed because " + "the email address you gave is insecure.<p>") + except mm_err.MMAlreadyAMember: + results = results + "You are already subscribed!<p>" + + +PrintResults() +list.Unlock()
\ No newline at end of file |
