refactor: centralize getModelCardUrl and HTML/URL utilities in resolver/utils
This commit is contained in:
@@ -3,6 +3,7 @@ import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { createModuleLogger } from "../../log_system/log_funcs.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
|
||||
const log = createModuleLogger('resolve_download_methods');
|
||||
|
||||
@@ -293,7 +294,7 @@ export const resolveDownloadMethods = {
|
||||
url,
|
||||
filename: result[candidate.filenameKey] || missing.civitai_info?.expected_filename || filename,
|
||||
directory: result[candidate.categoryKey] || result.directory || result.category || missing.category || 'checkpoints',
|
||||
model_url: this.getModelCardUrl(rawModelUrl) || rawModelUrl,
|
||||
model_url: getModelCardUrl(rawModelUrl) || rawModelUrl,
|
||||
name: result.name || result.repo_id || result.repo || result.filename || filename,
|
||||
size: result.size,
|
||||
type: result.type || missing.category
|
||||
@@ -2571,7 +2572,7 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: popular.url,
|
||||
downloadFilename: popularFilename,
|
||||
category: popular.directory || missingCategory,
|
||||
openUrl: this.getModelCardUrl(popular.url),
|
||||
openUrl: getModelCardUrl(popular.url),
|
||||
searchedAt: this.getSearchResultTimestamp(popular)
|
||||
});
|
||||
}
|
||||
@@ -2589,14 +2590,14 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: modelListResult.url,
|
||||
downloadFilename: modelListResult.filename,
|
||||
category: modelListResult.directory || missingCategory,
|
||||
openUrl: this.getModelCardUrl(modelListResult.url),
|
||||
openUrl: getModelCardUrl(modelListResult.url),
|
||||
searchedAt: this.getSearchResultTimestamp(modelListResult)
|
||||
});
|
||||
}
|
||||
|
||||
if (hfResult && hfResult.url) {
|
||||
const hfRepo = hfResult.repo_id || hfResult.repo || '';
|
||||
const hfModelUrl = hfRepo ? `https://huggingface.co/${hfRepo}` : this.getModelCardUrl(hfResult.url);
|
||||
const hfModelUrl = hfRepo ? `https://huggingface.co/${hfRepo}` : getModelCardUrl(hfResult.url);
|
||||
const missingCategory = this.getMissingDownloadCategory?.(missing, 'checkpoints') || missing.category || 'checkpoints';
|
||||
addRow({
|
||||
sourceKey: 'huggingface',
|
||||
@@ -2670,7 +2671,7 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: loraManagerArchiveResult.download_url || '',
|
||||
downloadFilename: archiveFilename,
|
||||
category: archiveCategory,
|
||||
openUrl: loraManagerArchiveResult.url || this.getModelCardUrl(loraManagerArchiveResult.download_url),
|
||||
openUrl: loraManagerArchiveResult.url || getModelCardUrl(loraManagerArchiveResult.download_url),
|
||||
searchedAt: this.getSearchResultTimestamp(loraManagerArchiveResult),
|
||||
detailsContext: {
|
||||
...loraManagerArchiveResult,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
export const searchPanelMethods = {
|
||||
/**
|
||||
* Build stable cache key for a missing model entry
|
||||
@@ -1701,7 +1702,7 @@ export const searchPanelMethods = {
|
||||
const rawModelUrl = downloadSource.model_url
|
||||
|| downloadSource.workflow_model_url
|
||||
|| downloadSource.url;
|
||||
const modelUrl = this.getModelCardUrl(rawModelUrl) || rawModelUrl;
|
||||
const modelUrl = getModelCardUrl(rawModelUrl) || rawModelUrl;
|
||||
const versionName = downloadSource.version_name || missing.civitai_info?.version_name || '';
|
||||
const modelParts = this.getModelVersionParts(
|
||||
downloadSource.name || missing.civitai_info?.model_name || '',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
export const workflowUpdateMethods = {
|
||||
async refreshComfyModelCatalogAfterApply(workflow = null, resolutions = []) {
|
||||
if (!this.isAutoRefreshComfyModelsAfterApplyEnabled()) {
|
||||
@@ -270,38 +271,7 @@ export const workflowUpdateMethods = {
|
||||
* CivitAI: https://civitai.com/api/download/models/123?type=Model -> https://civitai.com/models/123
|
||||
*/
|
||||
getModelCardUrl(downloadUrl) {
|
||||
if (!downloadUrl) return null;
|
||||
|
||||
try {
|
||||
// HuggingFace URLs
|
||||
if (downloadUrl.includes('huggingface.co')) {
|
||||
const fileMatch = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)\/(?:resolve|blob)\/([^\/]+)\/(.+)$/);
|
||||
if (fileMatch) {
|
||||
const repo = fileMatch[1];
|
||||
const revision = fileMatch[2];
|
||||
const filePath = fileMatch[3].split(/[?#]/)[0];
|
||||
return `https://huggingface.co/${repo}/blob/${revision}/${filePath}`;
|
||||
}
|
||||
|
||||
const match = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)/);
|
||||
if (match) {
|
||||
return `https://huggingface.co/${match[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// CivitAI URLs
|
||||
if (downloadUrl.includes('civitai.com')) {
|
||||
// Format: /api/download/models/123456 or /models/123456/...
|
||||
const modelIdMatch = downloadUrl.match(/models\/(\d+)/);
|
||||
if (modelIdMatch) {
|
||||
return `https://civitai.com/models/${modelIdMatch[1]}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing model card URL:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
return getModelCardUrl(downloadUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* HTML Utilities for Model Resolver
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escapes special characters for HTML to prevent XSS.
|
||||
* @param {any} value
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
export function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a value to be safely embedded inside inline JavaScript.
|
||||
* @param {any} value
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
export function escapeJsString(value) {
|
||||
return escapeHtml(JSON.stringify(String(value ?? '')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes HTML content for display, permitting only safe tags and attributes.
|
||||
* @param {string} html
|
||||
* @returns {string} Sanitized HTML
|
||||
*/
|
||||
export function sanitizeDescriptionHtml(html) {
|
||||
const raw = String(html ?? '').trim();
|
||||
if (!raw) return '';
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(`<div>${raw}</div>`, 'text/html');
|
||||
const root = doc.body.firstElementChild;
|
||||
if (!root) return escapeHtml(raw);
|
||||
|
||||
const allowedTags = new Set([
|
||||
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'code', 'pre',
|
||||
'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'a', 'span'
|
||||
]);
|
||||
const allowedStyles = new Set(['color']);
|
||||
|
||||
const sanitizeNode = (node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
node.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = node.tagName.toLowerCase();
|
||||
if (!allowedTags.has(tag)) {
|
||||
const parent = node.parentNode;
|
||||
if (!parent) {
|
||||
node.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.firstChild) {
|
||||
parent.insertBefore(node.firstChild, node);
|
||||
}
|
||||
parent.removeChild(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const attrs = Array.from(node.attributes);
|
||||
for (const attr of attrs) {
|
||||
const name = attr.name.toLowerCase();
|
||||
if (name.startsWith('on') || name === 'id' || name === 'class' || name.startsWith('data-')) {
|
||||
node.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name === 'href' && tag === 'a') {
|
||||
const href = node.getAttribute('href') || '';
|
||||
if (!/^https?:\/\//i.test(href)) {
|
||||
node.removeAttribute('href');
|
||||
} else {
|
||||
node.setAttribute('target', '_blank');
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name === 'style') {
|
||||
const safeStyles = [];
|
||||
const styleValue = node.getAttribute('style') || '';
|
||||
for (const part of styleValue.split(';')) {
|
||||
const [prop, value] = part.split(':').map(v => v?.trim());
|
||||
if (!prop || !value) continue;
|
||||
if (allowedStyles.has(prop.toLowerCase())) {
|
||||
safeStyles.push(`${prop}: ${value}`);
|
||||
}
|
||||
}
|
||||
if (safeStyles.length) {
|
||||
node.setAttribute('style', safeStyles.join('; '));
|
||||
} else {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(tag === 'a' && (name === 'target' || name === 'rel')) ) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(node.childNodes).forEach(child => sanitizeNode(child));
|
||||
};
|
||||
|
||||
Array.from(root.childNodes).forEach(child => sanitizeNode(child));
|
||||
return root.innerHTML;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* URL Utilities for Model Resolver
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parsuje adres URL pobierania i zwraca adres karty modelu (Model Card).
|
||||
* Obsługuje platformy HuggingFace oraz CivitAI.
|
||||
* @param {string} downloadUrl
|
||||
* @returns {string|null} URL karty modelu lub null w przypadku niepowodzenia
|
||||
*/
|
||||
export function getModelCardUrl(downloadUrl) {
|
||||
if (!downloadUrl) return null;
|
||||
|
||||
try {
|
||||
// HuggingFace URLs
|
||||
if (downloadUrl.includes('huggingface.co')) {
|
||||
const fileMatch = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)\/(?:resolve|blob)\/([^\/]+)\/(.+)$/);
|
||||
if (fileMatch) {
|
||||
const repo = fileMatch[1];
|
||||
const revision = fileMatch[2];
|
||||
const filePath = fileMatch[3].split(/[?#]/)[0];
|
||||
return `https://huggingface.co/${repo}/blob/${revision}/${filePath}`;
|
||||
}
|
||||
|
||||
const match = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)/);
|
||||
if (match) {
|
||||
return `https://huggingface.co/${match[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// CivitAI URLs
|
||||
if (downloadUrl.includes('civitai.com')) {
|
||||
// Format: /api/download/models/123456 or /models/123456/...
|
||||
const modelIdMatch = downloadUrl.match(/models\/(\d+)/);
|
||||
if (modelIdMatch) {
|
||||
return `https://civitai.com/models/${modelIdMatch[1]}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing model card URL:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { escapeHtml, escapeJsString, sanitizeDescriptionHtml } from "../utils/html_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
export const modelInfoMethods = {
|
||||
/**
|
||||
* Handle click outside context menu to hide it
|
||||
@@ -291,7 +293,7 @@ export const modelInfoMethods = {
|
||||
const normalizedUrl = this.normalizeContextMenuSourceUrl(candidate, candidateKey);
|
||||
if (!normalizedUrl) continue;
|
||||
|
||||
const cardUrl = this.getModelCardUrl?.(normalizedUrl) || normalizedUrl;
|
||||
const cardUrl = getModelCardUrl(normalizedUrl) || normalizedUrl;
|
||||
const sourceKey = this.getContextMenuSourceKeyFromUrl(cardUrl)
|
||||
|| this.getContextMenuSourceKeyFromUrl(normalizedUrl)
|
||||
|| candidateKey;
|
||||
@@ -1259,16 +1261,11 @@ export const modelInfoMethods = {
|
||||
},
|
||||
|
||||
escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return escapeHtml(value);
|
||||
},
|
||||
|
||||
escapeJsString(value) {
|
||||
return this.escapeHtml(JSON.stringify(String(value ?? '')));
|
||||
return escapeJsString(value);
|
||||
},
|
||||
|
||||
truncateText(value, maxLength = 160) {
|
||||
@@ -1278,93 +1275,7 @@ export const modelInfoMethods = {
|
||||
},
|
||||
|
||||
sanitizeDescriptionHtml(html) {
|
||||
const raw = String(html ?? '').trim();
|
||||
if (!raw) return '';
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(`<div>${raw}</div>`, 'text/html');
|
||||
const root = doc.body.firstElementChild;
|
||||
if (!root) return this.escapeHtml(raw);
|
||||
|
||||
const allowedTags = new Set([
|
||||
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'code', 'pre',
|
||||
'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'a', 'span'
|
||||
]);
|
||||
const allowedStyles = new Set(['color']);
|
||||
|
||||
const sanitizeNode = (node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
node.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = node.tagName.toLowerCase();
|
||||
if (!allowedTags.has(tag)) {
|
||||
const parent = node.parentNode;
|
||||
if (!parent) {
|
||||
node.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.firstChild) {
|
||||
parent.insertBefore(node.firstChild, node);
|
||||
}
|
||||
parent.removeChild(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const attrs = Array.from(node.attributes);
|
||||
for (const attr of attrs) {
|
||||
const name = attr.name.toLowerCase();
|
||||
if (name.startsWith('on') || name === 'id' || name === 'class' || name.startsWith('data-')) {
|
||||
node.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name === 'href' && tag === 'a') {
|
||||
const href = node.getAttribute('href') || '';
|
||||
if (!/^https?:\/\//i.test(href)) {
|
||||
node.removeAttribute('href');
|
||||
} else {
|
||||
node.setAttribute('target', '_blank');
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name === 'style') {
|
||||
const safeStyles = [];
|
||||
const styleValue = node.getAttribute('style') || '';
|
||||
for (const part of styleValue.split(';')) {
|
||||
const [prop, value] = part.split(':').map(v => v?.trim());
|
||||
if (!prop || !value) continue;
|
||||
if (allowedStyles.has(prop.toLowerCase())) {
|
||||
safeStyles.push(`${prop}: ${value}`);
|
||||
}
|
||||
}
|
||||
if (safeStyles.length) {
|
||||
node.setAttribute('style', safeStyles.join('; '));
|
||||
} else {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(tag === 'a' && (name === 'target' || name === 'rel')) ) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(node.childNodes).forEach(child => sanitizeNode(child));
|
||||
};
|
||||
|
||||
Array.from(root.childNodes).forEach(child => sanitizeNode(child));
|
||||
return root.innerHTML;
|
||||
return sanitizeDescriptionHtml(html);
|
||||
},
|
||||
|
||||
normalizeTrainedWords(words) {
|
||||
|
||||
Reference in New Issue
Block a user