Merge branch 'weblate-gnu-mailman-mailman' into 'master'
[mailman.git] / port_me / config_list.py
blob988d84aa74d8b08edd975e40839c723fe0b2369d
1 # Copyright (C) 1998-2023 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 import re
19 import sys
20 import time
21 import optparse
23 from mailman import errors, MailList
24 from mailman.configuration import config
25 from mailman.core.i18n import _
26 from mailman.initialize import initialize
27 from mailman.utilities.string import wrap
28 from mailman.version import MAILMAN_VERSION
31 NL = '\n'
32 nonasciipat = re.compile(r'[\x80-\xff]')
36 def parseargs():
37 parser = optparse.OptionParser(version=MAILMAN_VERSION,
38 usage=_("""\
39 %prog [options] listname
41 Configure a list from a text file description, or dump a list's configuration
42 settings."""))
43 parser.add_option('-i', '--inputfile',
44 metavar='FILENAME', default=None, type='string',
45 help=_("""\
46 Configure the list by assigning each module-global variable in the file to an
47 attribute on the mailing list object, then save the list. The named file is
48 loaded with execfile() and must be legal Python code. Any variable that isn't
49 already an attribute of the list object is ignored (a warning message is
50 printed). See also the -c option.
52 A special variable named 'mlist' is put into the globals during the execfile,
53 which is bound to the actual MailList object. This lets you do all manner of
54 bizarre thing to the list object, but BEWARE! Using this can severely (and
55 possibly irreparably) damage your mailing list!
57 The may not be used with the -o option."""))
58 parser.add_option('-o', '--outputfile',
59 metavar='FILENAME', default=None, type='string',
60 help=_("""\
61 Instead of configuring the list, print out a mailing list's configuration
62 variables in a format suitable for input using this script. In this way, you
63 can easily capture the configuration settings for a particular list and
64 imprint those settings on another list. FILENAME is the file to output the
65 settings to. If FILENAME is `-', standard out is used.
67 This may not be used with the -i option."""))
68 parser.add_option('-c', '--checkonly',
69 default=False, action='store_true', help=_("""\
70 With this option, the modified list is not actually changed. This is only
71 useful with the -i option."""))
72 parser.add_option('-v', '--verbose',
73 default=False, action='store_true', help=_("""\
74 Print the name of each attribute as it is being changed. This is only useful
75 with the -i option."""))
76 parser.add_option('-C', '--config',
77 help=_('Alternative configuration file to use'))
78 opts, args = parser.parse_args()
79 if len(args) > 1:
80 parser.print_help()
81 parser.error(_('Unexpected arguments'))
82 if not args:
83 parser.error(_('List name is required'))
84 return parser, opts, args
88 def do_output(listname, outfile, parser):
89 closep = False
90 try:
91 if outfile == '-':
92 outfp = sys.stdout
93 else:
94 outfp = open(outfile, 'w')
95 closep = True
96 # Open the specified list unlocked, since we're only reading it.
97 try:
98 mlist = MailList.MailList(listname, lock=False)
99 except errors.MMListError:
100 parser.error(_('No such list: $listname'))
101 # Preamble for the config info. PEP 263 charset and capture time.
102 charset = mlist.preferred_language.charset
103 # Set the system's default language.
104 _.default = mlist.preferred_language.code
105 if not charset:
106 charset = 'us-ascii'
107 when = time.ctime(time.time())
108 print >> outfp, _('''\
109 # -*- python -*-
110 # -*- coding: $charset -*-
111 ## "$listname" mailing list configuration settings
112 ## captured on $when
113 ''')
114 # Get all the list config info. All this stuff is accessible via the
115 # web interface.
116 for k in config.ADMIN_CATEGORIES:
117 subcats = mlist.GetConfigSubCategories(k)
118 if subcats is None:
119 do_list_categories(mlist, k, None, outfp)
120 else:
121 for subcat in [t[0] for t in subcats]:
122 do_list_categories(mlist, k, subcat, outfp)
123 finally:
124 if closep:
125 outfp.close()
129 def do_list_categories(mlist, k, subcat, outfp):
130 info = mlist.GetConfigInfo(k, subcat)
131 label, gui = mlist.GetConfigCategories()[k]
132 if info is None:
133 return
134 charset = mlist.preferred_language.charset
135 print >> outfp, '##', k.capitalize(), _('options')
136 print >> outfp, '#'
137 # First, massage the descripton text, which could have obnoxious
138 # leading whitespace on second and subsequent lines due to
139 # triple-quoted string nonsense in the source code.
140 desc = NL.join([s.lstrip() for s in info[0].splitlines()])
141 # Print out the category description
142 desc = wrap(desc)
143 for line in desc.splitlines():
144 print >> outfp, '#', line
145 print >> outfp
146 for data in info[1:]:
147 if not isinstance(data, tuple):
148 continue
149 varname = data[0]
150 # Variable could be volatile
151 if varname[0] == '_':
152 continue
153 vtype = data[1]
154 # First, massage the descripton text, which could have
155 # obnoxious leading whitespace on second and subsequent lines
156 # due to triple-quoted string nonsense in the source code.
157 desc = NL.join([s.lstrip() for s in data[-1].splitlines()])
158 # Now strip out all HTML tags
159 desc = re.sub('<.*?>', '', desc)
160 # And convert &lt;/&gt; to <>
161 desc = re.sub('&lt;', '<', desc)
162 desc = re.sub('&gt;', '>', desc)
163 # Print out the variable description.
164 desc = wrap(desc)
165 for line in desc.split('\n'):
166 print >> outfp, '#', line
167 # munge the value based on its type
168 value = None
169 if hasattr(gui, 'getValue'):
170 value = gui.getValue(mlist, vtype, varname, data[2])
171 if value is None and not varname.startswith('_'):
172 value = getattr(mlist, varname)
173 if vtype in (config.String, config.Text, config.FileUpload):
174 print >> outfp, varname, '=',
175 lines = value.splitlines()
176 if not lines:
177 print >> outfp, "''"
178 elif len(lines) == 1:
179 if charset != 'us-ascii' and nonasciipat.search(lines[0]):
180 # This is more readable for non-english list.
181 print >> outfp, '"' + lines[0].replace('"', '\\"') + '"'
182 else:
183 print >> outfp, repr(lines[0])
184 else:
185 if charset == 'us-ascii' and nonasciipat.search(value):
186 # Normally, an english list should not have non-ascii char.
187 print >> outfp, repr(NL.join(lines))
188 else:
189 outfp.write(' """')
190 outfp.write(NL.join(lines).replace('"', '\\"'))
191 outfp.write('"""\n')
192 elif vtype in (config.Radio, config.Toggle):
193 print >> outfp, '#'
194 print >> outfp, '#', _('legal values are:')
195 # TBD: This is disgusting, but it's special cased
196 # everywhere else anyway...
197 if varname == 'subscribe_policy' and \
198 not config.ALLOW_OPEN_SUBSCRIBE:
199 i = 1
200 else:
201 i = 0
202 for choice in data[2]:
203 print >> outfp, '# ', i, '= "%s"' % choice
204 i += 1
205 print >> outfp, varname, '=', repr(value)
206 else:
207 print >> outfp, varname, '=', repr(value)
208 print >> outfp
212 def getPropertyMap(mlist):
213 guibyprop = {}
214 categories = mlist.GetConfigCategories()
215 for category, (label, gui) in categories.items():
216 if not hasattr(gui, 'GetConfigInfo'):
217 continue
218 subcats = mlist.GetConfigSubCategories(category)
219 if subcats is None:
220 subcats = [(None, None)]
221 for subcat, sclabel in subcats:
222 for element in gui.GetConfigInfo(mlist, category, subcat):
223 if not isinstance(element, tuple):
224 continue
225 propname = element[0]
226 wtype = element[1]
227 guibyprop[propname] = (gui, wtype)
228 return guibyprop
231 class FakeDoc:
232 # Fake the error reporting API for the htmlformat.Document class
233 def addError(self, s, tag=None, *args):
234 if tag:
235 print >> sys.stderr, tag
236 print >> sys.stderr, s % args
238 def set_language(self, val):
239 pass
243 def do_input(listname, infile, checkonly, verbose, parser):
244 fakedoc = FakeDoc()
245 # Open the specified list locked, unless checkonly is set
246 try:
247 mlist = MailList.MailList(listname, lock=not checkonly)
248 except errors.MMListError as error:
249 parser.error(_('No such list "$listname"\n$error'))
250 savelist = False
251 guibyprop = getPropertyMap(mlist)
252 try:
253 globals = {'mlist': mlist}
254 # Any exception that occurs in execfile() will cause the list to not
255 # be saved, but any other problems are not save-fatal.
256 execfile(infile, globals)
257 savelist = True
258 for k, v in globals.items():
259 if k in ('mlist', '__builtins__'):
260 continue
261 if not hasattr(mlist, k):
262 print >> sys.stderr, _('attribute "$k" ignored')
263 continue
264 if verbose:
265 print >> sys.stderr, _('attribute "$k" changed')
266 missing = []
267 gui, wtype = guibyprop.get(k, (missing, missing))
268 if gui is missing:
269 # This isn't an official property of the list, but that's
270 # okay, we'll just restore it the old fashioned way
271 print >> sys.stderr, _('Non-standard property restored: $k')
272 setattr(mlist, k, v)
273 else:
274 # BAW: This uses non-public methods. This logic taken from
275 # the guts of GUIBase.handleForm().
276 try:
277 validval = gui._getValidValue(mlist, k, wtype, v)
278 except ValueError:
279 print >> sys.stderr, _('Invalid value for property: $k')
280 except errors.EmailAddressError:
281 print >> sys.stderr, _(
282 'Bad email address for option $k: $v')
283 else:
284 # BAW: Horrible hack, but then this is special cased
285 # everywhere anyway. :( Privacy._setValue() knows that
286 # when ALLOW_OPEN_SUBSCRIBE is false, the web values are
287 # 0, 1, 2 but these really should be 1, 2, 3, so it adds
288 # one. But we really do provide [0..3] so we need to undo
289 # the hack that _setValue adds. :( :(
290 if k == 'subscribe_policy' and \
291 not config.ALLOW_OPEN_SUBSCRIBE:
292 validval -= 1
293 # BAW: Another horrible hack. This one is just too hard
294 # to fix in a principled way in Mailman 2.1
295 elif k == 'new_member_options':
296 # Because this is a Checkbox, _getValidValue()
297 # transforms the value into a list of one item.
298 validval = validval[0]
299 validval = [bitfield for bitfield, bitval
300 in config.OPTINFO.items()
301 if validval & bitval]
302 gui._setValue(mlist, k, validval, fakedoc)
303 # BAW: when to do gui._postValidate()???
304 finally:
305 if savelist and not checkonly:
306 mlist.Save()
307 mlist.Unlock()
311 def main():
312 parser, opts, args = parseargs()
313 initialize(opts.config)
314 listname = args[0]
316 # Sanity check
317 if opts.inputfile and opts.outputfile:
318 parser.error(_('Only one of -i or -o is allowed'))
319 if not opts.inputfile and not opts.outputfile:
320 parser.error(_('One of -i or -o is required'))
322 if opts.outputfile:
323 do_output(listname, opts.outputfile, parser)
324 else:
325 do_input(listname, opts.inputfile, opts.checkonly,
326 opts.verbose, parser)
330 if __name__ == '__main__':
331 main()