mailman  2.1.39
About: Mailman 2 - The GNU Mailing List Management System.
  Fossies Dox: mailman-2.1.39.tgz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

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