import argparse import ctypes import os import sys import tempfile import threading import time import webbrowser from typing import Dict, Optional from django.conf import ENVIRONMENT_VARIABLE from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import get_random_string from mypy_extensions import NoReturn DEVELOPMENT_VERSION = "Development Version" UNIX_VERSION = "Unix Version" WINDOWS_VERSION = "Windows Version" WINDOWS_PORTABLE_VERSION = "Windows Portable Version" class PortableDirNotWritable(Exception): pass class PortIsBlockedError(Exception): pass class DatabaseInSettingsError(Exception): pass class UnknownCommand(Exception): pass class ExceptionArgumentParser(argparse.ArgumentParser): def error(self, message: str) -> NoReturn: raise UnknownCommand(message) def detect_openslides_type() -> str: """ Returns the type of this OpenSlides version. """ if sys.platform == "win32": if os.path.basename(sys.executable).lower() == "openslides.exe": # Note: sys.executable is the path of the *interpreter* # the portable version embeds python so it *is* the interpreter. # The wrappers generated by pip and co. will spawn the usual # python(w).exe, so there is no danger of mistaking them # for the portable even though they may also be called # openslides.exe openslides_type = WINDOWS_PORTABLE_VERSION else: openslides_type = WINDOWS_VERSION else: openslides_type = UNIX_VERSION return openslides_type def get_default_settings_dir(openslides_type: str = None) -> str: """ Returns the default settings path according to the OpenSlides type. The argument 'openslides_type' has to be one of the three types mentioned in openslides.utils.main. """ if openslides_type is None: openslides_type = detect_openslides_type() if openslides_type == UNIX_VERSION: parent_directory = os.environ.get( "XDG_CONFIG_HOME", os.path.expanduser("~/.config") ) elif openslides_type == WINDOWS_VERSION: parent_directory = get_win32_app_data_dir() elif openslides_type == WINDOWS_PORTABLE_VERSION: parent_directory = get_win32_portable_dir() else: raise TypeError("%s is not a valid OpenSlides type." % openslides_type) return os.path.join(parent_directory, "openslides") def get_local_settings_dir() -> str: """ Returns the path to a local settings. On Unix systems: 'personal_data/var/' """ return os.path.join("personal_data", "var") def setup_django_settings_module( settings_path: str = None, local_installation: bool = False ) -> None: """ Sets the environment variable ENVIRONMENT_VARIABLE, that means 'DJANGO_SETTINGS_MODULE', to the given settings. If no settings_path is given and the environment variable is already set, then this function does nothing. If the argument settings_path is set, then the environment variable is always overwritten. """ if settings_path is None and os.environ.get(ENVIRONMENT_VARIABLE, ""): return if settings_path is None: if local_installation: settings_dir = get_local_settings_dir() else: settings_dir = get_default_settings_dir() settings_path = os.path.join(settings_dir, "settings.py") settings_file = os.path.basename(settings_path) settings_module_name = ".".join(settings_file.split(".")[:-1]) if "." in settings_module_name: raise ImproperlyConfigured( "'.' is not an allowed character in the settings-file" ) # Change the python path. Also set the environment variable python path, so # change of the python path also works after a reload settings_module_dir = os.path.abspath(os.path.dirname(settings_path)) sys.path.insert(0, settings_module_dir) try: os.environ["PYTHONPATH"] = os.pathsep.join( (settings_module_dir, os.environ["PYTHONPATH"]) ) except KeyError: # The environment variable is empty os.environ["PYTHONPATH"] = settings_module_dir # Set the environment variable to the settings module os.environ[ENVIRONMENT_VARIABLE] = settings_module_name def get_default_settings_context(user_data_dir: str = None) -> Dict[str, str]: """ Returns the default context values for the settings template: 'openslides_user_data_path', 'import_function' and 'debug'. The argument 'user_data_path' is a given path for user specific data or None. """ # Setup path for user specific data (SQLite3 database, media, ...): # Take it either from command line or get default path default_context = {} if user_data_dir: default_context["openslides_user_data_dir"] = repr(user_data_dir) default_context["import_function"] = "" else: openslides_type = detect_openslides_type() if openslides_type == WINDOWS_PORTABLE_VERSION: default_context[ "openslides_user_data_dir" ] = "get_win32_portable_user_data_dir()" default_context[ "import_function" ] = "from openslides.utils.main import get_win32_portable_user_data_dir" else: data_dir = get_default_user_data_dir(openslides_type) default_context["openslides_user_data_dir"] = repr( os.path.join(data_dir, "openslides") ) default_context["import_function"] = "" default_context["debug"] = "False" return default_context def get_default_user_data_dir(openslides_type: str) -> str: """ Returns the default directory for user specific data according to the OpenSlides type. The argument 'openslides_type' has to be one of the three types mentioned in openslides.utils.main. """ if openslides_type == UNIX_VERSION: default_user_data_dir = os.environ.get( "XDG_DATA_HOME", os.path.expanduser("~/.local/share") ) elif openslides_type == WINDOWS_VERSION: default_user_data_dir = get_win32_app_data_dir() elif openslides_type == WINDOWS_PORTABLE_VERSION: default_user_data_dir = get_win32_portable_dir() else: raise TypeError("%s is not a valid OpenSlides type." % openslides_type) return default_user_data_dir def get_win32_app_data_dir() -> str: """ Returns the directory of Windows' AppData directory. """ shell32 = ctypes.WinDLL("shell32.dll") # type: ignore SHGetFolderPath = shell32.SHGetFolderPathW SHGetFolderPath.argtypes = ( ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_wchar_p, ) SHGetFolderPath.restype = ctypes.c_uint32 CSIDL_LOCAL_APPDATA = 0x001C MAX_PATH = 260 buf = ctypes.create_unicode_buffer(MAX_PATH) res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf) if res != 0: # TODO: Write other exception raise Exception("Could not determine Windows' APPDATA path") return buf.value # type: ignore def get_win32_portable_dir() -> str: """ Returns the directory of the Windows portable version. """ # NOTE: sys.executable will be the path to openslides.exe # since it is essentially a small wrapper that embeds the # python interpreter portable_dir = os.path.dirname(os.path.abspath(sys.executable)) try: fd, test_file = tempfile.mkstemp(dir=portable_dir) except OSError: raise PortableDirNotWritable( "Portable directory is not writeable. " "Please choose another directory for settings and data files." ) else: os.close(fd) os.unlink(test_file) return portable_dir def get_win32_portable_user_data_dir() -> str: """ Returns the user data directory to the Windows portable version. """ return os.path.join(get_win32_portable_dir(), "openslides") def write_settings( settings_dir: str = None, settings_filename: str = "settings.py", template: str = None, **context: str, ) -> str: """ Creates the settings file at the given dir using the given values for the file template. Retuns the path to the created settings. """ if settings_dir is None: settings_dir = get_default_settings_dir() settings_path = os.path.join(settings_dir, settings_filename) if template is None: with open( os.path.join(os.path.dirname(__file__), "settings.py.tpl") ) as template_file: template = template_file.read() # Create a random SECRET_KEY to put it in the settings. # from django.core.management.commands.startproject chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)" context.setdefault("secret_key", get_random_string(50, chars)) for key, value in get_default_settings_context().items(): context.setdefault(key, value) content = template % context settings_module = os.path.realpath(settings_dir) if not os.path.exists(settings_module): os.makedirs(settings_module) with open(settings_path, "w") as settings_file: settings_file.write(content) if context["openslides_user_data_dir"] == "get_win32_portable_user_data_dir()": openslides_user_data_dir = get_win32_portable_user_data_dir() else: openslides_user_data_dir = context["openslides_user_data_dir"].strip("'") os.makedirs(os.path.join(openslides_user_data_dir, "static"), exist_ok=True) return os.path.realpath(settings_path) def open_browser(host: str, port: int) -> None: """ Launches the default web browser at the given host and port and opens the webinterface. Uses start_browser internally. """ if host == "0.0.0.0": # Windows does not support 0.0.0.0, so use 'localhost' instead start_browser("http://localhost:%s" % port) else: start_browser("http://%s:%s" % (host, port)) def start_browser(browser_url: str) -> None: """ Launches the default web browser at the given url and opens the webinterface. """ try: browser = webbrowser.get() except webbrowser.Error: print("Could not locate runnable browser: Skipping start") else: def function() -> None: # TODO: Use a nonblocking sleep event here. Tornado has such features. time.sleep(1) browser.open(browser_url) thread = threading.Thread(target=function) thread.start() def get_database_path_from_settings() -> Optional[str]: """ Retrieves the database path out of the settings file. Returns None, if it is not a SQLite3 database. Needed for the backupdb command. """ from django.conf import settings as django_settings from django.db import DEFAULT_DB_ALIAS db_settings = django_settings.DATABASES default = db_settings.get(DEFAULT_DB_ALIAS) if not default: raise DatabaseInSettingsError("Default databases is not configured") database_path = default.get("NAME") if not database_path: raise DatabaseInSettingsError("No path or name specified for default database.") if default.get("ENGINE") != "django.db.backends.sqlite3": database_path = None return database_path def is_local_installation() -> bool: """ Returns True if the command is called for a local installation This is the case if manage.py is used, or when the --local-installation flag is set. """ return ( True if "--local-installation" in sys.argv or "manage.py" in sys.argv[0] else False ) def is_windows() -> bool: """ Returns True if the current system is Windows. Returns False otherwise. """ return sys.platform == "win32"