# Copyright (C) 2002-2018 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""Base class for all web GUI components."""

import re
from types import TupleType, ListType

from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Errors
from Mailman.i18n import _

NL = '\n'
BADJOINER = '</code>, <code>'



class GUIBase:
    # Providing a common interface for GUI component form processing.  Most
    # GUI components won't need to override anything, but some may want to
    # override _setValue() to provide some specialized processing for some
    # attributes.
    def _getValidValue(self, mlist, property, wtype, val):
        # Coerce and validate the new value.
        #
        # Radio buttons and boolean toggles both have integral type
        if wtype in (mm_cfg.Radio, mm_cfg.Toggle):
            # Let ValueErrors propagate
            return int(val)
        # String and Text widgets both just return their values verbatim
        if wtype in (mm_cfg.String, mm_cfg.Text):
            return val
        # This widget contains a single email address
        if wtype == mm_cfg.Email:
            # BAW: We must allow blank values otherwise reply_to_address can't
            # be cleared.  This is currently the only mm_cfg.Email type widget
            # in the interface, so watch out if we ever add any new ones.
            if val:
                # Let MMBadEmailError and MMHostileAddress propagate
                Utils.ValidateEmail(val)
            return val
        # These widget types contain lists of email addresses, one per line.
        # The EmailListEx allows each line to contain either an email address
        # or a regular expression
        if wtype in (mm_cfg.EmailList, mm_cfg.EmailListEx):
            # BAW: value might already be a list, if this is coming from
            # config_list input.  Sigh.
            if isinstance(val, ListType):
                return val
            addrs = []
            bad_addrs = []
            for addr in [s.strip() for s in val.split(NL)]:
                # Discard empty lines
                if not addr:
                    continue
                try:
                    # This throws an exception if the address is invalid
                    Utils.ValidateEmail(addr)
                except Errors.EmailAddressError:
                    # See if this is a context that accepts regular
                    # expressions, and that the re is legal
                    if wtype == mm_cfg.EmailListEx and addr.startswith('^'):
                        try:
                            re.compile(addr)
                        except re.error:
                            bad_addrs.append(addr)
                    elif (wtype == mm_cfg.EmailListEx and addr.startswith('@')
                            and (property.endswith('_these_nonmembers') or
                                property == 'subscribe_auto_approval')):
                        # XXX Needs to be reviewed for list@domain names.
                        # don't reference your own list
                        if addr[1:] == mlist.internal_name():
                            bad_addrs.append(addr)
                        # check for existence of list?  For now allow
                        # reference to list before creating it.
                    else:
                        bad_addrs.append(addr)
                if property in ('regular_exclude_lists',
                                'regular_include_lists'):
                    if addr.lower() == mlist.GetListEmail().lower():
                        bad_addrs.append(addr)
                addrs.append(addr)
            if bad_addrs:
                raise Errors.EmailAddressError, ', '.join(bad_addrs)
            return addrs
        # This is a host name, i.e. verbatim
        if wtype == mm_cfg.Host:
            return val
        # This is a number, either a float or an integer
        if wtype == mm_cfg.Number:
            # The int/float code below doesn't work if we are called from
            # config_list with a value that is already a float.  It will
            # truncate the value to an int.
            if isinstance(val, float):
                return val
            num = -1
            try:
                num = int(val)
            except ValueError:
                # Let ValueErrors percolate up
                num = float(val)
            if num < 0:
                return getattr(mlist, property)
            return num
        # This widget is a select box, i.e. verbatim
        if wtype == mm_cfg.Select:
            return val
        # Checkboxes return a list of the selected items, even if only one is
        # selected.
        if wtype == mm_cfg.Checkbox:
            if isinstance(val, ListType):
                return val
            return [val]
        if wtype == mm_cfg.FileUpload:
            return val
        if wtype == mm_cfg.Topics:
            return val
        if wtype == mm_cfg.HeaderFilter:
            return val
        # Should never get here
        assert 0, 'Bad gui widget type: %s' % wtype

    def _setValue(self, mlist, property, val, doc):
        # Set the value, or override to take special action on the property
        if not property.startswith('_') and getattr(mlist, property) <> val:
            setattr(mlist, property, val)

    def _postValidate(self, mlist, doc):
        # Validate all the attributes for this category
        pass

    def handleForm(self, mlist, category, subcat, cgidata, doc):
        for item in self.GetConfigInfo(mlist, category, subcat):
            # Skip descriptions and legacy non-attributes
            if not isinstance(item, TupleType) or len(item) < 5:
                continue
            # Unpack the gui item description
            property, wtype, args, deps, desc = item[0:5]
            # BAW: I know this code is a little crufty but I wanted to
            # reproduce the semantics of the original code in admin.py as
            # closely as possible, for now.  We can clean it up later.
            #
            # The property may be uploadable...
            uploadprop = property + '_upload'
            if cgidata.has_key(uploadprop) and cgidata[uploadprop].value:
                val = cgidata[uploadprop].value
            elif not cgidata.has_key(property):
                continue
            elif isinstance(cgidata[property], ListType):
                val = [x.value for x in cgidata[property]]
            else:
                val = cgidata[property].value
            # Coerce the value to the expected type, raising exceptions if the
            # value is invalid.
            try:
                val = self._getValidValue(mlist, property, wtype, val)
            except ValueError:
                doc.addError(_('Invalid value for variable: %(property)s'))
            # This is the parent of MMBadEmailError and MMHostileAddress
            except Errors.EmailAddressError, error:
                error = Utils.websafe(str(error))
                doc.addError(
                    _('Bad email address for option %(property)s: %(error)s'))
            else:
                # Set the attribute, which will normally delegate to the mlist
                self._setValue(mlist, property, val, doc)
        # Do a final sweep once all the attributes have been set.  This is how
        # we can do cross-attribute assertions
        self._postValidate(mlist, doc)

    # Convenience method for handling $-string attributes
    def _convertString(self, mlist, property, alloweds, val, doc):
        # Is the list using $-strings?
        dollarp = getattr(mlist, 'use_dollar_strings', 0)
        if dollarp:
            ids = Utils.dollar_identifiers(val)
        else:
            # %-strings
            ids = Utils.percent_identifiers(val)
        # Here's the list of allowable interpolations
        for allowed in alloweds:
            if ids.has_key(allowed):
                del ids[allowed]
        if ids:
            # What's left are not allowed
            badkeys = ids.keys()
            badkeys.sort()
            bad = BADJOINER.join(badkeys)
            doc.addError(_(
                """The following illegal substitution variables were
                found in the <code>%(property)s</code> string:
                <code>%(bad)s</code>
                <p>Your list may not operate properly until you correct this
                problem."""), tag=_('Warning: '))
            return val
        # Now if we're still using %-strings, do a roundtrip conversion and
        # see if the converted value is the same as the new value.  If not,
        # then they probably left off a trailing `s'.  We'll warn them and use
        # the corrected string.
        if not dollarp:
            fixed = Utils.to_percent(Utils.to_dollar(val))
            if fixed <> val:
                doc.addError(_(
                    """Your <code>%(property)s</code> string appeared to
                    have some correctable problems in its new value.
                    The fixed value will be used instead.  Please
                    double check that this is what you intended.
                    """))
                return fixed
        return val
