Comfy Registry flagged 1.1.2, likely on the inline Add-Type C# compilation and raw ComImport/Marshal COM interop used for the native folder dialog — that pattern reads as COM-hijack-style code to automated security scanners. Swapped for the plain System.Windows.Forms.FolderBrowserDialog (no dynamic code compilation, no COM interop), preferring pwsh (modern Explorer-style look) and falling back to powershell.exe (classic tree-view) when pwsh isn't installed.
162 lines
6.7 KiB
Python
162 lines
6.7 KiB
Python
WEB_DIRECTORY = "./js"
|
|
|
|
from .smart_prompt_controller import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
|
|
from .smart_profile_switch import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
|
|
from .smart_resize import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
|
|
from .smart_batch_resize import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES
|
|
from .smart_resolution_multiplier import NODE_CLASS_MAPPINGS as E, NODE_DISPLAY_NAME_MAPPINGS as E_NAMES
|
|
|
|
NODE_CLASS_MAPPINGS = {**A, **B, **C, **D, **E}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES, **E_NAMES}
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
|
try:
|
|
import asyncio
|
|
import concurrent.futures
|
|
import os
|
|
import threading
|
|
from aiohttp import web
|
|
from server import PromptServer
|
|
|
|
_dialog_lock = threading.Lock()
|
|
# Must match the literal 'CRAFTKIT_DIALOG_ERROR:' prefix written by the
|
|
# PowerShell catch block below (kept literal there to avoid brace-escaping
|
|
# a dense here-string).
|
|
_DIALOG_ERROR_PREFIX = "CRAFTKIT_DIALOG_ERROR:"
|
|
|
|
def _open_dialog_macos():
|
|
import subprocess
|
|
script = 'POSIX path of (choose folder with prompt "Select input folder")'
|
|
try:
|
|
result = subprocess.run(
|
|
["osascript", "-e", script],
|
|
capture_output=True, text=True, timeout=300
|
|
)
|
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
return ""
|
|
if result.returncode != 0:
|
|
return "" # cancelled, or osascript unavailable
|
|
return result.stdout.strip()
|
|
|
|
_LINUX_PICKER_COMMANDS = [
|
|
["zenity", "--file-selection", "--directory", "--title=Select input folder"],
|
|
["kdialog", "--getexistingdirectory", os.path.expanduser("~"), "--title", "Select input folder"],
|
|
]
|
|
|
|
def _open_dialog_linux():
|
|
import subprocess
|
|
for cmd in _LINUX_PICKER_COMMANDS:
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
except FileNotFoundError:
|
|
continue
|
|
except subprocess.TimeoutExpired:
|
|
return ""
|
|
if result.returncode != 0:
|
|
return "" # cancelled
|
|
return result.stdout.strip()
|
|
print("[CraftKit] No folder picker found (tried zenity, kdialog); paste input folder paths manually.")
|
|
return ""
|
|
|
|
def _open_dialog_windows():
|
|
# Deliberately uses the plain .NET FolderBrowserDialog instead of the
|
|
# earlier IFileDialog/COM-interop approach (which used Add-Type to
|
|
# compile inline C#, ComImport, and raw Marshal/IntPtr calls). That
|
|
# pattern reads as COM-hijack-style shellcode to automated registry
|
|
# scanners even though it was benign; this trades the modern Explorer
|
|
# look for a plain tree-view dialog, in exchange for a much smaller,
|
|
# ordinary-looking script with no dynamic code compilation.
|
|
#
|
|
# Reverting to the modern look: the full IFileDialog/COM version
|
|
# (modern Explorer-style picker, works on Windows PowerShell 5.1 too)
|
|
# lived here before commit that introduced this comment — `git log -p
|
|
# -- __init__.py` to dig it out — and can be restored if the registry
|
|
# confirms that code pattern wasn't actually the scan trigger, or once
|
|
# pwsh is common enough that this fallback rarely matters. See
|
|
# [[comfyui-browse-folder-dialog]] memory for the full history/tradeoffs.
|
|
import subprocess
|
|
ps = r"""
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
$r = ''
|
|
$o = New-Object System.Windows.Forms.Form
|
|
$o.TopMost = $true
|
|
$o.ShowInTaskbar = $false
|
|
$o.FormBorderStyle = 'None'
|
|
$o.Width = 1; $o.Height = 1; $o.Opacity = 0
|
|
$o.StartPosition = 'CenterScreen'
|
|
$o.Add_Shown({
|
|
$o.Activate()
|
|
try {
|
|
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
$dlg.Description = 'Select input folder'
|
|
$dlg.ShowNewFolderButton = $true
|
|
if ($dlg.ShowDialog($o) -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
$script:r = $dlg.SelectedPath
|
|
}
|
|
} catch {
|
|
# Must match _DIALOG_ERROR_PREFIX in __init__.py.
|
|
$script:r = 'CRAFTKIT_DIALOG_ERROR:' + $_.Exception.Message
|
|
}
|
|
$o.Close()
|
|
})
|
|
$o.ShowDialog() | Out-Null
|
|
$r
|
|
"""
|
|
# PowerShell 7+ (pwsh, .NET 5+) renders FolderBrowserDialog with the
|
|
# modern Explorer-style picker automatically; Windows PowerShell 5.1
|
|
# (.NET Framework) only has the classic tree-view. Prefer pwsh when
|
|
# present and fall back to the always-available powershell.exe.
|
|
for exe in ("pwsh", "powershell"):
|
|
try:
|
|
result = subprocess.run(
|
|
[exe, "-NoProfile", "-Command", ps],
|
|
capture_output=True, text=True, timeout=300
|
|
)
|
|
except FileNotFoundError:
|
|
continue
|
|
except subprocess.TimeoutExpired:
|
|
return ""
|
|
return result.stdout.strip()
|
|
return ""
|
|
|
|
def _open_dialog():
|
|
import sys
|
|
if sys.platform == "win32":
|
|
return _open_dialog_windows()
|
|
elif sys.platform == "darwin":
|
|
return _open_dialog_macos()
|
|
else:
|
|
return _open_dialog_linux()
|
|
|
|
async def browse_folder(request):
|
|
if request.remote not in ("127.0.0.1", "::1"):
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
origin = request.headers.get("Origin")
|
|
if origin is not None:
|
|
host = request.headers.get("Host", "")
|
|
origin_host = origin.split("://", 1)[-1]
|
|
if origin_host != host:
|
|
return web.json_response({"ok": False, "error": "forbidden"}, status=403)
|
|
|
|
if not _dialog_lock.acquire(blocking=False):
|
|
return web.json_response({"ok": False, "error": "A folder dialog is already open."}, status=409)
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
folder = await loop.run_in_executor(pool, _open_dialog)
|
|
finally:
|
|
_dialog_lock.release()
|
|
|
|
if folder.startswith(_DIALOG_ERROR_PREFIX):
|
|
return web.json_response({"ok": False, "error": folder.removeprefix(_DIALOG_ERROR_PREFIX)}, status=500)
|
|
|
|
if folder and os.path.isdir(folder):
|
|
return web.json_response({"ok": True, "path": folder})
|
|
return web.json_response({"ok": False, "cancelled": True})
|
|
|
|
PromptServer.instance.routes.post("/craftkit/browse_folder")(browse_folder)
|
|
except Exception as e:
|
|
print(f"[CraftKit] Browse folder endpoint unavailable ({e}); paste input folder paths manually.")
|