[Jan Gerber] fix unicode filenames in wikibooks2epub (from https://code.launchpad...
[objavi2.git] / objavi / cgi_utils.py
blob174e52fdb0e4535dc8dd2f5998da2bf9be26b834
1 # Part of Objavi2, which turns html manuals into books
3 # Copyright (C) 2009 Douglas Bagnall
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 import os, sys
20 import cgi, re
21 import urllib
22 from getopt import gnu_getopt
24 from objavi.book_utils import log
25 from objavi import config
27 SERVER_NAME = os.environ.get('SERVER_NAME', 'localhost')
29 def parse_args(arg_validators):
30 """Read and validate CGI or commandline arguments, putting the
31 good ones into the returned dictionary. Command line arguments
32 should be in the form --title='A Book'.
33 """
34 query = cgi.FieldStorage()
35 options, args = gnu_getopt(sys.argv[1:], '', [x + '=' for x in arg_validators])
36 options = dict(options)
37 log(query, debug='STARTUP')
38 data = {}
39 for key, validator in arg_validators.items():
40 value = query.getfirst(key, options.get('--' + key, None))
41 log('%s: %s' % (key, value), debug='STARTUP')
42 if value is not None:
43 if validator is not None and not validator(value):
44 log("argument '%s' is not valid ('%s')" % (key, value))
45 continue
46 data[key] = value
48 log(data, debug='STARTUP')
49 return data
51 def super_bleach(dirty_name):
52 """Replace potentially nasty characters with safe ones."""
53 # a bit drastic: refine if it matters
54 name = ''.join((x if x.isalnum() else '-') for x in dirty_name)
55 if name:
56 return name
57 return 'untitled'
60 ## common between different versions of objavi
62 def get_server_list():
63 return sorted(k for k, v in config.SERVER_DEFAULTS.items() if v['display'])
65 def get_size_list():
66 #order by increasing areal size.
67 def calc_size(name, pointsize, klass):
68 if pointsize:
69 mmx = pointsize[0] * config.POINT_2_MM
70 mmy = pointsize[1] * config.POINT_2_MM
71 return (mmx * mmy, name, klass,
72 '%s (%dmm x %dmm)' % (name, mmx, mmy))
74 return (0, name, klass, name) # presumably 'custom'
76 return [x[1:] for x in sorted(calc_size(k, v.get('pointsize'), v.get('class', ''))
77 for k, v in config.PAGE_SIZE_DATA.iteritems())
80 def url2path(url):
81 """convert htdocs-relative addresses to local file paths"""
82 return config.HTDOCS + '/' + url.lstrip('/')
84 _htdocs = os.path.abspath(config.HTDOCS)
85 def path2url(path, default='/missing_path?%(path)s', full=False):
86 """convert local file paths to htdocs-relative addresses. If the
87 file is not in the web tree, return default"""
88 if path.startswith('file:///'):
89 path = path[7:]
90 path = os.path.abspath(path)
91 if path.startswith(_htdocs):
92 path = path[len(_htdocs):]
93 else:
94 path = default % {'path': urllib.quote(path)}
95 if full:
96 return 'http://%s%s' % (SERVER_NAME, path)
97 return path
100 def get_default_css(server=config.DEFAULT_SERVER, mode='book'):
101 """Get the default CSS text for the selected server"""
102 log(server)
103 cssfile = url2path(config.SERVER_DEFAULTS[server]['css-%s' % mode])
104 log(cssfile)
105 f = open(cssfile)
106 s = f.read()
107 f.close()
108 return s
110 def font_links():
111 """Links to various example pdfs."""
112 links = []
113 for script in os.listdir(config.FONT_EXAMPLE_SCRIPT_DIR):
114 if not script.isalnum():
115 log("warning: font-sample %s won't work; skipping" % script)
116 continue
117 links.append('<a href="%s?script=%s">%s</a>' % (config.FONT_LIST_URL, script, script))
118 return links
120 ## Helper functions for parse_args
122 def isfloat(s):
123 #spaces?, digits!, dot?, digits?, spaces?
124 #return re.compile(r'^\s*[+-]?\d+\.?\d*\s*$').match
125 try:
126 float(s)
127 return True
128 except ValueError:
129 return False
131 def isfloat_or_auto(s):
132 return isfloat(s) or s.lower() in ('', 'auto')
134 def is_isbn(s):
135 # 10 or 13 digits with any number of hyphens, perhaps with check-digit missing
136 s = s.replace('-', '')
137 return (re.match(r'^\d+[\dXx*]$', s) and len(s) in (9, 10, 12, 13))
139 def is_url(s):
140 """Check whether the string approximates a valid http URL."""
141 s = s.strip()
142 if not '://' in s:
143 s = 'http://' + s
144 return re.match(r'^https?://'
145 r'(?:(?:[a-z0-9]+(?:-*[a-z0-9]+)*\.)+[a-z]{2,8}|'
146 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
147 r'(?::\d+)?'
148 r'(?:/?|/\S+)$', s, re.I
151 def is_name(s):
152 return re.match(r'^[\w-]+$', s)
154 def is_utf8(s):
155 try:
156 s.decode('utf-8')
157 return True
158 except UnicodeDecodeError:
159 return False
163 ## Formatting of lists
165 def optionise(items, default=None):
166 """Make a list of strings into an html option string, as would fit
167 inside <select> tags."""
168 options = []
169 for x in items:
170 if isinstance(x, str):
171 x = (x, x)
172 if len(x) == 2:
173 # couple: value, name
174 if x[0] == default:
175 options.append('<option selected="selected" value="%s">%s</option>' % x)
176 else:
177 options.append('<option value="%s">%s</option>' % x)
178 else:
179 # triple: value, class, name
180 if x[0] == default:
181 options.append('<option selected="selected" value="%s" class="%s">%s</option>' % x)
182 else:
183 options.append('<option value="%s" class="%s">%s</option>' % x)
185 return '\n'.join(options)
188 def listify(items):
189 """Make a list of strings into html <li> items, to fit in a <ul>
190 or <ol> element."""
191 return '\n'.join('<li>%s</li>' % x for x in items)
193 #output functions
195 def output_blob_and_exit(blob, content_type="application/octet-stream", filename=None):
196 print 'Content-type: %s\nContent-length: %s' % (content_type, len(blob))
197 if filename is not None:
198 print 'Content-Disposition: attachment; filename="%s"' % filename
199 print
200 print blob
201 sys.exit()
203 def output_blob_and_shut_up(blob, content_type="application/octet-stream", filename=None):
204 print 'Content-type: %s\nContent-length: %s' % (content_type, len(blob))
205 if filename is not None:
206 print 'Content-Disposition: attachment; filename="%s"' % filename
207 print
208 print blob
209 sys.stdout.flush()
210 devnull = open('/dev/null', 'w')
211 os.dup2(devnull.fileno(), sys.stdout.fileno())
212 log(sys.stdout)
214 ##Decorator function for output
215 def output_and_exit(f, content_type="text/html; charset=utf-8"):
216 """Decorator: prefix function output with http headers and exit
217 immediately after."""
218 def output(*args, **kwargs):
219 content = f(*args, **kwargs)
220 print "Content-type: %s" % content_type
221 print "Content-length: %s" % len(content)
222 print
223 print content
224 sys.exit()
225 return output
227 @output_and_exit
228 def print_template_and_exit(template, mapping):
229 f = open(template)
230 string = f.read()
231 f.close()
232 return string % mapping
234 def try_to_kill(pid, signal=15):
235 log('kill -%s %s ' % (signal, pid))
236 try:
237 os.kill(int(pid), signal)
238 except OSError, e:
239 log('PID %s seems dead (kill -%s gives %s)' % (pid, signal, e))