#!/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 <http://www.gnu.org/licenses/>. import sys import argparse import html import tempfile import os.path import threading import time import codecs import mimetypes import re import markdown import gettext import glob import traceback from urllib.parse import unquote_plus, quote from urllib.parse 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 = """<tr><td>%s</td><td>%%s</td><td>%s</td></tr>\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 = """<textarea name="%s" cols="%s" rows="%s">%s</textarea>""" % \ (name, max((len(l) for l in val))+10, len(val)+1, default or a.default) elif a.choices: options = "\n".join( ("""<option value="%s"%s>%s</option>""" % (e, ' selected="selected"' if e == (default or a.default) else "", _(e)) for e in a.choices)) input = """<select name="%s" size="1">\n%s</select>\n""" % (name, options) else: input = """<input name="%s" type="text" value="%s">""" % \ (name, default or a.default) return row % input scripts = """ <script> function showHide(id) { var e = document.getElementById(id); var h = document.getElementById("h-" + id); if(e.style.display == null || e.style.display == "none") { e.style.display = "block"; h.classList.add("open"); } else { e.style.display = "none"; h.classList.remove("open"); } } function hideargs() { for ( i=0; i<%i; i++) { showHide(i); } } </script> """ 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 = ["""<!DOCTYPE html> <html> <head> <title>""" + _("Boxes - %s") % _(name), """</title> <link rel="icon" type="image/svg+xml" href="static/boxes-logo.svg" sizes="any"> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico"> <link rel="stylesheet" href="static/self.css" type="text/css" /> """, self.scripts % (len(box.argparser._action_groups)-3), """ <meta name="flattr:id" content="456799"> </head> <body onload="hideargs()"> <div class="container" style="background-color: #FFF8EA;"> <div style="float: left;"> <a href="./""" + langparam + '"><h1>' + _("Boxes.py") + """</h1></a> </div> <div style="width: 120px; float: right;"> <img alt="self-Logo" src="static/boxes-logo.svg" width="120" > </div> <div> <div class="clear"></div> <hr /> <h2 style="margin: 0px 0px 0px 20px;" >""", _(name), """</h2> <p>""", _(box.__doc__) if box.__doc__ else "", """</p> <form action="%s" method="GET"> """ % (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('''<h3 id="h-%s" class="open" onclick="showHide(%s)">%s</h3>\n<table id="%s">\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("</table>") groupid += 1 result.append(""" <p> <button name="render" value="1" formtarget="_blank">""" + _("Generate") + """</button> <button name="render" value="0" formtarget="_self">""" + _("Save to URL") + """</button> </p> </form> </div> <!-- <div style="width: 5%; float: left;"></div> <div style="width: 35%; float: left;"> <img alt="sample" src="examples/box.svg" width="300" > <span id="sicherheitshinweise">hier kommt dann der AJAX-Inhalt</span> </div> --> <div class="clear"></div> <hr /> """) no_img_msg = _('There is no image yet. Please donate an image of your project on <a href="https://github.com/florianfesti/boxes/issues/140">GitHub</a>!') result.append(f'''<div> <img src="static/samples/{box.__class__.__name__}.jpg" width="100%" onerror="this.parentElement.innerHTML = '{no_img_msg}';"/> </div> ''') if box.description: result.append(markdown.markdown(_(box.description))) result.append(""" </div> """ + self.footer(lang) + """</body> </html> """ ) 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 = ["""<!DOCTYPE html> <html> <head> <title>""" + _("Boxes.py") + """</title> <link rel="icon" type="image/svg+xml" href="static/boxes-logo.svg" sizes="any"> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico"> <link rel="stylesheet" href="static/self.css" type="text/css" /> <script> function change(group, img_link){ document.getElementById("sample-"+group).src = img_link; document.getElementById("sample-"+group).style.height = "auto"; } function changeback(group){ document.getElementById("sample-" + group).src= "static/nothing.png"; document.getElementById("sample-" + group).style.height= "0px"; } </script>""", self.scripts % len(self.groups), """ <meta name="flattr:id" content="456799"> </head> <body onload="hideargs()"> <div class="container" style="background-color: #FFF8EA;"> <div style="width: 75%; float: left;"> <h1>""" + _("Boxes.py") + """</h1> <p> """ + _("Create boxes and more with a laser cutter!") + """ </p> <p> """ + _(""" <a href="https://hackaday.io/project/10649-boxespy">Boxes.py</a> is an <a href="https://www.gnu.org/licenses/gpl-3.0.en.html">Open Source</a> box generator written in <a href="https://www.python.org/">Python</a>. 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.""") + """ </p> </div> <div style="width: 25%; float: left;"> <img alt="self-Logo" src="static/boxes-logo.svg" width="250" > </div> <div> <div class="clear"></div> <hr /> <div style="width: 100%"> """ ] for nr, group in enumerate(self.groups): result.append('''<h3 id="h-%s" class="open" onclick="showHide('%s')">%s</h3>\n<div id="%s">\n''' % (nr, nr, _(group.title), nr)) result.append(""" <img style="width: 200px;" id="sample-%s" src="static/nothing.png" alt="" /> <ul>\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(""" <li onmouseenter="change('%s', 'static/samples/%s-thumb.jpg')" onmouseleave="changeback('%s')"><a href="%s%s">%s</a>%s</li>\n""" % ( group.name, name, group.name, name, langparam, _(name), docs)) result.append("</ul>\n</div>\n") result.append(""" </div> <div style="width: 5%; float: left;"></div> <div class="clear"></div> <hr /> </div> </div>""" + self.footer(lang) + """ </body> </html> """) return (s.encode("utf-8") for s in result) def footer(self, lang): _ = lang.gettext language = lang.info().get('language', '') return """ <div class="footer container"> <ul> <li><form><select name="language" onchange='if(this.value != "%s") { this.form.submit(); }'>""" % language + \ ("<option value='' selected></option>" if not language else "") + \ "\n".join( ("<option value='%s' %s>%s</option>" % (l, "selected" if l==language else "", l) for l in self.getLanguages())) + """ </select></form></li> <li><a href="https://github.com/florianfesti/boxes">""" + _("Get Source at GitHub") + """</a></li> <li><a href="https://florianfesti.github.io/boxes/html/index.html">""" + _("Documentation and API Description") + """</a></li> <li><a href="https://hackaday.io/project/10649-boxespy">""" + _("Hackaday.io Project Page") + """</a></li> </ul> </div> """ def errorMessage(self, name, e, _): return [ ("""<html> <head> <title>""" + _("Error generating %s") % _(name) + """</title> <meta name="flattr:id" content="456799"> </head> <body> <h1>""" + _("An error occurred!") + "</h1>" + "".join(u"<p>%s</p>" % html.escape(s) for s in type(u"")(e).split(u"\n")) + """ </body> </html> """).encode("utf-8") ] def serveStatic(self, environ, start_response): filename = environ["PATH_INFO"][len("/static/"):] path = os.path.join(self.staticdir, filename) if (not re.match(r"[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+", filename) or not os.path.exists(path)): if re.match(r"samples/.*-thumb.jpg", filename): path = os.path.join(self.staticdir, "nothing.png") else: 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] = html.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: if not isinstance(e, ValueError): print("Exception during rendering:") traceback.print_exc() start_response("500 Internal Server Error", headers) return self.errorMessage(name, e, _) 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, 'rb').readlines() os.close(fd) os.remove(box.output) return (l for l in result) 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