Merge pull request #90 from Gallore/f_swupdate_bitbucket

F swupdate bitbucket
This commit is contained in:
Andy Werner 2017-05-02 11:30:45 +02:00 committed by GitHub
commit 2df4da2af0
25 changed files with 355 additions and 209 deletions

View file

@ -74,6 +74,7 @@ date of first contribution):
* [Mathias Rangel Wulff](https://github.com/mathiasrw) * [Mathias Rangel Wulff](https://github.com/mathiasrw)
* [Clemens Niemeyer](https://github.com/clemniem) * [Clemens Niemeyer](https://github.com/clemniem)
* ["I-am-me"](https://github.com/I-am-me) * ["I-am-me"](https://github.com/I-am-me)
* [J-J Heinonen](https://github.com/jammi)
* [Noah Martin](https://github.com/noahsmartin) * [Noah Martin](https://github.com/noahsmartin)
* [Eyal Soha](https://github.com/eyal0) * [Eyal Soha](https://github.com/eyal0)
* [Greg Hulands](https://github.com/ghulands) * [Greg Hulands](https://github.com/ghulands)

View file

@ -14,34 +14,31 @@ import octoprint_setuptools
# Requirements for our application # Requirements for our application
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [
"flask>=0.9,<0.11", "flask>=0.12,<0.13",
"werkzeug>=0.11.1,<0.12",
"tornado>=4.4.2,<4.5",
"Jinja2>=2.8,<2.9", # Jinja 2.9 has breaking changes WRT template scope - we can't "Jinja2>=2.8,<2.9", # Jinja 2.9 has breaking changes WRT template scope - we can't
# guarantee backwards compatibility for plugins and such with that # guarantee backwards compatibility for plugins and such with that
# version, hence we need to pin to a lower version for now. See #1697 # version, hence we need to pin to a lower version for now. See #1697
"werkzeug>=0.8.3,<0.9",
"tornado==4.0.2", # pinned for now, we need to migrate to a newer tornado, but due
# to some voodoo needed to get large streamed uploads and downloads
# to work that is probably not completely straightforward and therefore
# something for post-1.3.0-stable release
"sockjs-tornado>=1.0.2,<1.1", "sockjs-tornado>=1.0.2,<1.1",
"PyYAML>=3.10,<3.11", "PyYAML>=3.12,<3.13",
"Flask-Login>=0.2.2,<0.3", "Flask-Login>=0.4,<0.5",
"Flask-Principal>=0.3.5,<0.4", "Flask-Principal>=0.4,<0.5",
"Flask-Babel>=0.9,<0.10", "Flask-Babel>=0.11,<0.12",
"Flask-Assets>=0.10,<0.11", "Flask-Assets>=0.12,<0.13",
"markdown>=2.6.4,<2.7", "markdown>=2.6.4,<2.7",
"pyserial>=2.7,<2.8", "pyserial>=3.2.1,<3.3",
"netaddr>=0.7.17,<0.8", "netaddr>=0.7.17,<0.8",
"watchdog>=0.8.3,<0.9", "watchdog>=0.8.3,<0.9",
"sarge>=0.1.4,<0.2", "sarge>=0.1.4,<0.2",
"netifaces>=0.10,<0.11", "netifaces>=0.10,<0.11",
"pylru>=1.0.9,<1.1", "pylru>=1.0.9,<1.1",
"rsa>=3.2,<3.3", "rsa>=3.4,<3.5",
"pkginfo>=1.2.1,<1.3", "pkginfo>=1.4.1,<1.5",
"requests>=2.7,<2.8", "requests>=2.13.0,<2.14",
"semantic_version>=2.4.2,<2.5", "semantic_version>=2.6.0,<2.7",
"psutil>=3.2.1,<3.3", "psutil>=5.1.3,<5.2",
"Click>=6.2,<6.3", "Click>=6.7,<6.8",
"awesome-slugify>=1.6.5,<1.7", "awesome-slugify>=1.6.5,<1.7",
"feedparser>=5.2.1,<5.3", "feedparser>=5.2.1,<5.3",
"chainmap>=1.0.2,<1.1", "chainmap>=1.0.2,<1.1",
@ -58,7 +55,7 @@ EXTRA_REQUIRES = dict(
# Dependencies for developing OctoPrint # Dependencies for developing OctoPrint
develop=[ develop=[
# Testing dependencies # Testing dependencies
"mock>=1.0.1,<1.1", "mock>=2.0,<2.1",
"nose>=1.3.0,<1.4", "nose>=1.3.0,<1.4",
"ddt", "ddt",
@ -73,7 +70,7 @@ EXTRA_REQUIRES = dict(
# Dependencies for developing OctoPrint plugins # Dependencies for developing OctoPrint plugins
plugins=[ plugins=[
"cookiecutter>=1.4,<1.5" "cookiecutter>=1.5,<1.6"
] ]
) )

View file

@ -1409,7 +1409,7 @@ class SettingsPlugin(OctoPrintPlugin):
:return: the current settings of the plugin, as a dictionary :return: the current settings of the plugin, as a dictionary
""" """
from flask.ext.login import current_user from flask_login import current_user
import copy import copy
data = copy.deepcopy(self._settings.get_all_data(merged=True)) data = copy.deepcopy(self._settings.get_all_data(merged=True))
@ -1454,8 +1454,8 @@ class SettingsPlugin(OctoPrintPlugin):
else: else:
node[key] = None node[key] = None
conditions = dict(user=lambda: current_user is not None and not current_user.is_anonymous(), conditions = dict(user=lambda: current_user is not None and not current_user.is_anonymous,
admin=lambda: current_user is not None and not current_user.is_anonymous() and current_user.is_admin(), admin=lambda: current_user is not None and not current_user.is_anonymous and current_user.is_admin,
never=lambda: False) never=lambda: False)
for level, condition in conditions.items(): for level, condition in conditions.items():

View file

@ -20,7 +20,7 @@ import flask
from octoprint.server import admin_permission from octoprint.server import admin_permission
from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag
from flask.ext.babel import gettext from flask_babel import gettext
class AnnouncementPlugin(octoprint.plugin.AssetPlugin, class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
octoprint.plugin.SettingsPlugin, octoprint.plugin.SettingsPlugin,

View file

@ -9,7 +9,7 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms
import octoprint.plugin import octoprint.plugin
from flask.ext.babel import gettext from flask_babel import gettext
class CoreWizardPlugin(octoprint.plugin.AssetPlugin, class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
@ -87,9 +87,9 @@ class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
from flask import request from flask import request
from octoprint.server.api import valid_boolean_trues, NO_CONTENT from octoprint.server.api import valid_boolean_trues, NO_CONTENT
data = request.values data = request.get_json()
if hasattr(request, "json") and request.json: if data is None:
data = request.json data = request.values
if "ac" in data and data["ac"] in valid_boolean_trues and \ if "ac" in data and data["ac"] in valid_boolean_trues and \
"user" in data.keys() and "pass1" in data.keys() and \ "user" in data.keys() and "pass1" in data.keys() and \

View file

@ -15,7 +15,7 @@ from octoprint.server import admin_permission, VERSION
from octoprint.util.pip import LocalPipCaller, UnknownPip from octoprint.util.pip import LocalPipCaller, UnknownPip
from flask import jsonify, make_response from flask import jsonify, make_response
from flask.ext.babel import gettext from flask_babel import gettext
import logging import logging
import sarge import sarge

View file

@ -501,7 +501,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
if not "application/json" in flask.request.headers["Content-Type"]: if not "application/json" in flask.request.headers["Content-Type"]:
return flask.make_response("Expected content-type JSON", 400) return flask.make_response("Expected content-type JSON", 400)
json_data = flask.request.json json_data = flask.request.get_json(silent=True)
if json_data is None:
return flask.make_response("Invalid JSON", 400)
if "check" in json_data: if "check" in json_data:
check_targets = map(lambda x: x.strip(), json_data["check"]) check_targets = map(lambda x: x.strip(), json_data["check"])
@ -528,7 +530,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
##~~ TemplatePlugin API ##~~ TemplatePlugin API
def get_template_configs(self): def get_template_configs(self):
from flask.ext.babel import gettext from flask_babel import gettext
return [ return [
dict(type="settings", name=gettext("Software Update")) dict(type="settings", name=gettext("Software Update"))
] ]
@ -861,7 +863,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
result = dict(check) result = dict(check)
if target == "octoprint": if target == "octoprint":
from flask.ext.babel import gettext from flask_babel import gettext
result["displayName"] = to_unicode(check.get("displayName"), errors="replace") result["displayName"] = to_unicode(check.get("displayName"), errors="replace")
if result["displayName"] is None: if result["displayName"] is None:

View file

@ -10,7 +10,7 @@ import logging
from ..exceptions import ConfigurationInvalid from ..exceptions import ConfigurationInvalid
BRANCH_HEAD_URL = "https://api.bitbucket.org/2.0/repositories/{user}/{repo}/commits/{branch}?pagelen=1" BRANCH_HEAD_URL = "https://api.bitbucket.org/2.0/repositories/{user}/{repo}/commit/{branch}"
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.bitbucket_commit") logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.bitbucket_commit")
@ -22,12 +22,11 @@ def _get_latest_commit(user, repo, branch):
return None return None
reference = r.json() reference = r.json()
if not "values" in reference \
or len(reference["values"]) < 1 \ if not "hash" in reference:
or not 'hash' in reference["values"][0]:
return None return None
return reference["values"][0]['hash'] return reference["hash"]
def get_latest(target, check): def get_latest(target, check):

View file

@ -8,10 +8,10 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import uuid import uuid
from sockjs.tornado import SockJSRouter from sockjs.tornado import SockJSRouter
from flask import Flask, g, request, session, Blueprint, Request, Response from flask import Flask, g, request, session, Blueprint, Request, Response
from flask.ext.login import LoginManager, current_user from flask_login import LoginManager, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed from flask_principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
from flask.ext.babel import Babel, gettext, ngettext from flask_babel import Babel, gettext, ngettext
from flask.ext.assets import Environment, Bundle from flask_assets import Environment, Bundle
from babel import Locale from babel import Locale
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver from watchdog.observers.polling import PollingObserver
@ -89,9 +89,9 @@ def on_identity_loaded(sender, identity):
return return
identity.provides.add(UserNeed(user.get_id())) identity.provides.add(UserNeed(user.get_id()))
if user.is_user(): if user.is_user:
identity.provides.add(RoleNeed("user")) identity.provides.add(RoleNeed("user"))
if user.is_admin(): if user.is_admin:
identity.provides.add(RoleNeed("admin")) identity.provides.add(RoleNeed("admin"))
def load_user(id): def load_user(id):
@ -170,8 +170,7 @@ class Server(object):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
pluginManager = self._plugin_manager pluginManager = self._plugin_manager
# monkey patch a bunch of stuff # monkey patch some stuff
util.tornado.fix_ioloop_scheduling()
util.flask.enable_additional_translations(additional_folders=[self._settings.getBaseFolder("translations")]) util.flask.enable_additional_translations(additional_folders=[self._settings.getBaseFolder("translations")])
# setup app # setup app
@ -337,6 +336,7 @@ class Server(object):
loginManager = LoginManager() loginManager = LoginManager()
loginManager.session_protection = "strong" loginManager.session_protection = "strong"
loginManager.user_callback = load_user loginManager.user_callback = load_user
loginManager.anonymous_user = users.AnonymousUser # TODO: remove in 1.5.0
if not userManager.enabled: if not userManager.enabled:
loginManager.anonymous_user = users.DummyUser loginManager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader) principals.identity_loaders.appendleft(users.dummy_identity_loader)
@ -610,11 +610,14 @@ class Server(object):
return Locale.parse(request.accept_languages.best_match(LANGUAGES)) return Locale.parse(request.accept_languages.best_match(LANGUAGES))
def _setup_app(self, app): def _setup_app(self, app):
from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse, deprecate_flaskext
deprecate_flaskext() # TODO: remove in OctoPrint 1.5.0
s = settings() s = settings()
app.debug = self._debug app.debug = self._debug
app.config["TEMPLATES_AUTO_RELOAD"] = True
secret_key = s.get(["server", "secretKey"]) secret_key = s.get(["server", "secretKey"])
if not secret_key: if not secret_key:

View file

@ -10,8 +10,8 @@ import netaddr
import sarge import sarge
from flask import Blueprint, request, jsonify, abort, current_app, session, make_response, g from flask import Blueprint, request, jsonify, abort, current_app, session, make_response, g
from flask.ext.login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from flask.ext.principal import Identity, identity_changed, AnonymousIdentity from flask_principal import Identity, identity_changed, AnonymousIdentity
import octoprint.util as util import octoprint.util as util
import octoprint.users import octoprint.users
@ -62,7 +62,7 @@ def pluginData(name):
return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500) return make_response("More than one api provider registered for {name}, can't proceed".format(name=name), 500)
api_plugin = api_plugins[0] api_plugin = api_plugins[0]
if api_plugin.is_api_adminonly() and not current_user.is_admin(): if api_plugin.is_api_adminonly() and not current_user.is_admin:
return make_response("Forbidden", 403) return make_response("Forbidden", 403)
response = api_plugin.on_api_get(request) response = api_plugin.on_api_get(request)
@ -89,7 +89,7 @@ def pluginCommand(name):
if valid_commands is None: if valid_commands is None:
return make_response("Method not allowed", 405) return make_response("Method not allowed", 405)
if api_plugin.is_api_adminonly() and not current_user.is_admin(): if api_plugin.is_api_adminonly() and not current_user.is_admin:
return make_response("Forbidden", 403) return make_response("Forbidden", 403)
command, data, response = get_json_command_from_request(request, valid_commands) command, data, response = get_json_command_from_request(request, valid_commands)
@ -134,10 +134,13 @@ def wizardFinish():
data = dict() data = dict()
try: try:
data = request.json data = request.get_json()
except: except:
abort(400) abort(400)
if data is None:
abort(400)
if not "handled" in data: if not "handled" in data:
abort(400) abort(400)
handled = data["handled"] handled = data["handled"]
@ -186,9 +189,9 @@ def apiVersion():
@api.route("/login", methods=["POST"]) @api.route("/login", methods=["POST"])
def login(): def login():
data = request.values data = request.get_json()
if hasattr(request, "json") and request.json: if data is None:
data = request.json data = request.values
if octoprint.server.userManager.enabled and "user" in data and "pass" in data: if octoprint.server.userManager.enabled and "user" in data and "pass" in data:
username = data["user"] username = data["user"]

View file

@ -26,7 +26,7 @@ from octoprint.server.util.flask import restricted_access
from octoprint.plugin import plugin_manager from octoprint.plugin import plugin_manager
from flask.ext.babel import Locale from flask_babel import Locale
@api.route("/languages", methods=["GET"]) @api.route("/languages", methods=["GET"])
@restricted_access @restricted_access

View file

@ -326,10 +326,13 @@ def printerCommand():
return make_response("Expected content type JSON", 400) return make_response("Expected content type JSON", 400)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
if "command" in data and "commands" in data: if "command" in data and "commands" in data:
return make_response("'command' and 'commands' are mutually exclusive", 400) return make_response("'command' and 'commands' are mutually exclusive", 400)
elif ("command" in data or "commands" in data) and "script" in data: elif ("command" in data or "commands" in data) and "script" in data:

View file

@ -50,10 +50,13 @@ def printerProfilesAdd():
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
json_data = request.json json_data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if json_data is None:
return make_response("Malformed JSON body in request", 400)
if not "profile" in json_data: if not "profile" in json_data:
return make_response("No profile included in request", 400) return make_response("No profile included in request", 400)
@ -117,10 +120,13 @@ def printerProfilesUpdate(identifier):
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
json_data = request.json json_data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if json_data is None:
return make_response("Malformed JSON body in request", 400)
if not "profile" in json_data: if not "profile" in json_data:
return make_response("No profile included in request", 400) return make_response("No profile included in request", 400)

View file

@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import logging import logging
from flask import request, jsonify, make_response from flask import request, jsonify, make_response
from flask.ext.login import current_user from flask_login import current_user
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from octoprint.events import eventManager, Events from octoprint.events import eventManager, Events
@ -39,7 +39,7 @@ def _etag(lm=None):
for key in sorted(plugin_settings.keys()): for key in sorted(plugin_settings.keys()):
sorted_plugin_settings[key] = plugin_settings.get(key, dict()) sorted_plugin_settings[key] = plugin_settings.get(key, dict())
if current_user is not None and not current_user.is_anonymous(): if current_user is not None and not current_user.is_anonymous:
roles = sorted(current_user.roles) roles = sorted(current_user.roles)
else: else:
roles = [] roles = []
@ -251,10 +251,13 @@ def setSettings():
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
_saveSettings(data) _saveSettings(data)
return getSettings() return getSettings()

View file

@ -120,10 +120,13 @@ def slicingAddSlicerProfile(slicer, name):
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
json_data = request.json json_data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if json_data is None:
return make_response("Malformed JSON body in request", 400)
data = dict() data = dict()
display_name = None display_name = None
description = None description = None
@ -159,10 +162,13 @@ def slicingPatchSlicerProfile(slicer, name):
return make_response("Profile {name} for slicer {slicer} not found".format(**locals()), 404) return make_response("Profile {name} for slicer {slicer} not found".format(**locals()), 404)
try: try:
json_data = request.json json_data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if json_data is None:
return make_response("Malformed JSON body in request", 400)
data = dict() data = dict()
display_name = None display_name = None
description = None description = None

View file

@ -9,7 +9,7 @@ import logging
import sarge import sarge
from flask import request, make_response, jsonify, url_for from flask import request, make_response, jsonify, url_for
from flask.ext.babel import gettext from flask_babel import gettext
from octoprint.settings import settings as s from octoprint.settings import settings as s
@ -24,9 +24,9 @@ from octoprint.server.util.flask import restricted_access, get_remote_address
def performSystemAction(): def performSystemAction():
logging.getLogger(__name__).warn("Deprecated API call to /api/system made by {}, should be migrated to use /system/commands/custom/<action>".format(get_remote_address(request))) logging.getLogger(__name__).warn("Deprecated API call to /api/system made by {}, should be migrated to use /system/commands/custom/<action>".format(get_remote_address(request)))
data = request.values data = request.get_json(silent=True)
if hasattr(request, "json") and request.json: if data is None:
data = request.json data = request.values
if not "action" in data: if not "action" in data:
return make_response("action to perform is not defined", 400) return make_response("action to perform is not defined", 400)

View file

@ -162,9 +162,9 @@ def processUnrenderedTimelapseCommand(name):
@api.route("/timelapse", methods=["POST"]) @api.route("/timelapse", methods=["POST"])
@restricted_access @restricted_access
def setTimelapseConfig(): def setTimelapseConfig():
data = request.values data = request.get_json(silent=True)
if hasattr(request, "json") and request.json: if data is None:
data = request.json data = request.values
if "type" in data: if "type" in data:
config = { config = {

View file

@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
from flask import request, jsonify, abort, make_response from flask import request, jsonify, abort, make_response
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from flask.ext.login import current_user from flask_login import current_user
import octoprint.users as users import octoprint.users as users
@ -40,10 +40,13 @@ def addUser():
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
if not "name" in data: if not "name" in data:
return make_response("Missing mandatory name field", 400) return make_response("Missing mandatory name field", 400)
if not "password" in data: if not "password" in data:
@ -72,7 +75,7 @@ def getUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): if current_user is not None and not current_user.is_anonymous and (current_user.get_name() == username or current_user.is_admin):
user = userManager.findUser(username) user = userManager.findUser(username)
if user is not None: if user is not None:
return jsonify(user.asDict()) return jsonify(user.asDict())
@ -95,10 +98,13 @@ def updateUser(username):
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
# change roles # change roles
roles = ["user"] roles = ["user"]
if "admin" in data and data["admin"] in valid_boolean_trues: if "admin" in data and data["admin"] in valid_boolean_trues:
@ -133,15 +139,18 @@ def changePasswordForUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): if current_user is not None and not current_user.is_anonymous and (current_user.get_name() == username or current_user.is_admin):
if not "application/json" in request.headers["Content-Type"]: if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
if not "password" in data or not data["password"]: if not "password" in data or not data["password"]:
return make_response("password is missing from request", 400) return make_response("password is missing from request", 400)
@ -161,7 +170,7 @@ def getSettingsForUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()): if current_user is None or current_user.is_anonymous or (current_user.get_name() != username and not current_user.is_admin):
return make_response("Forbidden", 403) return make_response("Forbidden", 403)
try: try:
@ -175,14 +184,17 @@ def changeSettingsForUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()): if current_user is None or current_user.is_anonymous or (current_user.get_name() != username and not current_user.is_admin):
return make_response("Forbidden", 403) return make_response("Forbidden", 403)
try: try:
data = request.json data = request.get_json()
except BadRequest: except BadRequest:
return make_response("Malformed JSON body in request", 400) return make_response("Malformed JSON body in request", 400)
if data is None:
return make_response("Malformed JSON body in request", 400)
try: try:
userManager.changeUserSettings(username, data) userManager.changeUserSettings(username, data)
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -195,7 +207,7 @@ def deleteApikeyForUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): if current_user is not None and not current_user.is_anonymous and (current_user.get_name() == username or current_user.is_admin):
try: try:
userManager.deleteApikey(username) userManager.deleteApikey(username)
except users.UnknownUser: except users.UnknownUser:
@ -211,7 +223,7 @@ def generateApikeyForUser(username):
if not userManager.enabled: if not userManager.enabled:
return jsonify(SUCCESS) return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): if current_user is not None and not current_user.is_anonymous and (current_user.get_name() == username or current_user.is_admin):
try: try:
apikey = userManager.generateApiKey(username) apikey = userManager.generateApiKey(username)
except users.UnknownUser: except users.UnknownUser:

View file

@ -29,7 +29,10 @@ def verifySessionKey():
if not "application/json" in request.headers["Content-Type"]: if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400) return None, None, make_response("Expected content-type JSON", 400)
data = request.json data = request.get_json()
if data is None:
return make_response("Malformed JSON body in request", 400)
for key in ("appid", "key", "_sig"): for key in ("appid", "key", "_sig"):
if not key in data: if not key in data:
return make_response("Missing argument: {key}".format(key=key), 400) return make_response("Missing argument: {key}".format(key=key), 400)

View file

@ -13,6 +13,8 @@ from octoprint.users import ApiUser
from octoprint.util import deprecated from octoprint.util import deprecated
import flask as _flask import flask as _flask
import flask_login
import flask_principal
from . import flask from . import flask
from . import sockjs from . import sockjs
@ -58,9 +60,9 @@ def loginFromApiKeyRequestHandler():
if apikey and apikey != octoprint.server.UI_API_KEY and not octoprint.server.appSessionManager.validate(apikey): if apikey and apikey != octoprint.server.UI_API_KEY and not octoprint.server.appSessionManager.validate(apikey):
user = get_user_for_apikey(apikey) user = get_user_for_apikey(apikey)
if user is not None and _flask.ext.login.login_user(user, remember=False): if user is not None and not user.is_anonymous and flask_login.login_user(user, remember=False):
_flask.ext.principal.identity_changed.send(_flask.current_app._get_current_object(), flask_principal.identity_changed.send(_flask.current_app._get_current_object(),
identity=_flask.ext.principal.Identity(user.get_id())) identity=flask_principal.Identity(user.get_id()))
else: else:
return _flask.make_response("Invalid API key", 401) return _flask.make_response("Invalid API key", 401)

View file

@ -8,9 +8,9 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import tornado.web import tornado.web
import flask import flask
import flask.ext.login import flask_login
import flask.ext.principal import flask_principal
import flask.ext.assets import flask_assets
import webassets.updater import webassets.updater
import webassets.utils import webassets.utils
import functools import functools
@ -40,7 +40,7 @@ def enable_additional_translations(default_locale="en", additional_folders=None)
import os import os
from flask import _request_ctx_stack from flask import _request_ctx_stack
from babel import support, Locale from babel import support, Locale
import flask.ext.babel import flask_babel
if additional_folders is None: if additional_folders is None:
additional_folders = [] additional_folders = []
@ -91,7 +91,7 @@ def enable_additional_translations(default_locale="en", additional_folders=None)
return None return None
translations = getattr(ctx, 'babel_translations', None) translations = getattr(ctx, 'babel_translations', None)
if translations is None: if translations is None:
locale = flask.ext.babel.get_locale() locale = flask_babel.get_locale()
translations = support.Translations() translations = support.Translations()
if str(locale) != default_locale: if str(locale) != default_locale:
@ -129,8 +129,8 @@ def enable_additional_translations(default_locale="en", additional_folders=None)
ctx.babel_translations = translations ctx.babel_translations = translations
return translations return translations
flask.ext.babel.Babel.list_translations = fixed_list_translations flask_babel.Babel.list_translations = fixed_list_translations
flask.ext.babel.get_translations = fixed_get_translations flask_babel.get_translations = fixed_get_translations
def fix_webassets_cache(): def fix_webassets_cache():
from webassets import cache from webassets import cache
@ -229,6 +229,27 @@ def fix_webassets_filtertool():
FilterTool._wrap_cache = fixed_wrap_cache FilterTool._wrap_cache = fixed_wrap_cache
# TODO: Remove compatibility layer in OctoPrint 1.5.0
def deprecate_flaskext():
import flask
import importlib
class FlaskExtDeprecator(object):
def __getattr__(self, item):
old_name = "flask.ext.{}".format(item)
new_name = "flask_{}".format(item)
module = importlib.import_module(new_name)
from warnings import warn
message = "The {old} import is deprecated in Flask versions >= 0.11, which OctoPrint now uses. " + \
"Import {new} instead. This compatibility layer will be removed in OctoPrint 1.5.0."
warn(DeprecationWarning(message.format(old=old_name, new=new_name)), stacklevel=2)
return module
flask.ext = FlaskExtDeprecator()
#~~ WSGI environment wrapper for reverse proxying #~~ WSGI environment wrapper for reverse proxying
class ReverseProxiedEnvironment(object): class ReverseProxiedEnvironment(object):
@ -478,12 +499,12 @@ class OctoPrintFlaskResponse(flask.Response):
def passive_login(): def passive_login():
if octoprint.server.userManager.enabled: if octoprint.server.userManager.enabled:
user = octoprint.server.userManager.login_user(flask.ext.login.current_user) user = octoprint.server.userManager.login_user(flask_login.current_user)
else: else:
user = flask.ext.login.current_user user = flask_login.current_user
if user is not None and not user.is_anonymous(): if user is not None and not user.is_anonymous:
flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) flask_principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask_principal.Identity(user.get_id()))
if hasattr(user, "get_session"): if hasattr(user, "get_session"):
flask.session["usersession.id"] = user.get_session() flask.session["usersession.id"] = user.get_session()
flask.g.user = user flask.g.user = user
@ -505,8 +526,8 @@ def passive_login():
user = octoprint.server.userManager.login_user(user) user = octoprint.server.userManager.login_user(user)
flask.session["usersession.id"] = user.get_session() flask.session["usersession.id"] = user.get_session()
flask.g.user = user flask.g.user = user
flask.ext.login.login_user(user) flask_login.login_user(user)
flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) flask_principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask_principal.Identity(user.get_id()))
return flask.jsonify(user.asDict()) return flask.jsonify(user.asDict())
except: except:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1023,7 +1044,7 @@ def admin_validator(request):
""" """
user = _get_flask_user_from_request(request) user = _get_flask_user_from_request(request)
if user is None or not user.is_authenticated() or not user.is_admin(): if user is None or not user.is_authenticated or not user.is_admin:
raise tornado.web.HTTPError(403) raise tornado.web.HTTPError(403)
@ -1038,7 +1059,7 @@ def user_validator(request):
""" """
user = _get_flask_user_from_request(request) user = _get_flask_user_from_request(request)
if user is None or not user.is_authenticated(): if user is None or not user.is_authenticated:
raise tornado.web.HTTPError(403) raise tornado.web.HTTPError(403)
@ -1051,14 +1072,14 @@ def _get_flask_user_from_request(request):
:return: the user or None if no user could be determined :return: the user or None if no user could be determined
""" """
import octoprint.server.util import octoprint.server.util
import flask.ext.login import flask_login
from octoprint.settings import settings from octoprint.settings import settings
apikey = octoprint.server.util.get_api_key(request) apikey = octoprint.server.util.get_api_key(request)
if settings().getBoolean(["api", "enabled"]) and apikey is not None: if settings().getBoolean(["api", "enabled"]) and apikey is not None:
user = octoprint.server.util.get_user_for_apikey(apikey) user = octoprint.server.util.get_user_for_apikey(apikey)
else: else:
user = flask.ext.login.current_user user = flask_login.current_user
return user return user
@ -1103,7 +1124,7 @@ def restricted_access(func):
if settings().getBoolean(["server", "firstRun"]) and settings().getBoolean(["accessControl", "enabled"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()): if settings().getBoolean(["server", "firstRun"]) and settings().getBoolean(["accessControl", "enabled"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
return flask.make_response("OctoPrint isn't setup yet", 403) return flask.make_response("OctoPrint isn't setup yet", 403)
return flask.ext.login.login_required(func)(*args, **kwargs) return flask_login.login_required(func)(*args, **kwargs)
return decorated_view return decorated_view
@ -1188,7 +1209,10 @@ def get_json_command_from_request(request, valid_commands):
if content_type is None or not "application/json" in content_type: if content_type is None or not "application/json" in content_type:
return None, None, make_response("Expected content-type JSON", 400) return None, None, make_response("Expected content-type JSON", 400)
data = request.json data = request.get_json()
if data is None:
return make_response("Malformed JSON body in request", 400)
if not "command" in data.keys() or not data["command"] in valid_commands.keys(): if not "command" in data.keys() or not data["command"] in valid_commands.keys():
return None, None, make_response("Expected valid command", 400) return None, None, make_response("Expected valid command", 400)
@ -1201,7 +1225,7 @@ def get_json_command_from_request(request, valid_commands):
##~~ Flask-Assets resolver with plugin asset support ##~~ Flask-Assets resolver with plugin asset support
class PluginAssetResolver(flask.ext.assets.FlaskResolver): class PluginAssetResolver(flask_assets.FlaskResolver):
def split_prefix(self, ctx, item): def split_prefix(self, ctx, item):
app = ctx.environment._app app = ctx.environment._app
@ -1210,14 +1234,14 @@ class PluginAssetResolver(flask.ext.assets.FlaskResolver):
prefix, plugin, name = item.split("/", 2) prefix, plugin, name = item.split("/", 2)
blueprint = prefix + "." + plugin blueprint = prefix + "." + plugin
directory = flask.ext.assets.get_static_folder(app.blueprints[blueprint]) directory = flask_assets.get_static_folder(app.blueprints[blueprint])
item = name item = name
endpoint = blueprint + ".static" endpoint = blueprint + ".static"
return directory, item, endpoint return directory, item, endpoint
except (ValueError, KeyError): except (ValueError, KeyError):
pass pass
return flask.ext.assets.FlaskResolver.split_prefix(self, ctx, item) return flask_assets.FlaskResolver.split_prefix(self, ctx, item)
def resolve_output_to_path(self, ctx, target, bundle): def resolve_output_to_path(self, ctx, target, bundle):
import os import os

View file

@ -7,11 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import logging import logging
import os import os
import datetime
import stat
import mimetypes import mimetypes
import email
import time
import re import re
import tornado import tornado
@ -29,36 +25,6 @@ import tornado.util
import octoprint.util import octoprint.util
#~~ Monkey patching
def fix_ioloop_scheduling():
"""
This monkey patches tornado's :meth:``tornado.ioloop.PeriodicCallback._schedule_next`` method so it no longer
blocks for long times on slow machines (RPi) when the system time happens to change by a large amount (e.g. due to
the first ever contact to an NTP server).
Patch by @nosyjoe on Github. See this PR against tornado: https://github.com/tornadoweb/tornado/pull/1290
"""
import math
# patched implementation taken from PR
def _schedule_next(self):
if self._running:
current_time = self.io_loop.time()
if self._next_timeout <= current_time:
callback_time_sec = self.callback_time / 1000.0
self._next_timeout += (math.floor((current_time - self._next_timeout) / callback_time_sec) + 1) * callback_time_sec
self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run)
# replace original implementation with patched version
import tornado.ioloop
tornado.ioloop.PeriodicCallback._schedule_next = _schedule_next
#~~ WSGI middleware #~~ WSGI middleware
@ -560,7 +526,8 @@ class WsgiInputContainer(object):
if not data: if not data:
raise Exception("WSGI app did not call start_response") raise Exception("WSGI app did not call start_response")
status_code = int(data["status"].split()[0]) status_code, reason = data["status"].split(" ", 1)
status_code = int(status_code)
headers = data["headers"] headers = data["headers"]
header_set = set(k.lower() for (k, v) in headers) header_set = set(k.lower() for (k, v) in headers)
body = tornado.escape.utf8(body) body = tornado.escape.utf8(body)
@ -572,13 +539,12 @@ class WsgiInputContainer(object):
if "server" not in header_set: if "server" not in header_set:
headers.append(("Server", "TornadoServer/%s" % tornado.version)) headers.append(("Server", "TornadoServer/%s" % tornado.version))
parts = [tornado.escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")] start_line = tornado.httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
header_obj = tornado.httputil.HTTPHeaders()
for key, value in headers: for key, value in headers:
parts.append(tornado.escape.utf8(key) + b": " + tornado.escape.utf8(value) + b"\r\n") header_obj.add(key, value)
parts.append(b"\r\n") request.connection.write_headers(start_line, header_obj, chunk=body)
parts.append(body) request.connection.finish()
request.write(b"".join(parts))
request.finish()
self._log(status_code, request) self._log(status_code, request)
@staticmethod @staticmethod
@ -613,22 +579,22 @@ class WsgiInputContainer(object):
host = request.host host = request.host
port = 443 if request.protocol == "https" else 80 port = 443 if request.protocol == "https" else 80
environ = { environ = {
"REQUEST_METHOD": request.method, "REQUEST_METHOD": request.method,
"SCRIPT_NAME": "", "SCRIPT_NAME": "",
"PATH_INFO": to_wsgi_str(tornado.escape.url_unescape( "PATH_INFO": to_wsgi_str(tornado.escape.url_unescape(
request.path, encoding=None, plus=False)), request.path, encoding=None, plus=False)),
"QUERY_STRING": request.query, "QUERY_STRING": request.query,
"REMOTE_ADDR": request.remote_ip, "REMOTE_ADDR": request.remote_ip,
"SERVER_NAME": host, "SERVER_NAME": host,
"SERVER_PORT": str(port), "SERVER_PORT": str(port),
"SERVER_PROTOCOL": request.version, "SERVER_PROTOCOL": request.version,
"wsgi.version": (1, 0), "wsgi.version": (1, 0),
"wsgi.url_scheme": request.protocol, "wsgi.url_scheme": request.protocol,
"wsgi.input": request_body, "wsgi.input": request_body,
"wsgi.errors": sys.stderr, "wsgi.errors": sys.stderr,
"wsgi.multithread": False, "wsgi.multithread": False,
"wsgi.multiprocess": True, "wsgi.multiprocess": True,
"wsgi.run_once": False, "wsgi.run_once": False,
} }
if "Content-Type" in request.headers: if "Content-Type" in request.headers:
environ["CONTENT_TYPE"] = request.headers.pop("Content-Type") environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
@ -649,7 +615,7 @@ class WsgiInputContainer(object):
log_method = access_log.error log_method = access_log.error
request_time = 1000.0 * request.request_time() request_time = 1000.0 * request.request_time()
summary = request.method + " " + request.uri + " (" + \ summary = request.method + " " + request.uri + " (" + \
request.remote_ip + ")" request.remote_ip + ")"
log_method("%d %s %.2fms", status_code, summary, request_time) log_method("%d %s %.2fms", status_code, summary, request_time)
@ -672,36 +638,24 @@ class CustomHTTPServer(tornado.httpserver.HTTPServer):
``default_max_body_size`` is the default maximum body size to apply if no specific one from ``max_body_sizes`` matches. ``default_max_body_size`` is the default maximum body size to apply if no specific one from ``max_body_sizes`` matches.
""" """
def __init__(self, *args, **kwargs):
pass
def __init__(self, request_callback, no_keep_alive=False, io_loop=None, def initialize(self, *args, **kwargs):
xheaders=False, ssl_options=None, protocol=None, default_max_body_size = kwargs.pop("default_max_body_size", None)
decompress_request=False, max_body_sizes = kwargs.pop("max_body_sizes", None)
chunk_size=None, max_header_size=None,
idle_connection_timeout=None, body_timeout=None, tornado.httpserver.HTTPServer.initialize(self, *args, **kwargs)
max_body_sizes=None, default_max_body_size=None, max_buffer_size=None):
self.request_callback = request_callback additional = dict(default_max_body_size=default_max_body_size,
self.no_keep_alive = no_keep_alive max_body_sizes=max_body_sizes)
self.xheaders = xheaders self.conn_params = CustomHTTP1ConnectionParameters.from_stock_params(self.conn_params, **additional)
self.protocol = protocol
self.conn_params = CustomHTTP1ConnectionParameters(
decompress=decompress_request,
chunk_size=chunk_size,
max_header_size=max_header_size,
header_timeout=idle_connection_timeout or 3600,
max_body_sizes=max_body_sizes,
default_max_body_size=default_max_body_size,
body_timeout=body_timeout)
tornado.tcpserver.TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
max_buffer_size=max_buffer_size,
read_chunk_size=chunk_size)
self._connections = set()
def handle_stream(self, stream, address): def handle_stream(self, stream, address):
context = tornado.httpserver._HTTPRequestContext(stream, address, context = tornado.httpserver._HTTPRequestContext(stream, address,
self.protocol) self.protocol)
conn = CustomHTTP1ServerConnection( conn = CustomHTTP1ServerConnection(stream, self.conn_params, context)
stream, self.conn_params, context)
self._connections.add(conn) self._connections.add(conn)
conn.start_serving(self) conn.start_serving(self)
@ -718,7 +672,7 @@ class CustomHTTP1ServerConnection(tornado.http1connection.HTTP1ServerConnection)
try: try:
while True: while True:
conn = CustomHTTP1Connection(self.stream, False, conn = CustomHTTP1Connection(self.stream, False,
self.params, self.context) self.params, self.context)
request_delegate = delegate.start_request(self, conn) request_delegate = delegate.start_request(self, conn)
try: try:
ret = yield conn.read_response(request_delegate) ret = yield conn.read_response(request_delegate)
@ -762,8 +716,13 @@ class CustomHTTP1Connection(tornado.http1connection.HTTP1Connection):
current request exceeds the individual max content length, the request processing is aborted and an current request exceeds the individual max content length, the request processing is aborted and an
``HTTPInputError`` is raised. ``HTTPInputError`` is raised.
""" """
content_length = headers.get("Content-Length")
if "Content-Length" in headers: if "Content-Length" in headers:
if "Transfer-Encoding" in headers:
# Response cannot contain both Content-Length and
# Transfer-Encoding headers.
# http://tools.ietf.org/html/rfc7230#section-3.3.3
raise tornado.httputil.HTTPInputError(
"Response with both Transfer-Encoding and Content-Length")
if "," in headers["Content-Length"]: if "," in headers["Content-Length"]:
# Proxies sometimes cause Content-Length headers to get # Proxies sometimes cause Content-Length headers to get
# duplicated. If all the values are identical then we can # duplicated. If all the values are identical then we can
@ -774,9 +733,14 @@ class CustomHTTP1Connection(tornado.http1connection.HTTP1Connection):
"Multiple unequal Content-Lengths: %r" % "Multiple unequal Content-Lengths: %r" %
headers["Content-Length"]) headers["Content-Length"])
headers["Content-Length"] = pieces[0] headers["Content-Length"] = pieces[0]
content_length = int(headers["Content-Length"])
content_length = int(content_length) try:
content_length = int(headers["Content-Length"])
except ValueError:
# Handles non-integer Content-Length value.
raise tornado.httputil.HTTPInputError(
"Only integer Content-Length is allowed: %s" % headers["Content-Length"])
max_content_length = self._get_max_content_length(self._request_start_line.method, self._request_start_line.path) max_content_length = self._get_max_content_length(self._request_start_line.method, self._request_start_line.path)
if max_content_length is not None and 0 <= max_content_length < content_length: if max_content_length is not None and 0 <= max_content_length < content_length:
raise tornado.httputil.HTTPInputError("Content-Length too long") raise tornado.httputil.HTTPInputError("Content-Length too long")
@ -828,9 +792,20 @@ class CustomHTTP1ConnectionParameters(tornado.http1connection.HTTP1ConnectionPar
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
tornado.http1connection.HTTP1ConnectionParameters.__init__(self, args, kwargs) max_body_sizes = kwargs.pop("max_body_sizes", list())
self.max_body_sizes = kwargs["max_body_sizes"] if "max_body_sizes" in kwargs else list() default_max_body_size = kwargs.pop("default_max_body_size", None)
self.default_max_body_size = kwargs["default_max_body_size"] if "default_max_body_size" in kwargs else None
tornado.http1connection.HTTP1ConnectionParameters.__init__(self, *args, **kwargs)
self.max_body_sizes = max_body_sizes
self.default_max_body_size = default_max_body_size
@classmethod
def from_stock_params(cls, other, **additional):
kwargs = dict(other.__dict__)
for key, value in additional.items():
kwargs[key] = value
return cls(**kwargs)
#~~ customized large response handler #~~ customized large response handler

View file

@ -482,6 +482,17 @@ $(function() {
// reload overlay // reload overlay
$("#reloadui_overlay_reload").click(function() { location.reload(); }); $("#reloadui_overlay_reload").click(function() { location.reload(); });
var changeTab = function()
{
var hashtag = window.location.hash;
var tab = $('#tabs a[href="' + hashtag + '"]');
if (tab.length)
{
tab.tab("show");
onTabChange(hashtag);
}
}
//~~ view model binding //~~ view model binding
var bindViewModels = function() { var bindViewModels = function() {
@ -570,6 +581,15 @@ $(function() {
callViewModels(allViewModels, "onBrowserTabVisibilityChange", [status]); callViewModels(allViewModels, "onBrowserTabVisibilityChange", [status]);
}); });
$(window).on("hashchange", function() {
changeTab();
});
if (window.location.hash != "")
{
changeTab();
}
log.info("Application startup complete"); log.info("Application startup complete");
}; };

View file

@ -5,8 +5,8 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
from flask.ext.login import UserMixin from flask_login import UserMixin, AnonymousUserMixin
from flask.ext.principal import Identity from flask_principal import Identity
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
import hashlib import hashlib
import os import os
@ -416,6 +416,65 @@ class UnknownRole(Exception):
##~~ User object ##~~ User object
class MethodReplacedByBooleanProperty(object):
def __init__(self, name, message, getter):
self._name = name
self._message = message
self._getter = getter
@property
def _attr(self):
return self._getter()
def __call__(self):
from warnings import warn
warn(DeprecationWarning(self._message.format(name=self._name)), stacklevel=2)
return self._attr
def __eq__(self, other):
return self._attr == other
def __ne__(self, other):
return self._attr != other
def __bool__(self):
# Python 3
return self._attr
def __nonzero__(self):
# Python 2
return self._attr
def __hash__(self):
return hash(self._attr)
def __repr__(self):
return "MethodReplacedByProperty({}, {}, {})".format(self._name, self._message, self._getter)
def __str__(self):
return str(self._attr)
# TODO: Remove compatibility layer in OctoPrint 1.5.0
class FlaskLoginMethodReplacedByBooleanProperty(MethodReplacedByBooleanProperty):
def __init__(self, name, getter):
message = "{name} is now a property in Flask-Login versions >= 0.3.0, which OctoPrint now uses. " + \
"Use {name} instead of {name}(). This compatibility layer will be removed in OctoPrint 1.5.0."
MethodReplacedByBooleanProperty.__init__(self, name, message, getter)
# TODO: Remove compatibility layer in OctoPrint 1.5.0
class OctoPrintUserMethodReplacedByBooleanProperty(MethodReplacedByBooleanProperty):
def __init__(self, name, getter):
message = "{name} is now a property for consistency reasons with Flask-Login versions >= 0.3.0, which " + \
"OctoPrint now uses. Use {name} instead of {name}(). This compatibility layer will be removed " + \
"in OctoPrint 1.5.0."
MethodReplacedByBooleanProperty.__init__(self, name, message, getter)
class User(UserMixin): class User(UserMixin):
def __init__(self, username, passwordHash, active, roles, apikey=None, settings=None): def __init__(self, username, passwordHash, active, roles, apikey=None, settings=None):
self._username = username self._username = username
@ -431,9 +490,9 @@ class User(UserMixin):
def asDict(self): def asDict(self):
return { return {
"name": self._username, "name": self._username,
"active": self.is_active(), "active": bool(self.is_active),
"admin": self.is_admin(), "admin": bool(self.is_admin),
"user": self.is_user(), "user": bool(self.is_user),
"apikey": self._apikey, "apikey": self._apikey,
"settings": self._settings "settings": self._settings
} }
@ -447,14 +506,25 @@ class User(UserMixin):
def get_name(self): def get_name(self):
return self._username return self._username
@property
def is_anonymous(self):
return FlaskLoginMethodReplacedByBooleanProperty("is_anonymous", lambda: False)
@property
def is_authenticated(self):
return FlaskLoginMethodReplacedByBooleanProperty("is_authenticated", lambda: True)
@property
def is_active(self): def is_active(self):
return self._active return FlaskLoginMethodReplacedByBooleanProperty("is_active", lambda: self._active)
@property
def is_user(self): def is_user(self):
return "user" in self._roles return OctoPrintUserMethodReplacedByBooleanProperty("is_user", lambda: "user" in self._roles)
@property
def is_admin(self): def is_admin(self):
return "admin" in self._roles return OctoPrintUserMethodReplacedByBooleanProperty("is_admin", lambda: "admin" in self._roles)
def get_all_settings(self): def get_all_settings(self):
return self._settings return self._settings
@ -503,7 +573,23 @@ class User(UserMixin):
return True return True
def __repr__(self): def __repr__(self):
return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin()) return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), bool(self.is_active), bool(self.is_user), bool(self.is_admin))
class AnonymousUser(AnonymousUserMixin):
@property
def is_anonymous(self):
return FlaskLoginMethodReplacedByBooleanProperty("is_anonymous", lambda: True)
@property
def is_authenticated(self):
return FlaskLoginMethodReplacedByBooleanProperty("is_authenticated", lambda: False)
@property
def is_active(self):
return FlaskLoginMethodReplacedByBooleanProperty("is_active", lambda: False)
class SessionUser(User): class SessionUser(User):
def __init__(self, user): def __init__(self, user):
@ -535,7 +621,7 @@ class SessionUser(User):
self._user = user self._user = user
def __repr__(self): def __repr__(self):
return "SessionUser(id=%s,name=%s,active=%r,user=%r,admin=%r,session=%s,created=%s)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin(), self._session, self._created) return "SessionUser(id=%s,name=%s,active=%r,user=%r,admin=%r,session=%s,created=%s)" % (self.get_id(), self.get_name(), bool(self.is_active), bool(self.is_user), bool(self.is_admin), self._session, self._created)
##~~ DummyUser object to use when accessControl is disabled ##~~ DummyUser object to use when accessControl is disabled

View file

@ -147,6 +147,7 @@ def serialList():
+ glob.glob("/dev/tty.usb*") \ + glob.glob("/dev/tty.usb*") \
+ glob.glob("/dev/cu.*") \ + glob.glob("/dev/cu.*") \
+ glob.glob("/dev/cuaU*") \ + glob.glob("/dev/cuaU*") \
+ glob.glob("/dev/ttyS*") \
+ glob.glob("/dev/rfcomm*") + glob.glob("/dev/rfcomm*")
additionalPorts = settings().get(["serial", "additionalPorts"]) additionalPorts = settings().get(["serial", "additionalPorts"])
@ -1569,7 +1570,7 @@ class MachineCom(object):
def _poll_temperature(self): def _poll_temperature(self):
""" """
Polls the temperature after the temperature timeout, re-enqueues itself. Polls the temperature.
If the printer is not operational, closing the connection, not printing from sd, busy with a long running If the printer is not operational, closing the connection, not printing from sd, busy with a long running
command or heating, no poll will be done. command or heating, no poll will be done.
@ -1580,7 +1581,7 @@ class MachineCom(object):
def _poll_sd_status(self): def _poll_sd_status(self):
""" """
Polls the sd printing status after the sd status timeout, re-enqueues itself. Polls the sd printing status.
If the printer is not operational, closing the connection, not printing from sd, busy with a long running If the printer is not operational, closing the connection, not printing from sd, busy with a long running
command or heating, no poll will be done. command or heating, no poll will be done.