~wilunix/mailman/lmtp

« back to all changes in this revision

Viewing changes to mailman/Cgi/admin.py

  • Committer: William Mead
  • Date: 2008-07-10 12:36:55 UTC
  • Revision ID: wam22@quant.staff.uscs.susx.ac.uk-20080710123655-7qqhiw03a32cpn87
rev1

Show diffs side-by-side

added added

removed removed

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