# Copyright (C) 1998-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.

"""Script which implements admin editing of the list's html templates."""

import os
import cgi
import errno
import re

from Mailman import Utils
from Mailman import MailList
from Mailman.htmlformat import *
from Mailman.HTMLFormatter import HTMLFormatter
from Mailman import Errors
from Mailman.Cgi import Auth
from Mailman.Logging.Syslog import syslog
from Mailman import i18n
from Mailman.CSRFcheck import csrf_check

_ = i18n._

AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin)



def main():
    # Trick out pygettext since we want to mark template_data as translatable,
    # but we don't want to actually translate it here.
    def _(s):
        return s

    template_data = (
        ('listinfo.html',    _('General list information page')),
        ('subscribe.html',   _('Subscribe results page')),
        ('options.html',     _('User specific options page')),
        ('subscribeack.txt', _('Welcome email text file')),
        ('masthead.txt',     _('Digest masthead')),
        ('postheld.txt',     _('User notice of held post')),
        ('approve.txt',      _('User notice of held subscription')),
        ('refuse.txt',       _('Notice of post refused by moderator')),
        ('invite.txt',       _('Invitation to join list')),
        ('verify.txt',       _('Request to confirm subscription')),
        ('unsub.txt',        _('Request to confirm unsubscription')),
        ('nomoretoday.txt',  _('User notice of autoresponse limit')),
        ('postack.txt',      _('User post acknowledgement')),
        ('disabled.txt',     _('Subscription disabled by bounce warning')),
        ('admlogin.html',    _('Admin/moderator login page')),
        ('private.html',     _('Private archive login page')),
        ('userpass.txt',     _('On demand password reminder')),
        )

    _ = i18n._
    doc = Document()

    # Set up the system default language
    i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)

    parts = Utils.GetPathPieces()
    if not parts:
        doc.AddItem(Header(2, _("List name is required.")))
        print doc.Format()
        return

    listname = parts[0].lower()
    try:
        mlist = MailList.MailList(listname, lock=0)
    except Errors.MMListError, e:
        # Avoid cross-site scripting attacks
        safelistname = Utils.websafe(listname)
        doc.AddItem(Header(2, _('No such list <em>%(safelistname)s</em>')))
        # Send this with a 404 status.
        print 'Status: 404 Not Found'
        print doc.Format()
        syslog('error', 'edithtml: No such list "%s": %s', listname, e)
        return

    # Now that we have a valid list, set the language to its default
    i18n.set_language(mlist.preferred_language)
    doc.set_language(mlist.preferred_language)

    # Must be authenticated to get any farther
    cgidata = cgi.FieldStorage()
    try:
        cgidata.getfirst('adminpw', '')
    except TypeError:
        # Someone crafted a POST with a bad Content-Type:.
        doc.AddItem(Header(2, _("Error")))
        doc.AddItem(Bold(_('Invalid options to CGI script.')))
        # Send this with a 400 status.
        print 'Status: 400 Bad Request'
        print doc.Format()
        return

    # CSRF check
    safe_params = ['VARHELP', 'adminpw', 'admlogin']
    params = cgidata.keys()
    if set(params) - set(safe_params):
        csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token'))
    else:
        csrf_checked = True
    # if password is present, void cookie to force password authentication.
    if cgidata.getfirst('adminpw'):
        os.environ['HTTP_COOKIE'] = ''
        csrf_checked = True

    # Editing the html for a list is limited to the list admin and site admin.
    if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
                                  mm_cfg.AuthSiteAdmin),
                                 cgidata.getfirst('adminpw', '')):
        if cgidata.has_key('admlogin'):
            # This is a re-authorization attempt
            msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
            remote = os.environ.get('HTTP_FORWARDED_FOR',
                     os.environ.get('HTTP_X_FORWARDED_FOR',
                     os.environ.get('REMOTE_ADDR',
                                    'unidentified origin')))
            syslog('security',
                   'Authorization failed (edithtml): list=%s: remote=%s',
                   listname, remote)
        else:
            msg = ''
        Auth.loginpage(mlist, 'admin', msg=msg)
        return

    # See if the user want to see this page in other language
    language = cgidata.getfirst('language', '')
    if language not in mlist.GetAvailableLanguages():
        language = mlist.preferred_language
    i18n.set_language(language)
    doc.set_language(language)

    realname = mlist.real_name
    if len(parts) > 1:
        template_name = parts[1]
        for (template, info) in template_data:
            if template == template_name:
                template_info = _(info)
                doc.SetTitle(_(
                    '%(realname)s -- Edit html for %(template_info)s'))
                break
        else:
            # Avoid cross-site scripting attacks
            safetemplatename = Utils.websafe(template_name)
            doc.SetTitle(_('Edit HTML : Error'))
            doc.AddItem(Header(2, _("%(safetemplatename)s: Invalid template")))
            doc.AddItem(mlist.GetMailmanFooter())
            print doc.Format()
            return
    else:
        doc.SetTitle(_('%(realname)s -- HTML Page Editing'))
        doc.AddItem(Header(1, _('%(realname)s -- HTML Page Editing')))
        doc.AddItem(Header(2, _('Select page to edit:')))
        template_list = UnorderedList()
        for (template, info) in template_data:
            l = Link(mlist.GetScriptURL('edithtml') + '/' + template, _(info))
            template_list.AddItem(l)
        doc.AddItem(FontSize("+2", template_list))
        doc.AddItem(mlist.GetMailmanFooter())
        print doc.Format()
        return

    try:
        if cgidata.keys() and not cgidata.has_key('langform'):
            if csrf_checked:
                ChangeHTML(mlist, cgidata, template_name, doc, lang=language)
            else:
                doc.addError(
                  _('The form lifetime has expired. (request forgery check)'))
        FormatHTML(mlist, doc, template_name, template_info, lang=language)
    finally:
        doc.AddItem(mlist.GetMailmanFooter())
        print doc.Format()



def FormatHTML(mlist, doc, template_name, template_info, lang=None):
    if lang not in mlist.GetAvailableLanguages():
        lang = mlist.preferred_language
    lcset = Utils.GetCharSet(lang)
    doc.AddItem(Header(1,'%s:' % mlist.real_name))
    doc.AddItem(Header(1, template_info))
    doc.AddItem('<hr>')

    link = Link(mlist.GetScriptURL('admin'),
                _('View or edit the list configuration information.'))
    backlink = Link(mlist.GetScriptURL('edithtml'),
                    _('Edit the public HTML pages and text files'))

    doc.AddItem(FontSize("+1", link))
    doc.AddItem('<br>')
    doc.AddItem(FontSize("+1", backlink))
    doc.AddItem('<p>')
    doc.AddItem('<hr>')
    if len(mlist.GetAvailableLanguages()) > 1:
        langform = Form(mlist.GetScriptURL('edithtml') + '/' + template_name,
                        mlist=mlist, contexts=AUTH_CONTEXTS)
        langform.AddItem(
                    mlist.FormatButton('editlang-button',
                                       text = _("Edit this template for")))
        langform.AddItem(mlist.GetLangSelectBox(lang))
        langform.AddItem(Hidden('langform', 'True'))
        doc.AddItem(langform)
        doc.AddItem('<hr>')
    form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name,
               mlist=mlist, contexts=AUTH_CONTEXTS)
    text = Utils.maketext(template_name, raw=1, lang=lang, mlist=mlist)
    # MAS: Don't websafe twice.  TextArea does it.
    form.AddItem(TextArea('html_code', text, rows=40, cols=75))
    form.AddItem('<p>' + _('When you are done making changes...'))
    if lang != mlist.preferred_language:
        form.AddItem(Hidden('language', lang))
    form.AddItem(SubmitButton('submit', _('Submit Changes')))
    doc.AddItem(form)



def ChangeHTML(mlist, cgi_info, template_name, doc, lang=None):
    if lang not in mlist.GetAvailableLanguages():
        lang = mlist.preferred_language
    if not cgi_info.has_key('html_code'):
        doc.AddItem(Header(3,_("Can't have empty html page.")))
        doc.AddItem(Header(3,_("HTML Unchanged.")))
        doc.AddItem('<hr>')
        return
    code = cgi_info['html_code'].value
    if Utils.suspiciousHTML(code):
        doc.AddItem(Header(3,
           _("""The page you saved contains suspicious HTML that could
potentially expose your users to cross-site scripting attacks.  This change
has therefore been rejected.  If you still want to make these changes, you
must have shell access to your Mailman server.
             """)))
        doc.AddItem(_('See '))
        doc.AddItem(Link(
'http://wiki.list.org/x/jYA9',
                _('FAQ 4.48.')))
        doc.AddItem(Header(3,_("Page Unchanged.")))
        doc.AddItem('<hr>')
        return
    langdir = os.path.join(mlist.fullpath(), lang)
    # Make sure the directory exists
    omask = os.umask(0)
    try:
        try:
            os.mkdir(langdir, 02775)
        except OSError, e:
            if e.errno <> errno.EEXIST: raise
    finally:
        os.umask(omask)
    fp = open(os.path.join(langdir, template_name), 'w')
    try:
        fp.write(code)
    finally:
        fp.close()
    doc.AddItem(Header(3, _('HTML successfully updated.')))
    doc.AddItem('<hr>')
