Image Crop + Rotate + Pad, Load Image + Pad and Load Video refused a
picture named the way ComfyUI names a generated file
("photo.png [output]", "[temp]") with "The selected source is outside
ComfyUI's input folder." Core Load Image takes those names.
resolve_input_path now reads the name the same way the video nodes'
Outputs list already does (resolve_server_file): the tag picks the
input, output or temp folder, and the rest is a name below it. A whole
path, "~", "." and "..", and a network path are refused from the text
alone; a link that leads out of the folder is refused once resolved.
Both functions share that code. A whole path inside the input folder,
accepted before, is now refused too, as the security rule asks.
Load Video's player and the shared upstream preview stripped the tag
and asked /view for the input folder; they now keep the folder.
Tests cover input, [input], [output] and [temp] names through the
resolver and through ComfyUI's own validation next to core Load Image,
and the refused escapes. Help pages, SECURITY.md and CHANGELOG say so.
133 lines
5.5 KiB
JavaScript
133 lines
5.5 KiB
JavaScript
// Pure resolution rules for the upstream-input preview panel. DOM-free so
|
|
// node:test covers them without a browser; the wiring lives in
|
|
// js/input_preview/index.js.
|
|
//
|
|
// Best-effort order for a connected source node:
|
|
// 1. cached execution previews (sourceNode.imgs[0].src)
|
|
// 2. an uploadable image/video file combo widget -> deterministic /view query
|
|
// 3. nothing -> the caller shows a quiet placeholder
|
|
|
|
import { parseImageReference } from "./pad_canvas.mjs";
|
|
import { splitMediaName } from "./video_preview.mjs";
|
|
|
|
// Core loader nodes keep the uploaded file name in a combo widget with one of
|
|
// these names (LoadImage uses "image", video loaders use "video").
|
|
const FILE_WIDGET_NAMES = new Set(["image", "video"]);
|
|
|
|
// Graph link stores differ by frontend generation: a Map on the modern
|
|
// frontend, a plain id-keyed object on legacy LiteGraph.
|
|
export function linkById(graph, linkId) {
|
|
const links = graph?.links;
|
|
if (links == null || linkId == null) return null;
|
|
const link = typeof links.get === "function" ? links.get(linkId) : links[linkId];
|
|
return link ?? null;
|
|
}
|
|
|
|
// Walk the named input's link back to the node feeding it, or null.
|
|
export function upstreamNode(node, inputName) {
|
|
const graph = node?.graph;
|
|
if (!graph) return null;
|
|
const input = node.inputs?.find((candidate) => candidate?.name === inputName);
|
|
const link = linkById(graph, input?.link);
|
|
if (!link) return null;
|
|
return graph.getNodeById?.(link.origin_id) ?? null;
|
|
}
|
|
|
|
// The widget whose callback fires when the user picks a different file.
|
|
export function sourceFileWidget(sourceNode) {
|
|
return sourceNode?.widgets?.find((widget) => FILE_WIDGET_NAMES.has(widget?.name)) ?? null;
|
|
}
|
|
|
|
// Deterministic /view query (no cache buster) for a file widget's name,
|
|
// tolerating Windows separators. A name without a tag is in the input
|
|
// folder; "photo.png [output]" or "[temp]" is read from that folder.
|
|
export function viewQueryForFile(name) {
|
|
const { filename, subfolder } = splitMediaName(name);
|
|
const type = parseImageReference(name)?.type ?? "input";
|
|
return new URLSearchParams({ filename, subfolder, type }).toString();
|
|
}
|
|
|
|
// Describe how to preview a source node:
|
|
// { kind: "url", url } cached preview image
|
|
// { kind: "view", isVideo, query } file widget, serve via api /view
|
|
// null nothing resolvable
|
|
export function describeSourcePreview(sourceNode) {
|
|
if (!sourceNode) return null;
|
|
const cached = sourceNode.imgs?.[0]?.src;
|
|
if (typeof cached === "string" && cached) return { kind: "url", url: cached };
|
|
const widget = sourceFileWidget(sourceNode);
|
|
if (widget && typeof widget.value === "string" && widget.value) {
|
|
return {
|
|
kind: "view",
|
|
isVideo: widget.name === "video",
|
|
query: viewQueryForFile(widget.value),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// The node's own latest result, if it has produced one.
|
|
//
|
|
// imgs is where the frontend leaves both the streamed progress frames of a
|
|
// running node and the images from its ui payload, so the last entry is the
|
|
// most recent thing this node made. The panel prefers it over the upstream
|
|
// input for the obvious reason: once a node has a result, its result is what
|
|
// you want to look at.
|
|
export function describeOwnResult(node) {
|
|
const images = node?.imgs;
|
|
if (!Array.isArray(images) || images.length === 0) return null;
|
|
const src = images[images.length - 1]?.src;
|
|
return typeof src === "string" && src ? { kind: "url", url: src } : null;
|
|
}
|
|
|
|
// The key the frontend files a node's outputs under (app.nodeOutputs): the
|
|
// node's id in the root graph, "<subgraph id>:<id>" inside a subgraph.
|
|
export function outputLocatorId(node) {
|
|
const graph = node?.graph;
|
|
if (!graph || node.id == null) return null;
|
|
const root = graph.rootGraph ?? graph;
|
|
return graph !== root && graph.id ? `${graph.id}:${node.id}` : String(node.id);
|
|
}
|
|
|
|
// The /view query for an image in a node's output record
|
|
// ({ images: [{ filename, subfolder, type }] }): the newest one, or the one
|
|
// at `index` when given. Null when it has none.
|
|
// Nodes 2.0 fills node.imgs only from its own output preview, which the panel
|
|
// stands down, so there this record is the node's result.
|
|
export function outputRecordQuery(record, index = null) {
|
|
const images = record?.images;
|
|
const count = outputRecordCount(record);
|
|
const at = Number.isInteger(index) && index >= 0 && index < count ? index : count - 1;
|
|
const image = count ? images[at] : null;
|
|
if (!image || typeof image.filename !== "string" || !image.filename) return null;
|
|
return new URLSearchParams({
|
|
filename: image.filename,
|
|
subfolder: image.subfolder ?? "",
|
|
type: image.type ?? "output",
|
|
}).toString();
|
|
}
|
|
|
|
// How many pictures the node's last run left in its output record.
|
|
export function outputRecordCount(record) {
|
|
return Array.isArray(record?.images) ? record.images.length : 0;
|
|
}
|
|
|
|
// What the panel shows, best first: this node's result (node.imgs, then the
|
|
// URL of its stored output), then whatever feeds it, then nothing.
|
|
export function describeNodePreview(node, inputName, storedUrl = null) {
|
|
return describeOwnResult(node)
|
|
?? (storedUrl ? { kind: "url", url: storedUrl } : null)
|
|
?? describeSourcePreview(upstreamNode(node, inputName));
|
|
}
|
|
|
|
// Quiet ASCII placeholder copy; never an error.
|
|
export function placeholderText(connected, noun = "an image") {
|
|
return connected ? "Run the workflow to see it here" : `Connect ${noun} to see it here`;
|
|
}
|
|
|
|
// The node ran while its preview was off, so its last picture is older
|
|
// than its result.
|
|
export function staleText() {
|
|
return "Run the workflow again to see it here";
|
|
}
|