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 71 admin_overview() 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) 287 except Errors.MMUnknownListError: 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([' ', ' ']) 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 = \ 393 get_item_characteristics(item) 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> <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> <br>')) 484 otherlinks.AddItem(Link('%s/logout' % adminurl, 485 # BAW: What I really want is a blank line, but 486 # adding an 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([' ', ' ']) 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 = \ 639 get_item_characteristics(item) 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 653def get_item_characteristics(record): 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 = ' '*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([' ', ' ']) 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 ' ' + 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 ' ' + 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 1359def password_inputs(mlist): 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): 1506 raise Errors.MMAlreadyAMember 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) 1516 except Errors.MMAlreadyAMember: 1517 subscribe_errors.append((safeentry, _('Already a member'))) 1518 except Errors.MMBadEmailError: 1519 if userdesc.address == '': 1520 subscribe_errors.append((_('<blank line>'), 1521 _('Bad/Invalid email address'))) 1522 else: 1523 subscribe_errors.append((safeentry, 1524 _('Bad/Invalid email address'))) 1525 except Errors.MMHostileAddress: 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)) 1576 except Errors.NotAMemberError: 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) 1607 except (Errors.MMBadEmailError, Errors.MMHostileAddress): 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) 1615 except Errors.NotAMemberError: 1616 msg = _('%(schange_from)s is not a member') 1617 except Errors.MMAlreadyAMember: 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') 1692 except Errors.MMBadEmailError: 1693 if userdesc.address == '': 1694 subscribe_errors.append((_('<blank line>'), 1695 _('Bad/Invalid email address'))) 1696 else: 1697 subscribe_errors.append((safeentry, 1698 _('Bad/Invalid email address'))) 1699 except Errors.MMHostileAddress: 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) 1719 except Errors.NotAMemberError: 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) 1770 except Errors.NotAMemberError: 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) 1780 except (Errors.AlreadyReceivingDigests, 1781 Errors.AlreadyReceivingRegularDeliveries, 1782 Errors.CantDigestError, 1783 Errors.MustDigestError): 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>") 1822