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 19"""Routines for presentation of list-specific HTML text.""" 20 21import time 22import re 23 24from Mailman import mm_cfg 25from Mailman import Utils 26from Mailman import MemberAdaptor 27from Mailman.htmlformat import * 28 29from Mailman.i18n import _ 30 31from Mailman.CSRFcheck import csrf_token 32 33 34EMPTYSTRING = '' 35BR = '<br>' 36NL = '\n' 37COMMASPACE = ', ' 38 39 40 41class HTMLFormatter: 42 def GetMailmanFooter(self): 43 ownertext = Utils.ObscureEmail(self.GetOwnerEmail(), 1) 44 # Remove the .Format() when htmlformat conversion is done. 45 realname = self.real_name 46 hostname = self.host_name 47 listinfo_link = Link(self.GetScriptURL('listinfo'), realname).Format() 48 owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format() 49 innertext = _('%(listinfo_link)s list run by %(owner_link)s') 50 return Container( 51 '<hr>', 52 Address( 53 Container( 54 innertext, 55 '<br>', 56 Link(self.GetScriptURL('admin'), 57 _('%(realname)s administrative interface')), 58 _(' (requires authorization)'), 59 '<br>', 60 Link(Utils.ScriptURL('listinfo'), 61 _('Overview of all %(hostname)s mailing lists')), 62 '<p>', MailmanLogo()))).Format() 63 64 def FormatUsers(self, digest, lang=None, list_hidden=False): 65 if lang is None: 66 lang = self.preferred_language 67 conceal_sub = mm_cfg.ConcealSubscription 68 people = [] 69 if digest: 70 members = self.getDigestMemberKeys() 71 else: 72 members = self.getRegularMemberKeys() 73 for m in members: 74 if list_hidden or not self.getMemberOption(m, conceal_sub): 75 people.append(m) 76 num_concealed = len(members) - len(people) 77 if num_concealed == 1: 78 concealed = _('<em>(1 private member not shown)</em>') 79 elif num_concealed > 1: 80 concealed = _( 81 '<em>(%(num_concealed)d private members not shown)</em>') 82 else: 83 concealed = '' 84 items = [] 85 people.sort() 86 obscure = self.obscure_addresses 87 for person in people: 88 id = Utils.ObscureEmail(person) 89 url = self.GetOptionsURL(person, obscure=obscure) 90 person = self.getMemberCPAddress(person) 91 if obscure: 92 showing = Utils.ObscureEmail(person, for_text=1) 93 else: 94 showing = person 95 realname = Utils.uncanonstr(self.getMemberName(person), lang) 96 if realname and mm_cfg.ROSTER_DISPLAY_REALNAME: 97 showing += " (%s)" % Utils.websafe(realname) 98 got = Link(url, showing) 99 if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED: 100 got = Italic('(', got, ')') 101 items.append(got) 102 # Just return the .Format() so this works until I finish 103 # converting everything to htmlformat... 104 return concealed + UnorderedList(*tuple(items)).Format() 105 106 def FormatOptionButton(self, option, value, user): 107 if option == mm_cfg.DisableDelivery: 108 optval = self.getDeliveryStatus(user) <> MemberAdaptor.ENABLED 109 else: 110 optval = self.getMemberOption(user, option) 111 if optval == value: 112 checked = ' CHECKED' 113 else: 114 checked = '' 115 name = {mm_cfg.DontReceiveOwnPosts : 'dontreceive', 116 mm_cfg.DisableDelivery : 'disablemail', 117 mm_cfg.DisableMime : 'mime', 118 mm_cfg.AcknowledgePosts : 'ackposts', 119 mm_cfg.Digests : 'digest', 120 mm_cfg.ConcealSubscription : 'conceal', 121 mm_cfg.SuppressPasswordReminder : 'remind', 122 mm_cfg.ReceiveNonmatchingTopics : 'rcvtopic', 123 mm_cfg.DontReceiveDuplicates : 'nodupes', 124 }[option] 125 return '<input type=radio name="%s" value="%d"%s>' % ( 126 name, value, checked) 127 128 def FormatDigestButton(self): 129 if self.digest_is_default: 130 checked = ' CHECKED' 131 else: 132 checked = '' 133 return '<input type=radio name="digest" value="1"%s>' % checked 134 135 def FormatDisabledNotice(self, user): 136 status = self.getDeliveryStatus(user) 137 reason = None 138 info = self.getBounceInfo(user) 139 if status == MemberAdaptor.BYUSER: 140 reason = _('; it was disabled by you') 141 elif status == MemberAdaptor.BYADMIN: 142 reason = _('; it was disabled by the list administrator') 143 elif status == MemberAdaptor.BYBOUNCE: 144 date = time.strftime('%d-%b-%Y', 145 time.localtime(Utils.midnight(info.date))) 146 reason = _('''; it was disabled due to excessive bounces. The 147 last bounce was received on %(date)s''') 148 elif status == MemberAdaptor.UNKNOWN: 149 reason = _('; it was disabled for unknown reasons') 150 if reason: 151 note = FontSize('+1', _( 152 'Note: your list delivery is currently disabled%(reason)s.' 153 )).Format() 154 link = Link('#disable', _('Mail delivery')).Format() 155 mailto = Link('mailto:' + self.GetOwnerEmail(), 156 _('the list administrator')).Format() 157 return _('''<p>%(note)s 158 159 <p>You may have disabled list delivery intentionally, 160 or it may have been triggered by bounces from your email 161 address. In either case, to re-enable delivery, change the 162 %(link)s option below. Contact %(mailto)s if you have any 163 questions or need assistance.''') 164 elif info and info.score > 0: 165 # Provide information about their current bounce score. We know 166 # their membership is currently enabled. 167 score = info.score 168 total = self.bounce_score_threshold 169 return _('''<p>We have received some recent bounces from your 170 address. Your current <em>bounce score</em> is %(score)s out of a 171 maximum of %(total)s. Please double check that your subscribed 172 address is correct and that there are no problems with delivery to 173 this address. Your bounce score will be automatically reset if 174 the problems are corrected soon.''') 175 else: 176 return '' 177 178 def FormatUmbrellaNotice(self, user, type): 179 addr = self.GetMemberAdminEmail(user) 180 if self.umbrella_list: 181 return _("(Note - you are subscribing to a list of mailing lists, " 182 "so the %(type)s notice will be sent to the admin address" 183 " for your membership, %(addr)s.)<p>") 184 else: 185 return "" 186 187 def FormatSubscriptionMsg(self): 188 msg = '' 189 also = '' 190 if self.subscribe_policy == 1: 191 msg += _('''You will be sent email requesting confirmation, to 192 prevent others from gratuitously subscribing you.''') 193 elif self.subscribe_policy == 2: 194 msg += _("""This is a closed list, which means your subscription 195 will be held for approval. You will be notified of the list 196 moderator's decision by email.""") 197 also = _('also ') 198 elif self.subscribe_policy == 3: 199 msg += _("""You will be sent email requesting confirmation, to 200 prevent others from gratuitously subscribing you. Once 201 confirmation is received, your request will be held for approval 202 by the list moderator. You will be notified of the moderator's 203 decision by email.""") 204 also = _("also ") 205 if msg: 206 msg += ' ' 207 if self.private_roster == 1: 208 msg += _('''This is %(also)sa private list, which means that the 209 list of members is not available to non-members.''') 210 elif self.private_roster: 211 msg += _('''This is %(also)sa hidden list, which means that the 212 list of members is available only to the list administrator.''') 213 else: 214 msg += _('''This is %(also)sa public list, which means that the 215 list of members list is available to everyone.''') 216 if self.obscure_addresses: 217 msg += _(''' (but we obscure the addresses so they are not 218 easily recognizable by spammers).''') 219 220 if self.umbrella_list: 221 sfx = self.umbrella_member_suffix 222 msg += _("""<p>(Note that this is an umbrella list, intended to 223 have only other mailing lists as members. Among other things, 224 this means that your confirmation request will be sent to the 225 `%(sfx)s' account for your address.)""") 226 return msg 227 228 def FormatUndigestButton(self): 229 if self.digest_is_default: 230 checked = '' 231 else: 232 checked = ' CHECKED' 233 return '<input type=radio name="digest" value="0"%s>' % checked 234 235 def FormatMimeDigestsButton(self): 236 if self.mime_is_default_digest: 237 checked = ' CHECKED' 238 else: 239 checked = '' 240 return '<input type=radio name="mime" value="1"%s>' % checked 241 242 def FormatPlainDigestsButton(self): 243 if self.mime_is_default_digest: 244 checked = '' 245 else: 246 checked = ' CHECKED' 247 return '<input type=radio name="plain" value="1"%s>' % checked 248 249 def FormatEditingOption(self, lang): 250 if self.private_roster == 0: 251 either = _('<b><i>either</i></b> ') 252 else: 253 either = '' 254 realname = self.real_name 255 256 text = (_('''To unsubscribe from %(realname)s, get a password reminder, 257 or change your subscription options %(either)senter your subscription 258 email address: 259 <p><center> ''') 260 + TextBox('email', size=30).Format() 261 + ' ' 262 + SubmitButton('UserOptions', 263 _('Unsubscribe or edit options')).Format() 264 + Hidden('language', lang).Format() 265 + '</center>') 266 if self.private_roster == 0: 267 text += _('''<p>... <b><i>or</i></b> select your entry from 268 the subscribers list (see above).''') 269 text += _(''' If you leave the field blank, you will be prompted for 270 your email address''') 271 return text 272 273 def RestrictedListMessage(self, which, restriction): 274 if not restriction: 275 return '' 276 elif restriction == 1: 277 return _( 278 '''(<i>%(which)s is only available to the list 279 members.</i>)''') 280 else: 281 return _('''(<i>%(which)s is only available to the list 282 administrator.</i>)''') 283 284 def FormatRosterOptionForUser(self, lang): 285 return self.RosterOption(lang).Format() 286 287 def RosterOption(self, lang): 288 container = Container() 289 container.AddItem(Hidden('language', lang)) 290 if not self.private_roster: 291 container.AddItem(_("Click here for the list of ") 292 + self.real_name 293 + _(" subscribers: ")) 294 container.AddItem(SubmitButton('SubscriberRoster', 295 _("Visit Subscriber list"))) 296 else: 297 if self.private_roster == 1: 298 only = _('members') 299 whom = _('Address:') 300 else: 301 only = _('the list administrator') 302 whom = _('Admin address:') 303 # Solicit the user and password. 304 container.AddItem( 305 self.RestrictedListMessage(_('The subscribers list'), 306 self.private_roster) 307 + _(" <p>Enter your ") 308 + whom[:-1].lower() 309 + _(" and password to visit" 310 " the subscribers list: <p><center> ") 311 + whom 312 + " ") 313 container.AddItem(self.FormatBox('roster-email')) 314 container.AddItem(_("Password: ") 315 + self.FormatSecureBox('roster-pw') 316 + " ") 317 container.AddItem(SubmitButton('SubscriberRoster', 318 _('Visit Subscriber List'))) 319 container.AddItem("</center>") 320 return container 321 322 def FormatFormStart(self, name, extra='', 323 mlist=None, contexts=None, user=None): 324 base_url = self.GetScriptURL(name) 325 if extra: 326 full_url = "%s/%s" % (base_url, extra) 327 else: 328 full_url = base_url 329 if mlist: 330 return ("""<form method="POST" action="%s"> 331<input type="hidden" name="csrf_token" value="%s">""" 332 % (full_url, csrf_token(mlist, contexts, user))) 333 return ('<FORM Method=POST ACTION="%s">' % full_url) 334 335 def FormatArchiveAnchor(self): 336 return '<a href="%s">' % self.GetBaseArchiveURL() 337 338 def FormatFormEnd(self): 339 return '</FORM>' 340 341 def FormatBox(self, name, size=20, value=''): 342 if isinstance(value, str): 343 safevalue = Utils.websafe(value) 344 else: 345 safevalue = value 346 return '<INPUT type="Text" name="%s" size="%d" value="%s">' % ( 347 name, size, safevalue) 348 349 def FormatSecureBox(self, name): 350 return '<INPUT type="Password" name="%s" size="15">' % name 351 352 def FormatButton(self, name, text='Submit'): 353 return '<INPUT type="Submit" name="%s" value="%s">' % (name, text) 354 355 def FormatReminder(self, lang): 356 if self.send_reminders: 357 return _('Once a month, your password will be emailed to you as' 358 ' a reminder.') 359 return '' 360 361 def ParseTags(self, template, replacements, lang=None): 362 if lang is None: 363 charset = 'us-ascii' 364 else: 365 charset = Utils.GetCharSet(lang) 366 text = Utils.maketext(template, raw=1, lang=lang, mlist=self) 367 parts = re.split('(</?[Mm][Mm]-[^>]*>)', text) 368 i = 1 369 while i < len(parts): 370 tag = parts[i].lower() 371 if replacements.has_key(tag): 372 repl = replacements[tag] 373 if isinstance(repl, type(u'')): 374 repl = repl.encode(charset, 'replace') 375 parts[i] = repl 376 else: 377 parts[i] = '' 378 i = i + 2 379 return EMPTYSTRING.join(parts) 380 381 # This needs to wait until after the list is inited, so let's build it 382 # when it's needed only. 383 def GetStandardReplacements(self, lang=None): 384 dmember_len = len(self.getDigestMemberKeys()) 385 member_len = len(self.getRegularMemberKeys()) 386 # If only one language is enabled for this mailing list, omit the 387 # language choice buttons. 388 if len(self.GetAvailableLanguages()) == 1: 389 listlangs = _(Utils.GetLanguageDescr(self.preferred_language)) 390 else: 391 listlangs = self.GetLangSelectBox(lang).Format() 392 if lang: 393 cset = Utils.GetCharSet(lang) or 'us-ascii' 394 else: 395 cset = Utils.GetCharSet(self.preferred_language) or 'us-ascii' 396 d = { 397 '<mm-mailman-footer>' : self.GetMailmanFooter(), 398 '<mm-list-name>' : self.real_name, 399 '<mm-email-user>' : self._internal_name, 400 '<mm-list-description>' : 401 Utils.websafe(self.GetDescription(cset)), 402 '<mm-list-info>' : 403 '<!---->' + BR.join(self.info.split(NL)) + '<!---->', 404 '<mm-form-end>' : self.FormatFormEnd(), 405 '<mm-archive>' : self.FormatArchiveAnchor(), 406 '</mm-archive>' : '</a>', 407 '<mm-list-subscription-msg>' : self.FormatSubscriptionMsg(), 408 '<mm-restricted-list-message>' : \ 409 self.RestrictedListMessage(_('The current archive'), 410 self.archive_private), 411 '<mm-num-reg-users>' : `member_len`, 412 '<mm-num-digesters>' : `dmember_len`, 413 '<mm-num-members>' : (`member_len + dmember_len`), 414 '<mm-posting-addr>' : '%s' % self.GetListEmail(), 415 '<mm-request-addr>' : '%s' % self.GetRequestEmail(), 416 '<mm-owner>' : self.GetOwnerEmail(), 417 '<mm-reminder>' : self.FormatReminder(self.preferred_language), 418 '<mm-host>' : self.host_name, 419 '<mm-list-langs>' : listlangs, 420 } 421 if mm_cfg.IMAGE_LOGOS: 422 d['<mm-favicon>'] = mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON 423 return d 424 425 def GetAllReplacements(self, lang=None, list_hidden=False): 426 """ 427 returns standard replaces plus formatted user lists in 428 a dict just like GetStandardReplacements. 429 """ 430 if lang is None: 431 lang = self.preferred_language 432 d = self.GetStandardReplacements(lang) 433 d.update({"<mm-regular-users>": self.FormatUsers(0, lang, list_hidden), 434 "<mm-digest-users>": self.FormatUsers(1, lang, list_hidden)}) 435 return d 436 437 def GetLangSelectBox(self, lang=None, varname='language'): 438 if lang is None: 439 lang = self.preferred_language 440 # Figure out the available languages 441 values = self.GetAvailableLanguages() 442 legend = map(_, map(Utils.GetLanguageDescr, values)) 443 try: 444 selected = values.index(lang) 445 except ValueError: 446 try: 447 selected = values.index(self.preferred_language) 448 except ValueError: 449 selected = mm_cfg.DEFAULT_SERVER_LANGUAGE 450 # Return the widget 451 return SelectOptions(varname, values, legend, selected) 452