#!/bin/sh
# Put this file beside oneFlow's compiled index.html and app folder.
# On a Mac, run chmod +x start-oneflow.command once, then double-click this file.
# Requires Python 3.10 or newer. No Node.js, npm, or administrator access.
set -u
if ! command -v python3 >/dev/null 2>&1 || ! python3 -c 'import sys; sys.exit(0 if sys.version_info.major == 3 and sys.version_info >= (3, 10) else 1)' >/dev/null 2>&1; then
    printf '%s\n' 'oneFlow needs Python 3.10 or newer to start this local server.' 'Install Python 3 for macOS from https://www.python.org/downloads/macos/,' 'then double-click this file again. No Node.js or npm is needed.'
    if [ -t 0 ]; then printf '\nPress Return to close this window. '; read -r _oneflow_reply; fi
    exit 1
fi
python3 - "$0" "$@" <<'ONEFLOW_PYTHON'
# The server is deliberately limited to the compiled app, never the whole folder.
import errno
import mimetypes
import os
from pathlib import Path
import shutil
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlsplit
import webbrowser

PORT = 8080
ADDRESS = "http://localhost:8080"
LAUNCHER = Path(sys.argv[1]).resolve()


def find_app_root():
    candidates = [LAUNCHER.parent]
    if (LAUNCHER.parent.name.casefold() == "downloads"
            and LAUNCHER.parent.parent.name.casefold() == "retailsite"):
        candidates.append(LAUNCHER.parent.parent.parent)
    for candidate in candidates:
        index = candidate / "index.html"
        app = candidate / "app"
        if (index.is_file() and not index.is_symlink()
                and app.is_dir() and not app.is_symlink()
                and (app / "main.js").is_file()):
            return candidate.resolve()
    return None


def main():
    root = find_app_root()
    if root is None:
        print("The compiled oneFlow application was not found.", flush=True)
        print("Put this startup file beside oneFlow's index.html and app folder,")
        print("then start it again. Keep the complete app folder together.")
        return 1
    app_root = root / "app"

    class AppHandler(BaseHTTPRequestHandler):
        server_version = "oneFlowLocal/1.0"
        sys_version = ""

        def log_message(self, *_args):
            # Do not log requests, configuration contents, or personal records.
            pass

        def end_headers(self):
            self.send_header("X-Content-Type-Options", "nosniff")
            self.send_header("Cache-Control", "no-store")
            self.send_header("Referrer-Policy", "no-referrer")
            super().end_headers()

        def method_not_allowed(self):
            self.send_response(405)
            self.send_header("Allow", "GET, HEAD")
            self.send_header("Content-Length", "0")
            self.end_headers()

        def __getattr__(self, name):
            if name.startswith("do_"):
                return self.method_not_allowed
            raise AttributeError(name)

        def do_GET(self):
            self.serve_file(False)

        def do_HEAD(self):
            self.serve_file(True)

        def serve_file(self, head_only):
            if self.headers.get("Host", "").casefold() not in {
                "localhost", "localhost:8080", "127.0.0.1", "127.0.0.1:8080"
            }:
                self.send_error(403, "Local host required")
                return
            try:
                target = urlsplit(self.path)
                if target.scheme or target.netloc:
                    raise ValueError("Invalid request target")
                path = unquote(target.path, errors="strict")
            except (ValueError, UnicodeError):
                self.send_error(400, "Invalid path")
                return
            if (not path.startswith("/") or "\\" in path
                    or "\x00" in path or ":" in path):
                self.send_error(400, "Invalid path")
                return
            parts = path.lstrip("/").split("/")
            if any(part.startswith(".") for part in parts):
                self.send_error(404, "File unavailable")
                return
            if path in {"/", "/index.html"}:
                filename = root / "index.html"
                permitted_root = root
            elif path.startswith("/app/") and all(parts):
                filename = root.joinpath(*parts)
                permitted_root = app_root
            else:
                self.send_error(404, "File unavailable")
                return
            try:
                resolved = filename.resolve()
                relative = resolved.relative_to(permitted_root)
                if any(part.startswith(".") for part in relative.parts):
                    raise ValueError("Hidden file")
                if path in {"/", "/index.html"} and resolved != filename:
                    raise ValueError("Invalid index")
                if not resolved.is_file():
                    raise FileNotFoundError()
                stream = resolved.open("rb")
            except (ValueError, OSError):
                self.send_error(404, "File unavailable")
                return
            with stream:
                size = os.fstat(stream.fileno()).st_size
                content_type = {
                    ".js": "text/javascript",
                    ".mjs": "text/javascript",
                    ".wasm": "application/wasm",
                    ".html": "text/html",
                    ".css": "text/css",
                    ".json": "application/json",
                }.get(resolved.suffix.casefold())
                if content_type is None:
                    content_type = mimetypes.guess_type(str(resolved))[0]
                content_type = content_type or "application/octet-stream"
                if content_type.startswith("text/"):
                    content_type += "; charset=utf-8"
                self.send_response(200)
                self.send_header("Content-Type", content_type)
                self.send_header("Content-Length", str(size))
                self.end_headers()
                if not head_only:
                    try:
                        shutil.copyfileobj(stream, self.wfile)
                    except (BrokenPipeError, ConnectionResetError):
                        pass

    try:
        server = ThreadingHTTPServer(("127.0.0.1", PORT), AppHandler)
    except OSError as error:
        if error.errno in {errno.EADDRINUSE, 10048}:
            print("Port 8080 is already in use. Close the other local server or")
            print("oneFlow startup window, then start this file again.", flush=True)
        else:
            print("The local server could not start: {}".format(error), flush=True)
            print("Check that local connections on port 8080 are allowed.")
        return 1

    print("oneFlow Freelancer is ready at " + ADDRESS, flush=True)
    print("Keep this window open while using oneFlow.")
    print("Use the same browser and address each time to keep your workspace.")
    print("Press Ctrl+C here to stop local hosting.", flush=True)

    def open_browser():
        try:
            if not webbrowser.open(ADDRESS):
                print("Open " + ADDRESS + " in your browser.", flush=True)
        except Exception:
            print("Open " + ADDRESS + " in your browser.", flush=True)

    if os.environ.get("ONEFLOW_NO_BROWSER") != "1":
        timer = threading.Timer(0.25, open_browser)
        timer.daemon = True
        timer.start()
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\noneFlow local hosting stopped.", flush=True)
    finally:
        server.server_close()
    return 0


raise SystemExit(main())
ONEFLOW_PYTHON
_oneflow_status=$?
if [ "$_oneflow_status" -ne 0 ] && [ -t 0 ]; then
    printf '\nPress Return to close this window. '
    read -r _oneflow_reply
fi
exit "$_oneflow_status"

