1#!/usr/bin/python
2##
3## license:BSD-3-Clause
4## copyright-holders:Vas Crabb
5
6from . import dbaccess
7from . import htmltmpl
8
9import cgi
10import inspect
11import json
12import mimetypes
13import os.path
14import re
15import sys
16import urllib
17import wsgiref.util
18
19if sys.version_info >= (3, ):
20    import urllib.parse as urlparse
21    urlquote = urlparse.quote
22else:
23    import urlparse
24    urlquote = urllib.quote
25
26
27class HandlerBase(object):
28    STATUS_MESSAGE = {
29            400: 'Bad Request',
30            401: 'Unauthorized',
31            403: 'Forbidden',
32            404: 'Not Found',
33            405: 'Method Not Allowed',
34            500: 'Internal Server Error',
35            501: 'Not Implemented',
36            502: 'Bad Gateway',
37            503: 'Service Unavailable',
38            504: 'Gateway Timeout',
39            505: 'HTTP Version Not Supported' }
40
41    def __init__(self, app, application_uri, environ, start_response, **kwargs):
42        super(HandlerBase, self).__init__(**kwargs)
43        self.app = app
44        self.js_escape = app.js_escape
45        self.application_uri = application_uri
46        self.environ = environ
47        self.start_response = start_response
48
49    def error_page(self, code):
50        yield htmltmpl.ERROR_PAGE.substitute(code=cgi.escape('%d' % (code, )), message=cgi.escape(self.STATUS_MESSAGE[code])).encode('utf-8')
51
52
53class ErrorPageHandler(HandlerBase):
54    def __init__(self, code, app, application_uri, environ, start_response, **kwargs):
55        super(ErrorPageHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
56        self.code = code
57        self.start_response('%d %s' % (self.code, self.STATUS_MESSAGE[code]), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
58
59    def __iter__(self):
60        return self.error_page(self.code)
61
62
63class AssetHandler(HandlerBase):
64    def __init__(self, directory, app, application_uri, environ, start_response, **kwargs):
65        super(AssetHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
66        self.directory = directory
67        self.asset = wsgiref.util.shift_path_info(environ)
68
69    def __iter__(self):
70        if not self.asset:
71            self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
72            return self.error_page(403)
73        elif self.environ['PATH_INFO']:
74            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
75            return self.error_page(404)
76        else:
77            path = os.path.join(self.directory, self.asset)
78            if not os.path.isfile(path):
79                self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
80                return self.error_page(404)
81            elif self.environ['REQUEST_METHOD'] != 'GET':
82                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
83                return self.error_page(405)
84            else:
85                try:
86                    f = open(path, 'rb')
87                    type, encoding = mimetypes.guess_type(path)
88                    self.start_response('200 OK', [('Content-type', type or 'application/octet-stream'), ('Cache-Control', 'public, max-age=3600')])
89                    return wsgiref.util.FileWrapper(f)
90                except:
91                    self.start_response('500 %s' % (self.STATUS_MESSAGE[500], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
92                    return self.error_page(500)
93
94
95class QueryPageHandler(HandlerBase):
96    def __init__(self, app, application_uri, environ, start_response, **kwargs):
97        super(QueryPageHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
98        self.dbcurs = app.dbconn.cursor()
99
100    def machine_href(self, shortname):
101        return cgi.escape(urlparse.urljoin(self.application_uri, 'machine/%s' % (urlquote(shortname), )), True)
102
103    def sourcefile_href(self, sourcefile):
104        return cgi.escape(urlparse.urljoin(self.application_uri, 'sourcefile/%s' % (urlquote(sourcefile), )), True)
105
106    def softwarelist_href(self, softwarelist):
107        return cgi.escape(urlparse.urljoin(self.application_uri, 'softwarelist/%s' % (urlquote(softwarelist), )), True)
108
109    def software_href(self, softwarelist, software):
110        return cgi.escape(urlparse.urljoin(self.application_uri, 'softwarelist/%s/%s' % (urlquote(softwarelist), urlquote(software))), True)
111
112    def bios_data(self, machine):
113        result = { }
114        for name, description, isdefault in self.dbcurs.get_biossets(machine):
115            result[name] = { 'description': description, 'isdefault': True if isdefault else False }
116        return result
117
118    def flags_data(self, machine):
119        result = { 'features': { } }
120        for feature, status, overall in self.dbcurs.get_feature_flags(machine):
121            detail = { }
122            if status == 1:
123                detail['status'] = 'imperfect'
124            elif status > 1:
125                detail['status'] = 'unemulated'
126            if overall == 1:
127                detail['overall'] = 'imperfect'
128            elif overall > 1:
129                detail['overall'] = 'unemulated'
130            result['features'][feature] = detail
131        return result
132
133    def slot_data(self, machine):
134        result = { 'defaults': { }, 'slots': { } }
135
136        # get slot options
137        prev = None
138        for slot, option, shortname, description in self.dbcurs.get_slot_options(machine):
139            if slot != prev:
140                if slot in result['slots']:
141                    options = result['slots'][slot]
142                else:
143                    options = { }
144                    result['slots'][slot] = options
145                prev = slot
146            options[option] = { 'device': shortname, 'description': description }
147
148        # if there are any slots, get defaults
149        if result['slots']:
150            for slot, default in self.dbcurs.get_slot_defaults(machine):
151                result['defaults'][slot] = default
152
153            # remove slots that come from default cards in other slots
154            for slot in tuple(result['slots'].keys()):
155                slot += ':'
156                for candidate in tuple(result['slots'].keys()):
157                    if candidate.startswith(slot):
158                        del result['slots'][candidate]
159
160        return result
161
162    def softwarelist_data(self, machine):
163        result = { }
164
165        # get software lists referenced by machine
166        for softwarelist in self.dbcurs.get_machine_softwarelists(machine):
167            result[softwarelist['tag']] = {
168                    'status':               softwarelist['status'],
169                    'shortname':            softwarelist['shortname'],
170                    'description':          softwarelist['description'],
171                    'total':                softwarelist['total'],
172                    'supported':            softwarelist['supported'],
173                    'partiallysupported':   softwarelist['partiallysupported'],
174                    'unsupported':          softwarelist['unsupported'] }
175
176        # remove software lists that come from default cards in slots
177        if result:
178            for slot, default in self.dbcurs.get_slot_defaults(machine):
179                slot += ':'
180                for candidate in tuple(result.keys()):
181                    if candidate.startswith(slot):
182                        del result[candidate]
183
184        return result
185
186
187class MachineRpcHandlerBase(QueryPageHandler):
188    def __init__(self, app, application_uri, environ, start_response, **kwargs):
189        super(MachineRpcHandlerBase, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
190        self.shortname = wsgiref.util.shift_path_info(environ)
191
192    def __iter__(self):
193        if not self.shortname:
194            self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
195            return self.error_page(403)
196        elif self.environ['PATH_INFO']:
197            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
198            return self.error_page(404)
199        else:
200            machine = self.dbcurs.get_machine_id(self.shortname)
201            if machine is None:
202                self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
203                return self.error_page(404)
204            elif self.environ['REQUEST_METHOD'] != 'GET':
205                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
206                return self.error_page(405)
207            else:
208                self.start_response('200 OK', [('Content-type', 'application/json; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
209                return self.data_page(machine)
210
211
212class MachineHandler(QueryPageHandler):
213    def __init__(self, app, application_uri, environ, start_response, **kwargs):
214        super(MachineHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
215        self.shortname = wsgiref.util.shift_path_info(environ)
216
217    def __iter__(self):
218        if not self.shortname:
219            # could probably list machines here or something
220            self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
221            return self.error_page(403)
222        elif self.environ['PATH_INFO']:
223            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
224            return self.error_page(404)
225        else:
226            machine_info = self.dbcurs.get_machine_details(self.shortname).fetchone()
227            if not machine_info:
228                self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
229                return self.error_page(404)
230            elif self.environ['REQUEST_METHOD'] != 'GET':
231                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
232                return self.error_page(405)
233            else:
234                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
235                return self.machine_page(machine_info)
236
237    def machine_page(self, machine_info):
238        id = machine_info['id']
239        description = machine_info['description']
240        yield htmltmpl.MACHINE_PROLOGUE.substitute(
241                app=self.js_escape(cgi.escape(self.application_uri, True)),
242                assets=self.js_escape(cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True)),
243                sourcehref=self.sourcefile_href(machine_info['sourcefile']),
244                description=cgi.escape(description),
245                shortname=cgi.escape(self.shortname),
246                isdevice=cgi.escape('Yes' if machine_info['isdevice'] else 'No'),
247                runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'),
248                sourcefile=cgi.escape(machine_info['sourcefile'])).encode('utf-8')
249        if machine_info['year'] is not None:
250            yield (
251                    '    <tr><th>Year:</th><td>%s</td></tr>\n' \
252                    '    <tr><th>Manufacturer:</th><td>%s</td></tr>\n' %
253                    (cgi.escape(machine_info['year']), cgi.escape(machine_info['Manufacturer']))).encode('utf-8')
254        if machine_info['cloneof'] is not None:
255            parent = self.dbcurs.listfull(machine_info['cloneof']).fetchone()
256            if parent:
257                yield (
258                        '    <tr><th>Parent machine:</th><td><a href="%s">%s (%s)</a></td></tr>\n' %
259                        (self.machine_href(machine_info['cloneof']), cgi.escape(parent[1]), cgi.escape(machine_info['cloneof']))).encode('utf-8')
260            else:
261                yield (
262                        '    <tr><th>Parent machine:</th><td><a href="%s">%s</a></td></tr>\n' %
263                        (self.machine_href(machine_info['cloneof']), cgi.escape(machine_info['cloneof']))).encode('utf-8')
264        if (machine_info['romof'] is not None) and (machine_info['romof'] != machine_info['cloneof']):
265            parent = self.dbcurs.listfull(machine_info['romof']).fetchone()
266            if parent:
267                yield (
268                        '    <tr><th>Parent ROM set:</th><td><a href="%s">%s (%s)</a></td></tr>\n' %
269                        (self.machine_href(machine_info['romof']), cgi.escape(parent[1]), cgi.escape(machine_info['romof']))).encode('utf-8')
270            else:
271                yield (
272                        '    <tr><th>Parent machine:</th><td><a href="%s">%s</a></td></tr>\n' %
273                        (self.machine_href(machine_info['romof']), cgi.escape(machine_info['romof']))).encode('utf-8')
274        unemulated = []
275        imperfect = []
276        for feature, status, overall in self.dbcurs.get_feature_flags(id):
277            if overall == 1:
278                imperfect.append(feature)
279            elif overall > 1:
280                unemulated.append(feature)
281        if (unemulated):
282            unemulated.sort()
283            yield(
284                    ('    <tr><th>Unemulated Features:</th><td>%s' + (', %s' * (len(unemulated) - 1)) + '</td></tr>\n') %
285                    tuple(unemulated)).encode('utf-8');
286        if (imperfect):
287            yield(
288                    ('    <tr><th>Imperfect Features:</th><td>%s' + (', %s' * (len(imperfect) - 1)) + '</td></tr>\n') %
289                    tuple(imperfect)).encode('utf-8');
290        yield '</table>\n'.encode('utf-8')
291
292        # make a table of clones
293        first = True
294        for clone, clonedescription, cloneyear, clonemanufacturer in self.dbcurs.get_clones(self.shortname):
295            if first:
296                yield htmltmpl.MACHINE_CLONES_PROLOGUE.substitute().encode('utf-8')
297                first = False
298            yield htmltmpl.MACHINE_CLONES_ROW.substitute(
299                    href=self.machine_href(clone),
300                    shortname=cgi.escape(clone),
301                    description=cgi.escape(clonedescription),
302                    year=cgi.escape(cloneyear or ''),
303                    manufacturer=cgi.escape(clonemanufacturer or '')).encode('utf-8')
304        if not first:
305            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-clones').encode('utf-8')
306
307        # make a table of software lists
308        yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_PROLOGUE.substitute().encode('utf-8')
309        for softwarelist in self.dbcurs.get_machine_softwarelists(id):
310            total = softwarelist['total']
311            yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_ROW.substitute(
312                    rowid=cgi.escape(softwarelist['tag'].replace(':', '-'), True),
313                    href=self.softwarelist_href(softwarelist['shortname']),
314                    shortname=cgi.escape(softwarelist['shortname']),
315                    description=cgi.escape(softwarelist['description']),
316                    status=cgi.escape(softwarelist['status']),
317                    total=cgi.escape('%d' % (total, )),
318                    supported=cgi.escape('%.1f%%' % (softwarelist['supported'] * 100.0 / (total or 1), )),
319                    partiallysupported=cgi.escape('%.1f%%' % (softwarelist['partiallysupported'] * 100.0 / (total or 1), )),
320                    unsupported=cgi.escape('%.1f%%' % (softwarelist['unsupported'] * 100.0 / (total or 1), ))).encode('utf-8')
321        yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_EPILOGUE.substitute().encode('utf-8')
322
323        # allow system BIOS selection
324        haveoptions = False
325        for name, desc, isdef in self.dbcurs.get_biossets(id):
326            if not haveoptions:
327                haveoptions = True;
328                yield htmltmpl.MACHINE_OPTIONS_HEADING.substitute().encode('utf-8')
329                yield htmltmpl.MACHINE_BIOS_PROLOGUE.substitute().encode('utf-8')
330            yield htmltmpl.MACHINE_BIOS_OPTION.substitute(
331                    name=cgi.escape(name, True),
332                    description=cgi.escape(desc),
333                    isdefault=('yes' if isdef else 'no')).encode('utf-8')
334        if haveoptions:
335            yield '</select>\n<script>set_default_system_bios();</script>\n'.encode('utf-8')
336
337        # allow RAM size selection
338        first = True
339        for name, size, isdef in self.dbcurs.get_ram_options(id):
340            if first:
341                if not haveoptions:
342                    haveoptions = True;
343                    yield htmltmpl.MACHINE_OPTIONS_HEADING.substitute().encode('utf-8')
344                yield htmltmpl.MACHINE_RAM_PROLOGUE.substitute().encode('utf-8')
345                first = False
346            yield htmltmpl.MACHINE_RAM_OPTION.substitute(
347                    name=cgi.escape(name, True),
348                    size=cgi.escape('{:,}'.format(size)),
349                    isdefault=('yes' if isdef else 'no')).encode('utf-8')
350        if not first:
351            yield '</select>\n<script>set_default_ram_option();</script>\n'.encode('utf-8')
352
353        # placeholder for machine slots - populated by client-side JavaScript
354        if self.dbcurs.count_slots(id):
355            if not haveoptions:
356                haveoptions = True
357                yield htmltmpl.MACHINE_OPTIONS_HEADING.substitute().encode('utf-8')
358            yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER_PROLOGUE.substitute().encode('utf=8')
359            pending = set((self.shortname, ))
360            added = set((self.shortname, ))
361            haveextra = set()
362            while pending:
363                requested = pending.pop()
364                slots = self.slot_data(self.dbcurs.get_machine_id(requested))
365                yield ('    slot_info[%s] = %s;\n' % (self.sanitised_json(requested), self.sanitised_json(slots))).encode('utf-8')
366                for slotname, slot in slots['slots'].items():
367                    for choice, card in slot.items():
368                        carddev = card['device']
369                        if carddev not in added:
370                            pending.add(carddev)
371                            added.add(carddev)
372                        if (carddev not in haveextra) and (slots['defaults'].get(slotname) == choice):
373                            haveextra.add(carddev)
374                            cardid = self.dbcurs.get_machine_id(carddev)
375                            carddev = self.sanitised_json(carddev)
376                            yield (
377                                    '    bios_sets[%s] = %s;\n    machine_flags[%s] = %s;\n    softwarelist_info[%s] = %s;\n' %
378                                    (carddev, self.sanitised_json(self.bios_data(cardid)), carddev, self.sanitised_json(self.flags_data(cardid)), carddev, self.sanitised_json(self.softwarelist_data(cardid)))).encode('utf-8')
379            yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER_EPILOGUE.substitute(
380                    machine=self.sanitised_json(self.shortname)).encode('utf=8')
381
382        # list devices referenced by this system/device
383        first = True
384        for name, desc, src in self.dbcurs.get_devices_referenced(id):
385            if first:
386                yield \
387                        '<h2>Devices Referenced</h2>\n' \
388                        '<table id="tbl-dev-refs">\n' \
389                        '    <thead>\n' \
390                        '        <tr><th>Short name</th><th>Description</th><th>Source file</th></tr>\n' \
391                        '    </thead>\n' \
392                        '    <tbody>\n'.encode('utf-8')
393                first = False
394            yield self.machine_row(name, desc, src)
395        if not first:
396            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-dev-refs').encode('utf-8')
397
398        # list slots where this device is an option
399        first = True
400        for name, desc, slot, opt, src in self.dbcurs.get_compatible_slots(id):
401            if (first):
402                yield \
403                        '<h2>Compatible Slots</h2>\n' \
404                        '<table id="tbl-comp-slots">\n' \
405                        '    <thead>\n' \
406                        '        <tr><th>Short name</th><th>Description</th><th>Slot</th><th>Choice</th><th>Source file</th></tr>\n' \
407                        '    </thead>\n' \
408                        '    <tbody>\n'.encode('utf-8')
409                first = False
410            yield htmltmpl.COMPATIBLE_SLOT_ROW.substitute(
411                    machinehref=self.machine_href(name),
412                    sourcehref=self.sourcefile_href(src),
413                    shortname=cgi.escape(name),
414                    description=cgi.escape(desc),
415                    sourcefile=cgi.escape(src),
416                    slot=cgi.escape(slot),
417                    slotoption=cgi.escape(opt)).encode('utf-8')
418        if not first:
419            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-comp-slots').encode('utf-8')
420
421        # list systems/devices that reference this device
422        first = True
423        for name, desc, src in self.dbcurs.get_device_references(id):
424            if first:
425                yield \
426                        '<h2>Referenced By</h2>\n' \
427                        '<table id="tbl-ref-by">\n' \
428                        '    <thead>\n' \
429                        '        <tr><th>Short name</th><th>Description</th><th>Source file</th></tr>\n' \
430                        '    </thead>\n' \
431                        '    <tbody>\n'.encode('utf-8')
432                first = False
433            yield self.machine_row(name, desc, src)
434        if not first:
435            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-ref-by').encode('utf-8')
436
437        yield '</html>\n'.encode('utf-8')
438
439    def machine_row(self, shortname, description, sourcefile):
440        return (htmltmpl.MACHINE_ROW if description is not None else htmltmpl.EXCL_MACHINE_ROW).substitute(
441                machinehref=self.machine_href(shortname),
442                sourcehref=self.sourcefile_href(sourcefile),
443                shortname=cgi.escape(shortname),
444                description=cgi.escape(description or ''),
445                sourcefile=cgi.escape(sourcefile or '')).encode('utf-8')
446
447    @staticmethod
448    def sanitised_json(data):
449        return json.dumps(data).replace('<', '\\u003c').replace('>', '\\u003e')
450
451
452class SourceFileHandler(QueryPageHandler):
453    def __init__(self, app, application_uri, environ, start_response, **kwargs):
454        super(SourceFileHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
455
456    def __iter__(self):
457        self.filename = self.environ['PATH_INFO']
458        if self.filename and (self.filename[0] == '/'):
459            self.filename = self.filename[1:]
460        if not self.filename:
461            if self.environ['REQUEST_METHOD'] != 'GET':
462                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
463                return self.error_page(405)
464            else:
465                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
466                return self.sourcefile_listing_page(None)
467        else:
468            id = self.dbcurs.get_sourcefile_id(self.filename)
469            if id is None:
470                if ('*' not in self.filename) and ('?' not in self.filename) and ('?' not in self.filename):
471                    self.filename += '*' if self.filename[-1] == '/' else '/*'
472                    if not self.dbcurs.count_sourcefiles(self.filename):
473                        self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
474                        return self.error_page(404)
475                    elif self.environ['REQUEST_METHOD'] != 'GET':
476                        self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
477                        return self.error_page(405)
478                    else:
479                        self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
480                        return self.sourcefile_listing_page(self.filename)
481                else:
482                    self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
483                    return self.error_page(404)
484            elif self.environ['REQUEST_METHOD'] != 'GET':
485                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
486                return self.error_page(405)
487            else:
488                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
489                return self.sourcefile_page(id)
490
491    def sourcefile_listing_page(self, pattern):
492        if not pattern:
493            title = heading = 'All Source Files'
494        else:
495            heading = self.linked_title(pattern)
496            title = 'Source Files: ' + cgi.escape(pattern)
497        yield htmltmpl.SOURCEFILE_LIST_PROLOGUE.substitute(
498                assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
499                title=title,
500                heading=heading).encode('utf-8')
501        for filename, machines in self.dbcurs.get_sourcefiles(pattern):
502            yield htmltmpl.SOURCEFILE_LIST_ROW.substitute(
503                    sourcefile=self.linked_title(filename, True),
504                    machines=cgi.escape('%d' % (machines, ))).encode('utf-8')
505        yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-sourcefiles').encode('utf-8')
506
507    def sourcefile_page(self, id):
508        yield htmltmpl.SOURCEFILE_PROLOGUE.substitute(
509                assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
510                filename=cgi.escape(self.filename),
511                title=self.linked_title(self.filename)).encode('utf-8')
512
513        first = True
514        for machine_info in self.dbcurs.get_sourcefile_machines(id):
515            if first:
516                yield \
517                        '<table id="tbl-machines">\n' \
518                        '    <thead>\n' \
519                        '        <tr>\n' \
520                        '            <th>Short name</th>\n' \
521                        '            <th>Description</th>\n' \
522                        '            <th>Year</th>\n' \
523                        '            <th>Manufacturer</th>\n' \
524                        '            <th>Runnable</th>\n' \
525                        '            <th>Parent</th>\n' \
526                        '        </tr>\n' \
527                        '    </thead>\n' \
528                        '    <tbody>\n'.encode('utf-8')
529                first = False
530            yield self.machine_row(machine_info)
531        if first:
532            yield '<p>No machines found.</p>\n'.encode('utf-8')
533        else:
534            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-machines').encode('utf-8')
535
536        yield '</body>\n</html>\n'.encode('utf-8')
537
538    def linked_title(self, filename, linkfinal=False):
539        parts = filename.split('/')
540        final = parts[-1]
541        del parts[-1]
542        uri = urlparse.urljoin(self.application_uri, 'sourcefile')
543        title = ''
544        for part in parts:
545            uri = urlparse.urljoin(uri + '/', urlquote(part))
546            title += '<a href="{0}">{1}</a>/'.format(cgi.escape(uri, True), cgi.escape(part))
547        if linkfinal:
548            uri = urlparse.urljoin(uri + '/', urlquote(final))
549            return title + '<a href="{0}">{1}</a>'.format(cgi.escape(uri, True), cgi.escape(final))
550        else:
551            return title + final
552
553    def machine_row(self, machine_info):
554        return (htmltmpl.SOURCEFILE_ROW_PARENT if machine_info['cloneof'] is None else htmltmpl.SOURCEFILE_ROW_CLONE).substitute(
555                machinehref=self.machine_href(machine_info['shortname']),
556                parenthref=self.machine_href(machine_info['cloneof'] or '__invalid'),
557                shortname=cgi.escape(machine_info['shortname']),
558                description=cgi.escape(machine_info['description']),
559                year=cgi.escape(machine_info['year'] or ''),
560                manufacturer=cgi.escape(machine_info['manufacturer'] or ''),
561                runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'),
562                parent=cgi.escape(machine_info['cloneof'] or '')).encode('utf-8')
563
564
565class SoftwareListHandler(QueryPageHandler):
566    def __init__(self, app, application_uri, environ, start_response, **kwargs):
567        super(SoftwareListHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
568        self.shortname = wsgiref.util.shift_path_info(environ)
569        self.software = wsgiref.util.shift_path_info(environ)
570
571    def __iter__(self):
572        if self.environ['PATH_INFO']:
573            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
574            return self.error_page(404)
575        elif self.software and ('*' not in self.software) and ('?' not in self.software):
576            software_info = self.dbcurs.get_software_details(self.shortname, self.software).fetchone()
577            if not software_info:
578                self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
579                return self.error_page(404)
580            elif self.environ['REQUEST_METHOD'] != 'GET':
581                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
582                return self.error_page(405)
583            else:
584                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
585                return self.software_page(software_info)
586        elif self.software or (self.shortname and ('*' not in self.shortname) and ('?' not in self.shortname)):
587            softwarelist_info = self.dbcurs.get_softwarelist_details(self.shortname, self.software or None).fetchone()
588            if not softwarelist_info:
589                self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
590                return self.error_page(404)
591            elif self.environ['REQUEST_METHOD'] != 'GET':
592                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
593                return self.error_page(405)
594            else:
595                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
596                return self.softwarelist_page(softwarelist_info, self.software or None)
597        else:
598            if self.environ['REQUEST_METHOD'] != 'GET':
599                self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
600                return self.error_page(405)
601            else:
602                self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
603                return self.softwarelist_listing_page(self.shortname or None)
604
605    def softwarelist_listing_page(self, pattern):
606        if not pattern:
607            title = heading = 'All Software Lists'
608        else:
609            title = heading = 'Software Lists: ' + cgi.escape(pattern)
610        yield htmltmpl.SOFTWARELIST_LIST_PROLOGUE.substitute(
611                assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
612                title=title,
613                heading=heading).encode('utf-8')
614        for shortname, description, total, supported, partiallysupported, unsupported in self.dbcurs.get_softwarelists(pattern):
615            yield htmltmpl.SOFTWARELIST_LIST_ROW.substitute(
616                    href=self.softwarelist_href(shortname),
617                    shortname=cgi.escape(shortname),
618                    description=cgi.escape(description),
619                    total=cgi.escape('%d' % (total, )),
620                    supported=cgi.escape('%.1f%%' % (supported * 100.0 / (total or 1), )),
621                    partiallysupported=cgi.escape('%.1f%%' % (partiallysupported * 100.0 / (total or 1), )),
622                    unsupported=cgi.escape('%.1f%%' % (unsupported * 100.0 / (total or 1), ))).encode('utf-8')
623        yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-softwarelists').encode('utf-8')
624
625    def softwarelist_page(self, softwarelist_info, pattern):
626        if not pattern:
627            title = 'Software List: %s (%s)' % (cgi.escape(softwarelist_info['description']), cgi.escape(softwarelist_info['shortname']))
628            heading = cgi.escape(softwarelist_info['description'])
629        else:
630            title = 'Software List: %s (%s): %s' % (cgi.escape(softwarelist_info['description']), cgi.escape(softwarelist_info['shortname']), cgi.escape(pattern))
631            heading = '<a href="%s">%s</a>: %s' % (self.softwarelist_href(softwarelist_info['shortname']), cgi.escape(softwarelist_info['description']), cgi.escape(pattern))
632        yield htmltmpl.SOFTWARELIST_PROLOGUE.substitute(
633                assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
634                title=title,
635                heading=heading,
636                shortname=cgi.escape(softwarelist_info['shortname']),
637                total=cgi.escape('%d' % (softwarelist_info['total'], )),
638                supported=cgi.escape('%d' % (softwarelist_info['supported'], )),
639                supportedpc=cgi.escape('%.1f' % (softwarelist_info['supported'] * 100.0 / (softwarelist_info['total'] or 1), )),
640                partiallysupported=cgi.escape('%d' % (softwarelist_info['partiallysupported'], )),
641                partiallysupportedpc=cgi.escape('%.1f' % (softwarelist_info['partiallysupported'] * 100.0 / (softwarelist_info['total'] or 1), )),
642                unsupported=cgi.escape('%d' % (softwarelist_info['unsupported'], )),
643                unsupportedpc=cgi.escape('%.1f' % (softwarelist_info['unsupported'] * 100.0 / (softwarelist_info['total'] or 1), ))).encode('utf-8')
644
645        first = True
646        for machine_info in self.dbcurs.get_softwarelist_machines(softwarelist_info['id']):
647            if first:
648                yield htmltmpl.SOFTWARELIST_MACHINE_TABLE_HEADER.substitute().encode('utf-8')
649                first = False
650            yield htmltmpl.SOFTWARELIST_MACHINE_TABLE_ROW.substitute(
651                    machinehref=self.machine_href(machine_info['shortname']),
652                    shortname=cgi.escape(machine_info['shortname']),
653                    description=cgi.escape(machine_info['description']),
654                    year=cgi.escape(machine_info['year'] or ''),
655                    manufacturer=cgi.escape(machine_info['manufacturer'] or ''),
656                    status=cgi.escape(machine_info['status'])).encode('utf-8')
657        if not first:
658            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-machines').encode('utf-8')
659
660        first = True
661        for software_info in self.dbcurs.get_softwarelist_software(softwarelist_info['id'], self.software or None):
662            if first:
663                yield htmltmpl.SOFTWARELIST_SOFTWARE_TABLE_HEADER.substitute().encode('utf-8')
664                first = False
665            yield self.software_row(software_info)
666        if first:
667            yield '<p>No software found.</p>\n'.encode('utf-8')
668        else:
669            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-software').encode('utf-8')
670
671        yield '</body>\n</html>\n'.encode('utf-8')
672
673    def software_page(self, software_info):
674        yield htmltmpl.SOFTWARE_PROLOGUE.substitute(
675                assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
676                title=cgi.escape(software_info['description']),
677                heading=cgi.escape(software_info['description']),
678                softwarelisthref=self.softwarelist_href(self.shortname),
679                softwarelistdescription=cgi.escape(software_info['softwarelistdescription']),
680                softwarelist=cgi.escape(self.shortname),
681                shortname=cgi.escape(software_info['shortname']),
682                year=cgi.escape(software_info['year']),
683                publisher=cgi.escape(software_info['publisher'])).encode('utf-8')
684        if software_info['parent'] is not None:
685            yield ('    <tr><th>Parent:</th><td><a href="%s">%s</a></td>\n' % (self.software_href(software_info['parentsoftwarelist'], software_info['parent']), cgi.escape(software_info['parentdescription']))).encode('utf-8')
686        yield ('    <tr><th>Supported:</th><td>%s</td>\n' % (self.format_supported(software_info['supported']), )).encode('utf-8')
687        for name, value in self.dbcurs.get_software_info(software_info['id']):
688            yield ('    <tr><th>%s:</th><td>%s</td>\n' % (cgi.escape(name), cgi.escape(value))).encode('utf-8')
689        yield '</table>\n\n'.encode('utf-8')
690
691        first = True
692        for clone_info in self.dbcurs.get_software_clones(software_info['id']):
693            if first:
694                yield htmltmpl.SOFTWARE_CLONES_PROLOGUE.substitute().encode('utf-8')
695                first = False
696            yield self.clone_row(clone_info)
697        if not first:
698            yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-clones').encode('utf-8')
699
700        parts = self.dbcurs.get_software_parts(software_info['id']).fetchall()
701        first = True
702        for id, partname, interface, part_id in parts:
703            if first:
704                yield '<h2>Parts</h2>\n'.encode('utf-8')
705                first = False
706            yield htmltmpl.SOFTWARE_PART_PROLOGUE.substitute(
707                    heading=cgi.escape(('%s (%s)' % (part_id, partname)) if part_id is not None else partname),
708                    shortname=cgi.escape(partname),
709                    interface=cgi.escape(interface)).encode('utf-8')
710            for name, value in self.dbcurs.get_softwarepart_features(id):
711                yield ('    <tr><th>%s:</th><td>%s</td>\n' % (cgi.escape(name), cgi.escape(value))).encode('utf-8')
712            yield '</table>\n\n'.encode('utf-8')
713
714        yield '</body>\n</html>\n'.encode('utf-8')
715
716    def software_row(self, software_info):
717        parent = software_info['parent']
718        return htmltmpl.SOFTWARELIST_SOFTWARE_ROW.substitute(
719                softwarehref=self.software_href(self.shortname, software_info['shortname']),
720                shortname=cgi.escape(software_info['shortname']),
721                description=cgi.escape(software_info['description']),
722                year=cgi.escape(software_info['year']),
723                publisher=cgi.escape(software_info['publisher']),
724                supported=self.format_supported(software_info['supported']),
725                parts=cgi.escape('%d' % (software_info['parts'], )),
726                baddumps=cgi.escape('%d' % (software_info['baddumps'], )),
727                parent='<a href="%s">%s</a>' % (self.software_href(software_info['parentsoftwarelist'], parent), cgi.escape(parent)) if parent is not None else '').encode('utf-8')
728
729    def clone_row(self, clone_info):
730        return htmltmpl.SOFTWARE_CLONES_ROW.substitute(
731                href=self.software_href(clone_info['softwarelist'], clone_info['shortname']),
732                shortname=cgi.escape(clone_info['shortname']),
733                description=cgi.escape(clone_info['description']),
734                year=cgi.escape(clone_info['year']),
735                publisher=cgi.escape(clone_info['publisher']),
736                supported=self.format_supported(clone_info['supported'])).encode('utf-8')
737
738    @staticmethod
739    def format_supported(supported):
740        return 'Yes' if supported == 0 else 'Partial' if supported == 1 else 'No'
741
742
743class RomIdentHandler(QueryPageHandler):
744    def __init__(self, app, application_uri, environ, start_response, **kwargs):
745        super(QueryPageHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
746        self.dbcurs = app.dbconn.cursor()
747
748    def __iter__(self):
749        if self.environ['PATH_INFO']:
750            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
751            return self.error_page(404)
752        elif self.environ['REQUEST_METHOD'] != 'GET':
753            self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
754            return self.error_page(405)
755        else:
756            self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
757            return self.form_page()
758
759    def form_page(self):
760        yield htmltmpl.ROMIDENT_PAGE.substitute(
761                app=self.js_escape(cgi.escape(self.application_uri, True)),
762                assets=self.js_escape(cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True))).encode('utf-8')
763
764
765class BiosRpcHandler(MachineRpcHandlerBase):
766    def data_page(self, machine):
767        result = { }
768        for name, description, isdefault in self.dbcurs.get_biossets(machine):
769            result[name] = { 'description': description, 'isdefault': True if isdefault else False }
770        yield json.dumps(result).encode('utf-8')
771
772
773class FlagsRpcHandler(MachineRpcHandlerBase):
774    def data_page(self, machine):
775        yield json.dumps(self.flags_data(machine)).encode('utf-8')
776
777
778class SlotsRpcHandler(MachineRpcHandlerBase):
779    def data_page(self, machine):
780        yield json.dumps(self.slot_data(machine)).encode('utf-8')
781
782
783class SoftwareListsRpcHandler(MachineRpcHandlerBase):
784    def data_page(self, machine):
785        yield json.dumps(self.softwarelist_data(machine)).encode('utf-8')
786
787
788class RomDumpsRpcHandler(QueryPageHandler):
789    def __init__(self, app, application_uri, environ, start_response, **kwargs):
790        super(RomDumpsRpcHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
791
792    def __iter__(self):
793        if self.environ['PATH_INFO']:
794            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
795            return self.error_page(404)
796        elif self.environ['REQUEST_METHOD'] != 'GET':
797            self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
798            return self.error_page(405)
799        else:
800            try:
801                args = urlparse.parse_qs(self.environ['QUERY_STRING'], keep_blank_values=True, strict_parsing=True)
802                crc = args.get('crc')
803                sha1 = args.get('sha1')
804                if (len(args) == 2) and (crc is not None) and (len(crc) == 1) and (sha1 is not None) and (len(sha1) == 1):
805                    crc = int(crc[0], 16)
806                    sha1 = sha1[0]
807                    self.start_response('200 OK', [('Content-type', 'application/json; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
808                    return self.data_page(crc, sha1)
809            except BaseException as e:
810                pass
811            self.start_response('500 %s' % (self.STATUS_MESSAGE[500], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
812            return self.error_page(500)
813
814    def data_page(self, crc, sha1):
815        machines = { }
816        for shortname, description, label, bad in self.dbcurs.get_rom_dumps(crc, sha1):
817            machine = machines.get(shortname)
818            if machine is None:
819                machine = { 'description': description, 'matches': [ ] }
820                machines[shortname] = machine
821            machine['matches'].append({ 'name': label, 'bad': bool(bad) })
822
823        software = { }
824        for softwarelist, softwarelistdescription, shortname, description, part, part_id, label, bad in self.dbcurs.get_software_rom_dumps(crc, sha1):
825            listinfo = software.get(softwarelist)
826            if listinfo is None:
827                listinfo = { 'description': softwarelistdescription, 'software': { } }
828                software[softwarelist] = listinfo
829            softwareinfo = listinfo['software'].get(shortname)
830            if softwareinfo is None:
831                softwareinfo = { 'description': description, 'parts': { } }
832                listinfo['software'][shortname] = softwareinfo
833            partinfo = softwareinfo['parts'].get(part)
834            if partinfo is None:
835                partinfo = { 'matches': [ ] }
836                if part_id is not None:
837                    partinfo['description'] = part_id
838                softwareinfo['parts'][part] = partinfo
839            partinfo['matches'].append({ 'name': label, 'bad': bool(bad) })
840
841        result = { 'machines': machines, 'software': software }
842        yield json.dumps(result).encode('utf-8')
843
844
845class DiskDumpsRpcHandler(QueryPageHandler):
846    def __init__(self, app, application_uri, environ, start_response, **kwargs):
847        super(DiskDumpsRpcHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
848
849    def __iter__(self):
850        if self.environ['PATH_INFO']:
851            self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
852            return self.error_page(404)
853        elif self.environ['REQUEST_METHOD'] != 'GET':
854            self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
855            return self.error_page(405)
856        else:
857            try:
858                args = urlparse.parse_qs(self.environ['QUERY_STRING'], keep_blank_values=True, strict_parsing=True)
859                sha1 = args.get('sha1')
860                if (len(args) == 1) and (sha1 is not None) and (len(sha1) == 1):
861                    sha1 = sha1[0]
862                    self.start_response('200 OK', [('Content-type', 'application/json; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
863                    return self.data_page(sha1)
864            except BaseException as e:
865                pass
866            self.start_response('500 %s' % (self.STATUS_MESSAGE[500], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
867            return self.error_page(500)
868
869    def data_page(self, sha1):
870        machines = { }
871        for shortname, description, label, bad in self.dbcurs.get_disk_dumps(sha1):
872            machine = machines.get(shortname)
873            if machine is None:
874                machine = { 'description': description, 'matches': [ ] }
875                machines[shortname] = machine
876            machine['matches'].append({ 'name': label, 'bad': bool(bad) })
877
878        software = { }
879        for softwarelist, softwarelistdescription, shortname, description, part, part_id, label, bad in self.dbcurs.get_software_disk_dumps(sha1):
880            listinfo = software.get(softwarelist)
881            if listinfo is None:
882                listinfo = { 'description': softwarelistdescription, 'software': { } }
883                software[softwarelist] = listinfo
884            softwareinfo = listinfo['software'].get(shortname)
885            if softwareinfo is None:
886                softwareinfo = { 'description': description, 'parts': { } }
887                listinfo['software'][shortname] = softwareinfo
888            partinfo = softwareinfo['parts'].get(part)
889            if partinfo is None:
890                partinfo = { 'matches': [ ] }
891                if part_id is not None:
892                    partinfo['description'] = part_id
893                softwareinfo['parts'][part] = partinfo
894            partinfo['matches'].append({ 'name': label, 'bad': bool(bad) })
895
896        result = { 'machines': machines, 'software': software }
897        yield json.dumps(result).encode('utf-8')
898
899
900class MiniMawsApp(object):
901    JS_ESCAPE = re.compile('([\"\'\\\\])')
902    RPC_SERVICES = {
903            'bios':             BiosRpcHandler,
904            'flags':            FlagsRpcHandler,
905            'slots':            SlotsRpcHandler,
906            'softwarelists':    SoftwareListsRpcHandler,
907            'romdumps':         RomDumpsRpcHandler,
908            'diskdumps':        DiskDumpsRpcHandler }
909
910    def __init__(self, dbfile, **kwargs):
911        super(MiniMawsApp, self).__init__(**kwargs)
912        self.dbconn = dbaccess.QueryConnection(dbfile)
913        self.assetsdir = os.path.join(os.path.dirname(inspect.getfile(self.__class__)), 'assets')
914        if not mimetypes.inited:
915            mimetypes.init()
916
917    def __call__(self, environ, start_response):
918        application_uri = wsgiref.util.application_uri(environ)
919        if application_uri[-1] != '/':
920            application_uri += '/'
921        module = wsgiref.util.shift_path_info(environ)
922        if module == 'machine':
923            return MachineHandler(self, application_uri, environ, start_response)
924        elif module == 'sourcefile':
925            return SourceFileHandler(self, application_uri, environ, start_response)
926        elif module == 'softwarelist':
927            return SoftwareListHandler(self, application_uri, environ, start_response)
928        elif module == 'romident':
929            return RomIdentHandler(self, application_uri, environ, start_response)
930        elif module == 'static':
931            return AssetHandler(self.assetsdir, self, application_uri, environ, start_response)
932        elif module == 'rpc':
933            service = wsgiref.util.shift_path_info(environ)
934            if not service:
935                return ErrorPageHandler(403, self, application_uri, environ, start_response)
936            elif service in self.RPC_SERVICES:
937                return self.RPC_SERVICES[service](self, application_uri, environ, start_response)
938            else:
939                return ErrorPageHandler(404, self, application_uri, environ, start_response)
940        elif not module:
941            return ErrorPageHandler(403, self, application_uri, environ, start_response)
942        else:
943            return ErrorPageHandler(404, self, application_uri, environ, start_response)
944
945    def js_escape(self, str):
946        return self.JS_ESCAPE.sub('\\\\\\1', str).replace('\0', '\\0')
947