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                              + "&nbsp;&nbsp;")
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