Merge branch 'maintenance' into devel
# Conflicts: # src/octoprint/server/__init__.py # src/octoprint/util/jinja.py
This commit is contained in:
commit
64d484bd09
18 changed files with 3080 additions and 1187 deletions
|
|
@ -795,7 +795,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
else:
|
||||
result["current"] = VERSION
|
||||
|
||||
if check["type"] == "github_release" and (check["prerelease"] or BRANCH != stable_branch):
|
||||
if check["type"] == "github_release" and (check.get("prerelease", None) or BRANCH != stable_branch):
|
||||
# we are tracking github releases and are either also tracking prerelease OR are currently installed
|
||||
# from something that is not the stable (master) branch => we need to change some parameters
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
import uuid
|
||||
from sockjs.tornado import SockJSRouter
|
||||
from flask import Flask, g, request, session, Blueprint
|
||||
from flask import Flask, g, request, session, Blueprint, Request, Response
|
||||
from flask.ext.login import LoginManager, current_user
|
||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||
from flask.ext.babel import Babel, gettext, ngettext
|
||||
|
|
@ -169,7 +169,7 @@ class Server(object):
|
|||
util.flask.enable_additional_translations(additional_folders=[self._settings.getBaseFolder("translations")])
|
||||
|
||||
# setup app
|
||||
self._setup_app()
|
||||
self._setup_app(app)
|
||||
|
||||
# setup i18n
|
||||
self._setup_i18n(app)
|
||||
|
|
@ -235,6 +235,7 @@ class Server(object):
|
|||
components.update(dict(printer=printer))
|
||||
|
||||
def octoprint_plugin_inject_factory(name, implementation):
|
||||
"""Factory for injections for all OctoPrintPlugins"""
|
||||
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
|
||||
return None
|
||||
props = dict()
|
||||
|
|
@ -245,6 +246,7 @@ class Server(object):
|
|||
return props
|
||||
|
||||
def settings_plugin_inject_factory(name, implementation):
|
||||
"""Factory for additional injections depending on plugin type"""
|
||||
plugin_settings = octoprint.plugin.plugin_settings_for_settings_plugin(name, implementation)
|
||||
if plugin_settings is None:
|
||||
return
|
||||
|
|
@ -252,6 +254,8 @@ class Server(object):
|
|||
return dict(settings=plugin_settings)
|
||||
|
||||
def settings_plugin_config_migration_and_cleanup(name, implementation):
|
||||
"""Take care of migrating and cleaning up any old settings"""
|
||||
|
||||
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
|
||||
return
|
||||
|
||||
|
|
@ -293,6 +297,8 @@ class Server(object):
|
|||
|
||||
# setup jinja2
|
||||
self._setup_jinja2()
|
||||
|
||||
# make sure plugin lifecycle events relevant for jinja2 are taken care of
|
||||
def template_enabled(name, plugin):
|
||||
if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin):
|
||||
return
|
||||
|
|
@ -315,25 +321,6 @@ class Server(object):
|
|||
if self._debug:
|
||||
events.DebugEventListener()
|
||||
|
||||
app.wsgi_app = util.ReverseProxied(
|
||||
app.wsgi_app,
|
||||
self._settings.get(["server", "reverseProxy", "prefixHeader"]),
|
||||
self._settings.get(["server", "reverseProxy", "schemeHeader"]),
|
||||
self._settings.get(["server", "reverseProxy", "hostHeader"]),
|
||||
self._settings.get(["server", "reverseProxy", "prefixFallback"]),
|
||||
self._settings.get(["server", "reverseProxy", "schemeFallback"]),
|
||||
self._settings.get(["server", "reverseProxy", "hostFallback"])
|
||||
)
|
||||
|
||||
secret_key = self._settings.get(["server", "secretKey"])
|
||||
if not secret_key:
|
||||
import string
|
||||
from random import choice
|
||||
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
|
||||
secret_key = "".join(choice(chars) for _ in range(32))
|
||||
self._settings.set(["server", "secretKey"], secret_key)
|
||||
self._settings.save()
|
||||
app.secret_key = secret_key
|
||||
loginManager = LoginManager()
|
||||
loginManager.session_protection = "strong"
|
||||
loginManager.user_callback = load_user
|
||||
|
|
@ -342,18 +329,16 @@ class Server(object):
|
|||
principals.identity_loaders.appendleft(users.dummy_identity_loader)
|
||||
loginManager.init_app(app)
|
||||
|
||||
if self._host is None:
|
||||
self._host = self._settings.get(["server", "host"])
|
||||
if self._port is None:
|
||||
self._port = self._settings.getInt(["server", "port"])
|
||||
|
||||
app.debug = self._debug
|
||||
|
||||
# register API blueprint
|
||||
self._setup_blueprints()
|
||||
|
||||
## Tornado initialization starts here
|
||||
|
||||
if self._host is None:
|
||||
self._host = self._settings.get(["server", "host"])
|
||||
if self._port is None:
|
||||
self._port = self._settings.getInt(["server", "port"])
|
||||
|
||||
ioloop = IOLoop()
|
||||
ioloop.install()
|
||||
|
||||
|
|
@ -370,7 +355,7 @@ class Server(object):
|
|||
allow_client_caching=False
|
||||
)
|
||||
additional_mime_types=dict(mime_type_guesser=mime_type_guesser)
|
||||
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))
|
||||
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))
|
||||
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), status_code=404))
|
||||
|
||||
def joined_dict(*dicts):
|
||||
|
|
@ -407,6 +392,8 @@ class Server(object):
|
|||
(r"/online.gif", util.tornado.StaticDataHandler, dict(data=bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")),
|
||||
content_type="image/gif"))
|
||||
]
|
||||
|
||||
# fetch additional routes from plugins
|
||||
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
|
||||
try:
|
||||
result = hook(list(server_routes))
|
||||
|
|
@ -460,10 +447,13 @@ class Server(object):
|
|||
|
||||
self._stop_intermediary_server()
|
||||
|
||||
# initialize and bind the server
|
||||
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=self._settings.getInt(["server", "maxSize"]))
|
||||
self._server.listen(self._port, address=self._host)
|
||||
|
||||
eventManager.fire(events.Events.STARTUP)
|
||||
|
||||
# auto connect
|
||||
if self._settings.getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = self._settings.get(["serial", "port"]), self._settings.getInt(["serial", "baudrate"])
|
||||
printer_profile = printerProfileManager.get_default()
|
||||
|
|
@ -559,7 +549,8 @@ class Server(object):
|
|||
|
||||
def _create_socket_connection(self, session):
|
||||
global printer, fileManager, analysisQueue, userManager, eventManager
|
||||
return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager, eventManager, pluginManager, session)
|
||||
return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager,
|
||||
eventManager, pluginManager, session)
|
||||
|
||||
def _check_for_root(self):
|
||||
if "geteuid" in dir(os) and os.geteuid() == 0:
|
||||
|
|
@ -586,7 +577,41 @@ class Server(object):
|
|||
|
||||
return Locale.parse(request.accept_languages.best_match(LANGUAGES))
|
||||
|
||||
def _setup_app(self):
|
||||
def _setup_app(self, app):
|
||||
from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse
|
||||
|
||||
s = settings()
|
||||
|
||||
app.debug = self._debug
|
||||
|
||||
secret_key = s.get(["server", "secretKey"])
|
||||
if not secret_key:
|
||||
import string
|
||||
from random import choice
|
||||
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
|
||||
secret_key = "".join(choice(chars) for _ in range(32))
|
||||
s.set(["server", "secretKey"], secret_key)
|
||||
s.save()
|
||||
|
||||
app.secret_key = secret_key
|
||||
|
||||
reverse_proxied = ReverseProxiedEnvironment(
|
||||
header_prefix=s.get(["server", "reverseProxy", "prefixHeader"]),
|
||||
header_scheme=s.get(["server", "reverseProxy", "schemeHeader"]),
|
||||
header_host=s.get(["server", "reverseProxy", "hostHeader"]),
|
||||
header_server=s.get(["server", "reverseProxy", "serverHeader"]),
|
||||
header_port=s.get(["server", "reverseProxy", "portHeader"]),
|
||||
prefix=s.get(["server", "reverseProxy", "prefixFallback"]),
|
||||
scheme=s.get(["server", "reverseProxy", "schemeFallback"]),
|
||||
host=s.get(["server", "reverseProxy", "hostFallback"]),
|
||||
server=s.get(["server", "reverseProxy", "serverFallback"]),
|
||||
port=s.get(["server", "reverseProxy", "portFallback"])
|
||||
)
|
||||
|
||||
OctoPrintFlaskRequest.environment_wrapper = reverse_proxied
|
||||
app.request_class = OctoPrintFlaskRequest
|
||||
app.response_class = OctoPrintFlaskResponse
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.locale = self._get_locale()
|
||||
|
|
|
|||
|
|
@ -155,97 +155,3 @@ def get_plugin_hash():
|
|||
plugin_hash = hashlib.sha1()
|
||||
plugin_hash.update(",".join(ui_plugins))
|
||||
return plugin_hash.hexdigest()
|
||||
|
||||
|
||||
#~~ reverse proxy compatible WSGI middleware
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""
|
||||
Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
this to a URL other than / and to an HTTP scheme that is
|
||||
different than what is used locally.
|
||||
|
||||
In nginx:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
location /myprefix {
|
||||
proxy_pass http://192.168.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Script-Name /myprefix;
|
||||
}
|
||||
|
||||
Alternatively define prefix and scheme via config.yaml:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
server:
|
||||
baseUrl: /myprefix
|
||||
scheme: http
|
||||
|
||||
:param app: the WSGI application
|
||||
:param header_script_name: the HTTP header in the wsgi environment from which to determine the prefix
|
||||
:param header_scheme: the HTTP header in the wsgi environment from which to determine the scheme
|
||||
:param header_host: the HTTP header in the wsgi environment from which to determine the host for which to generate external URLs
|
||||
:param base_url: the prefix to use as fallback if headers are not set
|
||||
:param scheme: the scheme to use as fallback if headers are not set
|
||||
:param host: the host to use as fallback if headers are not set
|
||||
"""
|
||||
|
||||
def __init__(self, app, header_prefix="x-script-name", header_scheme="x-scheme", header_host="x-forwarded-host", base_url="", scheme="", host=""):
|
||||
self.app = app
|
||||
|
||||
# headers for prefix & scheme & host, converted to conform to WSGI format
|
||||
to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
|
||||
self._header_prefix = to_wsgi_format(header_prefix)
|
||||
self._header_scheme = to_wsgi_format(header_scheme)
|
||||
self._header_host = to_wsgi_format(header_host)
|
||||
|
||||
# fallback prefix & scheme & host from config
|
||||
self._fallback_prefix = base_url
|
||||
self._fallback_scheme = scheme
|
||||
self._fallback_host = host
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# determine prefix
|
||||
prefix = environ.get(self._header_prefix, "")
|
||||
if not prefix:
|
||||
prefix = self._fallback_prefix
|
||||
|
||||
# rewrite SCRIPT_NAME and if necessary also PATH_INFO based on prefix
|
||||
if prefix:
|
||||
environ["SCRIPT_NAME"] = prefix
|
||||
path_info = environ["PATH_INFO"]
|
||||
if path_info.startswith(prefix):
|
||||
environ["PATH_INFO"] = path_info[len(prefix):]
|
||||
|
||||
# determine scheme
|
||||
scheme = environ.get(self._header_scheme, "")
|
||||
if scheme and "," in scheme:
|
||||
# Scheme might be something like "https,https" if doubly-reverse-proxied
|
||||
# without stripping original scheme header first, make sure to only use
|
||||
# the first entry in such a case. See #1391.
|
||||
scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
|
||||
if not scheme:
|
||||
scheme = self._fallback_scheme
|
||||
|
||||
# rewrite wsgi.url_scheme based on scheme
|
||||
if scheme:
|
||||
environ["wsgi.url_scheme"] = scheme
|
||||
|
||||
# determine host
|
||||
host = environ.get(self._header_host, "")
|
||||
if not host:
|
||||
host = self._fallback_host
|
||||
|
||||
# rewrite host header based on host
|
||||
if host:
|
||||
environ["HTTP_HOST"] = host
|
||||
|
||||
# call wrapped app with rewritten environment
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
|
|
|||
|
|
@ -219,6 +219,193 @@ def fix_webassets_filtertool():
|
|||
|
||||
FilterTool._wrap_cache = fixed_wrap_cache
|
||||
|
||||
#~~ WSGI environment wrapper for reverse proxying
|
||||
|
||||
class ReverseProxiedEnvironment(object):
|
||||
|
||||
@staticmethod
|
||||
def to_header_candidates(values):
|
||||
if values is None:
|
||||
return []
|
||||
if not isinstance(values, (list, tuple)):
|
||||
values = [values]
|
||||
to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
|
||||
return map(to_wsgi_format, values)
|
||||
|
||||
def __init__(self,
|
||||
header_prefix=None,
|
||||
header_scheme=None,
|
||||
header_host=None,
|
||||
header_server=None,
|
||||
header_port=None,
|
||||
prefix=None,
|
||||
scheme=None,
|
||||
host=None,
|
||||
server=None,
|
||||
port=None):
|
||||
|
||||
# sensible defaults
|
||||
if header_prefix is None:
|
||||
header_prefix = ["x-script-name"]
|
||||
if header_scheme is None:
|
||||
header_scheme = ["x-forwarded-proto", "x-scheme"]
|
||||
if header_host is None:
|
||||
header_host = ["x-forwarded-host"]
|
||||
if header_server is None:
|
||||
header_server = ["x-forwarded-server"]
|
||||
if header_port is None:
|
||||
header_port = ["x-forwarded-port"]
|
||||
|
||||
# header candidates
|
||||
self._headers_prefix = self.to_header_candidates(header_prefix)
|
||||
self._headers_scheme = self.to_header_candidates(header_scheme)
|
||||
self._headers_host = self.to_header_candidates(header_host)
|
||||
self._headers_server = self.to_header_candidates(header_server)
|
||||
self._headers_port = self.to_header_candidates(header_port)
|
||||
|
||||
# fallback prefix & scheme & host from config
|
||||
self._fallback_prefix = prefix
|
||||
self._fallback_scheme = scheme
|
||||
self._fallback_host = host
|
||||
self._fallback_server = server
|
||||
self._fallback_port = port
|
||||
|
||||
def __call__(self, environ):
|
||||
def retrieve_header(header_type):
|
||||
candidates = getattr(self, "_headers_" + header_type, [])
|
||||
fallback = getattr(self, "_fallback_" + header_type, None)
|
||||
|
||||
for candidate in candidates:
|
||||
value = environ.get(candidate, None)
|
||||
if value is not None:
|
||||
return value
|
||||
else:
|
||||
return fallback
|
||||
|
||||
def host_to_server_and_port(host, scheme):
|
||||
if host is None:
|
||||
return None, None
|
||||
|
||||
if ":" in host:
|
||||
server, port = host.split(":", 1)
|
||||
else:
|
||||
server = host
|
||||
port = "443" if scheme == "https" else "80"
|
||||
|
||||
return server, port
|
||||
|
||||
# determine prefix
|
||||
prefix = retrieve_header("prefix")
|
||||
if prefix is not None:
|
||||
environ["SCRIPT_NAME"] = prefix
|
||||
path_info = environ["PATH_INFO"]
|
||||
if path_info.startswith(prefix):
|
||||
environ["PATH_INFO"] = path_info[len(prefix):]
|
||||
|
||||
# determine scheme
|
||||
scheme = retrieve_header("scheme")
|
||||
if scheme is not None and "," in scheme:
|
||||
# Scheme might be something like "https,https" if doubly-reverse-proxied
|
||||
# without stripping original scheme header first, make sure to only use
|
||||
# the first entry in such a case. See #1391.
|
||||
scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
|
||||
if scheme is not None:
|
||||
environ["wsgi.url_scheme"] = scheme
|
||||
|
||||
# determine host
|
||||
url_scheme = environ["wsgi.url_scheme"]
|
||||
host = retrieve_header("host")
|
||||
if host is not None:
|
||||
# if we have a host, we take server_name and server_port from it
|
||||
server, port = host_to_server_and_port(host, url_scheme)
|
||||
environ["HTTP_HOST"] = host
|
||||
environ["SERVER_NAME"] = server
|
||||
environ["SERVER_PORT"] = port
|
||||
else:
|
||||
# else we take a look at the server and port headers and if we have
|
||||
# something there we derive the host from it
|
||||
|
||||
# determine server - should usually not be used
|
||||
server = retrieve_header("server")
|
||||
if server is not None:
|
||||
environ["SERVER_NAME"] = server
|
||||
|
||||
# determine port - should usually not be used
|
||||
port = retrieve_header("port")
|
||||
if port is not None:
|
||||
environ["SERVER_PORT"] = port
|
||||
|
||||
# make sure HTTP_HOST matches SERVER_NAME and SERVER_PORT
|
||||
expected_server, expected_port = host_to_server_and_port(environ.get("HTTP_HOST", None), url_scheme)
|
||||
if expected_server != environ["SERVER_NAME"] or expected_port != environ["SERVER_PORT"]:
|
||||
# there's a difference, fix it!
|
||||
if url_scheme == "http" and environ["SERVER_PORT"] == "80" or url_scheme == "https" and environ["SERVER_PORT"] == "443":
|
||||
# default port for scheme, can be skipped
|
||||
environ["HTTP_HOST"] = environ["SERVER_NAME"]
|
||||
else:
|
||||
environ["HTTP_HOST"] = environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]
|
||||
|
||||
# call wrapped app with rewritten environment
|
||||
return environ
|
||||
|
||||
#~~ request and response versions
|
||||
|
||||
from werkzeug.wrappers import cached_property
|
||||
|
||||
class OctoPrintFlaskRequest(flask.Request):
|
||||
environment_wrapper = staticmethod(lambda x: x)
|
||||
|
||||
def __init__(self, environ, *args, **kwargs):
|
||||
# apply environment wrapper to provided WSGI environment
|
||||
flask.Request.__init__(self, self.environment_wrapper(environ), *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def cookies(self):
|
||||
# strip cookie_suffix from all cookies in the request, return result
|
||||
cookies = flask.Request.cookies.__get__(self)
|
||||
|
||||
def cookie_name_converter(key):
|
||||
return key[:-len(self.cookie_suffix)] if key.endswith(self.cookie_suffix) else key
|
||||
|
||||
return dict((cookie_name_converter(key), value) for key, value in cookies.items())
|
||||
|
||||
@cached_property
|
||||
def server_name(self):
|
||||
"""Short cut to the request's server name header"""
|
||||
return self.environ.get("SERVER_NAME")
|
||||
|
||||
@cached_property
|
||||
def server_port(self):
|
||||
"""Short cut to the request's server port header"""
|
||||
return self.environ.get("SERVER_PORT")
|
||||
|
||||
@cached_property
|
||||
def cookie_suffix(self):
|
||||
"""
|
||||
Request specific suffix for set and read cookies
|
||||
|
||||
We need this because cookies are not port-specific and we don't want to overwrite our
|
||||
session and other cookies from one OctoPrint instance on our machine with those of another
|
||||
one who happens to listen on the same address albeit a different port.
|
||||
"""
|
||||
return "_P" + self.server_port
|
||||
|
||||
|
||||
class OctoPrintFlaskResponse(flask.Response):
|
||||
def set_cookie(self, key, *args, **kwargs):
|
||||
# restrict cookie path to script root
|
||||
kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")
|
||||
|
||||
# add request specific cookie suffix to name
|
||||
flask.Response.set_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)
|
||||
|
||||
def delete_cookie(self, key, *args, **kwargs):
|
||||
# restrict cookie path to script root
|
||||
kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")
|
||||
|
||||
# add request specific cookie suffix to name
|
||||
flask.Response.delete_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)
|
||||
|
||||
#~~ passive login helper
|
||||
|
||||
def passive_login():
|
||||
|
|
|
|||
|
|
@ -104,18 +104,22 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
true
|
||||
------WebKitFormBoundarypYiSUx63abAmhT5C
|
||||
Content-Disposition: form-data; name="file.path"
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
/tmp/tmpzupkro
|
||||
------WebKitFormBoundarypYiSUx63abAmhT5C
|
||||
Content-Disposition: form-data; name="file.name"
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
test.gcode
|
||||
------WebKitFormBoundarypYiSUx63abAmhT5C
|
||||
Content-Disposition: form-data; name="file.content_type"
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
application/octet-stream
|
||||
------WebKitFormBoundarypYiSUx63abAmhT5C
|
||||
Content-Disposition: form-data; name="file.size"
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
349182
|
||||
------WebKitFormBoundarypYiSUx63abAmhT5C--
|
||||
|
|
@ -272,9 +276,19 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
header = header[header_check:]
|
||||
|
||||
# convert to dict
|
||||
header = tornado.httputil.HTTPHeaders.parse(header.decode("utf-8"))
|
||||
try:
|
||||
header = tornado.httputil.HTTPHeaders.parse(header.decode("utf-8"))
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
header = tornado.httputil.HTTPHeaders.parse(header.decode("iso-8859-1"))
|
||||
except:
|
||||
# looks like we couldn't decode something here neither as UTF-8 nor ISO-8859-1
|
||||
self._logger.warn("Could not decode multipart headers in request, should be either UTF-8 or ISO-8859-1")
|
||||
self.send_error(400)
|
||||
return
|
||||
|
||||
disp_header = header.get("Content-Disposition", "")
|
||||
disposition, disp_params = tornado.httputil._parse_header(disp_header)
|
||||
disposition, disp_params = _parse_header(disp_header, strip_quotes=False)
|
||||
|
||||
if disposition != "form-data":
|
||||
self._logger.warn("Got a multipart header without form-data content disposition, ignoring that one")
|
||||
|
|
@ -283,7 +297,22 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
self._logger.warn("Got a multipart header without name, ignoring that one")
|
||||
return
|
||||
|
||||
self._current_part = self._on_part_start(disp_params["name"], header.get("Content-Type", None), filename=disp_params["filename"] if "filename" in disp_params else None)
|
||||
filename = disp_params.get("filename*", None) # RFC 5987 header present?
|
||||
if filename is not None:
|
||||
try:
|
||||
filename = _extended_header_value(filename)
|
||||
except:
|
||||
# parse error, this is not RFC 5987 compliant after all
|
||||
self._logger.warn("extended filename* value {!r} is not RFC 5987 compliant")
|
||||
self.send_error(400)
|
||||
return
|
||||
else:
|
||||
# no filename* header, just strip quotes from filename header then and be done
|
||||
filename = _strip_value_quotes(disp_params.get("filename", None))
|
||||
|
||||
self._current_part = self._on_part_start(_strip_value_quotes(disp_params["name"]),
|
||||
header.get("Content-Type", None),
|
||||
filename=filename)
|
||||
|
||||
def _on_part_start(self, name, content_type, filename=None):
|
||||
"""
|
||||
|
|
@ -376,6 +405,7 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
key = name + "." + n
|
||||
self._new_body += b"--%s\r\n" % self._multipart_boundary
|
||||
self._new_body += b"Content-Disposition: form-data; name=\"%s\"\r\n" % key
|
||||
self._new_body += b"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
self._new_body += b"\r\n"
|
||||
self._new_body += b"%s\r\n" % p
|
||||
elif "data" in part:
|
||||
|
|
@ -430,6 +460,47 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
options = _handle_method
|
||||
|
||||
|
||||
def _parse_header(line, strip_quotes=True):
|
||||
parts = tornado.httputil._parseparam(';' + line)
|
||||
key = next(parts)
|
||||
pdict = {}
|
||||
for p in parts:
|
||||
i = p.find('=')
|
||||
if i >= 0:
|
||||
name = p[:i].strip().lower()
|
||||
value = p[i + 1:].strip()
|
||||
if strip_quotes:
|
||||
value = _strip_value_quotes(value)
|
||||
pdict[name] = value
|
||||
return key, pdict
|
||||
|
||||
|
||||
def _strip_value_quotes(value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
value = value[1:-1]
|
||||
value = value.replace('\\\\', '\\').replace('\\"', '"')
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _extended_header_value(value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
if value.lower().startswith("iso-8859-1'") or value.lower().startswith("utf-8'"):
|
||||
# RFC 5987 section 3.2
|
||||
from urllib import unquote
|
||||
encoding, _, value = value.split("'", 2)
|
||||
return unquote(value).decode(encoding)
|
||||
|
||||
else:
|
||||
# no encoding provided, strip potentially present quotes and call it a day
|
||||
return _strip_value_quotes(value)
|
||||
|
||||
|
||||
class WsgiInputContainer(object):
|
||||
"""
|
||||
A WSGI container for use with Tornado that allows supplying the request body to be used for ``wsgi.input`` in the
|
||||
|
|
|
|||
|
|
@ -119,12 +119,16 @@ default_settings = {
|
|||
"seenWizards": {},
|
||||
"secretKey": None,
|
||||
"reverseProxy": {
|
||||
"prefixHeader": "X-Script-Name",
|
||||
"schemeHeader": "X-Scheme",
|
||||
"hostHeader": "X-Forwarded-Host",
|
||||
"prefixFallback": "",
|
||||
"schemeFallback": "",
|
||||
"hostFallback": ""
|
||||
"prefixHeader": None,
|
||||
"schemeHeader": None,
|
||||
"hostHeader": None,
|
||||
"serverHeader": None,
|
||||
"portHeader": None,
|
||||
"prefixFallback": None,
|
||||
"schemeFallback": None,
|
||||
"hostFallback": None,
|
||||
"serverFallback": None,
|
||||
"portFallback": None
|
||||
},
|
||||
"uploads": {
|
||||
"maxSize": 1 * 1024 * 1024 * 1024, # 1GB
|
||||
|
|
|
|||
|
|
@ -606,12 +606,12 @@ $(function() {
|
|||
}
|
||||
}
|
||||
}
|
||||
output += gettext("Estimated Print Time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
|
||||
output += gettext("Estimated print time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
|
||||
}
|
||||
if (data["prints"] && data["prints"]["last"]) {
|
||||
output += gettext("Last Printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
|
||||
output += gettext("Last printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
|
||||
if (data["prints"]["last"]["lastPrintTime"]) {
|
||||
output += gettext("Last Print Time") + ": " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
|
||||
output += gettext("Last print time") + ": " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
|
|
|
|||
|
|
@ -473,17 +473,17 @@ $(function() {
|
|||
var output = [];
|
||||
output.push(gettext("Layer number") + ": " + (layer.number + 1));
|
||||
output.push(gettext("Layer height") + " (mm): " + layer.height);
|
||||
output.push(gettext("GCODE commands in layer") + ": " + layer.commands);
|
||||
output.push(gettext("GCODE commands") + ": " + layer.commands);
|
||||
if (layer.filament != undefined) {
|
||||
if (layer.filament.length == 1) {
|
||||
output.push(gettext("Filament used by layer") + ": " + layer.filament[0].toFixed(2) + "mm");
|
||||
output.push(gettext("Filament") + ": " + layer.filament[0].toFixed(2) + "mm");
|
||||
} else {
|
||||
for (var i = 0; i < layer.filament.length; i++) {
|
||||
output.push(gettext("Filament used by layer") + " (" + gettext("Tool") + " " + i + "): " + layer.filament[i].toFixed(2) + "mm");
|
||||
output.push(gettext("Filament") + " (" + gettext("Tool") + " " + i + "): " + layer.filament[i].toFixed(2) + "mm");
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push(gettext("Print time for layer") + ": " + formatFuzzyPrintTime(layer.printTime));
|
||||
output.push(gettext("Estimated print time") + ": " + formatDuration(layer.printTime));
|
||||
|
||||
self.ui_layerInfo(output.join("<br>"));
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<h1>{{ _('Model info') }}</h1>
|
||||
<p data-bind="html: ui_modelInfo"></p>
|
||||
|
||||
<h1>Layer info</h1>
|
||||
<h1>{{ _('Layer info') }}</h1>
|
||||
<p data-bind="html: ui_layerInfo"></p>
|
||||
</div>
|
||||
<div class="span5">
|
||||
|
|
@ -73,7 +73,13 @@
|
|||
</div>
|
||||
|
||||
<div class="muted">
|
||||
<small>{{ _('Note that the time estimates in this tab are calculated by the GCODE viewer in your browser and might differ from the values calculated by the server that are displayed in the "State" and "Files" panels in the sidebar due to slightly different implementations.') }}</small>
|
||||
<small>{% trans %}
|
||||
Note that the time and usage values in this tab are <strong>estimated</strong> by the GCODE viewer in your
|
||||
browser and might differ from the values <strong>estimated</strong> by the server that are displayed in the
|
||||
"State" and "Files" panels in the sidebar due to slightly different implementations. Also note that these
|
||||
<strong>estimated</strong> values may be inaccurate since they can also take information present in the
|
||||
GCODE file into account.
|
||||
{% endtrans %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: waitForApproval">
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
11
tests/server/__init__.py
Normal file
11
tests/server/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.server``.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
10
tests/server/util/__init__.py
Normal file
10
tests/server/util/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.server.util``.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
411
tests/server/util/flask.py
Normal file
411
tests/server/util/flask.py
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.server.util.flask``.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import unittest
|
||||
import mock
|
||||
from ddt import ddt, data, unpack
|
||||
|
||||
from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse
|
||||
|
||||
standard_environ = {
|
||||
"HTTP_HOST": "localhost:5000",
|
||||
"SERVER_NAME": "localhost",
|
||||
"SERVER_PORT": "5000",
|
||||
"SCRIPT_NAME": "",
|
||||
"PATH_INFO": "/",
|
||||
"wsgi.url_scheme": "http"
|
||||
}
|
||||
|
||||
@ddt
|
||||
class ReverseProxiedEnvironmentTest(unittest.TestCase):
|
||||
|
||||
@data(
|
||||
# defaults
|
||||
({},
|
||||
{}),
|
||||
|
||||
# prefix set, path info not prefixed
|
||||
({
|
||||
"HTTP_X_SCRIPT_NAME": "/octoprint",
|
||||
"PATH_INFO": "/static/online.gif"
|
||||
}, {
|
||||
"SCRIPT_NAME": "/octoprint"
|
||||
}),
|
||||
|
||||
# prefix set, path info prefixed
|
||||
({
|
||||
"HTTP_X_SCRIPT_NAME": "/octoprint",
|
||||
"PATH_INFO": "/octoprint/static/online.gif",
|
||||
}, {
|
||||
"SCRIPT_NAME": "/octoprint",
|
||||
"PATH_INFO": "/static/online.gif"
|
||||
}),
|
||||
|
||||
# host set
|
||||
({
|
||||
"HTTP_X_FORWARDED_HOST": "example.com"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "80"
|
||||
}),
|
||||
|
||||
# host set with port
|
||||
({
|
||||
"HTTP_X_FORWARDED_HOST": "example.com:1234"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com:1234",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "1234"
|
||||
}),
|
||||
|
||||
# host and scheme set
|
||||
({
|
||||
"HTTP_X_FORWARDED_HOST": "example.com",
|
||||
"HTTP_X_FORWARDED_PROTO": "https"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "443",
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# host and scheme 2 set
|
||||
({
|
||||
"HTTP_X_FORWARDED_HOST": "example.com",
|
||||
"HTTP_X_SCHEME": "https"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "443",
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# host, server and port headers set -> only host wins
|
||||
({
|
||||
"HTTP_X_FORWARDED_HOST": "example.com",
|
||||
"HTTP_X_FORWARDED_SERVER": "example2.com",
|
||||
"HTTP_X_FORWARDED_PORT": "444",
|
||||
"HTTP_X_FORWARDED_PROTO": "https"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "443",
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# server and port headers set -> host derived with port
|
||||
({
|
||||
"HTTP_X_FORWARDED_SERVER": "example2.com",
|
||||
"HTTP_X_FORWARDED_PORT": "444",
|
||||
"HTTP_X_FORWARDED_PROTO": "https"
|
||||
}, {
|
||||
"HTTP_HOST": "example2.com:444",
|
||||
"SERVER_NAME": "example2.com",
|
||||
"SERVER_PORT": "444",
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# server and port headers set, standard port -> host derived, no port
|
||||
({
|
||||
"HTTP_X_FORWARDED_SERVER": "example.com",
|
||||
"HTTP_X_FORWARDED_PORT": "80",
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "80",
|
||||
}),
|
||||
|
||||
# multiple scheme entries -> only use first one
|
||||
({
|
||||
"HTTP_X_FORWARDED_PROTO": "https,http",
|
||||
}, {
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# host = none -> should never happen but you never know...
|
||||
({
|
||||
"HTTP_HOST": None,
|
||||
"HTTP_X_FORWARDED_SERVER": "example.com",
|
||||
"HTTP_X_FORWARDED_PORT": "80"
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "80"
|
||||
})
|
||||
)
|
||||
@unpack
|
||||
def test_stock(self, environ, expected):
|
||||
reverse_proxied = ReverseProxiedEnvironment()
|
||||
|
||||
merged_environ = dict(standard_environ)
|
||||
merged_environ.update(environ)
|
||||
|
||||
actual = reverse_proxied(merged_environ)
|
||||
|
||||
merged_expected = dict(standard_environ)
|
||||
merged_expected.update(environ)
|
||||
merged_expected.update(expected)
|
||||
|
||||
self.assertDictEqual(merged_expected, actual)
|
||||
|
||||
@data(
|
||||
# prefix overridden
|
||||
({
|
||||
"prefix": "fallback_prefix"
|
||||
}, {
|
||||
}, {
|
||||
"SCRIPT_NAME": "fallback_prefix",
|
||||
}),
|
||||
|
||||
# scheme overridden
|
||||
({
|
||||
"scheme": "https"
|
||||
}, {
|
||||
}, {
|
||||
"wsgi.url_scheme": "https"
|
||||
}),
|
||||
|
||||
# host overridden, default port
|
||||
({
|
||||
"host": "example.com"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "80"
|
||||
}),
|
||||
|
||||
# host overridden, included port
|
||||
({
|
||||
"host": "example.com:81"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "example.com:81",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "81"
|
||||
}),
|
||||
|
||||
# server overridden
|
||||
({
|
||||
"server": "example.com"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "example.com:5000",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "5000"
|
||||
}),
|
||||
|
||||
# port overridden, standard port
|
||||
({
|
||||
"port": "80"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "localhost",
|
||||
"SERVER_PORT": "80"
|
||||
}),
|
||||
|
||||
# port overridden, non standard port
|
||||
({
|
||||
"port": "81"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "localhost:81",
|
||||
"SERVER_PORT": "81"
|
||||
}),
|
||||
|
||||
# server and port overridden, default port
|
||||
({
|
||||
"server": "example.com",
|
||||
"port": "80"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "80"
|
||||
}),
|
||||
|
||||
# server and port overridden, non default port
|
||||
({
|
||||
"server": "example.com",
|
||||
"port": "81"
|
||||
}, {
|
||||
}, {
|
||||
"HTTP_HOST": "example.com:81",
|
||||
"SERVER_NAME": "example.com",
|
||||
"SERVER_PORT": "81"
|
||||
}),
|
||||
|
||||
# prefix not really overridden
|
||||
({
|
||||
"prefix": "/octoprint"
|
||||
}, {
|
||||
"HTTP_X_SCRIPT_NAME": ""
|
||||
}, {
|
||||
}),
|
||||
|
||||
# scheme not really overridden
|
||||
({
|
||||
"scheme": "https"
|
||||
}, {
|
||||
"HTTP_X_FORWARDED_PROTO": "http"
|
||||
}, {
|
||||
}),
|
||||
|
||||
# scheme 2 not really overridden
|
||||
({
|
||||
"scheme": "https"
|
||||
}, {
|
||||
"HTTP_X_SCHEME": "http"
|
||||
}, {
|
||||
}),
|
||||
|
||||
# host not really overridden
|
||||
({
|
||||
"host": "example.com:444"
|
||||
}, {
|
||||
"HTTP_X_FORWARDED_HOST": "localhost:5000"
|
||||
}, {
|
||||
}),
|
||||
|
||||
# server not really overridden
|
||||
({
|
||||
"server": "example.com"
|
||||
}, {
|
||||
"HTTP_X_FORWARDED_SERVER": "localhost"
|
||||
}, {
|
||||
}),
|
||||
|
||||
# port not really overridden
|
||||
({
|
||||
"port": "444"
|
||||
}, {
|
||||
"HTTP_X_FORWARDED_PORT": "5000"
|
||||
}, {
|
||||
})
|
||||
)
|
||||
@unpack
|
||||
def test_fallbacks(self, fallbacks, environ, expected):
|
||||
reverse_proxied = ReverseProxiedEnvironment(**fallbacks)
|
||||
|
||||
merged_environ = dict(standard_environ)
|
||||
merged_environ.update(environ)
|
||||
|
||||
actual = reverse_proxied(merged_environ)
|
||||
|
||||
merged_expected = dict(standard_environ)
|
||||
merged_expected.update(environ)
|
||||
merged_expected.update(expected)
|
||||
|
||||
self.assertDictEqual(merged_expected, actual)
|
||||
|
||||
def test_header_config_ok(self):
|
||||
result = ReverseProxiedEnvironment.to_header_candidates(["prefix-header1", "prefix-header2"])
|
||||
self.assertEquals(result, ["HTTP_PREFIX_HEADER1", "HTTP_PREFIX_HEADER2"])
|
||||
|
||||
def test_header_config_string(self):
|
||||
result = ReverseProxiedEnvironment.to_header_candidates("prefix-header")
|
||||
self.assertEquals(result, ["HTTP_PREFIX_HEADER"])
|
||||
|
||||
def test_header_config_none(self):
|
||||
result = ReverseProxiedEnvironment.to_header_candidates(None)
|
||||
self.assertEquals(result, [])
|
||||
|
||||
##~~
|
||||
|
||||
class OctoPrintFlaskRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.orig_environment_wrapper = OctoPrintFlaskRequest.environment_wrapper
|
||||
|
||||
def tearDown(self):
|
||||
OctoPrintFlaskRequest.environment_wrapper = staticmethod(self.orig_environment_wrapper)
|
||||
|
||||
def test_environment_wrapper(self):
|
||||
def environment_wrapper(environ):
|
||||
environ.update({
|
||||
"TEST": "yes"
|
||||
})
|
||||
return environ
|
||||
|
||||
OctoPrintFlaskRequest.environment_wrapper = staticmethod(environment_wrapper)
|
||||
request = OctoPrintFlaskRequest(standard_environ)
|
||||
|
||||
self.assertTrue("TEST" in request.environ)
|
||||
|
||||
def test_server_name(self):
|
||||
request = OctoPrintFlaskRequest(standard_environ)
|
||||
self.assertEquals(request.server_name, "localhost")
|
||||
|
||||
def test_server_port(self):
|
||||
request = OctoPrintFlaskRequest(standard_environ)
|
||||
self.assertEquals(request.server_port, "5000")
|
||||
|
||||
def test_cookie_suffix(self):
|
||||
request = OctoPrintFlaskRequest(standard_environ)
|
||||
self.assertEquals(request.cookie_suffix, "_P5000")
|
||||
|
||||
def test_cookies(self):
|
||||
environ = dict(standard_environ)
|
||||
environ["HTTP_COOKIE"] = "postfixed_P5000=postfixed_value; " \
|
||||
"postfixed_wrong_P5001=postfixed_wrong_value; " \
|
||||
"unpostfixed=unpostfixed_value"
|
||||
|
||||
request = OctoPrintFlaskRequest(environ)
|
||||
|
||||
cookies = request.cookies
|
||||
self.assertDictEqual(cookies, {"postfixed": "postfixed_value",
|
||||
"postfixed_wrong_P5001": "postfixed_wrong_value",
|
||||
"unpostfixed": "unpostfixed_value"})
|
||||
|
||||
##~~
|
||||
|
||||
@ddt
|
||||
class OctoPrintFlaskResponseTest(unittest.TestCase):
|
||||
|
||||
@data([None, None],
|
||||
["/subfolder/", None],
|
||||
[None, "/some/other/script/root"],
|
||||
["/subfolder/", "/some/other/script/root"])
|
||||
@unpack
|
||||
def test_cookie_set_and_delete(self, path, scriptroot):
|
||||
environ = dict(standard_environ)
|
||||
|
||||
if scriptroot is not None:
|
||||
environ.update(dict(SCRIPT_NAME=scriptroot))
|
||||
|
||||
request = OctoPrintFlaskRequest(environ)
|
||||
|
||||
if path:
|
||||
expected_path = path
|
||||
else:
|
||||
expected_path = "/"
|
||||
if scriptroot:
|
||||
expected_path = scriptroot + expected_path
|
||||
|
||||
if path is not None:
|
||||
kwargs = dict(path=path)
|
||||
else:
|
||||
kwargs = dict()
|
||||
|
||||
with mock.patch("flask.request", new=request):
|
||||
response = OctoPrintFlaskResponse()
|
||||
|
||||
# test set_cookie
|
||||
with mock.patch("flask.Response.set_cookie") as set_cookie_mock:
|
||||
response.set_cookie("some_key", "some_value", **kwargs)
|
||||
set_cookie_mock.assert_called_once_with(response, "some_key_P5000", "some_value", path=expected_path)
|
||||
|
||||
# test delete_cookie
|
||||
with mock.patch("flask.Response.delete_cookie") as delete_cookie_mock:
|
||||
response.delete_cookie("some_key", "some_value", **kwargs)
|
||||
delete_cookie_mock.assert_called_once_with(response, "some_key_P5000", "some_value", path=expected_path)
|
||||
90
tests/server/util/tornado.py
Normal file
90
tests/server/util/tornado.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.server.util.tornado``.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import unittest
|
||||
import mock
|
||||
from ddt import ddt, data, unpack
|
||||
|
||||
|
||||
##~~ _parse_header
|
||||
|
||||
@ddt
|
||||
class ParseHeaderTest(unittest.TestCase):
|
||||
|
||||
@data(
|
||||
("form-data; filename=test.gco", "form-data", dict(filename="test.gco")),
|
||||
("form-data; filename=\"test.gco\"", "form-data", dict(filename="test.gco")),
|
||||
("form-data; filename=test\\\\.gco", "form-data", dict(filename="test\\\\.gco")),
|
||||
("form-data; filename=\"test\\\\.gco\"", "form-data", dict(filename="test\\.gco"))
|
||||
)
|
||||
@unpack
|
||||
def test_parse_header_strip_quotes(self, value, expected_key, expected_dict):
|
||||
from octoprint.server.util.tornado import _parse_header
|
||||
actual_key, actual_dict = _parse_header(value)
|
||||
|
||||
self.assertEqual(expected_key, actual_key)
|
||||
self.assertDictEqual(expected_dict, actual_dict)
|
||||
|
||||
@data(
|
||||
("form-data; filename=test.gco", "form-data", dict(filename="test.gco")),
|
||||
("form-data; filename=\"test.gco\"", "form-data", dict(filename="\"test.gco\"")),
|
||||
("form-data; filename=test\\\\.gco", "form-data", dict(filename="test\\\\.gco")),
|
||||
("form-data; filename=\"test\\\\.gco\"", "form-data", dict(filename="\"test\\\\.gco\"")),
|
||||
("form-data; filename=iso-8859-1'en'test.gco", "form-data", dict(filename="iso-8859-1'en'test.gco"))
|
||||
)
|
||||
@unpack
|
||||
def test_parse_header_leave_quotes(self, value, expected_key, expected_dict):
|
||||
from octoprint.server.util.tornado import _parse_header
|
||||
actual_key, actual_dict = _parse_header(value, strip_quotes=False)
|
||||
|
||||
self.assertEqual(expected_key, actual_key)
|
||||
self.assertDictEqual(expected_dict, actual_dict)
|
||||
|
||||
|
||||
##~~ _strip_value_quotes
|
||||
|
||||
@ddt
|
||||
class StripValueQuotesTest(unittest.TestCase):
|
||||
|
||||
@data(
|
||||
("", ""),
|
||||
(None, None),
|
||||
('"test.gco"', "test.gco"),
|
||||
('"test".gco', '"test".gco'),
|
||||
("test\\\\.gco", "test\\\\.gco"),
|
||||
('"test\\\\.gco"', "test\\.gco")
|
||||
)
|
||||
@unpack
|
||||
def test_strip_value_quotes(self, value, expected):
|
||||
from octoprint.server.util.tornado import _strip_value_quotes
|
||||
actual = _strip_value_quotes(value)
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
##~~ _extended_header_value
|
||||
|
||||
@ddt
|
||||
class ExtendedHeaderValueTest(unittest.TestCase):
|
||||
|
||||
@data(
|
||||
("", u""),
|
||||
(None, None),
|
||||
('"quoted-string"', u"quoted-string"),
|
||||
("iso-8859-1'en'%A3%20rates", u"£ rates"),
|
||||
("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", u"£ and € rates")
|
||||
)
|
||||
@unpack
|
||||
def test_extended_header_value(self, value, expected):
|
||||
from octoprint.server.util.tornado import _extended_header_value
|
||||
actual = _extended_header_value(value)
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -6,9 +6,9 @@
|
|||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OctoPrint 1.2.14.dev70+gf671006\n"
|
||||
"Project-Id-Version: OctoPrint 1.2.16.dev36+g2256a1e\n"
|
||||
"Report-Msgid-Bugs-To: i18n@octoprint.org\n"
|
||||
"POT-Creation-Date: 2016-07-28 11:50+0200\n"
|
||||
"POT-Creation-Date: 2016-09-07 12:06+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -655,24 +655,24 @@ msgstr ""
|
|||
|
||||
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:239
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2:26
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:101
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:107
|
||||
#: src/octoprint/templates/dialogs/confirmation.jinja2:11
|
||||
#: src/octoprint/templates/dialogs/slicing.jinja2:50
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:24
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:25
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:240
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:102
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:108
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/__init__.py:394
|
||||
#: src/octoprint/plugins/softwareupdate/__init__.py:445
|
||||
msgid "Software Update"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/__init__.py:700
|
||||
#: src/octoprint/server/views.py:165
|
||||
#: src/octoprint/plugins/softwareupdate/__init__.py:752
|
||||
#: src/octoprint/server/views.py:217
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:11
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:13
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:18
|
||||
|
|
@ -681,150 +681,150 @@ msgstr ""
|
|||
msgid "OctoPrint"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:29
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:117
|
||||
msgid "Release"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:30
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:118
|
||||
msgid "Commit"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:132
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:152
|
||||
#, python-format
|
||||
msgid "%(name)s: %(version)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:135
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:155
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:165
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:185
|
||||
msgid "There are updates available for the following components:"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:173
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:193
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2:14
|
||||
msgid "Release Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:179
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:199
|
||||
msgid ""
|
||||
"Those components marked with <i class=\"icon-ok\"></i> can be updated "
|
||||
"directly."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:184
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:204
|
||||
msgid "Update Available"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:195
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:215
|
||||
msgid "Ignore"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:199
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:219
|
||||
msgid ""
|
||||
"You can make this message display again via \"Settings\" > \"Software "
|
||||
"Update\" > \"Check for update now\""
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:203
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:223
|
||||
msgid "Update now"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:220
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:240
|
||||
msgid "Everything is up-to-date"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:285
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:305
|
||||
msgid "Updating..."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:286
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:306
|
||||
msgid "Now updating, please wait."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:312
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:332
|
||||
msgid "Update not started!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:313
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:333
|
||||
msgid ""
|
||||
"The update could not be started. Is it already active? Please consult the"
|
||||
" log for details."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:333
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:353
|
||||
msgid "Can't update while printing"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:334
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:354
|
||||
msgid ""
|
||||
"A print job is currently in progress. Updating will be prevented until it"
|
||||
" is done."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:387
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:407
|
||||
#, python-format
|
||||
msgid "Now updating %(name)s to %(version)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:395
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:415
|
||||
msgid "Update successful, restarting!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:396
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:416
|
||||
msgid "The update finished successfully and the server will now be restarted."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:407
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:449
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:427
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:469
|
||||
msgid "Restart failed"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:408
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:450
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:428
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:470
|
||||
msgid ""
|
||||
"The server apparently did not restart by itself, you'll have to do it "
|
||||
"manually. Please consult the log file on what went wrong."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:424
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:444
|
||||
msgid "The update finished successfully, please restart OctoPrint now."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:426
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:446
|
||||
msgid "The update finished successfully, please reboot the server now."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:430
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:450
|
||||
msgid "Update successful, restart required!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:443
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:463
|
||||
msgid ""
|
||||
"Restarting OctoPrint failed, please restart it manually. You might also "
|
||||
"want to consult the log file on what went wrong here."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:445
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:465
|
||||
msgid ""
|
||||
"Rebooting the server failed, please reboot it manually. You might also "
|
||||
"want to consult the log file on what went wrong here."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:463
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:483
|
||||
msgid "Update successful!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:464
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:484
|
||||
msgid "The update finished successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:476
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:496
|
||||
msgid "Update failed!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:477
|
||||
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:497
|
||||
msgid ""
|
||||
"The update did not finish successfully. Please consult the log for "
|
||||
"details."
|
||||
|
|
@ -932,116 +932,121 @@ msgid "OctoPrint version tracking"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:90
|
||||
msgid "OctoPrint Release Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:96
|
||||
msgid "Version cache TTL"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:73
|
||||
#: src/octoprint/server/views.py:125
|
||||
msgid "Plugins"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:131
|
||||
#: src/octoprint/server/views.py:183
|
||||
msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:132
|
||||
#: src/octoprint/server/views.py:184
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:1
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:133
|
||||
#: src/octoprint/server/views.py:185
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:139
|
||||
#: src/octoprint/server/views.py:191
|
||||
msgid "Temperature"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:140
|
||||
#: src/octoprint/server/views.py:192
|
||||
msgid "Control"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:141
|
||||
#: src/octoprint/server/views.py:193
|
||||
msgid "Terminal"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:144
|
||||
#: src/octoprint/server/views.py:196
|
||||
msgid "GCode Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:146
|
||||
#: src/octoprint/server/views.py:198
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:4
|
||||
msgid "Timelapse"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:151
|
||||
#: src/octoprint/server/views.py:203
|
||||
msgid "Printer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:153
|
||||
#: src/octoprint/server/views.py:205
|
||||
msgid "Serial Connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:154
|
||||
#: src/octoprint/server/views.py:206
|
||||
#: src/octoprint/templates/dialogs/settings/printerprofiles.jinja2:1
|
||||
msgid "Printer Profiles"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:155
|
||||
#: src/octoprint/server/views.py:207
|
||||
msgid "Temperatures"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:156
|
||||
#: src/octoprint/server/views.py:208
|
||||
msgid "Terminal Filters"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:157
|
||||
#: src/octoprint/server/views.py:209
|
||||
msgid "GCODE Scripts"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:159 src/octoprint/server/views.py:161
|
||||
#: src/octoprint/server/views.py:211 src/octoprint/server/views.py:213
|
||||
msgid "Features"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:162
|
||||
#: src/octoprint/server/views.py:214
|
||||
msgid "Webcam"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:163
|
||||
#: src/octoprint/server/views.py:215
|
||||
msgid "API"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:167
|
||||
#: src/octoprint/server/views.py:219
|
||||
#: src/octoprint/templates/dialogs/settings/folders.jinja2:2
|
||||
msgid "Folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:168
|
||||
#: src/octoprint/server/views.py:220
|
||||
msgid "Appearance"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:169
|
||||
#: src/octoprint/server/views.py:221
|
||||
#: src/octoprint/templates/dialogs/settings/logs.jinja2:2
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:170
|
||||
#: src/octoprint/server/views.py:222
|
||||
msgid "Server"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:173
|
||||
#: src/octoprint/server/views.py:225
|
||||
msgid "Access Control"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:179
|
||||
#: src/octoprint/server/views.py:231
|
||||
msgid "Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/views.py:180
|
||||
#: src/octoprint/server/views.py:232
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/dataupdater.js:96
|
||||
#: src/octoprint/static/js/app/dataupdater.js:131
|
||||
#: src/octoprint/static/js/app/helpers.js:451
|
||||
#: src/octoprint/static/js/app/helpers.js:562
|
||||
#: src/octoprint/templates/overlays/offline.jinja2:6
|
||||
msgid "Server is offline"
|
||||
msgstr ""
|
||||
|
|
@ -1085,12 +1090,82 @@ msgstr ""
|
|||
msgid "%(hour)02d:%(minute)02d:%(second)02d"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:392
|
||||
#: src/octoprint/static/js/app/helpers.js:429
|
||||
#: src/octoprint/static/js/app/helpers.js:436
|
||||
#, python-format
|
||||
msgid "%(days)d days"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:431
|
||||
#, python-format
|
||||
msgid "%(days)d.5 days"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:434
|
||||
#, python-format
|
||||
msgid "%(days)d day"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:445
|
||||
#, python-format
|
||||
msgid "%(hours)d hour"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:447
|
||||
#: src/octoprint/static/js/app/helpers.js:455
|
||||
#: src/octoprint/static/js/app/helpers.js:466
|
||||
#, python-format
|
||||
msgid "%(hours)d hours"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:451
|
||||
#, python-format
|
||||
msgid "%(hours)d.5 hours"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:460
|
||||
msgid "1 day"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:473
|
||||
msgid "a minute"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:475
|
||||
msgid "2 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:481
|
||||
#, python-format
|
||||
msgid "%(minutes)d minutes"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:483
|
||||
msgid "40 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:485
|
||||
msgid "50 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:487
|
||||
msgid "1 hour"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:492
|
||||
msgid "a couple of seconds"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:494
|
||||
msgid "less than a minute"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:503
|
||||
msgid "YYYY-MM-DD HH:mm"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/helpers.js:410
|
||||
#: src/octoprint/static/js/app/helpers.js:415
|
||||
#: src/octoprint/static/js/app/helpers.js:521
|
||||
#: src/octoprint/static/js/app/helpers.js:526
|
||||
msgid "off"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1178,19 +1253,22 @@ msgstr ""
|
|||
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:343
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:348
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:468
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:471
|
||||
msgid "Filament"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:352
|
||||
msgid "Estimated Print Time"
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:475
|
||||
msgid "Estimated print time"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:355
|
||||
msgid "Last Printed"
|
||||
msgid "Last printed"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:357
|
||||
msgid "Last Print Time"
|
||||
msgid "Last print time"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/files.js:460
|
||||
|
|
@ -1305,16 +1383,7 @@ msgid "Layer height"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:465
|
||||
msgid "GCODE commands in layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:468
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:471
|
||||
msgid "Filament used by layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/gcode.js:475
|
||||
msgid "Print time for layer"
|
||||
msgid "GCODE commands"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/loginstate.js:22
|
||||
|
|
@ -1474,11 +1543,13 @@ msgid "Pauses the print job"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerstate.js:81
|
||||
msgid "Calculating..."
|
||||
msgid "Still stabilizing..."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerstate.js:91
|
||||
msgid "Based on a linear approximation (accuracy highly dependent on the model)"
|
||||
msgid ""
|
||||
"Based on a linear approximation (very low accuracy, especially at the "
|
||||
"beginning of the print)"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerstate.js:94
|
||||
|
|
@ -1510,7 +1581,7 @@ msgid "Continue"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerstate.js:146
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:23
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:24
|
||||
msgid "Pause"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2095,7 +2166,7 @@ msgstr ""
|
|||
#: src/octoprint/templates/dialogs/settings/folders.jinja2:47
|
||||
#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:69
|
||||
#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:69
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:75
|
||||
#: src/octoprint/templates/tabs/timelapse.jinja2:13
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
|
@ -2721,42 +2792,68 @@ msgid "Release SD card"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:1
|
||||
msgid "Machine State"
|
||||
msgid "Current printer state"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:3
|
||||
msgid "Name of file currently selected for printing"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:3
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:4
|
||||
msgid "Current timelapse configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:8
|
||||
msgid "Estimated total print time base on statical analysis or past prints"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:8
|
||||
msgid "Approx. Total Print Time"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:10
|
||||
msgid "Total print time so far"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:10
|
||||
msgid "Print Time"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:11
|
||||
msgid ""
|
||||
"Estimated time until the print job is done. This is only an estimate and "
|
||||
"accuracy depends heavily on various factors!"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:11
|
||||
msgid "Print Time Left"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:12
|
||||
msgid "Bytes printed vs total bytes of file"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:12
|
||||
msgid "Printed"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:22
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:23
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:22
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:23
|
||||
msgid "Print"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:23
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:24
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:24
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:25
|
||||
msgid "Cancels the print job"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2838,6 +2935,10 @@ msgstr ""
|
|||
msgid "Model info"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:20
|
||||
msgid "Layer info"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:24
|
||||
msgid "Renderer options"
|
||||
msgstr ""
|
||||
|
|
@ -2876,13 +2977,20 @@ msgstr ""
|
|||
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:65
|
||||
msgid ""
|
||||
"Note that the time estimates in this tab are calculated by the GCODE "
|
||||
"viewer in your browser and might differ from the values calculated by the"
|
||||
" server that are displayed in the \"State\" and \"Files\" panels in the "
|
||||
"sidebar due to slightly different implementations."
|
||||
"\n"
|
||||
" Note that the time and usage values in this tab are "
|
||||
"<strong>estimated</strong> by the GCODE viewer in your\n"
|
||||
" browser and might differ from the values "
|
||||
"<strong>estimated</strong> by the server that are displayed in the\n"
|
||||
" \"State\" and \"Files\" panels in the sidebar due to slightly"
|
||||
" different implementations. Also note that these\n"
|
||||
" <strong>estimated</strong> values may be inaccurate since "
|
||||
"they can also take information present in the\n"
|
||||
" GCODE file into account.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:70
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:76
|
||||
msgid ""
|
||||
"<p>\n"
|
||||
" You've selected <strong data-bind=\"text: "
|
||||
|
|
@ -2899,7 +3007,7 @@ msgid ""
|
|||
" </p>"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:81
|
||||
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:87
|
||||
#, python-format
|
||||
msgid "Yes, please visualize %(name)s regardless of its size"
|
||||
msgstr ""
|
||||
|
|
|
|||
Loading…
Reference in a new issue