chiark / gitweb /
backend.py: Use configured delimiter for joining fields.
[chopwood] / cmd-cgi.py
1 ### -*-python-*-
2 ###
3 ### CGI commands
4 ###
5 ### (c) 2013 Mark Wooding
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of Chopwood: a password-changing service.
11 ###
12 ### Chopwood is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU Affero General Public License as
14 ### published by the Free Software Foundation; either version 3 of the
15 ### License, or (at your option) any later version.
16 ###
17 ### Chopwood is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ### GNU Affero General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU Affero General Public
23 ### License along with Chopwood; if not, see
24 ### <http://www.gnu.org/licenses/>.
25
26 from __future__ import with_statement
27
28 import errno as E
29 import os as OS
30
31 from auto import PACKAGE, VERSION
32 import agpl as AGPL
33 import cgi as CGI
34 import cmdutil as CU
35 import dbmaint as D
36 import httpauth as HA
37 import operation as OP
38 import output as O; OUT = O.OUT; PRINT = O.PRINT
39 import service as S
40 import subcommand as SC
41 import util as U
42
43 ###--------------------------------------------------------------------------
44 ### Utilities.
45
46 def operate(what, op, services, *args, **kw):
47   accts = CU.resolve_accounts(CU.USER, services)
48   o, ii, rq, ops = OP.operate(op, accts, *args, **kw)
49   CGI.page('operate.fhtml',
50            header = dict(pragma = 'no-cache', cache_control = 'no-cache'),
51            title = 'Chopwood: %s' % what,
52            what = what,
53            outcome = o, info = ii, results = ops)
54
55 ###--------------------------------------------------------------------------
56 ### Commands.
57
58 @CGI.subcommand('list', ['cgi-query'], 'List available accounts')
59 def cmd_list_cgi():
60   CGI.page('list.fhtml',
61            header = dict(pragma = 'no-cache', cache_control = 'no-cache'),
62            title = 'Chopwood: accounts list',
63            accts = CU.list_accounts(CU.USER),
64            nonce = HA.NONCE)
65
66 @CGI.subcommand(
67   'set', ['cgi'], 'Set password for a collection of services.',
68   methods = ['POST'],
69   params = [SC.Arg('first'), SC.Arg('second')],
70   rparam = SC.Arg('services'))
71 def cmd_set_cgi(first, second, services = []):
72   if first != second: raise U.ExpectedError, (400, "Passwords don't match")
73   operate('set passwords', 'set', services, first)
74
75 @CGI.subcommand(
76   'reset', ['cgi'],
77   'Reset passwords for a collection of services.',
78   methods = ['POST'],
79   rparam = SC.Arg('services'))
80 def cmd_reset_cgi(services = []):
81   operate('reset passwords', 'reset', services)
82
83 @CGI.subcommand(
84   'clear', ['cgi'],
85   'Clear passwords for a collection of services.',
86   methods = ['POST'],
87   rparam = SC.Arg('services'))
88 def cmd_clear_cgi(services = []):
89   operate('clear passwords', 'clear', services)
90
91 @CGI.subcommand(
92   'logout', ['cgi'],
93   'Log out of the web interface.',
94   methods = ['POST'])
95 def cmd_logout_cgi():
96   CGI.redirect(CGI.action('login', why = 'LOGOUT'),
97                set_cookie = HA.bake_cookie('logged-out'))
98
99 @CGI.subcommand(
100   'fail', ['cgi-noauth'],
101   'Raise an exception, to test the error reporting machinery.',
102   opts = [SC.Opt('partial', '-p', '--partial',
103                  'Raise exception after producing partial output.')])
104 def cmd_fail_cgi(partial = False):
105   if partial:
106     OUT.header(content_type = 'text/html')
107     PRINT("""\
108 <html>
109 <head><title>Chopwood: filler text</title></head>
110 <body>
111 <h1>Failure expected soon
112 <p>This is some normal output which will be rudely interrupted.""")
113   raise Exception, 'You asked for this.'
114
115 @CGI.subcommand(
116   'warn', ['cgi-noauth'],
117   'Raise an exception, to test the error reporting machinery.')
118 def cmd_warn_cgi():
119   OUT.header(content_type = 'text/html')
120   OUT.warn("Here's a very important warning.")
121   CGI.format_tmpl(CGI.TMPL['wrapper.fhtml'],
122                   title = "Warning test", warnings = OUT.warnings,
123                   payload = "<h1>Chopwood: warning test</h1>\n"
124                   "<p>There ought to be a warning below.\n")
125
126 ###--------------------------------------------------------------------------
127 ### Static content.
128
129 ## A map of file names to content objects.  See below.
130 CONTENT = {}
131
132 class PlainOutput (O.FileOutput):
133   def header(me, **kw):
134     pass
135
136 class StaticContent (object):
137   def __init__(me, type):
138     me._type = type
139   def emit(me):
140     OUT.header(content_type = me._type)
141     me._emit()
142   def _write(me, dest):
143     with open(dest, 'w') as f:
144       with OUT.redirect_to(PlainOutput(f)):
145         me.emit()
146   def write(me, dest):
147     new = dest + '.new'
148     try: OS.unlink(new)
149     except OSError, e:
150       if e.errno != E.ENOENT: raise
151     me._write(new)
152     OS.rename(new, dest)
153
154 class TemplateContent (StaticContent):
155   def __init__(me, template, *args, **kw):
156     super(TemplateContent, me).__init__(*args, **kw)
157     me._template = template
158   def _emit(me):
159     CGI.format_tmpl(CGI.TMPL[me._template])
160
161 class HTMLContent (StaticContent):
162   def __init__(me, title, template, type = 'text/html', *args, **kw):
163     super(HTMLContent, me).__init__(type = type, *args, **kw)
164     me._template = template
165     me._title = title
166   def emit(me):
167     CGI.page(me._template, title = me._title)
168
169 CONTENT.update({
170   'chpwd.css': TemplateContent(template = 'chpwd.css',
171                                type = 'text/css'),
172   'chpwd.js': TemplateContent(template = 'chpwd.js',
173                               type = 'text/javascript'),
174   'about.html': HTMLContent('Chopwood: about this program',
175                             template = 'about.fhtml'),
176   'cookies.html': HTMLContent('Chopwood: use of cookies',
177                               template = 'cookies.fhtml')
178 })
179
180 @CGI.subcommand(
181   'static', ['cgi-noauth'], 'Output a static file.',
182   rparam = SC.Arg('path'))
183 def cmd_static_cgi(path):
184   name = '/'.join(path)
185   try: content = CONTENT[name]
186   except KeyError: raise U.ExpectedError, (404, "Unknown file `%s'" % name)
187   content.emit()
188
189 @SC.subcommand(
190   'static', ['admin'], 'Write the static files to DIR.',
191   params = [SC.Arg('dir')])
192 def cmd_static_admin(dir):
193   try: OS.makedirs(dir, 0777)
194   except OSError, e:
195     if e.errno != E.EEXIST: raise
196   for f, c in CONTENT.iteritems():
197     c.write(OS.path.join(dir, f))
198
199 TARBALL = '%s-%s.tar.gz' % (PACKAGE, VERSION)
200 @CGI.subcommand(TARBALL, ['cgi-noauth'], """\
201 Download source code (in `.tar.gz' format).""")
202 def cmd_source_cgi():
203   OUT.header(content_type = 'application/octet-stream')
204   AGPL.source(OUT)
205
206 @CGI.subcommand('source', ['cgi-noauth'], """\
207 Redirect to the source code tarball (so that it's correctly named).""")
208 def cmd_sourceredirect_cgi():
209   CGI.redirect(CGI.action(TARBALL))
210
211 ###----- That's all, folks --------------------------------------------------