diff --git a/docs/features/accesscontrol.rst b/docs/features/accesscontrol.rst new file mode 100644 index 00000000..dd59bd6b --- /dev/null +++ b/docs/features/accesscontrol.rst @@ -0,0 +1,69 @@ +.. _sec-features-access_control: + +Access Control +============== + +When Access Control is enabled, anonymous users (not logged in) will only see +the read-only parts of the UI which are the following: + + * printer state + * available gcode files and stats (upload is disabled) + * temperature + * webcam + * gcode viewer + * terminal output (sending commands is disabled) + * available timelapse movies + * any components provided through plugins which are enabled for anonymous + users + +Logged in users will get access to everything besides the Settings and System +Commands, which are admin-only. + +If Access Control is disabled, everything is directly accessible. **That also +includes all administrative functionality as well as full control over the +printer!** + +Upon first start a configuration wizard is provided which allows configuration +of the first administrator account or alternatively disabling Access Control +(which is **NOT** recommended for systems that are directly accessible via the +Internet!). + +.. hint:: + + If you plan to have your OctoPrint instance accessible over the internet, + **always enable Access Control**. + +.. _sec-features-access_control-rerunning_wizard: + +Rerunning the wizard +-------------------- + +In case Access Control was disabled in the configuration wizard, it is +possibly to re-run it by editing ``config.yaml`` [#f1]_ and setting ``firstRun`` +in the ``server`` section and ``enabled`` in the ``accessControl`` section to +``true``: + +.. code-block-ext:: yaml + + accessControl: + enabled: true + # ... + server: + firstRun: true + +Then restart the server and connect to the web interface - the wizard should +be shown again. + +.. note:: + + If user accounts were created prior to disabling Access Control and those + user accounts are not to be used any more, remove ``.octoprint/users.yaml``. + If you don't remove this file, the above changes won't lead to the + configuration being shown again, instead Access Control will just be + enabled using the already existing login data. This is to prevent you from + resetting access control by accident. + +.. rubric:: Footnotes + +.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for + Mac ``~/Library/Application Support/OctoPrint/config.yaml`` diff --git a/docs/features/custom_controls.rst b/docs/features/custom_controls.rst index a904748f..983611fc 100644 --- a/docs/features/custom_controls.rst +++ b/docs/features/custom_controls.rst @@ -10,7 +10,7 @@ buttons which trigger sending of one or more lines of GCODE to the printer over parameterization of these commands with values entered by the user to full blown GCODE script templates backed by `Jinja2 `_. -Custom controls are configured within :ref:`config.yaml ` in a ``controls`` section which +Custom controls are configured within :ref:`config.yaml ` [#f1]_ in a ``controls`` section which basically represents a hierarchical structure of all configured custom controls of various types. .. note:: @@ -94,6 +94,13 @@ button that sends one or more commands to the printer when clicked, displaying o controls that just serve as *container* for other controls, the latter being identified by having a ``children`` attribute wrapping more controls. +.. hint:: + + Take a look at the `Custom Control Editor plugin `_ + which allows you configuring your Custom Controls through OctoPrint's + settings interface without the need to manually edit the configuration + file. + .. _sec-features-custom_controls-types: Types @@ -278,4 +285,9 @@ Parameterized GCODE Script G28 X0 Y0 Note the usage of the ``parameters.repetitions`` template variable in the GCODE script template, which will contain -the value selected by the user for the "Go arounds" slider. \ No newline at end of file +the value selected by the user for the "Go arounds" slider. + +.. rubric:: Footnotes + +.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for + Mac ``~/Library/Application Support/OctoPrint/config.yaml`` diff --git a/docs/features/index.rst b/docs/features/index.rst index 5ebc8620..c4b1a713 100644 --- a/docs/features/index.rst +++ b/docs/features/index.rst @@ -7,6 +7,7 @@ Features .. toctree:: :maxdepth: 2 + accesscontrol.rst custom_controls.rst gcode_scripts.rst action_commands.rst diff --git a/docs/features/plugins.rst b/docs/features/plugins.rst index c8fb1fd5..1b65b6dc 100644 --- a/docs/features/plugins.rst +++ b/docs/features/plugins.rst @@ -64,7 +64,7 @@ See :ref:`Developing Plugins `. .. rubric:: Footnotes .. [#f1] For Linux that will be ``~/.octoprint/plugins``, for Windows it will be ``%APPDATA%/OctoPrint/plugins`` and for - Mac ``~/Library/Application Support/OctoPrint`` + Mac ``~/Library/Application Support/OctoPrint/plugins`` .. [#f2] Make sure to use the exact same Python installation for installing the plugin that you also used for installing & running OctoPrint. For OctoPi this means using ``~/oprint/bin/pip`` for installing plugins instead of just ``pip``. diff --git a/docs/index.rst b/docs/index.rst index 06895909..1e619c43 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,10 @@ Welcome to OctoPrint's documentation! :alt: The OctoPrint Logo :align: right +This documentation is still in the process of being migrated from +`OctoPrint's wiki `_, so also take +a look there! + Contents ======== diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index e1ce090c..6b1b7291 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -218,7 +218,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, if not on_progress_kwargs: on_progress_kwargs = dict() - self._cura_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) + self._cura_logger.info(u"### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY) @@ -227,16 +227,15 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, return False, "Path to CuraEngine is not configured " working_dir, _ = os.path.split(executable) - args = ['"%s"' % executable, '-v', '-p'] + args = [executable, '-v', '-p'] for k, v in engine_settings.items(): - args += ["-s", '"%s=%s"' % (k, str(v))] - args += ['-o', '"%s"' % machinecode_path, '"%s"' % model_path] + args += ["-s", "%s=%s" % (k, str(v))] + args += ["-o", machinecode_path, model_path] + + self._logger.info(u"Running %r in %s" % (" ".join(args), working_dir)) import sarge - command = " ".join(args) - self._logger.info("Running %r in %s" % (command, working_dir)) - - p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + p = sarge.run(args, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) p.wait_events() self._slicing_commands[machinecode_path] = p.commands[0] @@ -254,6 +253,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, p.commands[0].poll() continue + line = octoprint.util.to_unicode(line, errors="replace") self._cura_logger.debug(line.strip()) if on_progress is not None: @@ -282,14 +282,14 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, # # with being 0 for "inset", 1 for "skin" and 2 for "export". - if line.startswith("Layer count:") and layer_count is None: + if line.startswith(u"Layer count:") and layer_count is None: try: - layer_count = float(line[len("Layer count:"):].strip()) + layer_count = float(line[len(u"Layer count:"):].strip()) except: pass - elif line.startswith("Progress:"): - split_line = line[len("Progress:"):].strip().split(":") + elif line.startswith(u"Progress:"): + split_line = line[len(u"Progress:"):].strip().split(":") if len(split_line) == 3: step, current_layer, _ = split_line try: @@ -302,21 +302,21 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, on_progress_kwargs["_progress"] = (step_factor[step] * layer_count + current_layer) / (layer_count * 3) on_progress(*on_progress_args, **on_progress_kwargs) - elif line.startswith("Print time:"): + elif line.startswith(u"Print time:"): try: - print_time = int(line[len("Print time:"):].strip()) + print_time = int(line[len(u"Print time:"):].strip()) if analysis is None: analysis = dict() analysis["estimatedPrintTime"] = print_time except: pass - elif line.startswith("Filament:") or line.startswith("Filament2:"): - if line.startswith("Filament:"): - filament_str = line[len("Filament:"):].strip() + elif line.startswith(u"Filament:") or line.startswith(u"Filament2:"): + if line.startswith(u"Filament:"): + filament_str = line[len(u"Filament:"):].strip() tool_key = "tool0" else: - filament_str = line[len("Filament2:"):].strip() + filament_str = line[len(u"Filament2:"):].strip() tool_key = "tool1" try: @@ -339,20 +339,20 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, with self._job_mutex: if machinecode_path in self._cancelled_jobs: - self._cura_logger.info("### Cancelled") + self._cura_logger.info(u"### Cancelled") raise octoprint.slicing.SlicingCancelled() - self._cura_logger.info("### Finished, returncode %d" % p.returncode) + self._cura_logger.info(u"### Finished, returncode %d" % p.returncode) if p.returncode == 0: return True, dict(analysis=analysis) else: - self._logger.warn("Could not slice via Cura, got return code %r" % p.returncode) + self._logger.warn(u"Could not slice via Cura, got return code %r" % p.returncode) return False, "Got returncode %r" % p.returncode except octoprint.slicing.SlicingCancelled as e: raise e except: - self._logger.exception("Could not slice via Cura, got an unknown error") + self._logger.exception(u"Could not slice via Cura, got an unknown error") return False, "Unknown error, please consult the log file" finally: @@ -371,7 +371,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, command = self._slicing_commands[machinecode_path] if command is not None: command.terminate() - self._logger.info("Cancelled slicing of %s" % machinecode_path) + self._logger.info(u"Cancelled slicing of %s" % machinecode_path) def _load_profile(self, path): import yaml diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 2c234bde..05d17b6a 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -420,13 +420,13 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, return self._pip_caller.execute(*args) def _log_call(self, *lines): - self._log(lines, prefix=" ", stream="call") + self._log(lines, prefix=u" ", stream="call") def _log_stdout(self, *lines): - self._log(lines, prefix=">", stream="stdout") + self._log(lines, prefix=u">", stream="stdout") def _log_stderr(self, *lines): - self._log(lines, prefix="!", stream="stderr") + self._log(lines, prefix=u"!", stream="stderr") def _log(self, lines, prefix=None, stream=None, strip=True): if strip: diff --git a/src/octoprint/plugins/softwareupdate/updaters/pip.py b/src/octoprint/plugins/softwareupdate/updaters/pip.py index b3fbdb5e..0828ec15 100644 --- a/src/octoprint/plugins/softwareupdate/updaters/pip.py +++ b/src/octoprint/plugins/softwareupdate/updaters/pip.py @@ -62,12 +62,12 @@ def perform_update(target, check, target_version, log_cb=None): install_arg = check["pip"].format(target_version=target_version) - logger.debug("Target: %s, executing pip install %s" % (target, install_arg)) + logger.debug(u"Target: %s, executing pip install %s" % (target, install_arg)) pip_args = ["install", check["pip"].format(target_version=target_version, target=target_version)] pip_caller.execute(*pip_args) - logger.debug("Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) + logger.debug(u"Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] pip_caller.execute(*pip_args) diff --git a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py index 0d2abf69..5acddc16 100644 --- a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py +++ b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py @@ -44,35 +44,71 @@ def _get_latest_release(user, repo, include_prerelease=False): return latest["name"], latest["tag_name"] -def _is_current(release_information, compare_type, custom=None): +def _get_sanitized_version(version_string): + if "-" in version_string: + version_string = version_string[:version_string.find("-")] + return version_string + + +def _get_comparable_version_pkg_resources(version_string, force_base=True): + import pkg_resources + + version = pkg_resources.parse_version(version_string) + + if force_base: + if isinstance(version, tuple): + # old setuptools + base_version = [] + for part in version: + if part.startswith("*"): + break + base_version.append(part) + version = tuple(base_version) + else: + # new setuptools + version = pkg_resources.parse_version(version.base_version) + + return version + + +def _get_comparable_version_semantic(version_string, force_base=True): + import semantic_version + + version = semantic_version.Version.coerce(version_string, partial=False) + + if force_base: + version_string = "{}.{}.{}".format(version.major, version.minor, version.patch) + version = semantic_version.Version.coerce(version_string, partial=False) + + return version + + +def _is_current(release_information, compare_type, custom=None, force_base=True): if release_information["remote"]["value"] is None: return True if not compare_type in ("python", "semantic", "unequal", "custom") or compare_type == "custom" and custom is None: compare_type = "python" + sanitized_local = _get_sanitized_version(release_information["local"]["value"]) + sanitized_remote = _get_sanitized_version(release_information["remote"]["value"]) + try: if compare_type == "python": - import pkg_resources - - local_version = pkg_resources.parse_version(release_information["local"]["value"]) - remote_version = pkg_resources.parse_version(release_information["remote"]["value"]) - + local_version = _get_comparable_version_pkg_resources(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base) return local_version >= remote_version elif compare_type == "semantic": - import semantic_version - - local_version = semantic_version.Version(release_information["local"]["value"]) - remote_version = semantic_version.Version(release_information["remote"]["value"]) - + local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base) return local_version >= remote_version elif compare_type == "custom": - return custom(release_information["local"], release_information["remote"]) + return custom(sanitized_local, sanitized_remote) else: - return release_information["local"]["value"] == release_information["remote"]["value"] + return sanitized_local == sanitized_remote except: logger.exception("Could not check if version is current due to an error, assuming it is") return True @@ -82,11 +118,13 @@ def get_latest(target, check, custom_compare=None): if not "user" in check or not "repo" in check: raise ConfigurationInvalid("github_release update configuration for %s needs user and repo set" % target) - current = None - if "current" in check: - current = check["current"] + current = check.get("current", None) + include_prerelease = check.get("prerelease", False) + force_base = check.get("force_base", True) - remote_name, remote_tag = _get_latest_release(check["user"], check["repo"], include_prerelease=check["prerelease"] == True if "prerelease" in check else False) + remote_name, remote_tag = _get_latest_release(check["user"], + check["repo"], + include_prerelease=include_prerelease) compare_type = check["release_compare"] if "release_compare" in check else "python" information =dict( @@ -96,4 +134,7 @@ def get_latest(target, check, custom_compare=None): logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_tag)) - return information, _is_current(information, compare_type, custom=custom_compare) + return information, _is_current(information, + compare_type, + custom=custom_compare, + force_base=force_base) diff --git a/src/octoprint/server/util/watchdog.py b/src/octoprint/server/util/watchdog.py index ae9e33c3..8bda659b 100644 --- a/src/octoprint/server/util/watchdog.py +++ b/src/octoprint/server/util/watchdog.py @@ -56,6 +56,11 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler): file_wrapper.filename, file_wrapper, allow_overwrite=True) + if os.path.exists(path): + try: + os.remove(path) + except: + self._logger.exception("Error while trying to clear a file from the watched folder") def on_created(self, event): self._upload(event.src_path) diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 9b63618e..3b3d03a4 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -352,9 +352,7 @@ def silent_remove(file): def sanitize_ascii(line): if not isinstance(line, basestring): raise ValueError("Expected either str or unicode but got {} instead".format(line.__class__.__name__ if line is not None else None)) - if isinstance(line, str): - line = unicode(line, 'ascii', 'replace') - return line.encode('ascii', 'replace').rstrip() + return to_unicode(line, encoding="ascii", errors="replace").rstrip() def filter_non_ascii(line): @@ -369,12 +367,28 @@ def filter_non_ascii(line): """ try: - unicode(line, 'ascii').encode('ascii') + to_str(to_unicode(line, encoding="ascii"), encoding="ascii") return False except ValueError: return True +def to_str(s_or_u, encoding="utf-8", errors="strict"): + """Make sure ``s_or_u`` is a str.""" + if isinstance(s_or_u, unicode): + return s_or_u.encode(encoding, errors=errors) + else: + return s_or_u + + +def to_unicode(s_or_u, encoding="utf-8", errors="strict"): + """Make sure ``s_or_u`` is a unicode string.""" + if isinstance(s_or_u, str): + return s_or_u.decode(encoding, errors=errors) + else: + return s_or_u + + def dict_merge(a, b): """ Recursively deep-merges two dictionaries.