diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2024-03-08 13:08:57 -0100 |
---|---|---|
committer | Daniel Kesselberg <mail@danielkesselberg.de> | 2024-11-07 19:33:58 +0100 |
commit | 53425bd06ae45507581fc8094c7c440e461bccb5 (patch) | |
tree | b9ad3f3c3acee32c47a2c0ca97b838116fafa4db | |
parent | d52e850623bd7f9ea8d880a39e440d58e7145257 (diff) | |
download | nextcloud-server-backport/42170/stable28.tar.gz nextcloud-server-backport/42170/stable28.zip |
feat(files): copy live photosbackport/42170/stable28
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
-rw-r--r-- | apps/files/lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | apps/files/lib/Listener/SyncLivePhotosListener.php | 81 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/Files/Node/HookConnector.php | 50 | ||||
-rw-r--r-- | lib/private/Server.php | 3 | ||||
-rw-r--r-- | lib/public/Exceptions/AbortedEventException.php | 34 | ||||
-rw-r--r-- | lib/public/Files/Events/Node/AbstractNodeEvent.php | 8 | ||||
-rw-r--r-- | lib/public/Files/Events/Node/AbstractNodesEvent.php | 12 | ||||
-rw-r--r-- | lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php | 20 | ||||
-rw-r--r-- | lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php | 20 | ||||
-rw-r--r-- | tests/lib/Files/Node/HookConnectorTest.php | 9 |
12 files changed, 153 insertions, 90 deletions
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 15299a3cebd..13bc938f3ea 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -58,8 +58,10 @@ use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\IProviderManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\IConfig; use OCP\IPreview; use OCP\IRequest; @@ -127,6 +129,8 @@ class Application extends App implements IBootstrap { $context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class); $context->registerEventListener(BeforeNodeDeletedEvent::class, SyncLivePhotosListener::class); $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class); + $context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class); + $context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class); $context->registerSearchProvider(FilesSearchProvider::class); diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php index 1779fdda18b..baf84432ae9 100644 --- a/apps/files/lib/Listener/SyncLivePhotosListener.php +++ b/apps/files/lib/Listener/SyncLivePhotosListener.php @@ -27,20 +27,23 @@ namespace OCA\Files\Listener; use OCA\Files\Service\LivePhotosService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\Exceptions\AbortedEventException; use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\Files\Events\Node\AbstractNodesEvent; +use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Folder; use OCP\Files\Node; use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; use OCP\FilesMetadata\IFilesMetadataManager; /** * @template-implements IEventListener<Event> */ class SyncLivePhotosListener implements IEventListener { - /** @var Array<int, string> */ + /** @var Array<int> */ private array $pendingRenames = []; /** @var Array<int, bool> */ private array $pendingDeletion = []; @@ -65,6 +68,8 @@ class SyncLivePhotosListener implements IEventListener { $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getNode()->getId()); } elseif ($event instanceof CacheEntryRemovedEvent) { $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getFileId()); + } elseif ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) { + $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId()); } if ($peerFileId === null) { @@ -72,18 +77,23 @@ class SyncLivePhotosListener implements IEventListener { } // Check the user's folder. - $peerFile = $this->userFolder->getById($peerFileId)[0]; + $peerFiles = array_values($this->userFolder->getById($peerFileId)); + $peerFile = $peerFiles[0]; if ($peerFile === null) { return; // Peer file not found. } if ($event instanceof BeforeNodeRenamedEvent) { - $this->handleMove($event, $peerFile); + $this->handleMove($event, $peerFile, false); } elseif ($event instanceof BeforeNodeDeletedEvent) { $this->handleDeletion($event, $peerFile); } elseif ($event instanceof CacheEntryRemovedEvent) { $peerFile->delete(); + } elseif ($event instanceof BeforeNodeCopiedEvent) { + $this->handleMove($event, $peerFile, true); + } elseif ($event instanceof NodeCopiedEvent) { + $this->handleCopy($event, $peerFile); } } @@ -94,44 +104,79 @@ class SyncLivePhotosListener implements IEventListener { * of pending renames inside the 'pendingRenames' property, * to prevent infinite recursive. */ - private function handleMove(BeforeNodeRenamedEvent $event, Node $peerFile): void { + private function handleMove(AbstractNodesEvent $event, Node $peerFile, bool $prepForCopyOnly = false): void { + if (!($event instanceof BeforeNodeCopiedEvent) && + !($event instanceof BeforeNodeRenamedEvent)) { + return; + } + $sourceFile = $event->getSource(); $targetFile = $event->getTarget(); $targetParent = $targetFile->getParent(); $sourceExtension = $sourceFile->getExtension(); $peerFileExtension = $peerFile->getExtension(); $targetName = $targetFile->getName(); - $targetPath = $targetFile->getPath(); - if (!str_ends_with($targetName, ".".$sourceExtension)) { - $event->abortOperation(new NotPermittedException("Cannot change the extension of a Live Photo")); + if (!str_ends_with($targetName, "." . $sourceExtension)) { + throw new AbortedEventException('Cannot change the extension of a Live Photo'); } try { $targetParent->get($targetName); - $event->abortOperation(new NotPermittedException("A file already exist at destination path of the Live Photo")); - } catch (NotFoundException $ex) { + throw new AbortedEventException('A file already exist at destination path of the Live Photo'); + } catch (NotFoundException) { } $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension; try { $targetParent->get($peerTargetName); - $event->abortOperation(new NotPermittedException("A file already exist at destination path of the Live Photo")); - } catch (NotFoundException $ex) { + throw new AbortedEventException('A file already exist at destination path of the Live Photo'); + } catch (NotFoundException) { } // in case the rename was initiated from this listener, we stop right now - if (array_key_exists($peerFile->getId(), $this->pendingRenames)) { + if ($prepForCopyOnly || in_array($peerFile->getId(), $this->pendingRenames)) { return; } - $this->pendingRenames[$sourceFile->getId()] = $targetPath; + $this->pendingRenames[] = $sourceFile->getId(); try { $peerFile->move($targetParent->getPath() . '/' . $peerTargetName); } catch (\Throwable $ex) { - $event->abortOperation($ex); + throw new AbortedEventException($ex->getMessage()); } - unset($this->pendingRenames[$sourceFile->getId()]); + + array_diff($this->pendingRenames, [$sourceFile->getId()]); + } + + + /** + * handle copy, we already know if it is doable from BeforeNodeCopiedEvent, so we just copy the linked file + * + * @param NodeCopiedEvent $event + * @param Node $peerFile + */ + private function handleCopy(NodeCopiedEvent $event, Node $peerFile): void { + $sourceFile = $event->getSource(); + $sourceExtension = $sourceFile->getExtension(); + $peerFileExtension = $peerFile->getExtension(); + $targetFile = $event->getTarget(); + $targetParent = $targetFile->getParent(); + $targetName = $targetFile->getName(); + $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension; + + /** + * let's use freshly set variable. + * we copy the file and get its id. We already have the id of the current copy + * We have everything to update metadata and keep the link between the 2 copies. + */ + $newPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName); + $targetMetadata = $this->filesMetadataManager->getMetadata($targetFile->getId(), true); + $targetMetadata->setString('files-live-photo', (string)$newPeerFile->getId()); + $this->filesMetadataManager->saveMetadata($targetMetadata); + $peerMetadata = $this->filesMetadataManager->getMetadata($newPeerFile->getId(), true); + $peerMetadata->setString('files-live-photo', (string)$targetFile->getId()); + $this->filesMetadataManager->saveMetadata($peerMetadata); } /** @@ -148,14 +193,14 @@ class SyncLivePhotosListener implements IEventListener { unset($this->pendingDeletion[$peerFile->getId()]); return; } else { - $event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo")); + throw new AbortedEventException("Cannot delete the video part of a live photo"); } } else { $this->pendingDeletion[$deletedFile->getId()] = true; try { $peerFile->delete(); } catch (\Throwable $ex) { - $event->abortOperation($ex); + throw new AbortedEventException($ex->getMessage()); } } return; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 972bd36e31a..a46e06b1f83 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -272,6 +272,7 @@ return array( 'OCP\\EventDispatcher\\GenericEvent' => $baseDir . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => $baseDir . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => $baseDir . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\Exceptions\\AbortedEventException' => $baseDir . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Federation\\Events\\TrustedServerRemovedEvent' => $baseDir . '/lib/public/Federation/Events/TrustedServerRemovedEvent.php', 'OCP\\Federation\\Exceptions\\ActionNotSupportedException' => $baseDir . '/lib/public/Federation/Exceptions/ActionNotSupportedException.php', 'OCP\\Federation\\Exceptions\\AuthenticationFailedException' => $baseDir . '/lib/public/Federation/Exceptions/AuthenticationFailedException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 806cb8d30a5..8649684f77b 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -305,6 +305,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\EventDispatcher\\GenericEvent' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\Exceptions\\AbortedEventException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Federation\\Events\\TrustedServerRemovedEvent' => __DIR__ . '/../../..' . '/lib/public/Federation/Events/TrustedServerRemovedEvent.php', 'OCP\\Federation\\Exceptions\\ActionNotSupportedException' => __DIR__ . '/../../..' . '/lib/public/Federation/Exceptions/ActionNotSupportedException.php', 'OCP\\Federation\\Exceptions\\AuthenticationFailedException' => __DIR__ . '/../../..' . '/lib/public/Federation/Exceptions/AuthenticationFailedException.php', diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index f61eedee66e..d66c7a60664 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -6,6 +6,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * @author Maxence Lange <maxence@artificial-owl.com> * @author Robin Appelman <robin@icewind.nl> * @author Roeland Jago Douma <roeland@famdouma.nl> * @@ -30,6 +31,7 @@ use OC\Files\Filesystem; use OC\Files\View; use OCP\EventDispatcher\GenericEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Exceptions\AbortedEventException; use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeCreatedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; @@ -46,27 +48,18 @@ use OCP\Files\Events\Node\NodeWrittenEvent; use OCP\Files\FileInfo; use OCP\Files\IRootFolder; use OCP\Util; +use Psr\Log\LoggerInterface; class HookConnector { - /** @var IRootFolder */ - private $root; - - /** @var View */ - private $view; - /** @var FileInfo[] */ - private $deleteMetaCache = []; - - /** @var IEventDispatcher */ - private $dispatcher; + private array $deleteMetaCache = []; public function __construct( - IRootFolder $root, - View $view, - IEventDispatcher $dispatcher) { - $this->root = $root; - $this->view = $view; - $this->dispatcher = $dispatcher; + private IRootFolder $root, + private View $view, + private IEventDispatcher $dispatcher, + private LoggerInterface $logger + ) { } public function viewToNode() { @@ -133,8 +126,13 @@ class HookConnector { $this->root->emit('\OC\Files', 'preDelete', [$node]); $this->dispatcher->dispatch('\OCP\Files::preDelete', new GenericEvent($node)); - $event = new BeforeNodeDeletedEvent($node, $arguments['run']); - $this->dispatcher->dispatchTyped($event); + $event = new BeforeNodeDeletedEvent($node); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('delete process aborted', ['exception' => $e]); + } } public function postDelete($arguments) { @@ -171,8 +169,13 @@ class HookConnector { $this->root->emit('\OC\Files', 'preRename', [$source, $target]); $this->dispatcher->dispatch('\OCP\Files::preRename', new GenericEvent([$source, $target])); - $event = new BeforeNodeRenamedEvent($source, $target, $arguments['run']); - $this->dispatcher->dispatchTyped($event); + $event = new BeforeNodeRenamedEvent($source, $target); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('rename process aborted', ['exception' => $e]); + } } public function postRename($arguments) { @@ -192,7 +195,12 @@ class HookConnector { $this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target])); $event = new BeforeNodeCopiedEvent($source, $target); - $this->dispatcher->dispatchTyped($event); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('copy process aborted', ['exception' => $e]); + } } public function postCopy($arguments) { diff --git a/lib/private/Server.php b/lib/private/Server.php index 19b789addc5..292871e1320 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -469,7 +469,8 @@ class Server extends ServerContainer implements IServerContainer { return new HookConnector( $c->get(IRootFolder::class), new View(), - $c->get(IEventDispatcher::class) + $c->get(IEventDispatcher::class), + $c->get(LoggerInterface::class) ); }); diff --git a/lib/public/Exceptions/AbortedEventException.php b/lib/public/Exceptions/AbortedEventException.php new file mode 100644 index 00000000000..d8b4203d7e2 --- /dev/null +++ b/lib/public/Exceptions/AbortedEventException.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Exceptions; + +use Exception; + +/** + * @since 28.0.13 + */ +class AbortedEventException extends Exception { +} diff --git a/lib/public/Files/Events/Node/AbstractNodeEvent.php b/lib/public/Files/Events/Node/AbstractNodeEvent.php index d6c1c6d0f95..768c0eda2d5 100644 --- a/lib/public/Files/Events/Node/AbstractNodeEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodeEvent.php @@ -32,14 +32,12 @@ use OCP\Files\Node; * @since 20.0.0 */ abstract class AbstractNodeEvent extends Event { - /** @var Node */ - private $node; - /** * @since 20.0.0 */ - public function __construct(Node $node) { - $this->node = $node; + public function __construct( + private Node $node + ) { } /** diff --git a/lib/public/Files/Events/Node/AbstractNodesEvent.php b/lib/public/Files/Events/Node/AbstractNodesEvent.php index 6bd9bc32a88..d736ebf1635 100644 --- a/lib/public/Files/Events/Node/AbstractNodesEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodesEvent.php @@ -32,17 +32,13 @@ use OCP\Files\Node; * @since 20.0.0 */ abstract class AbstractNodesEvent extends Event { - /** @var Node */ - private $source; - /** @var Node */ - private $target; - /** * @since 20.0.0 */ - public function __construct(Node $source, Node $target) { - $this->source = $source; - $this->target = $target; + public function __construct( + private Node $source, + private Node $target + ) { } /** diff --git a/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php b/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php index dd29a39a279..c0226a9b527 100644 --- a/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php +++ b/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php @@ -25,31 +25,17 @@ declare(strict_types=1); */ namespace OCP\Files\Events\Node; -use Exception; -use OCP\Files\Node; +use OCP\Exceptions\AbortedEventException; /** * @since 20.0.0 */ class BeforeNodeDeletedEvent extends AbstractNodeEvent { /** - * @since 20.0.0 - */ - public function __construct(Node $node, private bool &$run) { - parent::__construct($node); - } - - /** * @since 28.0.0 - * @return never + * @deprecated 29.0.0 - use OCP\Exceptions\AbortedEventException instead */ public function abortOperation(\Throwable $ex = null) { - $this->stopPropagation(); - $this->run = false; - if ($ex !== null) { - throw $ex; - } else { - throw new Exception('Operation aborted'); - } + throw new AbortedEventException($ex?->getMessage() ?? 'Operation aborted'); } } diff --git a/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php b/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php index c6876666713..4c2c566c8c6 100644 --- a/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php +++ b/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php @@ -25,31 +25,17 @@ declare(strict_types=1); */ namespace OCP\Files\Events\Node; -use Exception; -use OCP\Files\Node; +use OCP\Exceptions\AbortedEventException; /** * @since 20.0.0 */ class BeforeNodeRenamedEvent extends AbstractNodesEvent { /** - * @since 20.0.0 - */ - public function __construct(Node $source, Node $target, private bool &$run) { - parent::__construct($source, $target); - } - - /** * @since 28.0.0 - * @return never + * @deprecated 29.0.0 - use OCP\Exceptions\AbortedEventException instead */ public function abortOperation(\Throwable $ex = null) { - $this->stopPropagation(); - $this->run = false; - if ($ex !== null) { - throw $ex; - } else { - throw new Exception('Operation aborted'); - } + throw new AbortedEventException($ex?->getMessage() ?? 'Operation aborted'); } } diff --git a/tests/lib/Files/Node/HookConnectorTest.php b/tests/lib/Files/Node/HookConnectorTest.php index 0501c175a5f..5a2e459b898 100644 --- a/tests/lib/Files/Node/HookConnectorTest.php +++ b/tests/lib/Files/Node/HookConnectorTest.php @@ -51,6 +51,8 @@ class HookConnectorTest extends TestCase { /** @var IEventDispatcher */ protected $eventDispatcher; + private LoggerInterface $logger; + /** @var View */ private $view; @@ -78,6 +80,7 @@ class HookConnectorTest extends TestCase { $this->createMock(IEventDispatcher::class) ); $this->eventDispatcher = \OC::$server->query(IEventDispatcher::class); + $this->logger = \OC::$server->query(LoggerInterface::class); } protected function tearDown(): void { @@ -143,7 +146,7 @@ class HookConnectorTest extends TestCase { * @dataProvider viewToNodeProvider */ public function testViewToNode(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent) { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookNode */ @@ -212,7 +215,7 @@ class HookConnectorTest extends TestCase { * @dataProvider viewToNodeProviderCopyRename */ public function testViewToNodeCopyRename(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent) { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookSourceNode */ @@ -267,7 +270,7 @@ class HookConnectorTest extends TestCase { } public function testPostDeleteMeta() { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookNode */ |