-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0e49061
commit c257caa
Showing
15 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<?php | ||
|
||
namespace CodeQ\BulkTitleImporter\Controller; | ||
|
||
/* | ||
* This file is part of the CodeQ.BulkTitleImporter package. | ||
*/ | ||
|
||
use CodeQ\BulkTitleImporter\Service\ImportService; | ||
use InvalidArgumentException; | ||
use Neos\ContentRepository\Domain\Model\Workspace; | ||
use Neos\Flow\Annotations as Flow; | ||
use Neos\Flow\Mvc\View\ViewInterface; | ||
use Neos\Flow\ResourceManagement\Exception; | ||
use Neos\Flow\ResourceManagement\PersistentResource; | ||
use Neos\Fusion\View\FusionView; | ||
use Neos\Neos\Controller\Module\AbstractModuleController; | ||
use Neos\Neos\Service\UserService; | ||
use Neos\Neos\Ui\ContentRepository\Service\WorkspaceService; | ||
|
||
class ImportModuleController extends AbstractModuleController | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
protected $defaultViewObjectName = FusionView::class; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var ImportService | ||
*/ | ||
protected $importService; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var WorkspaceService | ||
*/ | ||
protected $workspaceService; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var UserService | ||
*/ | ||
protected $userService; | ||
|
||
public function indexAction(): void | ||
{ | ||
$this->view->assign('workspaces', [ | ||
[ | ||
'name' => $this->userService->getPersonalWorkspaceName(), | ||
'title' => 'Persönlicher Arbeitsbereich' | ||
], | ||
...array_filter($this->workspaceService->getAllowedTargetWorkspaces(), static fn (array $workspace) => $workspace['name'] !== 'live') | ||
]); | ||
} | ||
|
||
/** | ||
* @param PersistentResource $file | ||
* @param string $targetWorkspaceName | ||
* | ||
* @return void | ||
* @throws \Neos\Flow\Mvc\Exception\StopActionException | ||
*/ | ||
public function importAction(PersistentResource $file, string $targetWorkspaceName): void | ||
{ | ||
try { | ||
$importResult = $this->importService->importFromPersistentResource($file, $targetWorkspaceName); | ||
} catch (Exception | InvalidArgumentException $exception) { | ||
$this->addFlashMessage('The file could not be imported: ' . $exception->getMessage(), 'Error', \Neos\Error\Messages\Message::SEVERITY_ERROR); | ||
$this->redirect('index'); | ||
return; | ||
} | ||
$this->view->assign('importResult', $importResult); | ||
$this->view->assign('targetWorkspaceName', $targetWorkspaceName); | ||
} | ||
|
||
/** | ||
* @param FusionView $view | ||
* | ||
* @return void | ||
*/ | ||
protected function initializeView(ViewInterface $view): void | ||
{ | ||
parent::initializeView($view); | ||
$view->setFusionPathPattern('resource://CodeQ.BulkTitleImporter/Private/Fusion/Module'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
|
||
namespace CodeQ\BulkTitleImporter\Dto; | ||
|
||
use Neos\Flow\Annotations as Flow; | ||
|
||
/** | ||
* @Flow\ValueObject | ||
*/ | ||
class ImportResult | ||
{ | ||
/** | ||
* @var array<string> Error messages that occurred during the import | ||
*/ | ||
protected array $errors = []; | ||
|
||
/** | ||
* @var int Number of nodes that have been imported | ||
*/ | ||
protected int $importedNodes = 0; | ||
|
||
public function __construct(array $errors = [], int $importedNodes = 0) | ||
{ | ||
$this->errors = $errors; | ||
$this->importedNodes = $importedNodes - count($errors); | ||
} | ||
|
||
public function getErrors(): array | ||
{ | ||
return $this->errors; | ||
} | ||
|
||
public function getImportedNodes(): int | ||
{ | ||
return $this->importedNodes; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
namespace CodeQ\BulkTitleImporter\Service; | ||
|
||
use InvalidArgumentException; | ||
use PhpOffice\PhpSpreadsheet\IOFactory; | ||
use PhpOffice\PhpSpreadsheet\Reader\IReader; | ||
use PhpOffice\PhpSpreadsheet\Reader\Xlsx; | ||
|
||
class FileService | ||
{ | ||
/** | ||
* @var Xlsx | ||
*/ | ||
protected IReader $reader; | ||
|
||
public function __construct() | ||
{ | ||
$this->reader = IOFactory::createReader(IOFactory::READER_XLSX); | ||
} | ||
|
||
/** | ||
* @param string $filePath | ||
* | ||
* @return array | ||
* @throws InvalidArgumentException | ||
*/ | ||
public function read(string $filePath): array | ||
{ | ||
$spreadsheet = $this->reader->load($filePath, IReader::READ_DATA_ONLY); | ||
$worksheet = $spreadsheet->getAllSheets()[0]; | ||
$rows = $worksheet->toArray(); | ||
$this->validateFileContents($rows); | ||
// Remove the header row | ||
array_shift($rows); | ||
return $rows; | ||
} | ||
|
||
/** | ||
* @param array $rows | ||
* | ||
* @return void | ||
* @throws InvalidArgumentException | ||
*/ | ||
protected function validateFileContents(array $rows): void | ||
{ | ||
if (count($rows) < 2) { | ||
throw new InvalidArgumentException('The file must contain at least two rows.'); | ||
} | ||
if ($rows[0] !== ['URL', 'Title']) { | ||
throw new InvalidArgumentException('The first row must contain the headers "URL" and "Title".'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?php | ||
|
||
namespace CodeQ\BulkTitleImporter\Service; | ||
|
||
use CodeQ\BulkTitleImporter\Dto\ImportResult; | ||
use InvalidArgumentException; | ||
use Neos\Flow\Annotations as Flow; | ||
use Neos\Flow\ResourceManagement\Exception; | ||
use Neos\Flow\ResourceManagement\PersistentResource; | ||
|
||
class ImportService | ||
{ | ||
/** | ||
* @Flow\Inject | ||
* @var FileService | ||
*/ | ||
protected FileService $fileService; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var NodeFindingService | ||
*/ | ||
protected NodeFindingService $nodeFindingService; | ||
|
||
/** | ||
* @Flow\InjectConfiguration(path="nodeProperty") | ||
* @var string | ||
*/ | ||
protected string $nodeProperty; | ||
|
||
/** | ||
* @param PersistentResource $file | ||
* @param string $targetWorkspaceName | ||
* | ||
* @return ImportResult | ||
* @throws Exception | ||
* @throws InvalidArgumentException | ||
*/ | ||
public function importFromPersistentResource(PersistentResource $file, string $targetWorkspaceName): ImportResult | ||
{ | ||
$errorBuffer = []; | ||
$temporaryFilePath = $file->createTemporaryLocalCopy(); | ||
$rows = $this->fileService->read($temporaryFilePath); | ||
$this->import($rows, $targetWorkspaceName, $errorBuffer); | ||
return new ImportResult( | ||
$errorBuffer, | ||
count($rows) | ||
); | ||
} | ||
|
||
/** | ||
* @param array $rows | ||
* @param string $targetWorkspaceName | ||
* @param array $errorBuffer | ||
* | ||
* @return void | ||
*/ | ||
protected function import(array $rows, string $targetWorkspaceName, array &$errorBuffer): void | ||
{ | ||
foreach ($rows as [$url, $title]) { | ||
$node = $this->nodeFindingService->tryToResolvePublicUriToNode($url, $targetWorkspaceName); | ||
if ($node === null) { | ||
$errorBuffer[] = $url; | ||
continue; | ||
} | ||
$node->setProperty($this->nodeProperty, self::sanitizeTitle($title)); | ||
} | ||
} | ||
|
||
protected static function sanitizeTitle(string $title): string | ||
{ | ||
return trim(strip_tags($title)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?php | ||
|
||
namespace CodeQ\BulkTitleImporter\Service; | ||
|
||
use GuzzleHttp\Psr7\Uri; | ||
use Neos\ContentRepository\Domain\Model\NodeInterface; | ||
use Neos\ContentRepository\Domain\Utility\NodePaths; | ||
use Neos\Flow\Annotations as Flow; | ||
use Neos\Flow\Mvc\Routing\Dto\RouteParameters; | ||
use Neos\Flow\ObjectManagement\ObjectManagerInterface; | ||
use Neos\Neos\Controller\CreateContentContextTrait; | ||
use Neos\Neos\Routing\FrontendNodeRoutePartHandlerInterface; | ||
|
||
class NodeFindingService | ||
{ | ||
use CreateContentContextTrait; | ||
|
||
/** | ||
* @Flow\InjectConfiguration(package="Neos.Flow", path="mvc.routes") | ||
* @var array | ||
*/ | ||
protected array $routesConfiguration; | ||
|
||
/** | ||
* @Flow\InjectConfiguration(path="overrideNodeUriPathSuffix") | ||
* @var string|null | ||
*/ | ||
protected ?string $overrideNodeUriPathSuffix; | ||
|
||
/** | ||
* @Flow\Inject | ||
* @var ObjectManagerInterface | ||
*/ | ||
protected $objectManager; | ||
|
||
/** | ||
* @param mixed $term | ||
* @param string $targetWorkspaceName | ||
* | ||
* @return NodeInterface|null | ||
*/ | ||
public function tryToResolvePublicUriToNode(mixed $term, string $targetWorkspaceName): ?NodeInterface | ||
{ | ||
if (!preg_match('/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/', $term)) { | ||
return null; | ||
} | ||
|
||
$uri = new Uri($term); | ||
// Remove the starting slash. | ||
$path = str_starts_with($uri->getPath(), '/') ? substr($uri->getPath(), 1) : $uri->getPath(); | ||
|
||
$routeHandler = $this->objectManager->get(FrontendNodeRoutePartHandlerInterface::class); | ||
$routeHandler->setName('node'); | ||
|
||
$uriPathSuffix = !empty($this->overrideNodeUriPathSuffix) ? $this->overrideNodeUriPathSuffix : $this->routesConfiguration['Neos.Neos']['variables']['defaultUriSuffix']; | ||
$routeHandler->setOptions(['uriPathSuffix' => $uriPathSuffix]); | ||
|
||
$routeParameters = RouteParameters::createEmpty(); | ||
// This is needed for the FrontendNodeRoutePartHandler to correctly identify the current site | ||
$routeParameters = $routeParameters->withParameter('requestUriHost', $uri->getHost()); | ||
$matchResult = $routeHandler->matchWithParameters($path, $routeParameters); | ||
|
||
if (!$matchResult || !$matchResult->getMatchedValue()) { | ||
return null; | ||
} | ||
|
||
$nodeContextPath = $matchResult->getMatchedValue(); | ||
$nodeContextPathSegments = NodePaths::explodeContextPath($nodeContextPath); | ||
$nodePath = $nodeContextPathSegments['nodePath']; | ||
$context = $this->createContentContext($targetWorkspaceName, $nodeContextPathSegments['dimensions']); | ||
$matchingNode = $context->getNode($nodePath); | ||
|
||
if (!$matchingNode) { | ||
return null; | ||
} | ||
|
||
return $matchingNode; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
privilegeTargets: | ||
'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': | ||
'CodeQ.BulkTitleImporter:CanImportTitles': | ||
matcher: 'within(CodeQ\BulkTitleImporter\Controller\ImportModuleController)' | ||
|
||
roles: | ||
'CodeQ.BulkTitleImporter:TitleImporter': | ||
privileges: | ||
- privilegeTarget: 'CodeQ.BulkTitleImporter:CanImportTitles' | ||
permission: GRANT | ||
'Neos.Neos:Administrator': | ||
privileges: | ||
- privilegeTarget: 'CodeQ.BulkTitleImporter:CanImportTitles' | ||
permission: GRANT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
Neos: | ||
Neos: | ||
modules: | ||
management: | ||
submodules: | ||
bulkTitleImporter: | ||
label: 'SEO-Titel importieren' | ||
description: '' | ||
icon: 'fa fa-file-import' | ||
controller: 'CodeQ\BulkTitleImporter\Controller\ImportModuleController' | ||
privilegeTarget: 'CodeQ.BulkTitleImporter:CanImportTitles' | ||
|
||
CodeQ: | ||
BulkTitleImporter: | ||
overrideNodeUriPathSuffix: null | ||
nodeProperty: 'titleOverride' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# CodeQ.BulkTitleImporter |
Oops, something went wrong.