Apply 80_fix_string_search.patch
[mspang/vmailman.git] / Mailman / Cgi / admin.py
1 # Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
17
18 """Process and produce the list-administration options forms."""
19
20 # For Python 2.1.x compatibility
21 from __future__ import nested_scopes
22
23 import sys
24 import os
25 import re
26 import cgi
27 import sha
28 import urllib
29 import signal
30 from types import *
31 from string import lowercase, digits
32
33 from email.Utils import unquote, parseaddr, formataddr
34
35 from Mailman import mm_cfg
36 from Mailman import Utils
37 from Mailman import MailList
38 from Mailman import Errors
39 from Mailman import MemberAdaptor
40 from Mailman import i18n
41 from Mailman.UserDesc import UserDesc
42 from Mailman.htmlformat import *
43 from Mailman.Cgi import Auth
44 from Mailman.Logging.Syslog import syslog
45
46 # Set up i18n
47 _ = i18n._
48 i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
49
50 NL = '\n'
51 OPTCOLUMNS = 11
52
53 try:
54     True, False
55 except NameError:
56     True = 1
57     False = 0
58
59
60 \f
61 def main():
62     # Try to find out which list is being administered
63     parts = Utils.GetPathPieces()
64     if not parts:
65         # None, so just do the admin overview and be done with it
66         admin_overview()
67         return
68     # Get the list object
69     listname = parts[0].lower()
70     try:
71         mlist = MailList.MailList(listname, lock=0)
72     except Errors.MMListError, e:
73         # Avoid cross-site scripting attacks
74         safelistname = Utils.websafe(listname)
75         admin_overview(_('No such list <em>%(safelistname)s</em>'))
76         syslog('error', 'admin.py access for non-existent list: %s',
77                listname)
78         return
79     # Now that we know what list has been requested, all subsequent admin
80     # pages are shown in that list's preferred language.
81     i18n.set_language(mlist.preferred_language)
82     # If the user is not authenticated, we're done.
83     cgidata = cgi.FieldStorage(keep_blank_values=1)
84
85     if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
86                                   mm_cfg.AuthSiteAdmin),
87                                  cgidata.getvalue('adminpw', '')):
88         if cgidata.has_key('adminpw'):
89             # This is a re-authorization attempt
90             msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
91         else:
92             msg = ''
93         Auth.loginpage(mlist, 'admin', msg=msg)
94         return
95
96     # Which subcategory was requested?  Default is `general'
97     if len(parts) == 1:
98         category = 'general'
99         subcat = None
100     elif len(parts) == 2:
101         category = parts[1]
102         subcat = None
103     else:
104         category = parts[1]
105         subcat = parts[2]
106
107     # Is this a log-out request?
108     if category == 'logout':
109         print mlist.ZapCookie(mm_cfg.AuthListAdmin)
110         Auth.loginpage(mlist, 'admin', frontpage=1)
111         return
112
113     # Sanity check
114     if category not in mlist.GetConfigCategories().keys():
115         category = 'general'
116
117     # Is the request for variable details?
118     varhelp = None
119     qsenviron = os.environ.get('QUERY_STRING')
120     parsedqs = None
121     if qsenviron:
122         parsedqs = cgi.parse_qs(qsenviron)
123     if cgidata.has_key('VARHELP'):
124         varhelp = cgidata.getvalue('VARHELP')
125     elif parsedqs:
126         # POST methods, even if their actions have a query string, don't get
127         # put into FieldStorage's keys :-(
128         qs = parsedqs.get('VARHELP')
129         if qs and isinstance(qs, ListType):
130             varhelp = qs[0]
131     if varhelp:
132         option_help(mlist, varhelp)
133         return
134
135     # The html page document
136     doc = Document()
137     doc.set_language(mlist.preferred_language)
138
139     # From this point on, the MailList object must be locked.  However, we
140     # must release the lock no matter how we exit.  try/finally isn't enough,
141     # because of this scenario: user hits the admin page which may take a long
142     # time to render; user gets bored and hits the browser's STOP button;
143     # browser shuts down socket; server tries to write to broken socket and
144     # gets a SIGPIPE.  Under Apache 1.3/mod_cgi, Apache catches this SIGPIPE
145     # (I presume it is buffering output from the cgi script), then turns
146     # around and SIGTERMs the cgi process.  Apache waits three seconds and
147     # then SIGKILLs the cgi process.  We /must/ catch the SIGTERM and do the
148     # most reasonable thing we can in as short a time period as possible.  If
149     # we get the SIGKILL we're screwed (because it's uncatchable and we'll
150     # have no opportunity to clean up after ourselves).
151     #
152     # This signal handler catches the SIGTERM, unlocks the list, and then
153     # exits the process.  The effect of this is that the changes made to the
154     # MailList object will be aborted, which seems like the only sensible
155     # semantics.
156     #
157     # BAW: This may not be portable to other web servers or cgi execution
158     # models.
159     def sigterm_handler(signum, frame, mlist=mlist):
160         # Make sure the list gets unlocked...
161         mlist.Unlock()
162         # ...and ensure we exit, otherwise race conditions could cause us to
163         # enter MailList.Save() while we're in the unlocked state, and that
164         # could be bad!
165         sys.exit(0)
166
167     mlist.Lock()
168     try:
169         # Install the emergency shutdown signal handler
170         signal.signal(signal.SIGTERM, sigterm_handler)
171
172         if cgidata.keys():
173             # There are options to change
174             change_options(mlist, category, subcat, cgidata, doc)
175             # Let the list sanity check the changed values
176             mlist.CheckValues()
177         # Additional sanity checks
178         if not mlist.digestable and not mlist.nondigestable:
179             doc.addError(
180                 _('''You have turned off delivery of both digest and
181                 non-digest messages.  This is an incompatible state of
182                 affairs.  You must turn on either digest delivery or
183                 non-digest delivery or your mailing list will basically be
184                 unusable.'''), tag=_('Warning: '))
185
186         if not mlist.digestable and mlist.getDigestMemberKeys():
187             doc.addError(
188                 _('''You have digest members, but digests are turned
189                 off. Those people will not receive mail.'''),
190                 tag=_('Warning: '))
191         if not mlist.nondigestable and mlist.getRegularMemberKeys():
192             doc.addError(
193                 _('''You have regular list members but non-digestified mail is
194                 turned off.  They will receive mail until you fix this
195                 problem.'''), tag=_('Warning: '))
196         # Glom up the results page and print it out
197         show_results(mlist, doc, category, subcat, cgidata)
198         print doc.Format()
199         mlist.Save()
200     finally:
201         # Now be sure to unlock the list.  It's okay if we get a signal here
202         # because essentially, the signal handler will do the same thing.  And
203         # unlocking is unconditional, so it's not an error if we unlock while
204         # we're already unlocked.
205         mlist.Unlock()
206
207
208 \f
209 def admin_overview(msg=''):
210     # Show the administrative overview page, with the list of all the lists on
211     # this host.  msg is an optional error message to display at the top of
212     # the page.
213     #
214     # This page should be displayed in the server's default language, which
215     # should have already been set.
216     hostname = Utils.get_domain()
217     legend = _('%(hostname)s mailing lists - Admin Links')
218     # The html `document'
219     doc = Document()
220     doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
221     doc.SetTitle(legend)
222     # The table that will hold everything
223     table = Table(border=0, width="100%")
224     table.AddRow([Center(Header(2, legend))])
225     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
226                       bgcolor=mm_cfg.WEB_HEADER_COLOR)
227     # Skip any mailing list that isn't advertised.
228     advertised = []
229     listnames = Utils.list_names()
230     listnames.sort()
231
232     for name in listnames:
233         mlist = MailList.MailList(name, lock=0)
234         if mlist.advertised:
235             if mm_cfg.VIRTUAL_HOST_OVERVIEW and \
236                    mlist.web_page_url.find(hostname) == -1:
237                 # List is for different identity of this host - skip it.
238                 continue
239             else:
240                 advertised.append((mlist.GetScriptURL('admin'),
241                                    mlist.real_name,
242                                    mlist.description))
243     # Greeting depends on whether there was an error or not
244     if msg:
245         greeting = FontAttr(msg, color="ff5060", size="+1")
246     else:
247         greeting = _("Welcome!")
248
249     welcome = []
250     mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
251     if not advertised:
252         welcome.extend([
253             greeting,
254             _('''<p>There currently are no publicly-advertised %(mailmanlink)s
255             mailing lists on %(hostname)s.'''),
256             ])
257     else:
258         welcome.extend([
259             greeting,
260             _('''<p>Below is the collection of publicly-advertised
261             %(mailmanlink)s mailing lists on %(hostname)s.  Click on a list
262             name to visit the configuration pages for that list.'''),
263             ])
264
265     creatorurl = Utils.ScriptURL('create')
266     mailman_owner = Utils.get_site_email()
267     extra = msg and _('right ') or ''
268     welcome.extend([
269         _('''To visit the administrators configuration page for an
270         unadvertised list, open a URL similar to this one, but with a '/' and
271         the %(extra)slist name appended.  If you have the proper authority,
272         you can also <a href="%(creatorurl)s">create a new mailing list</a>.
273
274         <p>General list information can be found at '''),
275         Link(Utils.ScriptURL('listinfo'),
276              _('the mailing list overview page')),
277         '.',
278         _('<p>(Send questions and comments to '),
279         Link('mailto:%s' % mailman_owner, mailman_owner),
280         '.)<p>',
281         ])
282
283     table.AddRow([Container(*welcome)])
284     table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
285
286     if advertised:
287         table.AddRow(['&nbsp;', '&nbsp;'])
288         table.AddRow([Bold(FontAttr(_('List'), size='+2')),
289                       Bold(FontAttr(_('Description'), size='+2'))
290                       ])
291         highlight = 1
292         for url, real_name, description in advertised:
293             table.AddRow(
294                 [Link(url, Bold(real_name)),
295                       description or Italic(_('[no description available]'))])
296             if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
297                 table.AddRowInfo(table.GetCurrentRowIndex(),
298                                  bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
299             highlight = not highlight
300
301     doc.AddItem(table)
302     doc.AddItem('<hr>')
303     doc.AddItem(MailmanLogo())
304     print doc.Format()
305
306
307 \f
308 def option_help(mlist, varhelp):
309     # The html page document
310     doc = Document()
311     doc.set_language(mlist.preferred_language)
312     # Find out which category and variable help is being requested for.
313     item = None
314     reflist = varhelp.split('/')
315     if len(reflist) >= 2:
316         category = subcat = None
317         if len(reflist) == 2:
318             category, varname = reflist
319         elif len(reflist) == 3:
320             category, subcat, varname = reflist
321         options = mlist.GetConfigInfo(category, subcat)
322         for i in options:
323             if i and i[0] == varname:
324                 item = i
325                 break
326     # Print an error message if we couldn't find a valid one
327     if not item:
328         bad = _('No valid variable name found.')
329         doc.addError(bad)
330         doc.AddItem(mlist.GetMailmanFooter())
331         print doc.Format()
332         return
333     # Get the details about the variable
334     varname, kind, params, dependancies, description, elaboration = \
335              get_item_characteristics(item)
336     # Set up the document
337     realname = mlist.real_name
338     legend = _("""%(realname)s Mailing list Configuration Help
339     <br><em>%(varname)s</em> Option""")
340
341     header = Table(width='100%')
342     header.AddRow([Center(Header(3, legend))])
343     header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
344                        bgcolor=mm_cfg.WEB_HEADER_COLOR)
345     doc.SetTitle(_("Mailman %(varname)s List Option Help"))
346     doc.AddItem(header)
347     doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description))
348     if elaboration:
349         doc.AddItem("%s<p>" % elaboration)
350
351     if subcat:
352         url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
353     else:
354         url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
355     form = Form(url)
356     valtab = Table(cellspacing=3, cellpadding=4, width='100%')
357     add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
358     form.AddItem(valtab)
359     form.AddItem('<p>')
360     form.AddItem(Center(submit_button()))
361     doc.AddItem(Center(form))
362
363     doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here
364     could cause other screens to be out-of-sync.  Be sure to reload any other
365     pages that are displaying this option for this mailing list.  You can also
366     """))
367
368     adminurl = mlist.GetScriptURL('admin')
369     if subcat:
370         url = '%s/%s/%s' % (adminurl, category, subcat)
371     else:
372         url = '%s/%s' % (adminurl, category)
373     categoryname = mlist.GetConfigCategories()[category][0]
374     doc.AddItem(Link(url, _('return to the %(categoryname)s options page.')))
375     doc.AddItem('</em>')
376     doc.AddItem(mlist.GetMailmanFooter())
377     print doc.Format()
378
379
380 \f
381 def show_results(mlist, doc, category, subcat, cgidata):
382     # Produce the results page
383     adminurl = mlist.GetScriptURL('admin')
384     categories = mlist.GetConfigCategories()
385     label = _(categories[category][0])
386
387     # Set up the document's headers
388     realname = mlist.real_name
389     doc.SetTitle(_('%(realname)s Administration (%(label)s)'))
390     doc.AddItem(Center(Header(2, _(
391         '%(realname)s mailing list administration<br>%(label)s Section'))))
392     doc.AddItem('<hr>')
393     # Now we need to craft the form that will be submitted, which will contain
394     # all the variable settings, etc.  This is a bit of a kludge because we
395     # know that the autoreply and members categories supports file uploads.
396     encoding = None
397     if category in ('autoreply', 'members'):
398         encoding = 'multipart/form-data'
399     if subcat:
400         form = Form('%s/%s/%s' % (adminurl, category, subcat),
401                     encoding=encoding)
402     else:
403         form = Form('%s/%s' % (adminurl, category), encoding=encoding)
404     # This holds the two columns of links
405     linktable = Table(valign='top', width='100%')
406     linktable.AddRow([Center(Bold(_("Configuration Categories"))),
407                       Center(Bold(_("Other Administrative Activities")))])
408     # The `other links' are stuff in the right column.
409     otherlinks = UnorderedList()
410     otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'),
411                             _('Tend to pending moderator requests')))
412     otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'),
413                             _('Go to the general list information page')))
414     otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'),
415                             _('Edit the public HTML pages and text files')))
416     otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(),
417                             _('Go to list archives')).Format() +
418                        '<br>&nbsp;<br>')
419     # We do not allow through-the-web deletion of the site list!
420     if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS and \
421            mlist.internal_name() <> mm_cfg.MAILMAN_SITE_LIST:
422         otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'),
423                                 _('Delete this mailing list')).Format() +
424                            _(' (requires confirmation)<br>&nbsp;<br>'))
425     otherlinks.AddItem(Link('%s/logout' % adminurl,
426                             # BAW: What I really want is a blank line, but
427                             # adding an &nbsp; won't do it because of the
428                             # bullet added to the list item.
429                             '<FONT SIZE="+2"><b>%s</b></FONT>' %
430                             _('Logout')))
431     # These are links to other categories and live in the left column
432     categorylinks_1 = categorylinks = UnorderedList()
433     categorylinks_2 = ''
434     categorykeys = categories.keys()
435     half = len(categorykeys) / 2
436     counter = 0
437     subcat = None
438     for k in categorykeys:
439         label = _(categories[k][0])
440         url = '%s/%s' % (adminurl, k)
441         if k == category:
442             # Handle subcategories
443             subcats = mlist.GetConfigSubCategories(k)
444             if subcats:
445                 subcat = Utils.GetPathPieces()[-1]
446                 for k, v in subcats:
447                     if k == subcat:
448                         break
449                 else:
450                     # The first subcategory in the list is the default
451                     subcat = subcats[0][0]
452                 subcat_items = []
453                 for sub, text in subcats:
454                     if sub == subcat:
455                         text = Bold('[%s]' % text).Format()
456                     subcat_items.append(Link(url + '/' + sub, text))
457                 categorylinks.AddItem(
458                     Bold(label).Format() +
459                     UnorderedList(*subcat_items).Format())
460             else:
461                 categorylinks.AddItem(Link(url, Bold('[%s]' % label)))
462         else:
463             categorylinks.AddItem(Link(url, label))
464         counter += 1
465         if counter >= half:
466             categorylinks_2 = categorylinks = UnorderedList()
467             counter = -len(categorykeys)
468     # Make the emergency stop switch a rude solo light
469     etable = Table()
470     # Add all the links to the links table...
471     etable.AddRow([categorylinks_1, categorylinks_2])
472     etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top')
473     if mlist.emergency:
474         label = _('Emergency moderation of all list traffic is enabled')
475         etable.AddRow([Center(
476             Link('?VARHELP=general/emergency', Bold(label)))])
477         color = mm_cfg.WEB_ERROR_COLOR
478         etable.AddCellInfo(etable.GetCurrentRowIndex(), 0,
479                            colspan=2, bgcolor=color)
480     linktable.AddRow([etable, otherlinks])
481     # ...and add the links table to the document.
482     form.AddItem(linktable)
483     form.AddItem('<hr>')
484     form.AddItem(
485         _('''Make your changes in the following section, then submit them
486         using the <em>Submit Your Changes</em> button below.''')
487         + '<p>')
488
489     # The members and passwords categories are special in that they aren't
490     # defined in terms of gui elements.  Create those pages here.
491     if category == 'members':
492         # Figure out which subcategory we should display
493         subcat = Utils.GetPathPieces()[-1]
494         if subcat not in ('list', 'add', 'remove'):
495             subcat = 'list'
496         # Add member category specific tables
497         form.AddItem(membership_options(mlist, subcat, cgidata, doc, form))
498         form.AddItem(Center(submit_button('setmemberopts_btn')))
499         # In "list" subcategory, we can also search for members
500         if subcat == 'list':
501             form.AddItem('<hr>\n')
502             table = Table(width='100%')
503             table.AddRow([Center(Header(2, _('Additional Member Tasks')))])
504             table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
505                               bgcolor=mm_cfg.WEB_HEADER_COLOR)
506             # Add a blank separator row
507             table.AddRow(['&nbsp;', '&nbsp;'])
508             # Add a section to set the moderation bit for all members
509             table.AddRow([_("""<li>Set everyone's moderation bit, including
510             those members not currently visible""")])
511             table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
512             table.AddRow([RadioButtonArray('allmodbit_val',
513                                            (_('Off'), _('On')),
514                                            mlist.default_member_moderation),
515                           SubmitButton('allmodbit_btn', _('Set'))])
516             form.AddItem(table)
517     elif category == 'passwords':
518         form.AddItem(Center(password_inputs(mlist)))
519         form.AddItem(Center(submit_button()))
520     else:
521         form.AddItem(show_variables(mlist, category, subcat, cgidata, doc))
522         form.AddItem(Center(submit_button()))
523     # And add the form
524     doc.AddItem(form)
525     doc.AddItem(mlist.GetMailmanFooter())
526
527
528 \f
529 def show_variables(mlist, category, subcat, cgidata, doc):
530     options = mlist.GetConfigInfo(category, subcat)
531
532     # The table containing the results
533     table = Table(cellspacing=3, cellpadding=4, width='100%')
534
535     # Get and portray the text label for the category.
536     categories = mlist.GetConfigCategories()
537     label = _(categories[category][0])
538
539     table.AddRow([Center(Header(2, label))])
540     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
541                       bgcolor=mm_cfg.WEB_HEADER_COLOR)
542
543     # The very first item in the config info will be treated as a general
544     # description if it is a string
545     description = options[0]
546     if isinstance(description, StringType):
547         table.AddRow([description])
548         table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
549         options = options[1:]
550
551     if not options:
552         return table
553
554     # Add the global column headers
555     table.AddRow([Center(Bold(_('Description'))),
556                   Center(Bold(_('Value')))])
557     table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
558                       width='15%')
559     table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
560                       width='85%')
561
562     for item in options:
563         if type(item) == StringType:
564             # The very first banner option (string in an options list) is
565             # treated as a general description, while any others are
566             # treated as section headers - centered and italicized...
567             table.AddRow([Center(Italic(item))])
568             table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
569         else:
570             add_options_table_item(mlist, category, subcat, table, item)
571     table.AddRow(['<br>'])
572     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
573     return table
574
575
576 \f
577 def add_options_table_item(mlist, category, subcat, table, item, detailsp=1):
578     # Add a row to an options table with the item description and value.
579     varname, kind, params, extra, descr, elaboration = \
580              get_item_characteristics(item)
581     if elaboration is None:
582         elaboration = descr
583     descr = get_item_gui_description(mlist, category, subcat,
584                                      varname, descr, elaboration, detailsp)
585     val = get_item_gui_value(mlist, category, kind, varname, params, extra)
586     table.AddRow([descr, val])
587     table.AddCellInfo(table.GetCurrentRowIndex(), 0,
588                       bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
589     table.AddCellInfo(table.GetCurrentRowIndex(), 1,
590                       bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
591
592
593 \f
594 def get_item_characteristics(record):
595     # Break out the components of an item description from its description
596     # record:
597     #
598     # 0 -- option-var name
599     # 1 -- type
600     # 2 -- entry size
601     # 3 -- ?dependancies?
602     # 4 -- Brief description
603     # 5 -- Optional description elaboration
604     if len(record) == 5:
605         elaboration = None
606         varname, kind, params, dependancies, descr = record
607     elif len(record) == 6:
608         varname, kind, params, dependancies, descr, elaboration = record
609     else:
610         raise ValueError, _('Badly formed options entry:\n %(record)s')
611     return varname, kind, params, dependancies, descr, elaboration
612
613
614 \f
615 def get_item_gui_value(mlist, category, kind, varname, params, extra):
616     """Return a representation of an item's settings."""
617     # Give the category a chance to return the value for the variable
618     value = None
619     label, gui = mlist.GetConfigCategories()[category]
620     if hasattr(gui, 'getValue'):
621         value = gui.getValue(mlist, kind, varname, params)
622     # Filter out None, and volatile attributes
623     if value is None and not varname.startswith('_'):
624         value = getattr(mlist, varname)
625     # Now create the widget for this value
626     if kind == mm_cfg.Radio or kind == mm_cfg.Toggle:
627         # If we are returning the option for subscribe policy and this site
628         # doesn't allow open subscribes, then we have to alter the value of
629         # mlist.subscribe_policy as passed to RadioButtonArray in order to
630         # compensate for the fact that there is one fewer option.
631         # Correspondingly, we alter the value back in the change options
632         # function -scott
633         #
634         # TBD: this is an ugly ugly hack.
635         if varname.startswith('_'):
636             checked = 0
637         else:
638             checked = value
639         if varname == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
640             checked = checked - 1
641         # For Radio buttons, we're going to interpret the extra stuff as a
642         # horizontal/vertical flag.  For backwards compatibility, the value 0
643         # means horizontal, so we use "not extra" to get the parity right.
644         return RadioButtonArray(varname, params, checked, not extra)
645     elif (kind == mm_cfg.String or kind == mm_cfg.Email or
646           kind == mm_cfg.Host or kind == mm_cfg.Number):
647         return TextBox(varname, value, params)
648     elif kind == mm_cfg.Text:
649         if params:
650             r, c = params
651         else:
652             r, c = None, None
653         return TextArea(varname, value or '', r, c)
654     elif kind in (mm_cfg.EmailList, mm_cfg.EmailListEx):
655         if params:
656             r, c = params
657         else:
658             r, c = None, None
659         res = NL.join(value)
660         return TextArea(varname, res, r, c, wrap='off')
661     elif kind == mm_cfg.FileUpload:
662         # like a text area, but also with uploading
663         if params:
664             r, c = params
665         else:
666             r, c = None, None
667         container = Container()
668         container.AddItem(_('<em>Enter the text below, or...</em><br>'))
669         container.AddItem(TextArea(varname, value or '', r, c))
670         container.AddItem(_('<br><em>...specify a file to upload</em><br>'))
671         container.AddItem(FileUpload(varname+'_upload', r, c))
672         return container
673     elif kind == mm_cfg.Select:
674         if params:
675            values, legend, selected = params
676         else:
677            values = mlist.GetAvailableLanguages()
678            legend = map(_, map(Utils.GetLanguageDescr, values))
679            selected = values.index(mlist.preferred_language)
680         return SelectOptions(varname, values, legend, selected)
681     elif kind == mm_cfg.Topics:
682         # A complex and specialized widget type that allows for setting of a
683         # topic name, a mark button, a regexp text box, an "add after mark",
684         # and a delete button.  Yeesh!  params are ignored.
685         table = Table(border=0)
686         # This adds the html for the entry widget
687         def makebox(i, name, pattern, desc, empty=False, table=table):
688             deltag   = 'topic_delete_%02d' % i
689             boxtag   = 'topic_box_%02d' % i
690             reboxtag = 'topic_rebox_%02d' % i
691             desctag  = 'topic_desc_%02d' % i
692             wheretag = 'topic_where_%02d' % i
693             addtag   = 'topic_add_%02d' % i
694             newtag   = 'topic_new_%02d' % i
695             if empty:
696                 table.AddRow([Center(Bold(_('Topic %(i)d'))),
697                               Hidden(newtag)])
698             else:
699                 table.AddRow([Center(Bold(_('Topic %(i)d'))),
700                               SubmitButton(deltag, _('Delete'))])
701             table.AddRow([Label(_('Topic name:')),
702                           TextBox(boxtag, value=name, size=30)])
703             table.AddRow([Label(_('Regexp:')),
704                           TextArea(reboxtag, text=pattern,
705                                    rows=4, cols=30, wrap='off')])
706             table.AddRow([Label(_('Description:')),
707                           TextArea(desctag, text=desc,
708                                    rows=4, cols=30, wrap='soft')])
709             if not empty:
710                 table.AddRow([SubmitButton(addtag, _('Add new item...')),
711                               SelectOptions(wheretag, ('before', 'after'),
712                                             (_('...before this one.'),
713                                              _('...after this one.')),
714                                             selected=1),
715                               ])
716             table.AddRow(['<hr>'])
717             table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
718         # Now for each element in the existing data, create a widget
719         i = 1
720         data = getattr(mlist, varname)
721         for name, pattern, desc, empty in data:
722             makebox(i, name, pattern, desc, empty)
723             i += 1
724         # Add one more non-deleteable widget as the first blank entry, but
725         # only if there are no real entries.
726         if i == 1:
727             makebox(i, '', '', '', empty=True)
728         return table
729     elif kind == mm_cfg.HeaderFilter:
730         # A complex and specialized widget type that allows for setting of a
731         # spam filter rule including, a mark button, a regexp text box, an
732         # "add after mark", up and down buttons, and a delete button.  Yeesh!
733         # params are ignored.
734         table = Table(border=0)
735         # This adds the html for the entry widget
736         def makebox(i, pattern, action, empty=False, table=table):
737             deltag    = 'hdrfilter_delete_%02d' % i
738             reboxtag  = 'hdrfilter_rebox_%02d' % i
739             actiontag = 'hdrfilter_action_%02d' % i
740             wheretag  = 'hdrfilter_where_%02d' % i
741             addtag    = 'hdrfilter_add_%02d' % i
742             newtag    = 'hdrfilter_new_%02d' % i
743             uptag     = 'hdrfilter_up_%02d' % i
744             downtag   = 'hdrfilter_down_%02d' % i
745             if empty:
746                 table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
747                               Hidden(newtag)])
748             else:
749                 table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
750                               SubmitButton(deltag, _('Delete'))])
751             table.AddRow([Label(_('Spam Filter Regexp:')),
752                           TextArea(reboxtag, text=pattern,
753                                    rows=4, cols=30, wrap='off')])
754             values = [mm_cfg.DEFER, mm_cfg.HOLD, mm_cfg.REJECT,
755                       mm_cfg.DISCARD, mm_cfg.ACCEPT]
756             try:
757                 checked = values.index(action)
758             except ValueError:
759                 checked = 0
760             radio = RadioButtonArray(
761                 actiontag,
762                 (_('Defer'), _('Hold'), _('Reject'),
763                  _('Discard'), _('Accept')),
764                 values=values,
765                 checked=checked).Format()
766             table.AddRow([Label(_('Action:')), radio])
767             if not empty:
768                 table.AddRow([SubmitButton(addtag, _('Add new item...')),
769                               SelectOptions(wheretag, ('before', 'after'),
770                                             (_('...before this one.'),
771                                              _('...after this one.')),
772                                             selected=1),
773                               ])
774                 # BAW: IWBNI we could disable the up and down buttons for the
775                 # first and last item respectively, but it's not easy to know
776                 # which is the last item, so let's not worry about that for
777                 # now.
778                 table.AddRow([SubmitButton(uptag, _('Move rule up')),
779                               SubmitButton(downtag, _('Move rule down'))])
780             table.AddRow(['<hr>'])
781             table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
782         # Now for each element in the existing data, create a widget
783         i = 1
784         data = getattr(mlist, varname)
785         for pattern, action, empty in data:
786             makebox(i, pattern, action, empty)
787             i += 1
788         # Add one more non-deleteable widget as the first blank entry, but
789         # only if there are no real entries.
790         if i == 1:
791             makebox(i, '', mm_cfg.DEFER, empty=True)
792         return table
793     elif kind == mm_cfg.Checkbox:
794         return CheckBoxArray(varname, *params)
795     else:
796         assert 0, 'Bad gui widget type: %s' % kind
797
798
799 \f
800 def get_item_gui_description(mlist, category, subcat,
801                              varname, descr, elaboration, detailsp):
802     # Return the item's description, with link to details.
803     #
804     # Details are not included if this is a VARHELP page, because that /is/
805     # the details page!
806     if detailsp:
807         if subcat:
808             varhelp = '/?VARHELP=%s/%s/%s' % (category, subcat, varname)
809         else:
810             varhelp = '/?VARHELP=%s/%s' % (category, varname)
811         if descr == elaboration:
812             linktext = _('<br>(Edit <b>%(varname)s</b>)')
813         else:
814             linktext = _('<br>(Details for <b>%(varname)s</b>)')
815         link = Link(mlist.GetScriptURL('admin') + varhelp,
816                     linktext).Format()
817         text = Label('%s %s' % (descr, link)).Format()
818     else:
819         text = Label(descr).Format()
820     if varname[0] == '_':
821         text += Label(_('''<br><em><strong>Note:</strong>
822         setting this value performs an immediate action but does not modify
823         permanent state.</em>''')).Format()
824     return text
825
826
827 \f
828 def membership_options(mlist, subcat, cgidata, doc, form):
829     # Show the main stuff
830     adminurl = mlist.GetScriptURL('admin', absolute=1)
831     container = Container()
832     header = Table(width="100%")
833     # If we're in the list subcategory, show the membership list
834     if subcat == 'add':
835         header.AddRow([Center(Header(2, _('Mass Subscriptions')))])
836         header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
837                            bgcolor=mm_cfg.WEB_HEADER_COLOR)
838         container.AddItem(header)
839         mass_subscribe(mlist, container)
840         return container
841     if subcat == 'remove':
842         header.AddRow([Center(Header(2, _('Mass Removals')))])
843         header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
844                            bgcolor=mm_cfg.WEB_HEADER_COLOR)
845         container.AddItem(header)
846         mass_remove(mlist, container)
847         return container
848     # Otherwise...
849     header.AddRow([Center(Header(2, _('Membership List')))])
850     header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
851                        bgcolor=mm_cfg.WEB_HEADER_COLOR)
852     container.AddItem(header)
853     # Add a "search for member" button
854     table = Table(width='100%')
855     link = Link('http://www.python.org/doc/current/lib/re-syntax.html',
856                 _('(help)')).Format()
857     table.AddRow([Label(_('Find member %(link)s:')),
858                   TextBox('findmember',
859                           value=cgidata.getvalue('findmember', '')),
860                   SubmitButton('findmember_btn', _('Search...'))])
861     container.AddItem(table)
862     container.AddItem('<hr><p>')
863     usertable = Table(width="90%", border='2')
864     # If there are more members than allowed by chunksize, then we split the
865     # membership up alphabetically.  Otherwise just display them all.
866     chunksz = mlist.admin_member_chunksize
867     # The email addresses had /better/ be ASCII, but might be encoded in the
868     # database as Unicodes.
869     all = []
870     for _m in mlist.getMembers():
871         try:
872             all.append( _m.encode() )
873         except:
874             all.append( _m )
875     all.sort(lambda x, y: cmp(x.lower(), y.lower()))
876     # See if the query has a regular expression
877     regexp = cgidata.getvalue('findmember', '').strip()
878     if regexp:
879         try:
880             cre = re.compile(regexp, re.IGNORECASE)
881         except re.error:
882             doc.addError(_('Bad regular expression: ') + regexp)
883         else:
884             # BAW: There's got to be a more efficient way of doing this!
885             names = [mlist.getMemberName(s) or '' for s in all]
886             all = [a for n, a in zip(names, all)
887                    if cre.search(n) or cre.search(a)]
888     chunkindex = None
889     bucket = None
890     actionurl = None
891     if len(all) < chunksz:
892         members = all
893     else:
894         # Split them up alphabetically, and then split the alphabetical
895         # listing by chunks
896         buckets = {}
897         for addr in all:
898             members = buckets.setdefault(addr[0].lower(), [])
899             members.append(addr)
900         # Now figure out which bucket we want
901         bucket = None
902         qs = {}
903         # POST methods, even if their actions have a query string, don't get
904         # put into FieldStorage's keys :-(
905         qsenviron = os.environ.get('QUERY_STRING')
906         if qsenviron:
907             qs = cgi.parse_qs(qsenviron)
908             bucket = qs.get('letter', 'a')[0].lower()
909             if bucket not in digits + lowercase:
910                 bucket = None
911         if not bucket or not buckets.has_key(bucket):
912             keys = buckets.keys()
913             keys.sort()
914             bucket = keys[0]
915         members = buckets[bucket]
916         action = adminurl + '/members?letter=%s' % bucket
917         if len(members) <= chunksz:
918             form.set_action(action)
919         else:
920             i, r = divmod(len(members), chunksz)
921             numchunks = i + (not not r * 1)
922             # Now chunk them up
923             chunkindex = 0
924             if qs.has_key('chunk'):
925                 try:
926                     chunkindex = int(qs['chunk'][0])
927                 except ValueError:
928                     chunkindex = 0
929                 if chunkindex < 0 or chunkindex > numchunks:
930                     chunkindex = 0
931             members = members[chunkindex*chunksz:(chunkindex+1)*chunksz]
932             # And set the action URL
933             form.set_action(action + '&chunk=%s' % chunkindex)
934     # So now members holds all the addresses we're going to display
935     allcnt = len(all)
936     if bucket:
937         membercnt = len(members)
938         usertable.AddRow([Center(Italic(_(
939             '%(allcnt)s members total, %(membercnt)s shown')))])
940     else:
941         usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))])
942     usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
943                           usertable.GetCurrentCellIndex(),
944                           colspan=OPTCOLUMNS,
945                           bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
946     # Add the alphabetical links
947     if bucket:
948         cells = []
949         for letter in digits + lowercase:
950             if not buckets.get(letter):
951                 continue
952             url = adminurl + '/members?findmember=%s&letter=%s' %(urllib.quote(regexp) ,letter)
953             if letter == bucket:
954                 show = Bold('[%s]' % letter.upper()).Format()
955             else:
956                 show = letter.upper()
957             cells.append(Link(url, show).Format())
958         joiner = '&nbsp;'*2 + '\n'
959         usertable.AddRow([Center(joiner.join(cells))])
960     usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
961                           usertable.GetCurrentCellIndex(),
962                           colspan=OPTCOLUMNS,
963                           bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
964     usertable.AddRow([Center(h) for h in (_('unsub'),
965                                           _('member address<br>member name'),
966                                           _('mod'), _('hide'),
967                                           _('nomail<br>[reason]'),
968                                           _('ack'), _('not metoo'),
969                                           _('nodupes'),
970                                           _('digest'), _('plain'),
971                                           _('language'))])
972     rowindex = usertable.GetCurrentRowIndex()
973     for i in range(OPTCOLUMNS):
974         usertable.AddCellInfo(rowindex, i, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
975     # Find the longest name in the list
976     longest = 0
977     if members:
978         names = filter(None, [mlist.getMemberName(s) for s in members])
979         # Make the name field at least as long as the longest email address
980         longest = max([len(s) for s in names + members])
981     # Abbreviations for delivery status details
982     ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'),
983                   MemberAdaptor.BYUSER  : _('U'),
984                   MemberAdaptor.BYADMIN : _('A'),
985                   MemberAdaptor.BYBOUNCE: _('B'),
986                   }
987     # Now populate the rows
988     for addr in members:
989         link = Link(mlist.GetOptionsURL(addr, obscure=1),
990                     mlist.getMemberCPAddress(addr))
991         fullname = Utils.uncanonstr(mlist.getMemberName(addr),
992                                     mlist.preferred_language)
993         name = TextBox(addr + '_realname', fullname, size=longest).Format()
994         cells = [Center(CheckBox(addr + '_unsub', 'off', 0).Format()),
995                  link.Format() + '<br>' +
996                  name +
997                  Hidden('user', urllib.quote(addr)).Format(),
998                  ]
999         # Do the `mod' option
1000         if mlist.getMemberOption(addr, mm_cfg.Moderate):
1001             value = 'on'
1002             checked = 1
1003         else:
1004             value = 'off'
1005             checked = 0
1006         box = CheckBox('%s_mod' % addr, value, checked)
1007         cells.append(Center(box).Format())
1008         for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'):
1009             extra = ''
1010             if opt == 'nomail':
1011                 status = mlist.getDeliveryStatus(addr)
1012                 if status == MemberAdaptor.ENABLED:
1013                     value = 'off'
1014                     checked = 0
1015                 else:
1016                     value = 'on'
1017                     checked = 1
1018                     extra = '[%s]' % ds_abbrevs[status]
1019             elif mlist.getMemberOption(addr, mm_cfg.OPTINFO[opt]):
1020                 value = 'on'
1021                 checked = 1
1022             else:
1023                 value = 'off'
1024                 checked = 0
1025             box = CheckBox('%s_%s' % (addr, opt), value, checked)
1026             cells.append(Center(box.Format() + extra))
1027         # This code is less efficient than the original which did a has_key on
1028         # the underlying dictionary attribute.  This version is slower and
1029         # less memory efficient.  It points to a new MemberAdaptor interface
1030         # method.
1031         if addr in mlist.getRegularMemberKeys():
1032             cells.append(Center(CheckBox(addr + '_digest', 'off', 0).Format()))
1033         else:
1034             cells.append(Center(CheckBox(addr + '_digest', 'on', 1).Format()))
1035         if mlist.getMemberOption(addr, mm_cfg.OPTINFO['plain']):
1036             value = 'on'
1037             checked = 1
1038         else:
1039             value = 'off'
1040             checked = 0
1041         cells.append(Center(CheckBox('%s_plain' % addr, value, checked)))
1042         # User's preferred language
1043         langpref = mlist.getMemberLanguage(addr)
1044         langs = mlist.GetAvailableLanguages()
1045         langdescs = [_(Utils.GetLanguageDescr(lang)) for lang in langs]
1046         try:
1047             selected = langs.index(langpref)
1048         except ValueError:
1049             selected = 0
1050         cells.append(Center(SelectOptions(addr + '_language', langs,
1051                                           langdescs, selected)).Format())
1052         usertable.AddRow(cells)
1053     # Add the usertable and a legend
1054     legend = UnorderedList()
1055     legend.AddItem(
1056         _('<b>unsub</b> -- Click on this to unsubscribe the member.'))
1057     legend.AddItem(
1058         _("""<b>mod</b> -- The user's personal moderation flag.  If this is
1059         set, postings from them will be moderated, otherwise they will be
1060         approved."""))
1061     legend.AddItem(
1062         _("""<b>hide</b> -- Is the member's address concealed on
1063         the list of subscribers?"""))
1064     legend.AddItem(_(
1065         """<b>nomail</b> -- Is delivery to the member disabled?  If so, an
1066         abbreviation will be given describing the reason for the disabled
1067         delivery:
1068             <ul><li><b>U</b> -- Delivery was disabled by the user via their
1069                     personal options page.
1070                 <li><b>A</b> -- Delivery was disabled by the list
1071                     administrators.
1072                 <li><b>B</b> -- Delivery was disabled by the system due to
1073                     excessive bouncing from the member's address.
1074                 <li><b>?</b> -- The reason for disabled delivery isn't known.
1075                     This is the case for all memberships which were disabled
1076                     in older versions of Mailman.
1077             </ul>"""))
1078     legend.AddItem(
1079         _('''<b>ack</b> -- Does the member get acknowledgements of their
1080         posts?'''))
1081     legend.AddItem(
1082         _('''<b>not metoo</b> -- Does the member want to avoid copies of their
1083         own postings?'''))
1084     legend.AddItem(
1085         _('''<b>nodupes</b> -- Does the member want to avoid duplicates of the
1086         same message?'''))
1087     legend.AddItem(
1088         _('''<b>digest</b> -- Does the member get messages in digests?
1089         (otherwise, individual messages)'''))
1090     legend.AddItem(
1091         _('''<b>plain</b> -- If getting digests, does the member get plain
1092         text digests?  (otherwise, MIME)'''))
1093     legend.AddItem(_("<b>language</b> -- Language preferred by the user"))
1094     addlegend = ''
1095     parsedqs = 0
1096     qsenviron = os.environ.get('QUERY_STRING')
1097     if qsenviron:
1098         qs = cgi.parse_qs(qsenviron).get('legend')
1099         if qs and isinstance(qs, ListType):
1100             qs = qs[0]
1101         if qs == 'yes':
1102             addlegend = 'legend=yes&'
1103     if addlegend:
1104         container.AddItem(legend.Format() + '<p>')
1105         container.AddItem(
1106             Link(adminurl + '/members/list',
1107                  _('Click here to hide the legend for this table.')))
1108     else:
1109         container.AddItem(
1110             Link(adminurl + '/members/list?legend=yes',
1111                  _('Click here to include the legend for this table.')))
1112     container.AddItem(Center(usertable))
1113
1114     # There may be additional chunks
1115     if chunkindex is not None:
1116         buttons = []
1117         url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket)
1118         footer = _('''<p><em>To view more members, click on the appropriate
1119         range listed below:</em>''')
1120         chunkmembers = buckets[bucket]
1121         last = len(chunkmembers)
1122         for i in range(numchunks):
1123             if i == chunkindex:
1124                 continue
1125             start = chunkmembers[i*chunksz]
1126             end = chunkmembers[min((i+1)*chunksz, last)-1]
1127             link = Link(url + 'chunk=%d' % i, _('from %(start)s to %(end)s'))
1128             buttons.append(link)
1129         buttons = UnorderedList(*buttons)
1130         container.AddItem(footer + buttons.Format() + '<p>')
1131     return container
1132
1133
1134 \f
1135 def mass_subscribe(mlist, container):
1136     # MASS SUBSCRIBE
1137     GREY = mm_cfg.WEB_ADMINITEM_COLOR
1138     table = Table(width='90%')
1139     table.AddRow([
1140         Label(_('Subscribe these users now or invite them?')),
1141         RadioButtonArray('subscribe_or_invite',
1142                          (_('Subscribe'), _('Invite')),
1143                          0, values=(0, 1))
1144         ])
1145     table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1146     table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1147     table.AddRow([
1148         Label(_('Send welcome messages to new subscribees?')),
1149         RadioButtonArray('send_welcome_msg_to_this_batch',
1150                          (_('No'), _('Yes')),
1151                          mlist.send_welcome_msg,
1152                          values=(0, 1))
1153         ])
1154     table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1155     table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1156     table.AddRow([
1157         Label(_('Send notifications of new subscriptions to the list owner?')),
1158         RadioButtonArray('send_notifications_to_list_owner',
1159                          (_('No'), _('Yes')),
1160                          mlist.admin_notify_mchanges,
1161                          values=(0,1))
1162         ])
1163     table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1164     table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1165     table.AddRow([Italic(_('Enter one address per line below...'))])
1166     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1167     table.AddRow([Center(TextArea(name='subscribees',
1168                                   rows=10, cols='70%', wrap=None))])
1169     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1170     table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1171                   FileUpload('subscribees_upload', cols='50')])
1172     container.AddItem(Center(table))
1173     # Invitation text
1174     table.AddRow(['&nbsp;', '&nbsp;'])
1175     table.AddRow([Italic(_("""Below, enter additional text to be added to the
1176     top of your invitation or the subscription notification.  Include at least
1177     one blank line at the end..."""))])
1178     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1179     table.AddRow([Center(TextArea(name='invitation',
1180                                   rows=10, cols='70%', wrap=None))])
1181     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1182
1183
1184 \f
1185 def mass_remove(mlist, container):
1186     # MASS UNSUBSCRIBE
1187     GREY = mm_cfg.WEB_ADMINITEM_COLOR
1188     table = Table(width='90%')
1189     table.AddRow([
1190         Label(_('Send unsubscription acknowledgement to the user?')),
1191         RadioButtonArray('send_unsub_ack_to_this_batch',
1192                          (_('No'), _('Yes')),
1193                          0, values=(0, 1))
1194         ])
1195     table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1196     table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1197     table.AddRow([
1198         Label(_('Send notifications to the list owner?')),
1199         RadioButtonArray('send_unsub_notifications_to_list_owner',
1200                          (_('No'), _('Yes')),
1201                          mlist.admin_notify_mchanges,
1202                          values=(0, 1))
1203         ])
1204     table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1205     table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1206     table.AddRow([Italic(_('Enter one address per line below...'))])
1207     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1208     table.AddRow([Center(TextArea(name='unsubscribees',
1209                                   rows=10, cols='70%', wrap=None))])
1210     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1211     table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1212                   FileUpload('unsubscribees_upload', cols='50')])
1213     container.AddItem(Center(table))
1214
1215
1216 \f
1217 def password_inputs(mlist):
1218     adminurl = mlist.GetScriptURL('admin', absolute=1)
1219     table = Table(cellspacing=3, cellpadding=4)
1220     table.AddRow([Center(Header(2, _('Change list ownership passwords')))])
1221     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
1222                       bgcolor=mm_cfg.WEB_HEADER_COLOR)
1223     table.AddRow([_("""\
1224 The <em>list administrators</em> are the people who have ultimate control over
1225 all parameters of this mailing list.  They are able to change any list
1226 configuration variable available through these administration web pages.
1227
1228 <p>The <em>list moderators</em> have more limited permissions; they are not
1229 able to change any list configuration variable, but they are allowed to tend
1230 to pending administration requests, including approving or rejecting held
1231 subscription requests, and disposing of held postings.  Of course, the
1232 <em>list administrators</em> can also tend to pending requests.
1233
1234 <p>In order to split the list ownership duties into administrators and
1235 moderators, you must set a separate moderator password in the fields below,
1236 and also provide the email addresses of the list moderators in the
1237 <a href="%(adminurl)s/general">general options section</a>.""")])
1238     table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1239     # Set up the admin password table on the left
1240     atable = Table(border=0, cellspacing=3, cellpadding=4,
1241                    bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
1242     atable.AddRow([Label(_('Enter new administrator password:')),
1243                    PasswordBox('newpw', size=20)])
1244     atable.AddRow([Label(_('Confirm administrator password:')),
1245                    PasswordBox('confirmpw', size=20)])
1246     # Set up the moderator password table on the right
1247     mtable = Table(border=0, cellspacing=3, cellpadding=4,
1248                    bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
1249     mtable.AddRow([Label(_('Enter new moderator password:')),
1250                    PasswordBox('newmodpw', size=20)])
1251     mtable.AddRow([Label(_('Confirm moderator password:')),
1252                    PasswordBox('confirmmodpw', size=20)])
1253     # Add these tables to the overall password table
1254     table.AddRow([atable, mtable])
1255     return table
1256
1257
1258 \f
1259 def submit_button(name='submit'):
1260     table = Table(border=0, cellspacing=0, cellpadding=2)
1261     table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))])
1262     table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle')
1263     return table
1264
1265
1266 \f
1267 def change_options(mlist, category, subcat, cgidata, doc):
1268     def safeint(formvar, defaultval=None):
1269         try:
1270             return int(cgidata.getvalue(formvar))
1271         except (ValueError, TypeError):
1272             return defaultval
1273     confirmed = 0
1274     # Handle changes to the list moderator password.  Do this before checking
1275     # the new admin password, since the latter will force a reauthentication.
1276     new = cgidata.getvalue('newmodpw', '').strip()
1277     confirm = cgidata.getvalue('confirmmodpw', '').strip()
1278     if new or confirm:
1279         if new == confirm:
1280             mlist.mod_password = sha.new(new).hexdigest()
1281             # No re-authentication necessary because the moderator's
1282             # password doesn't get you into these pages.
1283         else:
1284             doc.addError(_('Moderator passwords did not match'))
1285     # Handle changes to the list administrator password
1286     new = cgidata.getvalue('newpw', '').strip()
1287     confirm = cgidata.getvalue('confirmpw', '').strip()
1288     if new or confirm:
1289         if new == confirm:
1290             mlist.password = sha.new(new).hexdigest()
1291             # Set new cookie
1292             print mlist.MakeCookie(mm_cfg.AuthListAdmin)
1293         else:
1294             doc.addError(_('Administrator passwords did not match'))
1295     # Give the individual gui item a chance to process the form data
1296     categories = mlist.GetConfigCategories()
1297     label, gui = categories[category]
1298     # BAW: We handle the membership page special... for now.
1299     if category <> 'members':
1300         gui.handleForm(mlist, category, subcat, cgidata, doc)
1301     # mass subscription, removal processing for members category
1302     subscribers = ''
1303     subscribers += cgidata.getvalue('subscribees', '')
1304     subscribers += cgidata.getvalue('subscribees_upload', '')
1305     if subscribers:
1306         entries = filter(None, [n.strip() for n in subscribers.splitlines()])
1307         send_welcome_msg = safeint('send_welcome_msg_to_this_batch',
1308                                    mlist.send_welcome_msg)
1309         send_admin_notif = safeint('send_notifications_to_list_owner',
1310                                    mlist.admin_notify_mchanges)
1311         # Default is to subscribe
1312         subscribe_or_invite = safeint('subscribe_or_invite', 0)
1313         invitation = cgidata.getvalue('invitation', '')
1314         digest = mlist.digest_is_default
1315         if not mlist.digestable:
1316             digest = 0
1317         if not mlist.nondigestable:
1318             digest = 1
1319         subscribe_errors = []
1320         subscribe_success = []
1321         # Now cruise through all the subscribees and do the deed.  BAW: we
1322         # should limit the number of "Successfully subscribed" status messages
1323         # we display.  Try uploading a file with 10k names -- it takes a while
1324         # to render the status page.
1325         for entry in entries:
1326             safeentry = Utils.websafe(entry)
1327             fullname, address = parseaddr(entry)
1328             # Canonicalize the full name
1329             fullname = Utils.canonstr(fullname, mlist.preferred_language)
1330             userdesc = UserDesc(address, fullname,
1331                                 Utils.MakeRandomPassword(),
1332                                 digest, mlist.preferred_language)
1333             try:
1334                 if subscribe_or_invite:
1335                     if mlist.isMember(address):
1336                         raise Errors.MMAlreadyAMember
1337                     else:
1338                         mlist.InviteNewMember(userdesc, invitation)
1339                 else:
1340                     mlist.ApprovedAddMember(userdesc, send_welcome_msg,
1341                                             send_admin_notif, invitation,
1342                                             whence='admin mass sub')
1343             except Errors.MMAlreadyAMember:
1344                 subscribe_errors.append((safeentry, _('Already a member')))
1345             except Errors.MMBadEmailError:
1346                 if userdesc.address == '':
1347                     subscribe_errors.append((_('&lt;blank line&gt;'),
1348                                              _('Bad/Invalid email address')))
1349                 else:
1350                     subscribe_errors.append((safeentry,
1351                                              _('Bad/Invalid email address')))
1352             except Errors.MMHostileAddress:
1353                 subscribe_errors.append(
1354                     (safeentry, _('Hostile address (illegal characters)')))
1355             except Errors.MembershipIsBanned, pattern:
1356                 subscribe_errors.append(
1357                     (safeentry, _('Banned address (matched %(pattern)s)')))
1358             else:
1359                 member = Utils.uncanonstr(formataddr((fullname, address)))
1360                 subscribe_success.append(Utils.websafe(member))
1361         if subscribe_success:
1362             if subscribe_or_invite:
1363                 doc.AddItem(Header(5, _('Successfully invited:')))
1364             else:
1365                 doc.AddItem(Header(5, _('Successfully subscribed:')))
1366             doc.AddItem(UnorderedList(*subscribe_success))
1367             doc.AddItem('<p>')
1368         if subscribe_errors:
1369             if subscribe_or_invite:
1370                 doc.AddItem(Header(5, _('Error inviting:')))
1371             else:
1372                 doc.AddItem(Header(5, _('Error subscribing:')))
1373             items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
1374             doc.AddItem(UnorderedList(*items))
1375             doc.AddItem('<p>')
1376     # Unsubscriptions
1377     removals = ''
1378     if cgidata.has_key('unsubscribees'):
1379         removals += cgidata['unsubscribees'].value
1380     if cgidata.has_key('unsubscribees_upload') and \
1381            cgidata['unsubscribees_upload'].value:
1382         removals += cgidata['unsubscribees_upload'].value
1383     if removals:
1384         names = filter(None, [n.strip() for n in removals.splitlines()])
1385         send_unsub_notifications = int(
1386             cgidata['send_unsub_notifications_to_list_owner'].value)
1387         userack = int(
1388             cgidata['send_unsub_ack_to_this_batch'].value)
1389         unsubscribe_errors = []
1390         unsubscribe_success = []
1391         for addr in names:
1392             try:
1393                 mlist.ApprovedDeleteMember(
1394                     addr, whence='admin mass unsub',
1395                     admin_notif=send_unsub_notifications,
1396                     userack=userack)
1397                 unsubscribe_success.append(Utils.websafe(addr))
1398             except Errors.NotAMemberError:
1399                 unsubscribe_errors.append(Utils.websafe(addr))
1400         if unsubscribe_success:
1401             doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
1402             doc.AddItem(UnorderedList(*unsubscribe_success))
1403             doc.AddItem('<p>')
1404         if unsubscribe_errors:
1405             doc.AddItem(Header(3, Bold(FontAttr(
1406                 _('Cannot unsubscribe non-members:'),
1407                 color='#ff0000', size='+2')).Format()))
1408             doc.AddItem(UnorderedList(*unsubscribe_errors))
1409             doc.AddItem('<p>')
1410     # See if this was a moderation bit operation
1411     if cgidata.has_key('allmodbit_btn'):
1412         val = cgidata.getvalue('allmodbit_val')
1413         try:
1414             val = int(val)
1415         except VallueError:
1416             val = None
1417         if val not in (0, 1):
1418             doc.addError(_('Bad moderation flag value'))
1419         else:
1420             for member in mlist.getMembers():
1421                 mlist.setMemberOption(member, mm_cfg.Moderate, val)
1422     # do the user options for members category
1423     if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'):
1424         user = cgidata['user']
1425         if type(user) is ListType:
1426             users = []
1427             for ui in range(len(user)):
1428                 users.append(urllib.unquote(user[ui].value))
1429         else:
1430             users = [urllib.unquote(user.value)]
1431         errors = []
1432         removes = []
1433         for user in users:
1434             if cgidata.has_key('%s_unsub' % user):
1435                 try:
1436                     mlist.ApprovedDeleteMember(user, whence='member mgt page')
1437                     removes.append(user)
1438                 except Errors.NotAMemberError:
1439                     errors.append((user, _('Not subscribed')))
1440                 continue
1441             if not mlist.isMember(user):
1442                 doc.addError(_('Ignoring changes to deleted member: %(user)s'),
1443                              tag=_('Warning: '))
1444                 continue
1445             value = cgidata.has_key('%s_digest' % user)
1446             try:
1447                 mlist.setMemberOption(user, mm_cfg.Digests, value)
1448             except (Errors.AlreadyReceivingDigests,
1449                     Errors.AlreadyReceivingRegularDeliveries,
1450                     Errors.CantDigestError,
1451                     Errors.MustDigestError):
1452                 # BAW: Hmm...
1453                 pass
1454
1455             newname = cgidata.getvalue(user+'_realname', '')
1456             newname = Utils.canonstr(newname, mlist.preferred_language)
1457             mlist.setMemberName(user, newname)
1458
1459             newlang = cgidata.getvalue(user+'_language')
1460             oldlang = mlist.getMemberLanguage(user)
1461             if Utils.IsLanguage(newlang) and newlang <> oldlang:
1462                 mlist.setMemberLanguage(user, newlang)
1463
1464             moderate = not not cgidata.getvalue(user+'_mod')
1465             mlist.setMemberOption(user, mm_cfg.Moderate, moderate)
1466
1467             # Set the `nomail' flag, but only if the user isn't already
1468             # disabled (otherwise we might change BYUSER into BYADMIN).
1469             if cgidata.has_key('%s_nomail' % user):
1470                 if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED:
1471                     mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN)
1472             else:
1473                 mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED)
1474             for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'):
1475                 opt_code = mm_cfg.OPTINFO[opt]
1476                 if cgidata.has_key('%s_%s' % (user, opt)):
1477                     mlist.setMemberOption(user, opt_code, 1)
1478                 else:
1479                     mlist.setMemberOption(user, opt_code, 0)
1480         # Give some feedback on who's been removed
1481         if removes:
1482             doc.AddItem(Header(5, _('Successfully Removed:')))
1483             doc.AddItem(UnorderedList(*removes))
1484             doc.AddItem('<p>')
1485         if errors:
1486             doc.AddItem(Header(5, _("Error Unsubscribing:")))
1487             items = ['%s -- %s' % (x[0], x[1]) for x in errors]
1488             doc.AddItem(apply(UnorderedList, tuple((items))))
1489             doc.AddItem("<p>")