getQueryParams()['model'] ?? 'market'); if (!$this->registry->has($modelKey)) { $modelKey = 'market'; } return $this->render($request, $modelKey, null, null, null); } public function processAction(ServerRequestInterface $request): ResponseInterface { $body = (array)$request->getParsedBody(); $modelKey = (string)($body['model'] ?? ''); if (!$this->registry->has($modelKey)) { return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecimport'), 303); } $op = (string)($body['op'] ?? 'preview'); // CSV: fresh upload wins, otherwise the hidden state field. $csv = null; $files = $request->getUploadedFiles(); $upload = $files['csvfile'] ?? null; if ($upload !== null && $upload->getError() === UPLOAD_ERR_OK) { $csv = $this->csvReader->parse((string)$upload->getStream()); if ($csv['columns'] === []) { $this->flash('The file could not be parsed as CSV.', 'Upload', false); $csv = null; } else { $this->flash( sprintf('%d rows, %d columns detected.', count($csv['rows']), count($csv['columns'])), 'CSV loaded', true ); } } elseif (($body['csvstate'] ?? '') !== '') { $decoded = json_decode((string)$body['csvstate'], true); if (is_array($decoded) && isset($decoded['columns'], $decoded['rows'])) { $csv = $decoded; } } // Mapping from the form, when present. $mapping = null; $identity = null; if (isset($body['map']) && is_array($body['map'])) { $mapping = []; foreach ($body['map'] as $column => $field) { if ((string)$field !== '') { $mapping[(string)$column] = (string)$field; } } $identity = (string)($body['identity'] ?? ''); } if ($op === 'saveMapping' && $mapping !== null && $identity !== '') { $this->mappingRepository->save($modelKey, $mapping, $identity); $this->flash('Mapping stored for this model - it will be preselected on the next upload.', 'Mapping saved', true); } if ($op === 'apply') { $this->apply($modelKey, $body); } return $this->render($request, $modelKey, $csv, $mapping, $identity); } // --------------------------------------------------- SEO research tab public function seoAction(ServerRequestInterface $request): ResponseInterface { $latest = $this->seoResearchRepository->latest(0); $previous = $this->seoResearchRepository->latest(1); $analysis = $latest !== null ? $this->seoResearchService->analyze($latest['rows'], $previous !== null ? $previous['rows'] : null) : null; $this->pageRenderer->addCssFile('EXT:vitec/Resources/Public/Css/backend-import.css'); $view = $this->moduleTemplateFactory->create($request); $view->assignMultiple([ 'models' => $this->registry->all(), 'latest' => $latest, 'previous' => $previous, 'analysis' => $analysis, ]); return $view->renderResponse('Import/Seo'); } public function seoUploadAction(ServerRequestInterface $request): ResponseInterface { $files = $request->getUploadedFiles(); $upload = $files['csvfile'] ?? null; if ($upload !== null && $upload->getError() === UPLOAD_ERR_OK) { $csv = $this->csvReader->parse((string)$upload->getStream()); $rows = $this->seoResearchService->normalizeRows($csv['rows']); if ($rows === []) { $this->flash('No usable rows found - is the "Potential URL" column present?', 'Upload', false); } else { $this->seoResearchRepository->add((string)($upload->getClientFilename() ?? 'upload.csv'), $rows); $this->flash(sprintf('%d rows stored as new delivery.', count($rows)), 'Delivery stored', true); } } else { $this->flash('No file received.', 'Upload', false); } return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecimport.seo'), 303); } // ------------------------------------------------------------ rendering /** * @param array{columns:array,rows:array>}|null $csv * @param array|null $mapping */ private function render( ServerRequestInterface $request, string $modelKey, ?array $csv, ?array $mapping, ?string $identity ): ResponseInterface { $model = $this->registry->get($modelKey); $fields = $this->registry->importableFields($model['table']); $saved = $this->mappingRepository->load($modelKey); if ($mapping === null) { $mapping = $saved['mapping'] ?? []; if ($mapping === [] && $csv !== null) { $mapping = $this->guessMapping($csv['columns'], $fields); } } if ($identity === null || $identity === '') { $identity = $saved['identity'] ?? (isset($fields['slug']) ? 'slug' : 'title'); } $union = $this->buildUnion($model['table'], $fields, $csv, $mapping, $identity); $counts = ['new' => 0, 'update' => 0, 'unchanged' => 0, 'dbonly' => 0]; foreach ($union as $row) { $counts[$row['status']]++; } // Column meta for the mapping panel: sample value + current target. $columns = []; foreach ($csv['columns'] ?? [] as $column) { $columns[] = [ 'name' => $column, 'sample' => (string)($csv['rows'][0][$column] ?? ''), 'target' => $mapping[$column] ?? '', ]; } $this->pageRenderer->addCssFile('EXT:vitec/Resources/Public/Css/backend-import.css'); $view = $this->moduleTemplateFactory->create($request); $view->assignMultiple([ 'models' => $this->registry->all(), 'model' => $model, 'fields' => $fields, 'columns' => $columns, 'hasCsv' => $csv !== null, 'csvRowCount' => count($csv['rows'] ?? []), 'csvstate' => $csv !== null ? (string)json_encode($csv, JSON_UNESCAPED_UNICODE) : '', 'identity' => $identity, 'mappingSaved' => $saved !== null, 'union' => $union, 'counts' => $counts, 'pid' => $this->registry->detectPid($model['table']), ]); return $view->renderResponse('Import/Index'); } /** * Name-equality guess for a first-time mapping. * * @param array $columns * @param array $fields * @return array */ private function guessMapping(array $columns, array $fields): array { $mapping = []; foreach ($columns as $column) { $normalized = mb_strtolower(trim($column)); if (isset($fields[$normalized])) { $mapping[$column] = $normalized; } } return $mapping; } /** * Merge CSV rows and DB records into one status-annotated list. * * @param array $fields * @param array{columns:array,rows:array>}|null $csv * @param array $mapping * @return array> */ private function buildUnion(string $table, array $fields, ?array $csv, array $mapping, string $identity): array { $mappedFields = array_values(array_unique(array_values($mapping))); $records = $this->registry->loadRecords($table, array_keys($fields)); $byIdentity = []; foreach ($records as $uid => $record) { $key = mb_strtolower(trim((string)($record[$identity] ?? ''))); if ($key !== '' && !isset($byIdentity[$key])) { $byIdentity[$key] = $uid; } } $union = []; $matchedUids = []; $index = 0; foreach ($csv['rows'] ?? [] as $raw) { $payload = []; foreach ($mapping as $column => $field) { $payload[$field] = (string)($raw[$column] ?? ''); } $identityValue = mb_strtolower(trim((string)($payload[$identity] ?? ''))); $uid = $identityValue !== '' ? ($byIdentity[$identityValue] ?? 0) : 0; if ($uid > 0) { $matchedUids[$uid] = true; $record = $records[$uid]; $diff = []; foreach ($payload as $field => $value) { if (trim($value) !== trim((string)($record[$field] ?? ''))) { $diff[] = $field; } } $status = $diff === [] ? 'unchanged' : 'update'; } else { $diff = array_keys($payload); $status = 'new'; } $union[] = [ 'index' => $index++, 'status' => $status, 'uid' => $uid, 'label' => (string)($payload['title'] ?? ($payload[$identity] ?? 'row')), 'diff' => implode(', ', $diff), 'payload' => (string)json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'checked' => $status === 'new' || $status === 'update', ]; } foreach ($records as $uid => $record) { if (isset($matchedUids[$uid])) { continue; } $union[] = [ 'index' => $index++, 'status' => 'dbonly', 'uid' => $uid, 'label' => (string)($record['title'] ?? $uid), 'diff' => '', 'payload' => '', 'checked' => false, ]; } return $union; } // ---------------------------------------------------------------- apply /** @param array $body */ private function apply(string $modelKey, array $body): void { $model = $this->registry->get($modelKey); $fields = $this->registry->importableFields($model['table']); $pid = (int)($body['pid'] ?? 0); $rows = is_array($body['rows'] ?? null) ? $body['rows'] : []; $datamap = []; $errors = []; foreach ($rows as $i => $row) { if (empty($row['selected'])) { continue; } $payload = json_decode((string)($row['payload'] ?? ''), true); if (!is_array($payload)) { $errors[] = sprintf('Row %s: payload is not valid JSON - skipped.', $i); continue; } // Whitelist against the TCA-derived field list. $payload = array_intersect_key($payload, $fields); $payload = array_map(static fn($v): string => is_scalar($v) ? (string)$v : '', $payload); if ($payload === []) { continue; } $uid = (int)($row['uid'] ?? 0); if ($uid > 0) { $datamap[(string)$uid] = $payload; } elseif ($pid > 0) { $datamap['NEW' . $i] = ['pid' => $pid] + $payload; } else { $errors[] = sprintf('Row %s: no storage pid for a new record - skipped.', $i); } } $created = 0; $updated = 0; if ($datamap !== []) { $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start([$model['table'] => $datamap], []); $dataHandler->process_datamap(); $errors = array_merge($errors, $dataHandler->errorLog); $created = count($dataHandler->substNEWwithIDs); $updated = count(array_filter(array_keys($datamap), 'is_numeric')); } $this->flash( sprintf('%d created, %d updated, %d error(s).', $created, $updated, count($errors)) . ($errors !== [] ? ' ' . implode(' | ', array_slice($errors, 0, 5)) : ''), 'Import finished', $errors === [] ); } private function flash(string $message, string $title, bool $ok): void { $flashMessage = GeneralUtility::makeInstance( FlashMessage::class, $message, $title, $ok ? ContextualFeedbackSeverity::OK : ContextualFeedbackSeverity::ERROR, true ); $this->flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage); } }