From afe31b66d0d5ae739811dbb2d62f3c20e3dad690 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Wed, 6 Nov 2024 10:25:15 -0100 Subject: [PATCH] fix duplicate name on fedcircle Signed-off-by: Maxence Lange --- lib/Db/CoreQueryBuilder.php | 4 +- lib/Db/MountPointRequest.php | 52 +++++++ lib/Db/MountPointRequestBuilder.php | 66 +++++++++ lib/Db/MountRequest.php | 6 +- .../MountPointNotFoundException.php | 14 ++ .../Version0031Date20241105133904.php | 42 ++++++ lib/Model/ModelManager.php | 14 +- lib/Model/Mount.php | 135 +++++++----------- lib/Model/Mountpoint.php | 99 ++++--------- lib/MountManager/CircleMountManager.php | 7 +- lib/MountManager/CircleMountProvider.php | 83 ++++++++++- psalm.xml | 7 + tests/psalm-baseline.xml | 32 +++-- 13 files changed, 379 insertions(+), 182 deletions(-) create mode 100644 lib/Db/MountPointRequest.php create mode 100644 lib/Db/MountPointRequestBuilder.php create mode 100644 lib/Exceptions/MountPointNotFoundException.php create mode 100644 lib/Migration/Version0031Date20241105133904.php diff --git a/lib/Db/CoreQueryBuilder.php b/lib/Db/CoreQueryBuilder.php index bb7215971..bf6daab09 100644 --- a/lib/Db/CoreQueryBuilder.php +++ b/lib/Db/CoreQueryBuilder.php @@ -1564,7 +1564,7 @@ public function limitToShareOwner( * * @throws RequestBuilderException */ - public function leftJoinMountpoint(string $aliasMount, string $aliasMountMemberships = '') { + public function leftJoinMountpoint(string $aliasMount, IFederatedUser $federatedUser, string $aliasMountMemberships = '') { $expr = $this->expr(); $aliasMountpoint = $this->generateAlias($aliasMount, self::MOUNTPOINT); @@ -1576,7 +1576,7 @@ public function leftJoinMountpoint(string $aliasMount, string $aliasMountMembers $aliasMountMemberships, CoreRequestBuilder::TABLE_MOUNTPOINT, $aliasMountpoint, $expr->andX( $expr->eq($aliasMountpoint . '.mount_id', $aliasMount . '.mount_id'), - $expr->eq($aliasMountpoint . '.single_id', $aliasMountMemberships . '.single_id') + $expr->eq($aliasMountpoint . '.single_id', $this->createNamedParameter($federatedUser->getSingleId())) ) ); diff --git a/lib/Db/MountPointRequest.php b/lib/Db/MountPointRequest.php new file mode 100644 index 000000000..9be98d268 --- /dev/null +++ b/lib/Db/MountPointRequest.php @@ -0,0 +1,52 @@ +getMountPointInsertSql(); + + $hash = ($mountpoint->getMountPoint() === '-') ? '' : md5($mountpoint->getMountPoint()); + $qb->setValue('mountpoint', $qb->createNamedParameter($mountpoint->getMountPoint())) + ->setValue('mountpoint_hash', $qb->createNamedParameter($hash)) + ->setValue('mount_id', $qb->createNamedParameter($mountpoint->getMountId())) + ->setValue('single_id', $qb->createNamedParameter($mountpoint->getSingleId())); + $qb->executeStatement(); + } + + /** + * @param Mountpoint $mountpoint + * @throws MountNotFoundException + */ + public function update(Mountpoint $mountpoint): void { + $qb = $this->getMountPointUpdateSql(); + + $hash = ($mountpoint->getMountPoint() === '-') ? '' : md5($mountpoint->getMountPoint()); + + $qb->set('mountpoint', $qb->createNamedParameter($mountpoint->getMountPoint())) + ->set('mountpoint_hash', $qb->createNamedParameter($hash)); + $qb->limit('mount_id', $mountpoint->getMountId()); + $qb->limitToSingleId($mountpoint->getSingleId()); + $nb = $qb->executeStatement(); + + if ($nb === 0) { + throw new MountNotFoundException('Mount not found'); + } + } +} diff --git a/lib/Db/MountPointRequestBuilder.php b/lib/Db/MountPointRequestBuilder.php new file mode 100644 index 000000000..4a0b8485e --- /dev/null +++ b/lib/Db/MountPointRequestBuilder.php @@ -0,0 +1,66 @@ +getQueryBuilder(); + $qb->insert(self::TABLE_MOUNTPOINT); + + return $qb; + } + + protected function getMountPointUpdateSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->update(self::TABLE_MOUNTPOINT); + + return $qb; + } + + protected function getMountPointSelectSql(string $alias = CoreQueryBuilder::MOUNTPOINT): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->generateSelect(self::TABLE_MOUNTPOINT, self::$tables[self::TABLE_MOUNTPOINT], $alias); + + return $qb; + } + + protected function getMountPointDeleteSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->delete(self::TABLE_MOUNTPOINT); + + return $qb; + } + + public function getItemFromRequest(CoreQueryBuilder $qb): MountPoint { + /** @var MountPoint $mountpoint */ + try { + $mountpoint = $qb->asItem(MountPoint::class); + } catch (RowNotFoundException $e) { + throw new MountNotFoundException('Mount not found'); + } + + return $mountpoint; + } + + /** + * @param CoreQueryBuilder $qb + * + * @return Mount[] + */ + public function getItemsFromRequest(CoreQueryBuilder $qb): array { + /** @var MountPoint[] $result */ + return $qb->asItems(MountPoint::class); + } +} diff --git a/lib/Db/MountRequest.php b/lib/Db/MountRequest.php index 093298143..eb1730cae 100644 --- a/lib/Db/MountRequest.php +++ b/lib/Db/MountRequest.php @@ -35,8 +35,8 @@ public function save(Mount $mount): void { ->setValue('single_id', $qb->createNamedParameter($mount->getOwner()->getSingleId())) ->setValue('token', $qb->createNamedParameter($mount->getToken())) ->setValue('parent', $qb->createNamedParameter($mount->getParent())) - ->setValue('mountpoint', $qb->createNamedParameter($mount->getMountPoint())) - ->setValue('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))); + ->setValue('mountpoint', $qb->createNamedParameter($mount->getOriginalMountPoint())) + ->setValue('mountpoint_hash', $qb->createNamedParameter(md5($mount->getOriginalMountPoint()))); $qb->execute(); } @@ -63,7 +63,7 @@ public function getForUser(IFederatedUser $federatedUser): array { $qb = $this->getMountSelectSql(); $qb->setOptions([CoreQueryBuilder::MOUNT], ['getData' => true]); $qb->leftJoinMember(CoreQueryBuilder::MOUNT); - $qb->leftJoinMountpoint(CoreQueryBuilder::MOUNT); + $qb->leftJoinMountpoint(CoreQueryBuilder::MOUNT, $federatedUser); $qb->limitToInitiator(CoreQueryBuilder::MOUNT, $federatedUser, 'circle_id'); return $this->getItemsFromRequest($qb); diff --git a/lib/Exceptions/MountPointNotFoundException.php b/lib/Exceptions/MountPointNotFoundException.php new file mode 100644 index 000000000..24fac7e68 --- /dev/null +++ b/lib/Exceptions/MountPointNotFoundException.php @@ -0,0 +1,14 @@ +getTable('circles_mountpoint'); + if (!$table->hasIndex('dname')) { + $table->addUniqueIndex(['single_id', 'mountpoint_hash'], 'mp_sid_hash'); + } + } catch (SchemaException $e) { + $this->logger->warning('Could not find circles_mountpoint', ['exception' => $e]); + } + + return $schema; + } +} diff --git a/lib/Model/ModelManager.php b/lib/Model/ModelManager.php index 8655959e7..6c82242ab 100644 --- a/lib/Model/ModelManager.php +++ b/lib/Model/ModelManager.php @@ -22,6 +22,7 @@ use OCA\Circles\Exceptions\FileCacheNotFoundException; use OCA\Circles\Exceptions\MemberNotFoundException; use OCA\Circles\Exceptions\MembershipNotFoundException; +use OCA\Circles\Exceptions\MountPointNotFoundException; use OCA\Circles\Exceptions\OwnerNotFoundException; use OCA\Circles\Exceptions\RemoteInstanceException; use OCA\Circles\Exceptions\RemoteNotFoundException; @@ -462,7 +463,7 @@ private function importIntoMount( $member = new Member(); $member->importFromDatabase($data, $prefix); $mount->setOwner($member); - } catch (MemberNotFoundException $e) { + } catch (MemberNotFoundException) { } break; @@ -471,7 +472,16 @@ private function importIntoMount( $initiator = new Member(); $initiator->importFromDatabase($data, $prefix); $mount->setInitiator($initiator); - } catch (MemberNotFoundException $e) { + } catch (MemberNotFoundException) { + } + break; + + case CoreQueryBuilder::MOUNTPOINT: + try { + $mountPoint = new Mountpoint(); + $mountPoint->importFromDatabase($data, $prefix); + $mount->setAlternateMountPoint($mountPoint); + } catch (MountPointNotFoundException) { } break; } diff --git a/lib/Model/Mount.php b/lib/Model/Mount.php index dd1ef9c8b..260072424 100644 --- a/lib/Model/Mount.php +++ b/lib/Model/Mount.php @@ -28,51 +28,21 @@ class Mount extends ManagedModel implements IDeserializable, IQueryRow, JsonSerializable { use TArrayTools; - - /** @var int */ - private $id = 0; - - /** @var string */ - private $mountId = ''; - - /** @var string */ - private $circleId = ''; - // - // /** @var string */ - // private $singleId = ''; - - /** @var Member */ - private $owner; - - /** @var Member */ - private $initiator; - - /** @var int */ - private $parent = -1; - - /** @var string */ - private $token = ''; - - /** @var string */ - private $password = ''; - - /** @var string */ - private $mountPoint = ''; - - /** @var string */ - private $mountPointHash = ''; - - /** @var string */ - private $storage; - - /** @var ICloudIdManager */ - private $cloudIdManager; - - /** @var IClientService */ - private $httpClientService; - - /** @var CircleMountManager */ - private $mountManager; + private int $id = 0; + private string $mountId = ''; + private string $circleId = ''; + private Member $owner; + private Member $initiator; + private int $parent = -1; + private string $token = ''; + private string $password = ''; + private string $originalMountPoint = ''; + private string $originalMountPointHash = ''; + private ?Mountpoint $alternateMountPoint = null; + private string $storage; + private ICloudIdManager $cloudIdManager; + private IClientService $httpClientService; + private CircleMountManager $mountManager; /** @@ -135,70 +105,65 @@ public function setCircleId(string $circleId): self { return $this; } - // - // /** - // * - // * @return string - // */ - // public function getSingleId(): string { - // return $this->singleId; - // } - // - // /** - // * @param string $singleId - // * - // * @return Mount - // */ - // public function setSingleId(string $singleId): self { - // $this->singleId = $singleId; - // - // return $this; - // } - - /** * @param bool $raw * * @return string */ public function getMountPoint(bool $raw = true): string { + $mountPoint = $this->getAlternateMountPoint()?->getMountPoint() ?? $this->getOriginalMountPoint(); if ($raw) { - return $this->mountPoint; + return $mountPoint; } - return '/' . $this->getInitiator()->getUserId() . '/files/' . ltrim($this->mountPoint, '/'); + return '/' . $this->getInitiator()->getUserId() . '/files/' . ltrim($mountPoint, '/'); } /** - * @param string $mountPoint + * @return string + */ + public function getOriginalMountPoint(): string { + return $this->originalMountPoint; + } + + /** + * @param string $originalMountPoint * * @return Mount */ - public function setMountPoint(string $mountPoint): self { - $this->mountPoint = $mountPoint; + public function setOriginalMountPoint(string $originalMountPoint): self { + $this->originalMountPoint = $originalMountPoint; return $this; } - /** * @return string */ - public function getMountPointHash(): string { - return $this->mountPointHash; + public function getOriginalMountPointHash(): string { + return $this->originalMountPointHash; } /** - * @param string $mountPointHash + * @param string $originalMountPointHash * * @return Mount */ - public function setMountPointHash(string $mountPointHash): self { - $this->mountPointHash = $mountPointHash; + public function setOriginalMountPointHash(string $originalMountPointHash): self { + $this->originalMountPointHash = $originalMountPointHash; + + return $this; + } + + public function setAlternateMountPoint(Mountpoint $mountPoint): self { + $this->alternateMountPoint = $mountPoint; return $this; } + public function getAlternateMountPoint(): ?Mountpoint { + return $this->alternateMountPoint; + } /** * @return int @@ -417,8 +382,8 @@ public function fromShare(ShareWrapper $wrappedShare) { $this->setOwner($wrappedShare->getOwner()); $this->setToken($wrappedShare->getToken()); $this->setParent(-1); - $this->setMountPoint($wrappedShare->getFileTarget()); - $this->setMountPointHash(md5($wrappedShare->getFileTarget())); + $this->setOriginalMountPoint($wrappedShare->getFileTarget()); + $this->setOriginalMountPointHash(md5($wrappedShare->getFileTarget())); } @@ -443,10 +408,9 @@ public function importFromDatabase(array $data, string $prefix = ''): IQueryRow $this->setCircleId($this->get('circle_id', $data)); $this->setToken($this->get('token', $data)); $this->setParent($this->getInt('parent', $data)); - $this->setMountPoint($this->get('mountpoint', $data)); - $this->setMountPointHash($this->get('mountpoint_hash', $data)); - - // $this->setDefaultMountPoint($this->get('mountpoint', $data)); + $this->setOriginalMountPoint($this->get('mountpoint', $data)); + $this->setOriginalMountPointHash($this->get('mountpoint_hash', $data)); + $this->setMountId($this->get('mount_id', $data)); $this->getManager()->manageImportFromDatabase($this, $data, $prefix); @@ -466,8 +430,9 @@ public function jsonSerialize(): array { 'owner' => $this->getOwner(), 'token' => $this->getToken(), 'password' => $this->getPassword(), - 'mountPoint' => $this->getMountPoint(), - 'mountPointHash' => $this->getMountPointHash(), + 'originalMountPoint' => $this->getOriginalMountPoint(), + 'originalMountPointHash' => $this->getOriginalMountPointHash(), + 'alternateMountPoint' => $this->getAlternateMountPoint() ]; if ($this->hasInitiator()) { diff --git a/lib/Model/Mountpoint.php b/lib/Model/Mountpoint.php index 7d50b420f..9ee603b90 100644 --- a/lib/Model/Mountpoint.php +++ b/lib/Model/Mountpoint.php @@ -12,6 +12,8 @@ namespace OCA\Circles\Model; use JsonSerializable; +use OCA\Circles\Exceptions\MountPointNotFoundException; +use OCA\Circles\Tools\Db\IQueryRow; use OCA\Circles\Tools\Traits\TArrayTools; /** @@ -19,112 +21,61 @@ * * @package OCA\Circles\Model */ -class Mountpoint implements JsonSerializable { +class Mountpoint implements IQueryRow, JsonSerializable { use TArrayTools; - - /** @var int */ - private $shareId = 0; - - /** @var string */ - private $userId = ''; - - /** @var string */ - private $mountPoint = ''; - - - /** - * GSShareMountpoint constructor. - * - * @param int $shareId - * @param string $userId - * @param string $mountPoint - */ - public function __construct(int $shareId = 0, string $userId = '', string $mountPoint = '') { - $this->shareId = $shareId; - $this->userId = $userId; - $this->mountPoint = $mountPoint; + public function __construct( + private string $mountId = '', + private string $singleId = '', + private string $mountPoint = '', + ) { } - - /** - * @return string - */ - public function getUserId(): string { - return $this->userId; + public function getMountId(): string { + return $this->mountId; } - /** - * @param string $userId - * - * @return Mountpoint - */ - public function setUserId(string $userId): self { - $this->userId = $userId; + public function setMountId(string $mountId): self { + $this->mountId = $mountId; return $this; } - - /** - * @return int - */ - public function getShareId(): int { - return $this->shareId; + public function getSingleId(): string { + return $this->singleId; } - /** - * @param int $shareId - * - * @return $this - */ - public function setShareId(int $shareId): self { - $this->shareId = $shareId; + public function setSingleId(string $singleId): self { + $this->singleId = $singleId; return $this; } - - /** - * @return string - */ public function getMountPoint(): string { return $this->mountPoint; } - - /** - * @param string $mountPoint - * - * @return Mountpoint - */ public function setMountPoint(string $mountPoint): self { $this->mountPoint = $mountPoint; return $this; } + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if ($this->get($prefix . 'mountpoint', $data) === '') { + throw new MountPointNotFoundException(); + } - /** - * @param array $data - * - * @return Mountpoint - */ - public function importFromDatabase(array $data): self { - $this->setShareId($this->getInt('share_id', $data)); - $this->setUserId($this->get('user_id', $data)); - $this->setMountPoint($this->get('mountpoint', $data)); + $this->setMountId($this->get($prefix . 'mount_id', $data)); + $this->setSingleId($this->get($prefix . 'single_id', $data)); + $this->setMountPoint($this->get($prefix . 'mountpoint', $data)); return $this; } - - /** - * @return array - */ public function jsonSerialize(): array { return [ - 'userId' => $this->getUserId(), - 'shareId' => $this->getShareId(), + 'mountId' => $this->getMountId(), + 'singleId' => $this->getSingleId(), 'mountPoint' => $this->getMountPoint(), ]; } diff --git a/lib/MountManager/CircleMountManager.php b/lib/MountManager/CircleMountManager.php index 40fe586d8..f0b84a753 100644 --- a/lib/MountManager/CircleMountManager.php +++ b/lib/MountManager/CircleMountManager.php @@ -13,6 +13,7 @@ use OCA\Circles\Db\GSSharesRequest; use OCA\Circles\Model\GlobalScale\GSShareMountpoint; +use OCA\Files_Sharing\External\Manager; use OCP\Share\Exceptions\ShareNotFound; /** @@ -20,7 +21,7 @@ * @deprecated * @package OCA\Circles\MountManager */ -class CircleMountManager { +class CircleMountManager extends Manager { /** @var string */ private $userId; @@ -33,6 +34,8 @@ class CircleMountManager { * * @param string $userId * @param GSSharesRequest $gsSharesRequest + * + * @noinspection PhpMissingParentConstructorInspection */ public function __construct($userId, GSSharesRequest $gsSharesRequest) { $this->userId = $userId; @@ -75,7 +78,7 @@ public function getMountManager() { } // TODO: implement ! - public function removeShare($mountPoint) { + public function removeShare($mountPoint): bool { } // TODO: implement ! diff --git a/lib/MountManager/CircleMountProvider.php b/lib/MountManager/CircleMountProvider.php index 9bf5d101d..192f42830 100644 --- a/lib/MountManager/CircleMountProvider.php +++ b/lib/MountManager/CircleMountProvider.php @@ -9,26 +9,35 @@ namespace OCA\Circles\MountManager; use Exception; +use OC\DB\Exceptions\DbalException; +use OCA\Circles\Db\MountPointRequest; use OCA\Circles\Db\MountRequest; use OCA\Circles\Exceptions\FederatedUserException; use OCA\Circles\Exceptions\FederatedUserNotFoundException; use OCA\Circles\Exceptions\InitiatorNotFoundException; use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Exceptions\MountNotFoundException; use OCA\Circles\Exceptions\MountPointConstructionException; use OCA\Circles\Exceptions\RequestBuilderException; use OCA\Circles\Exceptions\SingleCircleNotFoundException; +use OCA\Circles\IFederatedUser; use OCA\Circles\Model\Member; use OCA\Circles\Model\Mount; +use OCA\Circles\Model\Mountpoint; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\FederatedUserService; use OCA\Circles\Tools\Traits\TArrayTools; use OCA\Files_Sharing\External\Storage as ExternalStorage; use OCP\Federation\ICloudIdManager; use OCP\Files\Config\IMountProvider; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; +use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorageFactory; use OCP\Http\Client\IClientService; use OCP\IUser; +use Psr\Log\LoggerInterface; class CircleMountProvider implements IMountProvider { use TArrayTools; @@ -37,11 +46,14 @@ class CircleMountProvider implements IMountProvider { public function __construct( private IClientService $clientService, + private IRootFolder $rootFolder, private CircleMountManager $circleMountManager, private ICloudIdManager $cloudIdManager, private MountRequest $mountRequest, + private MountPointRequest $mountPointRequest, private FederatedUserService $federatedUserService, private ConfigService $configService, + private LoggerInterface $logger, ) { } @@ -63,11 +75,10 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader): array { $mounts = []; foreach ($items as $item) { try { - // if ($share->getMountPoint() !== '-') { - // $this->fixDuplicateFile($user->getUID(), $gss.share); + $this->fixDuplicateFile($user->getUID(), $item); $mounts[] = $this->generateCircleMount($item, $loader); - // } } catch (Exception $e) { + $this->logger->warning('issue with teams\' mounts', ['exception' => $e]); } } @@ -171,4 +182,70 @@ protected function stripPath($path) { // // return rtrim(substr($path, strlen($prefix)), '/'); } + + + private function fixDuplicateFile(string $userId, Mount $mount): void { + if ($mount->getOriginalMountPoint() === '-') { + return; + } + + $fs = $this->rootFolder->getUserFolder($userId); + + try { + $fs->get($mount->getMountPoint()); + } catch (NotFoundException) { + // in case no alternate mountpoint, we generate one in database (easier to catch duplicate mountpoint) + if ($mount->getAlternateMountPoint() !== null) { + return; + } + + $federatedUser = $this->federatedUserService->getLocalFederatedUser($userId); + $mountPoint = new Mountpoint($mount->getMountId(), $federatedUser->getSingleId(), $mount->getOriginalMountPoint()); + try { + $this->mountPointRequest->insert($mountPoint); + return; + } catch (DbalException $e) { + // meaning a duplicate mountpoint already exists, we need to set a new filename + if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + } + } + + $federatedUser = $this->federatedUserService->getLocalFederatedUser($userId); + $this->generateIncrementedMountpoint($fs, $mount, $federatedUser); + } + + private function generateIncrementedMountpoint(Folder $fs, Mount $mount, IFederatedUser $federatedUser): void { + $info = pathinfo($mount->getMountPoint()); + $filename = rtrim($this->get('dirname', $info), '/') . '/' . $this->get('filename', $info); + $extension = $this->get('extension', $info); + $extension = ($extension === '') ? '' : '.' . $extension; + + $n = 2; + while (true) { + $path = $filename . " ($n)" . $extension; + try { + $fs->get($path); + } catch (NotFoundException) { + $mountPoint = new Mountpoint($mount->getMountId(), $federatedUser->getSingleId(), $path); + $mount->setAlternateMountPoint($mountPoint); + try { + try { + $this->mountPointRequest->update($mountPoint); + } catch (MountNotFoundException) { + $this->mountPointRequest->insert($mountPoint); + } + return; + } catch (DbalException $e) { + // meaning path is already used by another share for this user, we keep incrementing + if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + } + } + + $n++; + } + } } diff --git a/psalm.xml b/psalm.xml index 63dabd9f0..bcd446cbb 100644 --- a/psalm.xml +++ b/psalm.xml @@ -25,8 +25,10 @@ + + @@ -44,5 +46,10 @@ + + + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 91c2eb4f3..0fdeef138 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -221,6 +221,13 @@ + + + + + + + @@ -229,7 +236,7 @@ getFileCache()->toCache(), OC::$server->getMimeTypeLoader())]]> - setAttributes($attributes);]]> + setAttributes($attributes)]]> @@ -265,12 +272,15 @@ + + + + + - - - + @@ -393,15 +403,15 @@ - - logger)]]> - - - logger)]]> - + + logger]]> + + + logger)]]> +