diff --git a/Model/AdminLogo.php b/Model/AdminLogo.php
deleted file mode 100644
index b61dda5..0000000
--- a/Model/AdminLogo.php
+++ /dev/null
@@ -1,72 +0,0 @@
-moduleConfig = $moduleConfig;
- $this->fileDriver = $fileDriver;
- $this->urlBuilder = $urlBuilder;
- }
-
- /**
- * @return string|null
- */
- public function getCustomAdminLoginLogoSrc(): ?string
- {
- if (!$logoFileName = $this->moduleConfig->getAdminLoginLogoFileName()) {
- return null;
- }
-
- return $this->fileDriver->getAbsolutePath(
- $this->urlBuilder->getBaseUrl() . DirectoryList::MEDIA . DIRECTORY_SEPARATOR,
- AdminLoginLogo::UPLOAD_DIR . DIRECTORY_SEPARATOR . $logoFileName
- );
- }
-
- /**
- * @return string|null
- */
- public function getCustomAdminMenuLogoSrc(): ?string
- {
- if (!$logoFileName = $this->moduleConfig->getAdminMenuLogoFileName()) {
- return null;
- }
-
- return $this->fileDriver->getAbsolutePath(
- $this->urlBuilder->getBaseUrl() . DirectoryList::MEDIA . DIRECTORY_SEPARATOR,
- AdminMenuLogo::UPLOAD_DIR . DIRECTORY_SEPARATOR . $logoFileName
- );
- }
-}
diff --git a/Model/Config/Backend/AdminLoginLogo.php b/Model/Config/Backend/AdminLoginLogo.php
index 9e3eca6..1d232ef 100644
--- a/Model/Config/Backend/AdminLoginLogo.php
+++ b/Model/Config/Backend/AdminLoginLogo.php
@@ -1,7 +1,7 @@
_mediaDirectory->getAbsolutePath(self::UPLOAD_DIR);
- }
-
/**
* @inheritDoc
*/
diff --git a/Model/Config/Backend/AdminMenuLogo.php b/Model/Config/Backend/AdminMenuLogo.php
index f03d88a..2200598 100644
--- a/Model/Config/Backend/AdminMenuLogo.php
+++ b/Model/Config/Backend/AdminMenuLogo.php
@@ -1,7 +1,7 @@
_mediaDirectory->getAbsolutePath(self::UPLOAD_DIR);
- }
-
/**
* @inheritDoc
*/
diff --git a/Plugin/Backend/Block/Page/HeaderPlugin.php b/Plugin/Backend/Block/Page/HeaderPlugin.php
new file mode 100644
index 0000000..c8ba710
--- /dev/null
+++ b/Plugin/Backend/Block/Page/HeaderPlugin.php
@@ -0,0 +1,132 @@
+scopeConfig = $scopeConfig;
+ $this->storeManager = $storeManager;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Set custom logo URL on the block before rendering, if configured.
+ *
+ * Reads the config path and upload directory from layout XML arguments
+ * to determine which custom logo (login or menu) applies to this context.
+ *
+ * @param Header $subject
+ * @return void
+ */
+ public function beforeToHtml(Header $subject): void
+ {
+ if ($subject->getData('show_part') !== 'logo') {
+ return;
+ }
+
+ $configPath = $subject->getData('custom_logo_config_path');
+ $uploadDir = $subject->getData('custom_logo_upload_dir');
+
+ if (!$configPath || !$uploadDir) {
+ return;
+ }
+
+ $filename = $this->scopeConfig->getValue($configPath);
+
+ if (!is_string($filename) || $filename === '') {
+ return;
+ }
+
+ $filename = basename($filename);
+
+ if ($filename === '') {
+ return;
+ }
+
+ try {
+ $mediaUrl = $this->storeManager->getStore()
+ ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);
+ } catch (NoSuchEntityException $e) {
+ $this->logger->warning(
+ 'Element119_CustomAdminLogo: Unable to resolve store for media URL.',
+ ['exception' => $e]
+ );
+ return;
+ } catch (\Throwable $e) {
+ $this->logger->error(
+ 'Element119_CustomAdminLogo: Unexpected error resolving media URL.',
+ ['exception' => $e]
+ );
+ return;
+ }
+
+ $subject->setLogoImageSrc($mediaUrl . $uploadDir . '/' . $filename);
+ }
+
+ /**
+ * Pass through full URLs without asset repository resolution.
+ *
+ * When a custom logo is configured, logo_image_src is set to a full media
+ * URL. The core template passes this to getViewFileUrl(), which would
+ * attempt static file resolution. This plugin short-circuits that for
+ * absolute URLs, returning the fileId as-is.
+ *
+ * @param Header $subject
+ * @param string $result
+ * @param string $fileId
+ * @return string
+ */
+ public function afterGetViewFileUrl(
+ Header $subject,
+ string $result,
+ string $fileId = ''
+ ): string {
+ if ($subject->getData('show_part') !== 'logo') {
+ return $result;
+ }
+
+ if (str_starts_with($fileId, 'http://') || str_starts_with($fileId, 'https://')) {
+ return $fileId;
+ }
+
+ return $result;
+ }
+}
diff --git a/Scope/Config.php b/Scope/Config.php
deleted file mode 100644
index 391eec0..0000000
--- a/Scope/Config.php
+++ /dev/null
@@ -1,45 +0,0 @@
-scopeConfig = $scopeConfig;
- }
-
- /**
- * @return string|null
- */
- public function getAdminLoginLogoFileName(): ?string
- {
- return $this->scopeConfig->getValue(self::XML_PATH_LOGIN_LOGO, ScopeInterface::SCOPE_STORE);
- }
-
- /**
- * @return string|null
- */
- public function getAdminMenuLogoFileName(): ?string
- {
- return $this->scopeConfig->getValue(self::XML_PATH_MENU_LOGO, ScopeInterface::SCOPE_STORE);
- }
-}
diff --git a/Test/Unit/Plugin/Backend/Block/Page/HeaderPluginTest.php b/Test/Unit/Plugin/Backend/Block/Page/HeaderPluginTest.php
new file mode 100644
index 0000000..fabce88
--- /dev/null
+++ b/Test/Unit/Plugin/Backend/Block/Page/HeaderPluginTest.php
@@ -0,0 +1,350 @@
+scopeConfig = $this->createMock(ScopeConfigInterface::class);
+ $this->storeManager = $this->createMock(StoreManagerInterface::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->header = $this->getMockBuilder(Header::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['setLogoImageSrc'])
+ ->onlyMethods(['getData'])
+ ->getMock();
+
+ $this->plugin = new HeaderPlugin(
+ $this->scopeConfig,
+ $this->storeManager,
+ $this->logger
+ );
+ }
+
+ /**
+ * Helper: configure getData to return the given map plus show_part=logo.
+ */
+ private function setHeaderData(array $extra = []): void
+ {
+ $map = [['show_part', null, 'logo']];
+ foreach ($extra as $key => $value) {
+ $map[] = [$key, null, $value];
+ }
+ $this->header->method('getData')->willReturnMap($map);
+ }
+
+ // ── show_part guard ──────────────────────────────────────────────
+
+ public function testBeforeToHtmlSkipsNonLogoBlock(): void
+ {
+ $this->header->method('getData')->willReturnMap([
+ ['show_part', null, 'user'],
+ ]);
+
+ $this->scopeConfig->expects($this->never())->method('getValue');
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testAfterGetViewFileUrlSkipsNonLogoBlock(): void
+ {
+ $this->header->method('getData')->willReturnMap([
+ ['show_part', null, 'user'],
+ ]);
+
+ $url = 'https://example.com/media/admin/logo/custom/menu/logo.png';
+ $result = $this->plugin->afterGetViewFileUrl($this->header, 'original-result', $url);
+
+ $this->assertSame('original-result', $result);
+ }
+
+ // ── beforeToHtml guard clauses ───────────────────────────────────
+
+ public function testBeforeToHtmlDoesNothingWhenNoConfigPath(): void
+ {
+ $this->setHeaderData([
+ 'custom_logo_config_path' => null,
+ 'custom_logo_upload_dir' => null,
+ ]);
+
+ $this->scopeConfig->expects($this->never())->method('getValue');
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlDoesNothingWhenNoUploadDir(): void
+ {
+ $this->setHeaderData([
+ 'custom_logo_config_path' => 'admin/e119_admin_logos/menu',
+ 'custom_logo_upload_dir' => null,
+ ]);
+
+ $this->scopeConfig->expects($this->never())->method('getValue');
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlDoesNothingWhenNoFilenameInConfig(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => 'admin/logo/custom/menu',
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn(null);
+
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlDoesNothingWhenFilenameIsEmptyString(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => 'admin/logo/custom/menu',
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn('');
+
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlDoesNothingWhenConfigReturnsNonString(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => 'admin/logo/custom/menu',
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn(['unexpected' => 'array']);
+
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ // ── beforeToHtml security ────────────────────────────────────────
+
+ public function testBeforeToHtmlStripsPathTraversalFromFilename(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+ $uploadDir = 'admin/logo/custom/menu';
+ $mediaUrl = 'https://example.com/media/';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => $uploadDir,
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn('../../etc/passwd');
+
+ $store = $this->createMock(Store::class);
+ $store->method('getBaseUrl')
+ ->with(UrlInterface::URL_TYPE_MEDIA)
+ ->willReturn($mediaUrl);
+ $this->storeManager->method('getStore')->willReturn($store);
+
+ $this->header->expects($this->once())
+ ->method('setLogoImageSrc')
+ ->with('https://example.com/media/admin/logo/custom/menu/passwd');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ // ── beforeToHtml happy path ──────────────────────────────────────
+
+ public function testBeforeToHtmlSetsMenuLogoUrl(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+ $uploadDir = 'admin/logo/custom/menu';
+ $filename = 'my-logo.png';
+ $mediaUrl = 'https://example.com/media/';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => $uploadDir,
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn($filename);
+
+ $store = $this->createMock(Store::class);
+ $store->method('getBaseUrl')
+ ->with(UrlInterface::URL_TYPE_MEDIA)
+ ->willReturn($mediaUrl);
+ $this->storeManager->method('getStore')->willReturn($store);
+
+ $this->header->expects($this->once())
+ ->method('setLogoImageSrc')
+ ->with('https://example.com/media/admin/logo/custom/menu/my-logo.png');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlSetsLoginLogoUrl(): void
+ {
+ $configPath = 'admin/e119_admin_logos/login';
+ $uploadDir = 'admin/logo/custom/login';
+ $filename = 'login-logo.jpg';
+ $mediaUrl = 'https://example.com/media/';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => $uploadDir,
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn($filename);
+
+ $store = $this->createMock(Store::class);
+ $store->method('getBaseUrl')
+ ->with(UrlInterface::URL_TYPE_MEDIA)
+ ->willReturn($mediaUrl);
+ $this->storeManager->method('getStore')->willReturn($store);
+
+ $this->header->expects($this->once())
+ ->method('setLogoImageSrc')
+ ->with('https://example.com/media/admin/logo/custom/login/login-logo.jpg');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ // ── beforeToHtml error handling ──────────────────────────────────
+
+ public function testBeforeToHtmlHandlesNoSuchEntityException(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => 'admin/logo/custom/menu',
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn('logo.png');
+
+ $this->storeManager->method('getStore')
+ ->willThrowException(new NoSuchEntityException(__('Store not found')));
+
+ $this->logger->expects($this->once())
+ ->method('warning')
+ ->with(
+ 'Element119_CustomAdminLogo: Unable to resolve store for media URL.',
+ $this->arrayHasKey('exception')
+ );
+
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ public function testBeforeToHtmlHandlesUnexpectedThrowable(): void
+ {
+ $configPath = 'admin/e119_admin_logos/menu';
+
+ $this->setHeaderData([
+ 'custom_logo_config_path' => $configPath,
+ 'custom_logo_upload_dir' => 'admin/logo/custom/menu',
+ ]);
+
+ $this->scopeConfig->method('getValue')
+ ->with($configPath)
+ ->willReturn('logo.png');
+
+ $this->storeManager->method('getStore')
+ ->willThrowException(new \RuntimeException('Unexpected failure'));
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with(
+ 'Element119_CustomAdminLogo: Unexpected error resolving media URL.',
+ $this->arrayHasKey('exception')
+ );
+
+ $this->header->expects($this->never())->method('setLogoImageSrc');
+
+ $this->plugin->beforeToHtml($this->header);
+ }
+
+ // ── afterGetViewFileUrl ──────────────────────────────────────────
+
+ public function testAfterGetViewFileUrlPassesThroughHttpsUrl(): void
+ {
+ $this->setHeaderData();
+ $url = 'https://example.com/media/admin/logo/custom/menu/logo.png';
+
+ $result = $this->plugin->afterGetViewFileUrl($this->header, 'ignored', $url);
+
+ $this->assertSame($url, $result);
+ }
+
+ public function testAfterGetViewFileUrlPassesThroughHttpUrl(): void
+ {
+ $this->setHeaderData();
+ $url = 'http://example.com/media/admin/logo/custom/login/logo.png';
+
+ $result = $this->plugin->afterGetViewFileUrl($this->header, 'ignored', $url);
+
+ $this->assertSame($url, $result);
+ }
+
+ public function testAfterGetViewFileUrlReturnsOriginalResultForViewFile(): void
+ {
+ $this->setHeaderData();
+ $resolvedUrl = 'https://example.com/static/adminhtml/Magento/backend/en_US/images/mage-os-icon.svg';
+
+ $result = $this->plugin->afterGetViewFileUrl(
+ $this->header,
+ $resolvedUrl,
+ 'images/mage-os-icon.svg'
+ );
+
+ $this->assertSame($resolvedUrl, $result);
+ }
+}
diff --git a/ViewModel/AdminLogo.php b/ViewModel/AdminLogo.php
deleted file mode 100644
index 71ddea6..0000000
--- a/ViewModel/AdminLogo.php
+++ /dev/null
@@ -1,52 +0,0 @@
-adminLogo = $adminLogo;
- $this->request = $request;
- }
-
- /**
- * @return AdminLogoModel
- */
- public function getAdminLogoModel(): AdminLogoModel
- {
- return $this->adminLogo;
- }
-
- /**
- * @return bool
- */
- public function isAdminLoginPage(): bool
- {
- return $this->request->getRouteName() === Area::AREA_ADMINHTML
- && $this->request->getControllerName() === 'auth'
- && $this->request->getActionName() === 'login';
- }
-}
diff --git a/composer.json b/composer.json
index d3181dd..c2ae9fd 100644
--- a/composer.json
+++ b/composer.json
@@ -10,7 +10,10 @@
}
],
"require": {
- "magento/module-config": "^101.0"
+ "magento/framework": "*",
+ "magento/module-backend": "*",
+ "magento/module-config": "^101.0",
+ "magento/module-store": "*"
},
"autoload": {
"psr-4": {
diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml
new file mode 100644
index 0000000..1fec063
--- /dev/null
+++ b/etc/adminhtml/di.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
index eef10f8..0b1f620 100644
--- a/etc/adminhtml/system.xml
+++ b/etc/adminhtml/system.xml
@@ -2,7 +2,7 @@
+ showInWebsite="0"
+ showInStore="0">
+
+
diff --git a/registration.php b/registration.php
index 704de43..1ffb1c6 100644
--- a/registration.php
+++ b/registration.php
@@ -1,7 +1,7 @@
+
+
+
+
+
+ admin/e119_admin_logos/login
+ admin/logo/custom/login
+
+
+
+
diff --git a/view/adminhtml/layout/default.xml b/view/adminhtml/layout/default.xml
index d1c3773..9434fa8 100644
--- a/view/adminhtml/layout/default.xml
+++ b/view/adminhtml/layout/default.xml
@@ -2,7 +2,7 @@
-
+
-
- Element119\CustomAdminLogo\ViewModel\AdminLogo
-
+ admin/e119_admin_logos/menu
+ admin/logo/custom/menu
diff --git a/view/adminhtml/templates/page/header.phtml b/view/adminhtml/templates/page/header.phtml
deleted file mode 100644
index d962375..0000000
--- a/view/adminhtml/templates/page/header.phtml
+++ /dev/null
@@ -1,88 +0,0 @@
-getShowPart();
-
-/** @var AdminLogo $adminLogoViewModel */
-$adminLogoViewModel = $block->getData('admin_logo_view_model');
-?>
-
- hasEdition()
- ? 'data-edition="' . $escaper->escapeHtml($block->getEdition()) . '"'
- : ''; ?>
- getAdminLogoModel()->getCustomAdminLoginLogoSrc(); ?>
- getAdminLogoModel()->getCustomAdminMenuLogoSrc(); ?>
-
-
- class="logo">
-
isAdminLoginPage()): ?>
- src="= $escaper->escapeUrl(
- $adminLoginLogoSrc ?: $block->getViewFileUrl('images/magento-logo.svg')
- ); ?>"
-
- src="= $escaper->escapeUrl(
- $adminMenuLogoSrc ?: $block->getViewFileUrl('images/magento-icon.svg')
- ); ?>"
-
- alt="= $escaper->escapeHtmlAttr(__('Magento Admin Panel')); ?>"
- title="= $escaper->escapeHtmlAttr(__('Magento Admin Panel')); ?>"/>
-
-
-
-
- = $block->getChildHtml(); ?>
-
diff --git a/view/adminhtml/web/css/module.less b/view/adminhtml/web/css/module.less
index 183a30a..1616b8e 100644
--- a/view/adminhtml/web/css/module.less
+++ b/view/adminhtml/web/css/module.less
@@ -1,6 +1,6 @@
/**
* Copyright © element119. All rights reserved.
- * See LICENCE.txt for licence details.
+ * See LICENSE for license details.
*/
.menu-wrapper a.logo .logo-img {
height: auto;