diff --git a/certbot.yml b/certbot.yml index 6a6a3eb5..80f49ebc 100755 --- a/certbot.yml +++ b/certbot.yml @@ -2,7 +2,7 @@ --- # Temporary # Wildcard certificate for MX servers -- hosts: titanic.adm.crans.org +- hosts: titanic.adm.crans.org, redisdead.adm.crans.org vars: certbot: dns_rfc2136_name: certbot_challenge. diff --git a/clean_servers.yml b/clean_servers.yml index 218948f2..0b6b7fd0 100755 --- a/clean_servers.yml +++ b/clean_servers.yml @@ -9,6 +9,7 @@ apt: state: absent name: + - at - arpwatch # old sniffing - collectd - collectd-utils # old monitoring @@ -28,6 +29,7 @@ - monitoring-plugins-standard - monitoring-plugins-basic - monitoring-plugins-common + - monit - libmonitoring-plugin-perl - snmp - nagios-plugins-contrib @@ -83,9 +85,12 @@ - /etc/munin - /etc/icinga2 - /etc/init.d/bcfg2 + - /etc/nagios + - /etc/nagios-plugins - /etc/nut - /etc/nginx/sites-enabled/status - /etc/nginx/sites-available/status + - /etc/pnp4nagios - /var/local/aptdater - /etc/apt-dater-host.conf - /etc/sudoers.d/apt-dater-host diff --git a/logos/crans.png b/logos/crans.png new file mode 100644 index 00000000..9c5e281a Binary files /dev/null and b/logos/crans.png differ diff --git a/mailman.yml b/mailman.yml new file mode 100755 index 00000000..2ce0e772 --- /dev/null +++ b/mailman.yml @@ -0,0 +1,23 @@ +#!/usr/bin/env ansible-playbook +# Mailman playbook +--- +- hosts: redisdead.adm.crans.org + vars: + mailman: + site_list: "nounou" + default_url: "https://lists.crans.org/" + default_host: "lists.crans.org" + default_language: "fr" + auth_basic: | + "On n'aime pas les spambots, donc on a mis un mot de passe. Le login est Stop et le mot de passe est Spam."; + spamassassin: "SpamAssassin_crans" + smtphost: "smtp.adm.crans.org" + mynetworks: ['138.231.0.0/16', '185.230.76.0/22', '2a0c:700:0::/40'] + nginx: + ssl: + cert: /etc/letsencrypt/live/crans.org/fullchain.pem + key: /etc/letsencrypt/live/crans.org/privkey.pem + trusted_cert: /etc/letsencrypt/live/crans.org/chain.pem + roles: + - mailman + - nginx-mailman diff --git a/roles/common-tools/tasks/main.yml b/roles/common-tools/tasks/main.yml index 70488e80..b92fea69 100644 --- a/roles/common-tools/tasks/main.yml +++ b/roles/common-tools/tasks/main.yml @@ -4,6 +4,7 @@ update_cache: true install_recommends: false name: + - apt-file - sudo - molly-guard # prevent reboot - ntp # network time sync diff --git a/roles/mailman/handlers/main.yml b/roles/mailman/handlers/main.yml new file mode 100644 index 00000000..77550456 --- /dev/null +++ b/roles/mailman/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reload mailman + systemd: + name: mailman + state: reloaded diff --git a/roles/mailman/tasks/main.yml b/roles/mailman/tasks/main.yml new file mode 100644 index 00000000..53ae09de --- /dev/null +++ b/roles/mailman/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Install mailman and SpamAssassin + apt: + update_cache: true + name: + - mailman + - spamassassin + register: apt_result + retries: 3 + until: apt_result is succeeded + +- name: Deploy mailman config + template: + src: "mailman/{{ item }}.j2" + dest: "/etc/mailman/{{ item }}" + mode: 0755 + loop: + - mm_cfg.py + - create.html + notify: Reload mailman + +# Fanciness +- name: Deploy crans logo + copy: + src: ../../../logos/crans.png + dest: /usr/share/images/mailman/crans.png + +- name: Deploy crans logo + template: + src: usr/lib/mailman/Mailman/htmlformat.py.j2 + dest: /usr/lib/mailman/Mailman/htmlformat.py + mode: 0755 + notify: Reload mailman + +- name: Indicate role in motd + template: + src: update-motd.d/05-mailman.j2 + dest: /etc/update-motd.d/05-mailman + mode: 0755 diff --git a/roles/mailman/templates/mailman/create.html.j2 b/roles/mailman/templates/mailman/create.html.j2 new file mode 100644 index 00000000..68236402 --- /dev/null +++ b/roles/mailman/templates/mailman/create.html.j2 @@ -0,0 +1,13 @@ +{{ ansible_header | comment('xml') }} + + + + +Creation de mailing list + + + +

Creation de mailing list

+Il faut s'adresser a nounou arobase crans point org. + + diff --git a/roles/mailman/templates/mailman/mm_cfg.py.j2 b/roles/mailman/templates/mailman/mm_cfg.py.j2 new file mode 100644 index 00000000..25f82461 --- /dev/null +++ b/roles/mailman/templates/mailman/mm_cfg.py.j2 @@ -0,0 +1,226 @@ +{{ ansible_header | comment }} +# -*- python -*- + +# Copyright (C) 1998,1999,2000 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 + + +"""This is the module which takes your site-specific settings. + +From a raw distribution it should be copied to mm_cfg.py. If you +already have an mm_cfg.py, be careful to add in only the new settings +you want. The complete set of distributed defaults, with annotation, +are in ./Defaults. In mm_cfg, override only those you want to +change, after the + + from Defaults import * + +line (see below). + +Note that these are just default settings - many can be overridden via the +admin and user interfaces on a per-list or per-user basis. + +Note also that some of the settings are resolved against the active list +setting by using the value as a format string against the +list-instance-object's dictionary - see the distributed value of +DEFAULT_MSG_FOOTER for an example.""" + + +####################################################### +# Here's where we get the distributed defaults. # + +from Defaults import * + + +##### +# General system-wide defaults +##### + +# Should image logos be used? Set this to 0 to disable image logos from "our +# sponsors" and just use textual links instead (this will also disable the +# shortcut "favicon"). Otherwise, this should contain the URL base path to +# the logo images (and must contain the trailing slash).. If you want to +# disable Mailman's logo footer altogther, hack +# Mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links +# and image names. +IMAGE_LOGOS = '/images/mailman/' + +#------------------------------------------------------------- +# The name of the list Mailman uses to send password reminders +# and similar. Don't change if you want mailman-owner to be +# a valid local part. +MAILMAN_SITE_LIST = '{{ mailman.site_list }}' + +DEFAULT_URL= '{{ mailman.default_url }}' +DEFAULT_URL_PATTERN = 'https://%s/' +add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST) + +#------------------------------------------------------------- +# Default domain for email addresses of newly created MLs +DEFAULT_EMAIL_HOST = '{{ mailman.default_host }}' +#------------------------------------------------------------- +# Default host for web interface of newly created MLs +DEFAULT_URL_HOST = '{{ mailman.default_host }}' +#------------------------------------------------------------- +# Required when setting any of its arguments. +add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST) + +#------------------------------------------------------------- +# Do we send monthly reminders? +DEFAULT_SEND_REMINDERS = No + +# Normally when a site administrator authenticates to a web page with the site +# password, they get a cookie which authorizes them as the list admin. It +# makes me nervous to hand out site auth cookies because if this cookie is +# cracked or intercepted, the intruder will have access to every list on the +# site. OTOH, it's dang handy to not have to re-authenticate to every list on +# the site. Set this value to Yes to allow site admin cookies. +ALLOW_SITE_ADMIN_COOKIES = Yes + +##### +# Archive defaults +##### + +PUBLIC_ARCHIVE_URL = '{{ mailman.default_url }}archives/%(listname)s' + +# Are archives on or off by default? +DEFAULT_ARCHIVE = Off + +# Are archives public or private by default? +# 0=public, 1=private +DEFAULT_ARCHIVE_PRIVATE = 1 + +# Pipermail assumes that messages bodies contain US-ASCII text. +# Change this option to define a different character set to be used as +# the default character set for the archive. The term "character set" +# is used in MIME to refer to a method of converting a sequence of +# octets into a sequence of characters. If you change the default +# charset, you might need to add it to VERBATIM_ENCODING below. +DEFAULT_CHARSET = 'utf-8' + +# Most character set encodings require special HTML entity characters to be +# quoted, otherwise they won't look right in the Pipermail archives. However +# some character sets must not quote these characters so that they can be +# rendered properly in the browsers. The primary issue is multi-byte +# encodings where the octet 0x26 does not always represent the & character. +# This variable contains a list of such characters sets which are not +# HTML-quoted in the archives. +VERBATIM_ENCODING = ['utf-8'] + +##### +# General defaults +##### + +# The default language for this server. Whenever we can't figure out the list +# context or user context, we'll fall back to using this language. See +# LC_DESCRIPTIONS below for legal values. +DEFAULT_SERVER_LANGUAGE = '{{ mailman.default_language }}' + +# How many members to display at a time on the admin cgi to unsubscribe them +# or change their options? +DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 50 + +# set this variable to Yes to allow list owners to delete their own mailing +# lists. You may not want to give them this power, in which case, setting +# this variable to No instead requires list removal to be done by the site +# administrator, via the command line script bin/rmlist. +#OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No + +# Set this variable to Yes to allow list owners to set the "personalized" +# flags on their mailing lists. Turning these on tells Mailman to send +# separate email messages to each user instead of batching them together for +# delivery to the MTA. This gives each member a more personalized message, +# but can have a heavy impact on the performance of your system. +#OWNERS_CAN_ENABLE_PERSONALIZATION = No + +##### +# List defaults. NOTE: Changing these values does NOT change the +# configuration of an existing list. It only defines the default for new +# lists you subsequently create. +##### + +# Should a list, by default be advertised? What is the default maximum number +# of explicit recipients allowed? What is the default maximum message size +# allowed? +DEFAULT_LIST_ADVERTISED = Yes + +# {header-name: regexp} spam filtering - we include some for example sake. +DEFAULT_BOUNCE_MATCHING_HEADERS = """ +# Les lignes commencant par # sont des commentairtes. +#from: .*-owner@yahoogroups.com +#from: .*@uplinkpro.com +#from: .*@coolstats.comic.com +#from: .*@trafficmagnet.com +#from: .*@hotmail.com +#X-Reject: 450 +#X-Reject: 554 +""" + +# Mailman can be configured to strip any existing Reply-To: header, or simply +# extend any existing Reply-To: with one based on the above setting. +DEFAULT_FIRST_STRIP_REPLY_TO = Yes + +# SUBSCRIBE POLICY +# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) ** +# 1 - confirmation required for subscribes +# 2 - admin approval required for subscribes +# 3 - both confirmation and admin approval required +# +# ** please do not choose option 0 if you are not allowing open +# subscribes (next variable) +DEFAULT_SUBSCRIBE_POLICY = 3 + +# Is the list owner notified of subscribes/unsubscribes? +DEFAULT_ADMIN_NOTIFY_MCHANGES = Yes + +# Do we send monthly reminders? +DEFAULT_SEND_REMINDERS = No + +# What should happen to non-member posts which do not match explicit +# non-member actions? +# 0 = Accept +# 1 = Hold +# 2 = Reject +# 3 = Discard +DEFAULT_GENERIC_NONMEMBER_ACTION = 1 + +# Use spamassassin automatically +GLOBAL_PIPELINE.insert(5, '{{ spamassassin }}') +# Discard messages with score higher than ... +SPAMASSASSIN_DISCARD_SCORE = 8 +# Hold in moderation messages with score higher than ... +SPAMASSASSIN_HOLD_SCORE = 2.1 + +# Add SpamAssassin administration interface on gui +# To make it work, you need to edit Gui/__init__.py +# with +# from SpamAssassin import SpamAssassin +ADMIN_CATEGORIES.append("spamassassin") + +# Add header to keep +PLAIN_DIGEST_KEEP_HEADERS.append('X-Spam-Score') + +# configure MTA +MTA = 'Postfix' +SMTPHOST = '{{ smtphost }}' +SMTP_MAX_RCPTS = 50 + + +POSTFIX_STYLE_VIRTUAL_DOMAINS = ["{{ mailman.default_host }}"] + +# Note - if you're looking for something that is imported from mm_cfg, but you +# didn't find it above, it's probably in /usr/lib/mailman/Mailman/Defaults.py. diff --git a/roles/mailman/templates/update-motd.d/05-mailman.j2 b/roles/mailman/templates/update-motd.d/05-mailman.j2 new file mode 100755 index 00000000..d3fee0db --- /dev/null +++ b/roles/mailman/templates/update-motd.d/05-mailman.j2 @@ -0,0 +1,3 @@ +#!/usr/bin/tail +14 +{{ ansible_header | comment }} +> Mailman a été déployé sur cette machine. Voir /etc/mailman/ et /var/lib/mailman/. diff --git a/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2 b/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2 new file mode 100644 index 00000000..146f9576 --- /dev/null +++ b/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2 @@ -0,0 +1,742 @@ +{{ ansible_header | comment }} +# 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 + '' + + for i in range(len(self.cells[row])): + output = output + self.FormatCell(row, i, indent + 2) + + output = output + '\n' + ' '*indent + '' + + return output + + def Format(self, indent=0): + output = '\n' + ' '*indent + '' + + for i in range(len(self.cells)): + output = output + self.FormatRow(i, indent + 2) + + output = output + '\n' + ' '*indent + '\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 '%s' % (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 = '' % self.size + for item in self.items: + output = output + HTMLFormatObject(item, indent) + output = output + '' + 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 = '' % SPACE.join(seq) + for item in self.items: + output = output + HTMLFormatObject(item, indent) + output = output + '' + 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 ('
' % self.align) + \ + Container.Format(self, indent) + \ + '
' + + +# 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' % charset] + output.append('Cache-control: no-cache\n') + if not self.suppress_head: + kws.setdefault('bgcolor', self.bgcolor) + tab = ' ' * indent + output.extend([tab, + '', + '' + ]) + if mm_cfg.IMAGE_LOGOS: + output.append('' % + (mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON)) + # Hit all the bases + output.append('' % charset) + if self.title: + output.append('%s%s' % (tab, self.title)) + # Add CSS to visually hide some labeling text but allow screen + # readers to read it. + output.append("""\ + +""") + if mm_cfg.WEB_HEAD_ADD: + output.append(mm_cfg.WEB_HEAD_ADD) + output.append('%s' % 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' % direction) + # Always do this... + output.append(Container.Format(self, indent)) + if not self.suppress_head: + output.append('%s' % tab) + output.append('%s' % 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' % (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' % ( + 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
\n' % ( + spaces, self.action, self.method, encoding) + if self.mlist: + output = output + \ + '\n' \ + % csrf_token(self.mlist, self.contexts, self.user) + output = output + Container.Format(self, indent+2) + output = '%s\n%s
\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 = ['') + 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): + # Double escape HTML entities in non-readonly areas. + doubleescape = not readonly + safetext = Utils.websafe(text, doubleescape) + 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 = '