# 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.
"""Library for program-based construction of an HTML documents.
Encapsulate HTML formatting directives in classes that act as containers
for python and, recursively, for nested HTML formatting objects.
"""
# Eventually could abstract down to HtmlItem, which outputs an arbitrary html
# object given start / end tags, valid options, and a value. Ug, objects
# shouldn't be adding their own newlines. The next object should.
import types
from Mailman import mm_cfg
from Mailman import Utils
from Mailman.i18n import _, get_translation
from Mailman.CSRFcheck import csrf_token
SPACE = ' '
EMPTYSTRING = ''
NL = '\n'
# Format an arbitrary object.
def HTMLFormatObject(item, indent):
"Return a presentation of an object, invoking their Format method if any."
if type(item) == type(''):
return item
elif not hasattr(item, "Format"):
return `item`
else:
return item.Format(indent)
def CaseInsensitiveKeyedDict(d):
result = {}
for (k,v) in d.items():
result[k.lower()] = v
return result
# Given references to two dictionaries, copy the second dictionary into the
# first one.
def DictMerge(destination, fresh_dict):
for (key, value) in fresh_dict.items():
destination[key] = value
class Table:
def __init__(self, **table_opts):
self.cells = []
self.cell_info = {}
self.row_info = {}
self.opts = table_opts
def AddOptions(self, opts):
DictMerge(self.opts, opts)
# Sets all of the cells. It writes over whatever cells you had there
# previously.
def SetAllCells(self, cells):
self.cells = cells
# Add a new blank row at the end
def NewRow(self):
self.cells.append([])
# Add a new blank cell at the end
def NewCell(self):
self.cells[-1].append('')
def AddRow(self, row):
self.cells.append(row)
def AddCell(self, cell):
self.cells[-1].append(cell)
def AddCellInfo(self, row, col, **kws):
kws = CaseInsensitiveKeyedDict(kws)
if not self.cell_info.has_key(row):
self.cell_info[row] = { col : kws }
elif self.cell_info[row].has_key(col):
DictMerge(self.cell_info[row], kws)
else:
self.cell_info[row][col] = kws
def AddRowInfo(self, row, **kws):
kws = CaseInsensitiveKeyedDict(kws)
if not self.row_info.has_key(row):
self.row_info[row] = kws
else:
DictMerge(self.row_info[row], kws)
# What's the index for the row we just put in?
def GetCurrentRowIndex(self):
return len(self.cells)-1
# What's the index for the col we just put in?
def GetCurrentCellIndex(self):
return len(self.cells[-1])-1
def ExtractCellInfo(self, info):
valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan',
'bgcolor']
output = ''
for (key, val) in info.items():
if not key in valid_mods:
continue
if key == 'nowrap':
output = output + ' NOWRAP'
continue
else:
output = output + ' %s="%s"' % (key.upper(), val)
return output
def ExtractRowInfo(self, info):
valid_mods = ['align', 'valign', 'bgcolor']
output = ''
for (key, val) in info.items():
if not key in valid_mods:
continue
output = output + ' %s="%s"' % (key.upper(), val)
return output
def ExtractTableInfo(self, info):
valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding',
'bgcolor']
output = ''
for (key, val) in info.items():
if not key in valid_mods:
continue
if key == 'border' and val == None:
output = output + ' BORDER'
continue
else:
output = output + ' %s="%s"' % (key.upper(), val)
return output
def FormatCell(self, row, col, indent):
try:
my_info = self.cell_info[row][col]
except:
my_info = None
output = '\n' + ' '*indent + '<td'
if my_info:
output = output + self.ExtractCellInfo(my_info)
item = self.cells[row][col]
item_format = HTMLFormatObject(item, indent+4)
output = '%s>%s</td>' % (output, item_format)
return output
def FormatRow(self, row, indent):
try:
my_info = self.row_info[row]
except:
my_info = None
output = '\n' + ' '*indent + '<tr'
if my_info:
output = output + self.ExtractRowInfo(my_info)
output = output + '>'
for i in range(len(self.cells[row])):
output = output + self.FormatCell(row, i, indent + 2)
output = output + '\n' + ' '*indent + '</tr>'
return output
def Format(self, indent=0):
output = '\n' + ' '*indent + '<table'
output = output + self.ExtractTableInfo(self.opts)
output = output + '>'
for i in range(len(self.cells)):
output = output + self.FormatRow(i, indent + 2)
output = output + '\n' + ' '*indent + '</table>\n'
return output
class Link:
def __init__(self, href, text, target=None):
self.href = href
self.text = text
self.target = target
def Format(self, indent=0):
texpr = ""
if self.target != None:
texpr = ' target="%s"' % self.target
return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent),
texpr,
HTMLFormatObject(self.text, indent))
class FontSize:
"""FontSize is being deprecated - use FontAttr(..., size="...") instead."""
def __init__(self, size, *items):
self.items = list(items)
self.size = size
def Format(self, indent=0):
output = '<font size="%s">' % self.size
for item in self.items:
output = output + HTMLFormatObject(item, indent)
output = output + '</font>'
return output
class FontAttr:
"""Present arbitrary font attributes."""
def __init__(self, *items, **kw):
self.items = list(items)
self.attrs = kw
def Format(self, indent=0):
seq = []
for k, v in self.attrs.items():
seq.append('%s="%s"' % (k, v))
output = '<font %s>' % SPACE.join(seq)
for item in self.items:
output = output + HTMLFormatObject(item, indent)
output = output + '</font>'
return output
class Container:
def __init__(self, *items):
if not items:
self.items = []
else:
self.items = items
def AddItem(self, obj):
self.items.append(obj)
def Format(self, indent=0):
output = []
for item in self.items:
output.append(HTMLFormatObject(item, indent))
return EMPTYSTRING.join(output)
class Label(Container):
align = 'right'
def __init__(self, *items):
Container.__init__(self, *items)
def Format(self, indent=0):
return ('<div align="%s">' % self.align) + \
Container.Format(self, indent) + \
'</div>'
# My own standard document template. YMMV.
# something more abstract would be more work to use...
class Document(Container):
title = None
language = None
bgcolor = mm_cfg.WEB_BG_COLOR
suppress_head = 0
def set_language(self, lang=None):
self.language = lang
def set_bgcolor(self, color):
self.bgcolor = color
def SetTitle(self, title):
self.title = title
def Format(self, indent=0, **kws):
charset = 'us-ascii'
if self.language and Utils.IsLanguage(self.language):
charset = Utils.GetCharSet(self.language)
output = ['Content-Type: text/html; charset=%s\n' % charset]
if not self.suppress_head:
kws.setdefault('bgcolor', self.bgcolor)
tab = ' ' * indent
output.extend([tab,
'<HTML>',
'<HEAD>'
])
if mm_cfg.IMAGE_LOGOS:
output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' %
(mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON))
# Hit all the bases
output.append('<META http-equiv="Content-Type" '
'content="text/html; charset=%s">' % charset)
if self.title:
output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
# Add CSS to visually hide some labeling text but allow screen
# readers to read it.
output.append("""\
<style type="text/css">
div.hidden
{position:absolute;
left:-10000px;
top:auto;
width:1px;
height:1px;
overflow:hidden;}
</style>
""")
if mm_cfg.WEB_HEAD_ADD:
output.append(mm_cfg.WEB_HEAD_ADD)
output.append('%s</HEAD>' % tab)
quals = []
# Default link colors
if mm_cfg.WEB_VLINK_COLOR:
kws.setdefault('vlink', mm_cfg.WEB_VLINK_COLOR)
if mm_cfg.WEB_ALINK_COLOR:
kws.setdefault('alink', mm_cfg.WEB_ALINK_COLOR)
if mm_cfg.WEB_LINK_COLOR:
kws.setdefault('link', mm_cfg.WEB_LINK_COLOR)
for k, v in kws.items():
quals.append('%s="%s"' % (k, v))
output.append('%s<BODY %s' % (tab, SPACE.join(quals)))
# Language direction
direction = Utils.GetDirection(self.language)
output.append('dir="%s">' % direction)
# Always do this...
output.append(Container.Format(self, indent))
if not self.suppress_head:
output.append('%s</BODY>' % tab)
output.append('%s</HTML>' % tab)
return NL.join(output)
def addError(self, errmsg, tag=None):
if tag is None:
tag = _('Error: ')
self.AddItem(Header(3, Bold(FontAttr(
_(tag), color=mm_cfg.WEB_ERROR_COLOR, size='+2')).Format() +
Italic(errmsg).Format()))
class HeadlessDocument(Document):
"""Document without head section, for templates that provide their own."""
suppress_head = 1
class StdContainer(Container):
def Format(self, indent=0):
# If I don't start a new I ignore indent
output = '<%s>' % self.tag
output = output + Container.Format(self, indent)
output = '%s</%s>' % (output, self.tag)
return output
class QuotedContainer(Container):
def Format(self, indent=0):
# If I don't start a new I ignore indent
output = '<%s>%s</%s>' % (
self.tag,
Utils.websafe(Container.Format(self, indent)),
self.tag)
return output
class Header(StdContainer):
def __init__(self, num, *items):
self.items = items
self.tag = 'h%d' % num
class Address(StdContainer):
tag = 'address'
class Underline(StdContainer):
tag = 'u'
class Bold(StdContainer):
tag = 'strong'
class Italic(StdContainer):
tag = 'em'
class Preformatted(QuotedContainer):
tag = 'pre'
class Subscript(StdContainer):
tag = 'sub'
class Superscript(StdContainer):
tag = 'sup'
class Strikeout(StdContainer):
tag = 'strike'
class Center(StdContainer):
tag = 'center'
class Form(Container):
def __init__(self, action='', method='POST', encoding=None,
mlist=None, contexts=None, user=None, *items):
apply(Container.__init__, (self,) + items)
self.action = action
self.method = method
self.encoding = encoding
self.mlist = mlist
self.contexts = contexts
self.user = user
def set_action(self, action):
self.action = action
def Format(self, indent=0):
spaces = ' ' * indent
encoding = ''
if self.encoding:
encoding = 'enctype="%s"' % self.encoding
output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
spaces, self.action, self.method, encoding)
if self.mlist:
output = output + \
'<input type="hidden" name="csrf_token" value="%s">\n' \
% csrf_token(self.mlist, self.contexts, self.user)
output = output + Container.Format(self, indent+2)
output = '%s\n%s</FORM>\n' % (output, spaces)
return output
class InputObj:
def __init__(self, name, ty, value, checked, **kws):
self.name = name
self.type = ty
self.value = value
self.checked = checked
self.kws = kws
def Format(self, indent=0):
charset = get_translation().charset() or 'us-ascii'
output = ['<INPUT name="%s" type="%s" value="%s"' %
(self.name, self.type, self.value)]
for item in self.kws.items():
output.append('%s="%s"' % item)
if self.checked:
output.append('CHECKED')
output.append('>')
ret = SPACE.join(output)
if self.type == 'TEXT' and isinstance(ret, unicode):
ret = ret.encode(charset, 'xmlcharrefreplace')
return ret
class SubmitButton(InputObj):
def __init__(self, name, button_text):
InputObj.__init__(self, name, "SUBMIT", button_text, checked=0)
class PasswordBox(InputObj):
def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size)
class TextBox(InputObj):
def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
if isinstance(value, str):
safevalue = Utils.websafe(value)
else:
safevalue = value
InputObj.__init__(self, name, "TEXT", safevalue, checked=0, size=size)
class Hidden(InputObj):
def __init__(self, name, value=''):
InputObj.__init__(self, name, 'HIDDEN', value, checked=0)
class TextArea:
def __init__(self, name, text='', rows=None, cols=None, wrap='soft',
readonly=0):
if isinstance(text, str):
safetext = Utils.websafe(text)
else:
safetext = text
self.name = name
self.text = safetext
self.rows = rows
self.cols = cols
self.wrap = wrap
self.readonly = readonly
def Format(self, indent=0):
charset = get_translation().charset() or 'us-ascii'
output = '<TEXTAREA NAME=%s' % self.name
if self.rows:
output += ' ROWS=%s' % self.rows
if self.cols:
output += ' COLS=%s' % self.cols
if self.wrap:
output += ' WRAP=%s' % self.wrap
if self.readonly:
output += ' READONLY'
output += '>%s</TEXTAREA>' % self.text
if isinstance(output, unicode):
output = output.encode(charset, 'xmlcharrefreplace')
return output
class FileUpload(InputObj):
def __init__(self, name, rows=None, cols=None, **kws):
apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws)
class RadioButton(InputObj):
def __init__(self, name, value, checked=0, **kws):
apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws)
class CheckBox(InputObj):
def __init__(self, name, value, checked=0, **kws):
apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws)
class VerticalSpacer:
def __init__(self, size=10):
self.size = size
def Format(self, indent=0):
output = '<spacer type="vertical" height="%d">' % self.size
return output
class WidgetArray:
Widget = None
def __init__(self, name, button_names, checked, horizontal, values):
self.name = name
self.button_names = button_names
self.checked = checked
self.horizontal = horizontal
self.values = values
assert len(values) == len(button_names)
# Don't assert `checked' because for RadioButtons it is a scalar while
# for CheckedBoxes it is a vector. Subclasses will assert length.
def ischecked(self, i):
raise NotImplemented
def Format(self, indent=0):
t = Table(cellspacing=5)
items = []
for i, name, value in zip(range(len(self.button_names)),
self.button_names,
self.values):
ischecked = (self.ischecked(i))
item = ('<label>' +
self.Widget(self.name, value, ischecked).Format() +
name + '</label>')
items.append(item)
if not self.horizontal:
t.AddRow(items)
items = []
if self.horizontal:
t.AddRow(items)
return t.Format(indent)
class RadioButtonArray(WidgetArray):
Widget = RadioButton
def __init__(self, name, button_names, checked=None, horizontal=1,
values=None):
if values is None:
values = range(len(button_names))
# BAW: assert checked is a scalar...
WidgetArray.__init__(self, name, button_names, checked, horizontal,
values)
def ischecked(self, i):
return self.checked == i
class CheckBoxArray(WidgetArray):
Widget = CheckBox
def __init__(self, name, button_names, checked=None, horizontal=0,
values=None):
if checked is None:
checked = [0] * len(button_names)
else:
assert len(checked) == len(button_names)
if values is None:
values = range(len(button_names))
WidgetArray.__init__(self, name, button_names, checked, horizontal,
values)
def ischecked(self, i):
return self.checked[i]
class UnorderedList(Container):
def Format(self, indent=0):
spaces = ' ' * indent
output = '\n%s<ul>\n' % spaces
for item in self.items:
output = output + '%s<li>%s\n' % \
(spaces, HTMLFormatObject(item, indent + 2))
output = output + '%s</ul>\n' % spaces
return output
class OrderedList(Container):
def Format(self, indent=0):
spaces = ' ' * indent
output = '\n%s<ol>\n' % spaces
for item in self.items:
output = output + '%s<li>%s\n' % \
(spaces, HTMLFormatObject(item, indent + 2))
output = output + '%s</ol>\n' % spaces
return output
class DefinitionList(Container):
def Format(self, indent=0):
spaces = ' ' * indent
output = '\n%s<dl>\n' % spaces
for dt, dd in self.items:
output = output + '%s<dt>%s\n<dd>%s\n' % \
(spaces, HTMLFormatObject(dt, indent+2),
HTMLFormatObject(dd, indent+2))
output = output + '%s</dl>\n' % spaces
return output
# Logo constants
#
# These are the URLs which the image logos link to. The Mailman home page now
# points at the gnu.org site instead of the www.list.org mirror.
#
from mm_cfg import MAILMAN_URL
PYTHON_URL = 'http://www.python.org/'
GNU_URL = 'http://www.gnu.org/'
# The names of the image logo files. These are concatentated onto
# mm_cfg.IMAGE_LOGOS (not urljoined).
DELIVERED_BY = 'mailman.jpg'
PYTHON_POWERED = 'PythonPowered.png'
GNU_HEAD = 'gnu-head-tiny.jpg'
def MailmanLogo():
t = Table(border=0, width='100%')
if mm_cfg.IMAGE_LOGOS:
def logo(file):
return mm_cfg.IMAGE_LOGOS + file
mmlink = '<img src="%s" alt="Delivered by Mailman" border=0>' \
'<br>version %s' % (logo(DELIVERED_BY), mm_cfg.VERSION)
pylink = '<img src="%s" alt="Python Powered" border=0>' % \
logo(PYTHON_POWERED)
gnulink = '<img src="%s" alt="GNU\'s Not Unix" border=0>' % \
logo(GNU_HEAD)
t.AddRow([mmlink, pylink, gnulink])
else:
# use only textual links
version = mm_cfg.VERSION
mmlink = Link(MAILMAN_URL,
_('Delivered by Mailman<br>version %(version)s'))
pylink = Link(PYTHON_URL, _('Python Powered'))
gnulink = Link(GNU_URL, _("Gnu's Not Unix"))
t.AddRow([mmlink, pylink, gnulink])
return t
class SelectOptions:
def __init__(self, varname, values, legend,
selected=0, size=1, multiple=None):
self.varname = varname
self.values = values
self.legend = legend
self.size = size
self.multiple = multiple
# we convert any type to tuple, commas are needed
if not multiple:
if type(selected) == types.IntType:
self.selected = (selected,)
elif type(selected) == types.TupleType:
self.selected = (selected[0],)
elif type(selected) == types.ListType:
self.selected = (selected[0],)
else:
self.selected = (0,)
def Format(self, indent=0):
spaces = " " * indent
items = min( len(self.values), len(self.legend) )
# jcrey: If there is no argument, we return nothing to avoid errors
if items == 0:
return ""
text = "\n" + spaces + "<Select name=\"%s\"" % self.varname
if self.size > 1:
text = text + " size=%d" % self.size
if self.multiple:
text = text + " multiple"
text = text + ">\n"
for i in range(items):
if i in self.selected:
checked = " Selected"
else:
checked = ""
opt = " <option value=\"%s\"%s> %s </option>" % (
self.values[i], checked, self.legend[i])
text = text + spaces + opt + "\n"
return text + spaces + '</Select>'