diff --git a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx index 98453bc9c..7669c4a4f 100644 --- a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx +++ b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx @@ -50,12 +50,38 @@ const LoadFileFormat = (_props: LoadFileFormatProps) => { }, [newMigrationData]); // Handle file format extraction - RUN IMMEDIATELY ON MOUNT AND WHENEVER THE FILE PATH CHANGES. - // The displayed format is always derived from the ACTUAL uploaded file extension, never from a - // stale selectedFileFormat (which gets pre-seeded to the CMS default on CMS selection). This is - // why editing the file path (e.g. zip → json) now updates the label, icon, and Redux in sync. + // Most CMS types have exactly one allowed format (e.g. Sitecore is always Zip) — for those, the + // displayed format must stay locked to that fixed format regardless of what extension the user + // types in the path; the separate validation effect below already flags a mismatched upload. + // Only when the selected CMS allows more than one format (currently just stack-to-stack + // Contentstack, which accepts JSON or Zip) do we derive the displayed format from the actual + // uploaded file extension, since there's genuinely more than one valid answer to show. useEffect(() => { const filePath = newMigrationData?.legacy_cms?.uploadedFile?.file_details?.localPath || ''; const currentFormat = newMigrationData?.legacy_cms?.selectedFileFormat?.title; + const allowedFormats = newMigrationData?.legacy_cms?.selectedCms?.allowed_file_formats; + + // Lock only when the CMS has EXACTLY one allowed format. An empty array means the CMS + // isn't resolved yet (e.g. DEFAULT_CMS_TYPE while a multi-version CMS like Sitecore is + // still waiting on the user to pick a version card) — that's "unknown", not "one fixed + // format", and must fall through to the extension-derived behavior below rather than + // lock to a blank format and blank the field. + if (validateArray(allowedFormats) && allowedFormats.length === 1) { + const fixedFormat = allowedFormats[0]; + setFileIcon(fixedFormat?.title); + setFileDisplayTitle(getDisplayTitle(fixedFormat?.title)); + if (newMigrationData?.legacy_cms?.selectedFileFormat?.fileformat_id?.toLowerCase() !== fixedFormat?.fileformat_id?.toLowerCase()) { + const latest = newMigrationDataRef.current; + dispatch(updateNewMigrationData({ + ...latest, + legacy_cms: { + ...latest?.legacy_cms, + selectedFileFormat: fixedFormat + } + })); + } + return; + } // No file yet — fall back to whatever format is already in Redux (e.g. SQL/directory CMS types // that don't carry a localPath). @@ -110,7 +136,8 @@ const LoadFileFormat = (_props: LoadFileFormatProps) => { }, [ newMigrationData?.legacy_cms?.uploadedFile?.file_details?.localPath, newMigrationData?.legacy_cms?.selectedFileFormat?.fileformat_id, - newMigrationData?.legacy_cms?.selectedFileFormat?.title + newMigrationData?.legacy_cms?.selectedFileFormat?.title, + newMigrationData?.legacy_cms?.selectedCms?.allowed_file_formats ]); // Validate the uploaded file's format against the selected CMS's allowed formats. diff --git a/upload-api/migration-wordpress/libs/extractAssets.ts b/upload-api/migration-wordpress/libs/extractAssets.ts index 0136c36de..59a966da3 100644 --- a/upload-api/migration-wordpress/libs/extractAssets.ts +++ b/upload-api/migration-wordpress/libs/extractAssets.ts @@ -1,4 +1,5 @@ import fs from 'fs'; +import * as cheerio from 'cheerio'; export interface AssetMappingRow { id: string; @@ -52,14 +53,136 @@ const getTitle = (item: any, filename: string): string => { return filename.split('.').slice(0, -1).join('.') || filename; }; +// Mirrors wordpress.service.ts's isValidImageUrl — keep in sync. +const isValidImageUrl = (url: string): boolean => { + if (!url || typeof url !== 'string') return false; + if (url.trim().startsWith('data:')) return false; + if (url.trim().length < 5) return false; + const lowerUrl = url.toLowerCase().trim(); + if (lowerUrl.startsWith('javascript:') || lowerUrl.startsWith('mailto:') || lowerUrl.startsWith('tel:')) { + return false; + } + return true; +}; + +/** True if URL path ends with a common image extension (for image links). Mirrors + * wordpress.service.ts's looksLikeImageFileUrl — keep in sync. */ +const looksLikeImageFileUrl = (url: string): boolean => { + if (!url || typeof url !== 'string') return false; + const pathOnly = url.trim().split('?')[0].split('#')[0]; + return /\.(jpe?g|png|gif|webp|svg|bmp|ico|avif|heic|heif)$/i.test(pathOnly); +}; + +// Mirrors wordpress.service.ts's toCheckUrl, except a relative URL with no baseSiteUrl to +// resolve against returns null instead of building an unreachable "undefined/..." string — +// the real run's own toCheckUrl produces exactly that unreachable URL in this case, so a row +// here would describe an asset the run can never actually create. +const toCheckUrl = (url: string, baseSiteUrl: string | undefined): string | null => { + const validPattern = /^(https?:\/\/|www\.)/; + if (validPattern.test(url)) return url; + if (!baseSiteUrl) return null; + return `${baseSiteUrl}${url.replace(/^\/+/, '')}`; +}; + +/** + * Finds embedded image (and audio) URLs in a post's content:encoded. Mirrors + * wordpress.service.ts's extractImageUrlsFromContent — img src/data-src/srcset, + * links to image files,