render($request, []); } public function submarketUploadAction(ServerRequestInterface $request): ResponseInterface { $uploads = $request->getUploadedFiles()['workbooks'] ?? []; if (!is_array($uploads)) { $uploads = [$uploads]; } $dir = $this->inboxPath(); if (!is_dir($dir)) { GeneralUtility::mkdir_deep($dir); } $stored = []; foreach ($uploads as $upload) { if ($upload === null || $upload->getError() !== UPLOAD_ERR_OK) { continue; } $name = $this->safeName((string)$upload->getClientFilename()); if ($name === '') { $this->message('Skipped a file that is not an .xlsx workbook.', ContextualFeedbackSeverity::WARNING); continue; } $upload->moveTo($dir . '/' . $name); GeneralUtility::fixPermissions($dir . '/' . $name); $stored[] = $name; } if ($stored !== []) { $this->message(count($stored) . ' workbook(s) stored: ' . implode(', ', $stored), ContextualFeedbackSeverity::OK); } else { $this->message('No workbook uploaded.', ContextualFeedbackSeverity::WARNING); } return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecimport.submarkets'), 303); } public function submarketProcessAction(ServerRequestInterface $request): ResponseInterface { $body = (array)$request->getParsedBody(); $apply = (string)($body['op'] ?? 'dry') === 'apply'; $selected = array_map('strval', (array)($body['files'] ?? [])); if ($selected === []) { $this->message('No workbook selected.', ContextualFeedbackSeverity::WARNING); return $this->render($request, []); } $available = array_flip(array_column($this->workbooks(), 'name')); $results = []; foreach ($selected as $name) { // Whitelist against the actual directory listing - the submitted // name never touches the filesystem on its own (no traversal). if (!isset($available[$name])) { $results[] = ['file' => $name, 'error' => 'File not found in the inbox.']; continue; } try { $results[] = $this->importer->importFile($this->inboxPath() . '/' . $name, $apply); } catch (\Throwable $e) { $results[] = ['file' => $name, 'error' => $e->getMessage()]; } } $errors = count(array_filter($results, static fn(array $r): bool => isset($r['error']))); $this->message( sprintf('%d workbook(s) processed (%s)%s.', count($results), $apply ? 'APPLY' : 'dry run', $errors > 0 ? ', ' . $errors . ' with errors' : '' ), $errors > 0 ? ContextualFeedbackSeverity::WARNING : ContextualFeedbackSeverity::OK ); return $this->render($request, $results); } // ------------------------------------------------------------ internals /** @param array> $results */ private function render(ServerRequestInterface $request, array $results): ResponseInterface { $view = $this->moduleTemplateFactory->create($request); $this->pageRenderer->addCssFile('EXT:vitec/Resources/Public/Css/backend-import.css'); $view->assignMultiple([ 'workbooks' => $this->workbooks(), 'inbox' => self::INBOX, 'results' => $results, 'hasResults' => $results !== [], ]); return $view->renderResponse('Import/Submarkets'); } /** @return array */ private function workbooks(): array { $dir = $this->inboxPath(); if (!is_dir($dir)) { return []; } $rows = []; foreach (scandir($dir) ?: [] as $name) { if (!str_ends_with($name, '.xlsx') || str_starts_with($name, '~$')) { continue; } $path = $dir . '/' . $name; if (!is_file($path)) { continue; } $rows[] = [ 'name' => $name, 'size' => sprintf('%.0f KB', filesize($path) / 1024), 'modified' => date('Y-m-d H:i', (int)filemtime($path)), ]; } usort($rows, static fn(array $a, array $b): int => strcasecmp($a['name'], $b['name'])); return $rows; } private function inboxPath(): string { return Environment::getProjectPath() . '/' . self::INBOX; } /** Client filename reduced to a safe basename; '' when it is no .xlsx. */ private function safeName(string $clientFilename): string { $name = basename(str_replace('\\', '/', trim($clientFilename))); if (!str_ends_with(mb_strtolower($name), '.xlsx') || str_starts_with($name, '~$') || str_starts_with($name, '.')) { return ''; } return (string)preg_replace('/[^\w.\-() ]+/u', '_', $name); } private function message(string $text, ContextualFeedbackSeverity $severity): void { $message = GeneralUtility::makeInstance(FlashMessage::class, $text, '', $severity, true); $this->flashMessageService->getMessageQueueByIdentifier()->addMessage($message); } }