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"""Library for program-based construction of an HTML documents. 20 21Encapsulate HTML formatting directives in classes that act as containers 22for python and, recursively, for nested HTML formatting objects. 23""" 24 25 26# Eventually could abstract down to HtmlItem, which outputs an arbitrary html 27# object given start / end tags, valid options, and a value. Ug, objects 28# shouldn't be adding their own newlines. The next object should. 29 30 31import types 32 33from Mailman import mm_cfg 34from Mailman import Utils 35from Mailman.i18n import _, get_translation 36 37from Mailman.CSRFcheck import csrf_token 38 39SPACE = ' ' 40EMPTYSTRING = '' 41NL = '\n' 42 43 44 45# Format an arbitrary object. 46def HTMLFormatObject(item, indent): 47 "Return a presentation of an object, invoking their Format method if any." 48 if type(item) == type(''): 49 return item 50 elif not hasattr(item, "Format"): 51 return `item` 52 else: 53 return item.Format(indent) 54 55def CaseInsensitiveKeyedDict(d): 56 result = {} 57 for (k,v) in d.items(): 58 result[k.lower()] = v 59 return result 60 61# Given references to two dictionaries, copy the second dictionary into the 62# first one. 63def DictMerge(destination, fresh_dict): 64 for (key, value) in fresh_dict.items(): 65 destination[key] = value 66 67class Table: 68 def __init__(self, **table_opts): 69 self.cells = [] 70 self.cell_info = {} 71 self.row_info = {} 72 self.opts = table_opts 73 74 def AddOptions(self, opts): 75 DictMerge(self.opts, opts) 76 77 # Sets all of the cells. It writes over whatever cells you had there 78 # previously. 79 80 def SetAllCells(self, cells): 81 self.cells = cells 82 83 # Add a new blank row at the end 84 def NewRow(self): 85 self.cells.append([]) 86 87 # Add a new blank cell at the end 88 def NewCell(self): 89 self.cells[-1].append('') 90 91 def AddRow(self, row): 92 self.cells.append(row) 93 94 def AddCell(self, cell): 95 self.cells[-1].append(cell) 96 97 def AddCellInfo(self, row, col, **kws): 98 kws = CaseInsensitiveKeyedDict(kws) 99 if not self.cell_info.has_key(row): 100 self.cell_info[row] = { col : kws } 101 elif self.cell_info[row].has_key(col): 102 DictMerge(self.cell_info[row], kws) 103 else: 104 self.cell_info[row][col] = kws 105 106 def AddRowInfo(self, row, **kws): 107 kws = CaseInsensitiveKeyedDict(kws) 108 if not self.row_info.has_key(row): 109 self.row_info[row] = kws 110 else: 111 DictMerge(self.row_info[row], kws) 112 113 # What's the index for the row we just put in? 114 def GetCurrentRowIndex(self): 115 return len(self.cells)-1 116 117 # What's the index for the col we just put in? 118 def GetCurrentCellIndex(self): 119 return len(self.cells[-1])-1 120 121 def ExtractCellInfo(self, info): 122 valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan', 123 'bgcolor'] 124 output = '' 125 126 for (key, val) in info.items(): 127 if not key in valid_mods: 128 continue 129 if key == 'nowrap': 130 output = output + ' NOWRAP' 131 continue 132 else: 133 output = output + ' %s="%s"' % (key.upper(), val) 134 135 return output 136 137 def ExtractRowInfo(self, info): 138 valid_mods = ['align', 'valign', 'bgcolor'] 139 output = '' 140 141 for (key, val) in info.items(): 142 if not key in valid_mods: 143 continue 144 output = output + ' %s="%s"' % (key.upper(), val) 145 146 return output 147 148 def ExtractTableInfo(self, info): 149 valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding', 150 'bgcolor'] 151 152 output = '' 153 154 for (key, val) in info.items(): 155 if not key in valid_mods: 156 continue 157 if key == 'border' and val == None: 158 output = output + ' BORDER' 159 continue 160 else: 161 output = output + ' %s="%s"' % (key.upper(), val) 162 163 return output 164 165 def FormatCell(self, row, col, indent): 166 try: 167 my_info = self.cell_info[row][col] 168 except: 169 my_info = None 170 171 output = '\n' + ' '*indent + '<td' 172 if my_info: 173 output = output + self.ExtractCellInfo(my_info) 174 item = self.cells[row][col] 175 item_format = HTMLFormatObject(item, indent+4) 176 output = '%s>%s</td>' % (output, item_format) 177 return output 178 179 def FormatRow(self, row, indent): 180 try: 181 my_info = self.row_info[row] 182 except: 183 my_info = None 184 185 output = '\n' + ' '*indent + '<tr' 186 if my_info: 187 output = output + self.ExtractRowInfo(my_info) 188 output = output + '>' 189 190 for i in range(len(self.cells[row])): 191 output = output + self.FormatCell(row, i, indent + 2) 192 193 output = output + '\n' + ' '*indent + '</tr>' 194 195 return output 196 197 def Format(self, indent=0): 198 output = '\n' + ' '*indent + '<table' 199 output = output + self.ExtractTableInfo(self.opts) 200 output = output + '>' 201 202 for i in range(len(self.cells)): 203 output = output + self.FormatRow(i, indent + 2) 204 205 output = output + '\n' + ' '*indent + '</table>\n' 206 207 return output 208 209 210class Link: 211 def __init__(self, href, text, target=None): 212 self.href = href 213 self.text = text 214 self.target = target 215 216 def Format(self, indent=0): 217 texpr = "" 218 if self.target != None: 219 texpr = ' target="%s"' % self.target 220 return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent), 221 texpr, 222 HTMLFormatObject(self.text, indent)) 223 224class FontSize: 225 """FontSize is being deprecated - use FontAttr(..., size="...") instead.""" 226 def __init__(self, size, *items): 227 self.items = list(items) 228 self.size = size 229 230 def Format(self, indent=0): 231 output = '<font size="%s">' % self.size 232 for item in self.items: 233 output = output + HTMLFormatObject(item, indent) 234 output = output + '</font>' 235 return output 236 237class FontAttr: 238 """Present arbitrary font attributes.""" 239 def __init__(self, *items, **kw): 240 self.items = list(items) 241 self.attrs = kw 242 243 def Format(self, indent=0): 244 seq = [] 245 for k, v in self.attrs.items(): 246 seq.append('%s="%s"' % (k, v)) 247 output = '<font %s>' % SPACE.join(seq) 248 for item in self.items: 249 output = output + HTMLFormatObject(item, indent) 250 output = output + '</font>' 251 return output 252 253 254class Container: 255 def __init__(self, *items): 256 if not items: 257 self.items = [] 258 else: 259 self.items = items 260 261 def AddItem(self, obj): 262 self.items.append(obj) 263 264 def Format(self, indent=0): 265 output = [] 266 for item in self.items: 267 output.append(HTMLFormatObject(item, indent)) 268 return EMPTYSTRING.join(output) 269 270 271class Label(Container): 272 align = 'right' 273 274 def __init__(self, *items): 275 Container.__init__(self, *items) 276 277 def Format(self, indent=0): 278 return ('<div align="%s">' % self.align) + \ 279 Container.Format(self, indent) + \ 280 '</div>' 281 282 283# My own standard document template. YMMV. 284# something more abstract would be more work to use... 285 286class Document(Container): 287 title = None 288 language = None 289 bgcolor = mm_cfg.WEB_BG_COLOR 290 suppress_head = 0 291 292 def set_language(self, lang=None): 293 self.language = lang 294 295 def set_bgcolor(self, color): 296 self.bgcolor = color 297 298 def SetTitle(self, title): 299 self.title = title 300 301 def Format(self, indent=0, **kws): 302 charset = 'us-ascii' 303 if self.language and Utils.IsLanguage(self.language): 304 charset = Utils.GetCharSet(self.language) 305 output = ['Content-Type: text/html; charset=%s\n' % charset] 306 if not self.suppress_head: 307 kws.setdefault('bgcolor', self.bgcolor) 308 tab = ' ' * indent 309 output.extend([tab, 310 '<HTML>', 311 '<HEAD>' 312 ]) 313 if mm_cfg.IMAGE_LOGOS: 314 output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' % 315 (mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON)) 316 # Hit all the bases 317 output.append('<META http-equiv="Content-Type" ' 318 'content="text/html; charset=%s">' % charset) 319 if self.title: 320 output.append('%s<TITLE>%s</TITLE>' % (tab, self.title)) 321 # Add CSS to visually hide some labeling text but allow screen 322 # readers to read it. 323 output.append("""\ 324<style type="text/css"> 325 div.hidden 326 {position:absolute; 327 left:-10000px; 328 top:auto; 329 width:1px; 330 height:1px; 331 overflow:hidden;} 332</style> 333""") 334 if mm_cfg.WEB_HEAD_ADD: 335 output.append(mm_cfg.WEB_HEAD_ADD) 336 output.append('%s</HEAD>' % tab) 337 quals = [] 338 # Default link colors 339 if mm_cfg.WEB_VLINK_COLOR: 340 kws.setdefault('vlink', mm_cfg.WEB_VLINK_COLOR) 341 if mm_cfg.WEB_ALINK_COLOR: 342 kws.setdefault('alink', mm_cfg.WEB_ALINK_COLOR) 343 if mm_cfg.WEB_LINK_COLOR: 344 kws.setdefault('link', mm_cfg.WEB_LINK_COLOR) 345 for k, v in kws.items(): 346 quals.append('%s="%s"' % (k, v)) 347 output.append('%s<BODY %s' % (tab, SPACE.join(quals))) 348 # Language direction 349 direction = Utils.GetDirection(self.language) 350 output.append('dir="%s">' % direction) 351 # Always do this... 352 output.append(Container.Format(self, indent)) 353 if not self.suppress_head: 354 output.append('%s</BODY>' % tab) 355 output.append('%s</HTML>' % tab) 356 return NL.join(output) 357 358 def addError(self, errmsg, tag=None): 359 if tag is None: 360 tag = _('Error: ') 361 self.AddItem(Header(3, Bold(FontAttr( 362 _(tag), color=mm_cfg.WEB_ERROR_COLOR, size='+2')).Format() + 363 Italic(errmsg).Format())) 364 365 366class HeadlessDocument(Document): 367 """Document without head section, for templates that provide their own.""" 368 suppress_head = 1 369 370 371class StdContainer(Container): 372 def Format(self, indent=0): 373 # If I don't start a new I ignore indent 374 output = '<%s>' % self.tag 375 output = output + Container.Format(self, indent) 376 output = '%s</%s>' % (output, self.tag) 377 return output 378 379 380class QuotedContainer(Container): 381 def Format(self, indent=0): 382 # If I don't start a new I ignore indent 383 output = '<%s>%s</%s>' % ( 384 self.tag, 385 Utils.websafe(Container.Format(self, indent)), 386 self.tag) 387 return output 388 389class Header(StdContainer): 390 def __init__(self, num, *items): 391 self.items = items 392 self.tag = 'h%d' % num 393 394class Address(StdContainer): 395 tag = 'address' 396 397class Underline(StdContainer): 398 tag = 'u' 399 400class Bold(StdContainer): 401 tag = 'strong' 402 403class Italic(StdContainer): 404 tag = 'em' 405 406class Preformatted(QuotedContainer): 407 tag = 'pre' 408 409class Subscript(StdContainer): 410 tag = 'sub' 411 412class Superscript(StdContainer): 413 tag = 'sup' 414 415class Strikeout(StdContainer): 416 tag = 'strike' 417 418class Center(StdContainer): 419 tag = 'center' 420 421class Form(Container): 422 def __init__(self, action='', method='POST', encoding=None, 423 mlist=None, contexts=None, user=None, *items): 424 apply(Container.__init__, (self,) + items) 425 self.action = action 426 self.method = method 427 self.encoding = encoding 428 self.mlist = mlist 429 self.contexts = contexts 430 self.user = user 431 432 def set_action(self, action): 433 self.action = action 434 435 def Format(self, indent=0): 436 spaces = ' ' * indent 437 encoding = '' 438 if self.encoding: 439 encoding = 'enctype="%s"' % self.encoding 440 output = '\n%s<FORM action="%s" method="%s" %s>\n' % ( 441 spaces, self.action, self.method, encoding) 442 if self.mlist: 443 output = output + \ 444 '<input type="hidden" name="csrf_token" value="%s">\n' \ 445 % csrf_token(self.mlist, self.contexts, self.user) 446 output = output + Container.Format(self, indent+2) 447 output = '%s\n%s</FORM>\n' % (output, spaces) 448 return output 449 450 451class InputObj: 452 def __init__(self, name, ty, value, checked, **kws): 453 self.name = name 454 self.type = ty 455 self.value = value 456 self.checked = checked 457 self.kws = kws 458 459 def Format(self, indent=0): 460 charset = get_translation().charset() or 'us-ascii' 461 output = ['<INPUT name="%s" type="%s" value="%s"' % 462 (self.name, self.type, self.value)] 463 for item in self.kws.items(): 464 output.append('%s="%s"' % item) 465 if self.checked: 466 output.append('CHECKED') 467 output.append('>') 468 ret = SPACE.join(output) 469 if self.type == 'TEXT' and isinstance(ret, unicode): 470 ret = ret.encode(charset, 'xmlcharrefreplace') 471 return ret 472 473 474class SubmitButton(InputObj): 475 def __init__(self, name, button_text): 476 InputObj.__init__(self, name, "SUBMIT", button_text, checked=0) 477 478class PasswordBox(InputObj): 479 def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH): 480 InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size) 481 482class TextBox(InputObj): 483 def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH): 484 if isinstance(value, str): 485 safevalue = Utils.websafe(value) 486 else: 487 safevalue = value 488 InputObj.__init__(self, name, "TEXT", safevalue, checked=0, size=size) 489 490class Hidden(InputObj): 491 def __init__(self, name, value=''): 492 InputObj.__init__(self, name, 'HIDDEN', value, checked=0) 493 494class TextArea: 495 def __init__(self, name, text='', rows=None, cols=None, wrap='soft', 496 readonly=0): 497 if isinstance(text, str): 498 # Double escape HTML entities in non-readonly areas. 499 doubleescape = not readonly 500 safetext = Utils.websafe(text, doubleescape) 501 else: 502 safetext = text 503 self.name = name 504 self.text = safetext 505 self.rows = rows 506 self.cols = cols 507 self.wrap = wrap 508 self.readonly = readonly 509 510 def Format(self, indent=0): 511 charset = get_translation().charset() or 'us-ascii' 512 output = '<TEXTAREA NAME=%s' % self.name 513 if self.rows: 514 output += ' ROWS=%s' % self.rows 515 if self.cols: 516 output += ' COLS=%s' % self.cols 517 if self.wrap: 518 output += ' WRAP=%s' % self.wrap 519 if self.readonly: 520 output += ' READONLY' 521 output += '>%s</TEXTAREA>' % self.text 522 if isinstance(output, unicode): 523 output = output.encode(charset, 'xmlcharrefreplace') 524 return output 525 526class FileUpload(InputObj): 527 def __init__(self, name, rows=None, cols=None, **kws): 528 apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws) 529 530class RadioButton(InputObj): 531 def __init__(self, name, value, checked=0, **kws): 532 apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) 533 534class CheckBox(InputObj): 535 def __init__(self, name, value, checked=0, **kws): 536 apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) 537 538class VerticalSpacer: 539 def __init__(self, size=10): 540 self.size = size 541 def Format(self, indent=0): 542 output = '<spacer type="vertical" height="%d">' % self.size 543 return output 544 545class WidgetArray: 546 Widget = None 547 548 def __init__(self, name, button_names, checked, horizontal, values): 549 self.name = name 550 self.button_names = button_names 551 self.checked = checked 552 self.horizontal = horizontal 553 self.values = values 554 assert len(values) == len(button_names) 555 # Don't assert `checked' because for RadioButtons it is a scalar while 556 # for CheckedBoxes it is a vector. Subclasses will assert length. 557 558 def ischecked(self, i): 559 raise NotImplemented 560 561 def Format(self, indent=0): 562 t = Table(cellspacing=5) 563 items = [] 564 for i, name, value in zip(range(len(self.button_names)), 565 self.button_names, 566 self.values): 567 ischecked = (self.ischecked(i)) 568 item = ('<label>' + 569 self.Widget(self.name, value, ischecked).Format() + 570 name + '</label>') 571 items.append(item) 572 if not self.horizontal: 573 t.AddRow(items) 574 items = [] 575 if self.horizontal: 576 t.AddRow(items) 577 return t.Format(indent) 578 579class RadioButtonArray(WidgetArray): 580 Widget = RadioButton 581 582 def __init__(self, name, button_names, checked=None, horizontal=1, 583 values=None): 584 if values is None: 585 values = range(len(button_names)) 586 # BAW: assert checked is a scalar... 587 WidgetArray.__init__(self, name, button_names, checked, horizontal, 588 values) 589 590 def ischecked(self, i): 591 return self.checked == i 592 593class CheckBoxArray(WidgetArray): 594 Widget = CheckBox 595 596 def __init__(self, name, button_names, checked=None, horizontal=0, 597 values=None): 598 if checked is None: 599 checked = [0] * len(button_names) 600 else: 601 assert len(checked) == len(button_names) 602 if values is None: 603 values = range(len(button_names)) 604 WidgetArray.__init__(self, name, button_names, checked, horizontal, 605 values) 606 607 def ischecked(self, i): 608 return self.checked[i] 609 610class UnorderedList(Container): 611 def Format(self, indent=0): 612 spaces = ' ' * indent 613 output = '\n%s<ul>\n' % spaces 614 for item in self.items: 615 output = output + '%s<li>%s\n' % \ 616 (spaces, HTMLFormatObject(item, indent + 2)) 617 output = output + '%s</ul>\n' % spaces 618 return output 619 620class OrderedList(Container): 621 def Format(self, indent=0): 622 spaces = ' ' * indent 623 output = '\n%s<ol>\n' % spaces 624 for item in self.items: 625 output = output + '%s<li>%s\n' % \ 626 (spaces, HTMLFormatObject(item, indent + 2)) 627 output = output + '%s</ol>\n' % spaces 628 return output 629 630class DefinitionList(Container): 631 def Format(self, indent=0): 632 spaces = ' ' * indent 633 output = '\n%s<dl>\n' % spaces 634 for dt, dd in self.items: 635 output = output + '%s<dt>%s\n<dd>%s\n' % \ 636 (spaces, HTMLFormatObject(dt, indent+2), 637 HTMLFormatObject(dd, indent+2)) 638 output = output + '%s</dl>\n' % spaces 639 return output 640 641 642 643# Logo constants 644# 645# These are the URLs which the image logos link to. The Mailman home page now 646# points at the gnu.org site instead of the www.list.org mirror. 647# 648from mm_cfg import MAILMAN_URL 649PYTHON_URL = 'http://www.python.org/' 650GNU_URL = 'http://www.gnu.org/' 651FREEBSD_URL = 'http://www.freebsd.org/' 652 653# The names of the image logo files. These are concatentated onto 654# mm_cfg.IMAGE_LOGOS (not urljoined). 655DELIVERED_BY = 'mailman.jpg' 656PYTHON_POWERED = 'PythonPowered.png' 657GNU_HEAD = 'gnu-head-tiny.jpg' 658FREEBSD_POWERED = 'powerlogo.png' 659 660 661def MailmanLogo(): 662 t = Table(border=0, width='100%') 663 if mm_cfg.IMAGE_LOGOS: 664 def logo(file): 665 return mm_cfg.IMAGE_LOGOS + file 666 mmlink = '<img src="%s" alt="Delivered by Mailman" border=0>' \ 667 '<br>version %s' % (logo(DELIVERED_BY), mm_cfg.VERSION) 668 pylink = '<img src="%s" alt="Python Powered" border=0>' % \ 669 logo(PYTHON_POWERED) 670 freebsdlink = '<img src="%s" alt="Powered by FreeBSD" border=0>' % \ 671 logo(FREEBSD_POWERED) 672 t.AddRow([mmlink, pylink, freebsdlink]) 673 else: 674 # use only textual links 675 version = mm_cfg.VERSION 676 mmlink = Link(MAILMAN_URL, 677 _('Delivered by Mailman<br>version %(version)s')) 678 pylink = Link(PYTHON_URL, _('Python Powered')) 679 freebsdlink = Link(FREEBSD_URL, "Powered by FreeBSD") 680 t.AddRow([mmlink, pylink, freebsdlink]) 681 return t 682 683 684class SelectOptions: 685 def __init__(self, varname, values, legend, 686 selected=0, size=1, multiple=None): 687 self.varname = varname 688 self.values = values 689 self.legend = legend 690 self.size = size 691 self.multiple = multiple 692 # we convert any type to tuple, commas are needed 693 if not multiple: 694 if type(selected) == types.IntType: 695 self.selected = (selected,) 696 elif type(selected) == types.TupleType: 697 self.selected = (selected[0],) 698 elif type(selected) == types.ListType: 699 self.selected = (selected[0],) 700 else: 701 self.selected = (0,) 702 703 def Format(self, indent=0): 704 spaces = " " * indent 705 items = min( len(self.values), len(self.legend) ) 706 707 # jcrey: If there is no argument, we return nothing to avoid errors 708 if items == 0: 709 return "" 710 711 text = "\n" + spaces + "<Select name=\"%s\"" % self.varname 712 if self.size > 1: 713 text = text + " size=%d" % self.size 714 if self.multiple: 715 text = text + " multiple" 716 text = text + ">\n" 717 718 for i in range(items): 719 if i in self.selected: 720 checked = " Selected" 721 else: 722 checked = "" 723 724 opt = " <option value=\"%s\"%s> %s </option>" % ( 725 self.values[i], checked, self.legend[i]) 726 text = text + spaces + opt + "\n" 727 728 return text + spaces + '</Select>' 729