diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index a812fe5..589961f 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -226,12 +226,14 @@ class Post < ApplicationRecord end def self.sanitised_svg_bytes(bytes) + parse_options = + Nokogiri::XML::ParseOptions::STRICT | + Nokogiri::XML::ParseOptions::NONET document = Nokogiri::XML( bytes, nil, nil, - Nokogiri::XML::ParseOptions::STRICT - | Nokogiri::XML::ParseOptions::NONET) + parse_options) root = document.root raise MiniMagick::Error, 'SVG が不正です.' if root == nil || root.name != 'svg' raise MiniMagick::Error, 'SVG が不正です.' if document.internal_subset != nil diff --git a/frontend/src/pages/posts/PostImportReviewPage.tsx b/frontend/src/pages/posts/PostImportReviewPage.tsx index b3e71ce..e02f410 100644 --- a/frontend/src/pages/posts/PostImportReviewPage.tsx +++ b/frontend/src/pages/posts/PostImportReviewPage.tsx @@ -427,7 +427,8 @@ const PostImportReviewPage: FC = ({ user }) => { : { duration: .2, ease: 'easeOut' as const } const [session, setSession] = useState (null) - const [loading, setLoading] = useState (false) + const [metadataLoading, setMetadataLoading] = useState (false) + const [submitting, setSubmitting] = useState (false) const [loadingRow, setLoadingRow] = useState (null) const [editingRow, setEditingRow] = useState (null) const [showExistingRows, setShowExistingRows] = useState (false) @@ -490,101 +491,106 @@ const PostImportReviewPage: FC = ({ user }) => { } const refreshRows = async (baseSession: PostImportSession) => { - setLoading (true) - let nextIndex = 0 - const previewErrors = new Map> () - let workingRows = [...(sessionRef.current ?? baseSession).rows] + if (active) + setMetadataLoading (true) + try + { + let nextIndex = 0 + const previewErrors = new Map> () + let workingRows = [...(sessionRef.current ?? baseSession).rows] - const mergeWorkingRow = ( - sourceRow: number, - updater: (row: PostImportRow) => PostImportRow, - ) => { - workingRows = workingRows.map (row => - row.sourceRow === sourceRow - ? updater (row) - : row) - const nextSession = buildSession (workingRows) - sessionRef.current = nextSession - setSession (nextSession) - } - const worker = async () => { - while (active) - { - const index = nextIndex - ++nextIndex - if (index >= baseSession.rows.length) - return + const mergeWorkingRow = ( + sourceRow: number, + updater: (row: PostImportRow) => PostImportRow, + ) => { + workingRows = workingRows.map (row => + row.sourceRow === sourceRow + ? updater (row) + : row) + const nextSession = buildSession (workingRows) + sessionRef.current = nextSession + setSession (nextSession) + } + const worker = async () => { + while (active) + { + const index = nextIndex + ++nextIndex + if (index >= baseSession.rows.length) + return - const baseRow = baseSession.rows[index] - if (baseRow.skipReason === 'manual' - || baseRow.importStatus === 'created' - || baseRow.importStatus === 'skipped' - || isNonRecoverableFailedRow (baseRow) - || baseRow.metadataUrl != null) - continue - const controller = new AbortController () - controllers.add (controller) - try - { - const preview = await apiGet ('/posts/metadata', { - params: { url: baseRow.url }, - signal: controller.signal }) - if (!(active) || previewSequenceRef.current !== previewSequence) - return - mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview)) - } - catch (requestError) - { - if (controller.signal.aborted) - return - if (!(active) || previewSequenceRef.current !== previewSequence) - return - if (!(isApiError<{ - errors?: Record - baseErrors?: string[] - }> (requestError))) + const baseRow = baseSession.rows[index] + if (baseRow.skipReason === 'manual' + || baseRow.importStatus === 'created' + || baseRow.importStatus === 'skipped' + || isNonRecoverableFailedRow (baseRow) + || baseRow.metadataUrl != null) continue - if (requestError.response?.status === 422) - { - const rowErrors = { - ...(requestError.response.data.errors ?? { }), - ...(requestError.response.data.baseErrors?.length - ? { - base: requestError.response.data.baseErrors } - : { }) } - previewErrors.set (baseRow.sourceRow, rowErrors) - mergeWorkingRow (baseRow.sourceRow, row => ({ - ...row, - validationErrors: rowErrors, - status: 'error' })) - } - } - finally - { - controllers.delete (controller) + const controller = new AbortController () + controllers.add (controller) + try + { + const preview = await apiGet ('/posts/metadata', { + params: { url: baseRow.url }, + signal: controller.signal }) + if (!(active) || previewSequenceRef.current !== previewSequence) + return + mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview)) + } + catch (requestError) + { + if (controller.signal.aborted) + return + if (!(active) || previewSequenceRef.current !== previewSequence) + return + if (!(isApiError<{ + errors?: Record + baseErrors?: string[] + }> (requestError))) + continue + if (requestError.response?.status === 422) + { + const rowErrors = { + ...(requestError.response.data.errors ?? { }), + ...(requestError.response.data.baseErrors?.length + ? { + base: requestError.response.data.baseErrors } + : { }) } + previewErrors.set (baseRow.sourceRow, rowErrors) + mergeWorkingRow (baseRow.sourceRow, row => ({ + ...row, + validationErrors: rowErrors, + status: 'error' })) + } + } + finally + { + controllers.delete (controller) + } } } - } - await Promise.all ( - Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ())) - if (!(active) || previewSequenceRef.current !== previewSequence) - { - setLoading (false) + await Promise.all ( + Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ())) + if (!(active) || previewSequenceRef.current !== previewSequence) return - } - const nextRows = applyDuplicateUrlErrors (workingRows.map (row => { - const rowErrors = previewErrors.get (row.sourceRow) - if (rowErrors == null) - return row - return { - ...row, - validationErrors: rowErrors, - status: 'error' as const } - })) - await persistSession (buildSession (nextRows)) - setLoading (false) + const nextRows = applyDuplicateUrlErrors (workingRows.map (row => { + const rowErrors = previewErrors.get (row.sourceRow) + if (rowErrors == null) + return row + return { + ...row, + validationErrors: rowErrors, + status: 'error' as const } + })) + await persistSession (buildSession (nextRows)) + } + finally + { + if (active) + setMetadataLoading (false) + } } const hydrate = async () => { @@ -643,7 +649,7 @@ const PostImportReviewPage: FC = ({ user }) => { : rows const existingRows = sortedRows.filter (row => isExistingSkipRow (row)) const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row))) - const busy = loading || loadingRow != null + const busy = metadataLoading || submitting || loadingRow != null const canSubmit = rows.every (row => isCompletedReviewRow (row)) || processableImportRows (rows).length > 0 @@ -856,7 +862,7 @@ const PostImportReviewPage: FC = ({ user }) => { return } - setLoading (true) + setSubmitting (true) try { const processingRows = processableImportRows (currentSession.rows) @@ -887,7 +893,7 @@ const PostImportReviewPage: FC = ({ user }) => { } finally { - setLoading (false) + setSubmitting (false) } } @@ -968,26 +974,29 @@ const PostImportReviewPage: FC = ({ user }) => { navigate (-1)} + onBack={() => navigate ('/posts/new')} onSubmit={() => submit ()}/> ) } const PostImportFooter = ( - { loading, + { metadataLoading, + submitting, canSubmit, creatableCount, manualSkippedCount, existingSkippedCount, pendingOrErrorCount, onBack, - onSubmit }: { loading: boolean + onSubmit }: { metadataLoading: boolean + submitting: boolean canSubmit: boolean creatableCount: number manualSkippedCount: number @@ -1008,13 +1017,17 @@ const PostImportFooter = ( 登録不可/未処理 {pendingOrErrorCount}件
-