#!/usr/bin/env python3 # Copyright (C) 2016-2017 Florian Festi # # 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 3 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, see . import sys import argparse import cgi import tempfile import os.path import threading import time import codecs import mimetypes import re import markdown import gettext import glob import traceback # Python 2 vs Python 3 compat try: from urllib.parse import unquote_plus, quote except ImportError: from urllib import unquote_plus, quote try: from urllib.parse import parse_qs except ImportError: from cgi import parse_qs from wsgiref.util import setup_testing_defaults from wsgiref.simple_server import make_server import wsgiref.util try: import boxes.generators except ImportError: sys.path.append(os.path.join(os.path.dirname(__file__), "..")) import boxes.generators class FileChecker(threading.Thread): def __init__(self, files=[], checkmodules=True): super(FileChecker, self).__init__() self.checkmodules = checkmodules self.timestamps = {} for path in files: self.timestamps[path] = os.stat(path).st_mtime if checkmodules: self._addModules() def _addModules(self): for name, module in sys.modules.items(): path = getattr(module, "__file__", None) if not path: continue if path not in self.timestamps: self.timestamps[path] = os.stat(path).st_mtime def filesOK(self): if self.checkmodules: self._addModules() for path, timestamp in self.timestamps.items(): try: if os.stat(path).st_mtime != timestamp: return False except FileNotFoundError: return False return True def run(self): while True: if not self.filesOK(): os.execv(__file__, sys.argv) time.sleep(1) class ArgumentParserError(Exception): pass class ThrowingArgumentParser(argparse.ArgumentParser): def error(self, message): raise ArgumentParserError(message) boxes.ArgumentParser = ThrowingArgumentParser # Evil hack class BServer: lang_re = re.compile(r"([a-z]{2,3}(-[-a-zA-Z0-9]*)?)\s*(;\s*q=(\d\.?\d*))?") def __init__(self): self.boxes = {b.__name__ : b for b in boxes.generators.getAllBoxGenerators().values() if b.webinterface} self.boxes['TrayLayout2'] = boxes.generators.traylayout.TrayLayout2 self.groups = boxes.generators.ui_groups self.groups_by_name = boxes.generators.ui_groups_by_name for name, box in self.boxes.items(): self.groups_by_name.get(box.ui_group, self.groups_by_name["Misc"]).add(box) self.staticdir = os.path.join(os.path.dirname(__file__), '../static/') self._languages = None def getLanguages(self, domain=None, localedir=None): if self._languages is not None: return self._languages self._languages = [] domain = "boxes.py" for localedir in ["locale", gettext._default_localedir]: files = glob.glob(os.path.join(localedir, '*', 'LC_MESSAGES', '%s.mo' % domain)) self._languages.extend([file.split(os.path.sep)[-3] for file in files]) self._languages.sort() return self._languages def getLanguage(self, args, accept_language): lang = None langs = [] for i, arg in enumerate(args): if arg.startswith("language="): lang = arg[len("language="):] del args[i] break if lang: try: return gettext.translation('boxes.py', localedir='locale', languages=[lang]) except OSError: pass try: return gettext.translation('boxes.py', languages=[lang]) except OSError: pass # selected language not found try browser default languages = accept_language.split(",") for l in languages: m = self.lang_re.match(l.strip()) if m: langs.append((float(m.group(4) or 1.0), m.group(1))) langs.sort(reverse=True) langs = [l[1].replace("-", "_") for l in langs] try: return gettext.translation('boxes.py', localedir='locale', languages=langs) except OSError: return gettext.translation('boxes.py', languages=langs, fallback=True) def arg2html(self, a, prefix, defaults={}, _=lambda s:s): name = a.option_strings[0].replace("-", "") if isinstance(a, argparse._HelpAction): return "" viewname = name if prefix and name.startswith(prefix + '_'): viewname = name[len(prefix)+1:] default = defaults.get(name, None) row = """%s%%s%s\n""" % \ (_(viewname), "" if not a.help else _(a.help)) if (isinstance(a, argparse._StoreAction) and hasattr(a.type, "html")): input = a.type.html(name, default or a.default) elif a.dest == "layout": val = (default or a.default).split("\n") input = """""" % \ (name, max((len(l) for l in val))+10, len(val)+1, default or a.default) elif a.choices: options = "\n".join( ("""""" % (e, ' selected="selected"' if e == (default or a.default) else "", _(e)) for e in a.choices)) input = """\n""" % (name, options) else: input = """""" % \ (name, default or a.default) return row % input scripts = """ """ def args2html(self, name, box, lang, action="", defaults={}): _ = lang.gettext lang_name = lang.info().get('language', None) if lang_name: langparam = "?language=" + lang_name else: langparam = "" result = [""" """ + _("Boxes - %s") % _(name), """ """, self.scripts % (len(box.argparser._action_groups)-3), """

' + _("Boxes.py") + """

self-Logo

""", _(name), """

""", _(box.__doc__) if box.__doc__ else "", """

""" % (action)] groupid = 0 for group in box.argparser._action_groups[3:] + box.argparser._action_groups[:3]: if not group._group_actions: continue if len(group._group_actions) == 1 and isinstance(group._group_actions[0], argparse._HelpAction): continue prefix = getattr(group, "prefix", None) result.append('''

%s

\n\n''' % (groupid, groupid, _(group.title), groupid)) for a in group._group_actions: if a.dest in ("input", "output"): continue result.append(self.arg2html(a, prefix, defaults, _)) result.append("
") groupid += 1 result.append("""


""") if box.description: result.append(markdown.markdown(_(box.description))) result.append("""
""" + self.footer(lang) + """ """ ) return (s.encode("utf-8") for s in result) def menu(self, lang): _ = lang.gettext lang_name = lang.info().get('language', None) if lang_name: langparam = "?language=" + lang_name else: langparam = "" result = [""" """ + _("Boxes.py") + """ """, self.scripts % len(self.groups), """

""" + _("Boxes.py") + """

""" + _("Create boxes and more with a laser cutter!") + """

""" + _(""" Boxes.py is an Open Source box generator written in Python. It features both finished parametrized generators as well as a Python API for writing your own. It features finger and (flat) dovetail joints, flex cuts, holes and slots for screws, hinges, gears, pulleys and much more.""") + """

self-Logo

""" ] for nr, group in enumerate(self.groups): result.append('''

%s

\n
\n''' % (nr, nr, _(group.title), nr)) result.append("""
    \n""" % (group.name)) for box in group.generators: name = box.__name__ if name in ("TrayLayout2", ): continue docs = "" if box.__doc__: docs = " - " + _(box.__doc__) result.append("""
  • %s%s
  • \n""" % ( group.name, name, group.name, name, langparam, _(name), docs)) result.append("
\n
\n") result.append("""

""" + self.footer(lang) + """ """) return (s.encode("utf-8") for s in result) def footer(self, lang): _ = lang.gettext language = lang.info().get('language', '') return """ """ def errorMessage(self, name, e, _): return [ (""" """ + _("Error generating %s") % _(name) + """

""" + _("An error occurred!") + "

" + "".join(u"

%s

" % cgi.escape(s) for s in type(u"")(e).split(u"\n")) + """ """).encode("utf-8") ] def serveStatic(self, environ, start_response): filename = environ["PATH_INFO"][len("/static/"):] path = os.path.join(self.staticdir, filename) print(filename, path) if (not re.match(r"[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+", filename) or not os.path.exists(path)): start_response("404 Not Found", [('Content-type', 'text/plain')]) return [b"Not found"] type_, encoding = mimetypes.guess_type(filename) if encoding is None: encoding = "utf8" start_response("200 OK", [('Content-type', "%s; charset=%s" % (type_, encoding))]) f = open(path, 'rb') return environ['wsgi.file_wrapper'](f, 512*1024) def getURL(self, environ): url = environ['wsgi.url_scheme']+'://' if environ.get('HTTP_HOST'): url += environ['HTTP_HOST'] else: url += environ['SERVER_NAME'] if environ['wsgi.url_scheme'] == 'https': if environ['SERVER_PORT'] != '443': url += ':' + environ['SERVER_PORT'] else: if environ['SERVER_PORT'] != '80': url += ':' + environ['SERVER_PORT'] url += quote(environ.get('SCRIPT_NAME', '')) url += quote(environ.get('PATH_INFO', '')) if environ.get('QUERY_STRING'): url += '?' + environ['QUERY_STRING'] return url def serve(self, environ, start_response): if environ["PATH_INFO"].startswith("/static/"): return self.serveStatic(environ, start_response) status = '200 OK' headers = [('Content-type', 'text/html; charset=utf-8'), ('X-XSS-Protection', '1; mode=block'), ('X-Content-Type-Options', 'nosniff'), ('x-frame-options', 'SAMEORIGIN'), ('Referrer-Policy', 'no-referrer')] d = parse_qs(environ.get('QUERY_STRING', '')) name = environ["PATH_INFO"][1:] args = [unquote_plus(arg) for arg in environ.get('QUERY_STRING', '').split("&")] lang = self.getLanguage(args, environ.get("HTTP_ACCEPT_LANGUAGE", "")) _ = lang.gettext box_cls = self.boxes.get(name, None) if not box_cls: start_response(status, headers) return self.menu(lang) if name == "TrayLayout2": box = box_cls(self, webargs=True) else: box = box_cls() if "render=1" not in args: defaults = { } for a in args: kv = a.split('=') if len(kv) == 2: k, v = kv defaults[k] = cgi.escape(v, True) start_response(status, headers) return self.args2html(name, box, lang, "./" + name, defaults=defaults) else: args = ["--"+ arg for arg in args if arg != "render=1"] try: box.parseArgs(args) except (ArgumentParserError) as e: start_response(status, headers) return self.errorMessage(name, e, _) if name == "TrayLayout": start_response(status, headers) box.fillDefault(box.x, box.y) layout2 = boxes.generators.traylayout.TrayLayout2(self, webargs=True) layout2.argparser.set_defaults(layout=str(box)) return self.args2html( name, layout2, lang, action="TrayLayout2") if name == "TrayLayout2": try: box.parse(box.layout.split("\n")) except Exception as e: start_response(status, headers) return self.errorMessage(name, e) try: fd, box.output = tempfile.mkstemp() box.metadata["url"] = self.getURL(environ) box.open() box.render() box.close() except Exception as e: traceback.print_exc() start_response("500 Internal Server Error", [('Content-type', 'text/plain; charset=utf-8')]) return([b"Server Error\n\n", str(e).encode("utf-8")]) http_headers = box.formats.http_headers.get( box.format, [('Content-type', 'application/unknown; charset=utf-8')])[:] if box.format != "svg": extension = box.format if extension == "svg_Ponoko": extension = "svg" http_headers.append(('Content-Disposition', 'attachment; filename="%s.%s"' % (box.__class__.__name__, extension))) start_response(status, http_headers) result = open(box.output).readlines() os.close(fd) os.remove(box.output) return (l.encode("utf-8") for l in result) if __name__=="__main__": if len(sys.argv) > 1: port = int(sys.argv[1]) else: port = 8000 fc = FileChecker() fc.start() boxserver = BServer() httpd = make_server('', port, boxserver.serve) print("BoxesServer serving on port %s..." %port) httpd.serve_forever() else: application = BServer().serve