From 9df060d0d6c48d4aabc38d67e61511d0bea427da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= <marko.ivancic@srce.hr>
Date: Mon, 10 Jul 2023 16:00:54 +0200
Subject: [PATCH] WIP

---
 public/assets/css/src/icons/no-image.svg      |  2 ++
 src/Entities/Bases/AbstractProvider.php       |  6 +++-
 src/Entities/Interfaces/ProviderInterface.php |  1 +
 .../Providers/Bases/AbstractOidcProvider.php  | 34 +++++++++++++++++++
 .../Providers/Bases/AbstractSaml2Provider.php | 26 ++++++++++++++
 src/Entities/Providers/Identity/Oidc.php      | 11 ++----
 src/Entities/Providers/Service/Oidc.php       | 11 ++----
 src/Helpers/Arr.php                           | 29 ++++++++++++++++
 templates/user/connected-organizations.twig   |  9 ++++-
 tests/src/Constants/StateArrays.php           | 32 ++++++++++++++++-
 .../Entities/Bases/AbstractProviderTest.php   |  5 +++
 .../Entities/Providers/Identity/OidcTest.php  |  1 +
 .../Entities/Providers/Service/OidcTest.php   |  1 +
 tests/src/Helpers/ArrayTest.php               | 32 +++++++++++++++++
 14 files changed, 179 insertions(+), 21 deletions(-)
 create mode 100644 public/assets/css/src/icons/no-image.svg
 create mode 100644 src/Entities/Providers/Bases/AbstractOidcProvider.php

diff --git a/public/assets/css/src/icons/no-image.svg b/public/assets/css/src/icons/no-image.svg
new file mode 100644
index 0000000..3744910
--- /dev/null
+++ b/public/assets/css/src/icons/no-image.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>no-image</title><path d="M30,3.4141,28.5859,2,2,28.5859,3.4141,30l2-2H26a2.0027,2.0027,0,0,0,2-2V5.4141ZM26,26H7.4141l7.7929-7.793,2.3788,2.3787a2,2,0,0,0,2.8284,0L22,19l4,3.9973Zm0-5.8318-2.5858-2.5859a2,2,0,0,0-2.8284,0L19,19.1682l-2.377-2.3771L26,7.4141Z"/><path d="M6,22V19l5-4.9966,1.3733,1.3733,1.4159-1.416-1.375-1.375a2,2,0,0,0-2.8284,0L6,16.1716V6H22V4H6A2.002,2.002,0,0,0,4,6V22Z"/><rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/></svg>
\ No newline at end of file
diff --git a/src/Entities/Bases/AbstractProvider.php b/src/Entities/Bases/AbstractProvider.php
index c4b66b6..e02dc39 100755
--- a/src/Entities/Bases/AbstractProvider.php
+++ b/src/Entities/Bases/AbstractProvider.php
@@ -6,15 +6,18 @@ namespace SimpleSAML\Module\accounting\Entities\Bases;
 
 use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface;
 use SimpleSAML\Module\accounting\Entities\Interfaces\ProviderInterface;
+use SimpleSAML\Module\accounting\Services\HelpersManager;
 
 abstract class AbstractProvider implements ProviderInterface
 {
     protected array $metadata;
+    protected HelpersManager $helpersManager;
     protected string $entityId;
 
-    public function __construct(array $metadata)
+    public function __construct(array $metadata, HelpersManager $helpersManager = null)
     {
         $this->metadata = $metadata;
+        $this->helpersManager = $helpersManager ?? new HelpersManager();
         $this->entityId = $this->resolveEntityId();
     }
 
@@ -57,6 +60,7 @@ abstract class AbstractProvider implements ProviderInterface
 
     abstract public function getName(string $locale = self::DEFAULT_LOCALE): ?string;
     abstract public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string;
+    abstract public function getLogoUrl(): ?string;
     abstract protected function resolveEntityId(): string;
     abstract public function getProtocol(): AuthenticationProtocolInterface;
     abstract protected function getProviderDescription(): string;
diff --git a/src/Entities/Interfaces/ProviderInterface.php b/src/Entities/Interfaces/ProviderInterface.php
index a5957a5..259db35 100755
--- a/src/Entities/Interfaces/ProviderInterface.php
+++ b/src/Entities/Interfaces/ProviderInterface.php
@@ -15,5 +15,6 @@ interface ProviderInterface
     public function getName(string $locale = self::DEFAULT_LOCALE): ?string;
     public function getEntityId(): string;
     public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string;
+    public function getLogoUrl(): ?string;
     public function getProtocol(): AuthenticationProtocolInterface;
 }
diff --git a/src/Entities/Providers/Bases/AbstractOidcProvider.php b/src/Entities/Providers/Bases/AbstractOidcProvider.php
new file mode 100644
index 0000000..5c1e73e
--- /dev/null
+++ b/src/Entities/Providers/Bases/AbstractOidcProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace SimpleSAML\Module\accounting\Entities\Providers\Bases;
+
+use SimpleSAML\Module\accounting\Entities\Authentication;
+use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider;
+use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface;
+
+abstract class AbstractOidcProvider extends AbstractProvider
+{
+    public const METADATA_KEY_LOGO_URI = 'logo_uri';
+
+    abstract public function getName(string $locale = self::DEFAULT_LOCALE): ?string;
+    abstract public function getDescription(string $locale = self::DEFAULT_LOCALE): ?string;
+    abstract protected function resolveEntityId(): string;
+    abstract protected function getProviderDescription(): string;
+
+    public function getLogoUrl(): ?string
+    {
+        if (
+            !empty($this->metadata[self::METADATA_KEY_LOGO_URI]) &&
+            is_string($this->metadata[self::METADATA_KEY_LOGO_URI])
+        ) {
+            return $this->metadata[self::METADATA_KEY_LOGO_URI];
+        }
+
+        return null;
+    }
+
+    public function getProtocol(): AuthenticationProtocolInterface
+    {
+        return new Authentication\Protocol\Oidc();
+    }
+}
diff --git a/src/Entities/Providers/Bases/AbstractSaml2Provider.php b/src/Entities/Providers/Bases/AbstractSaml2Provider.php
index 0a5fd98..3ffbdca 100644
--- a/src/Entities/Providers/Bases/AbstractSaml2Provider.php
+++ b/src/Entities/Providers/Bases/AbstractSaml2Provider.php
@@ -15,6 +15,8 @@ abstract class AbstractSaml2Provider extends AbstractProvider
     public const METADATA_KEY_UI_INFO = 'UIInfo';
     public const METADATA_KEY_UI_INFO_DESCRIPTION = 'Description';
     public const METADATA_KEY_UI_INFO_DISPLAY_NAME = 'DisplayName';
+    public const METADATA_KEY_UI_INFO_LOGO = 'Logo';
+    public const METADATA_KEY_UI_INFO_LOGO_URL = 'url';
 
     protected function getEntityInfoString(string $key, string $locale = self::DEFAULT_LOCALE): ?string
     {
@@ -49,6 +51,30 @@ abstract class AbstractSaml2Provider extends AbstractProvider
             $this->getEntityUiInfoString(self::METADATA_KEY_UI_INFO_DESCRIPTION, $locale);
     }
 
+    public function getLogoUrl(): ?string
+    {
+        $logoElement = $this->helpersManager->getArr()->getNestedElementByKey(
+            $this->metadata,
+            self::METADATA_KEY_UI_INFO,
+            self::METADATA_KEY_UI_INFO_LOGO,
+            0,
+            self::METADATA_KEY_UI_INFO_LOGO_URL
+        );
+
+        if (!is_array($logoElement)) {
+            return null;
+        }
+
+        /** @var mixed $logoUrl */
+        $logoUrl = current($logoElement);
+
+        if (is_string($logoUrl) && filter_var($logoUrl, FILTER_VALIDATE_URL)) {
+            return $logoUrl;
+        }
+
+        return null;
+    }
+
     /**
      * @throws MetadataException
      */
diff --git a/src/Entities/Providers/Identity/Oidc.php b/src/Entities/Providers/Identity/Oidc.php
index 9bc3784..0394cc5 100755
--- a/src/Entities/Providers/Identity/Oidc.php
+++ b/src/Entities/Providers/Identity/Oidc.php
@@ -4,13 +4,11 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Entities\Providers\Identity;
 
-use SimpleSAML\Module\accounting\Entities\Authentication;
-use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider;
-use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface;
 use SimpleSAML\Module\accounting\Entities\Interfaces\IdentityProviderInterface;
+use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider;
 use SimpleSAML\Module\accounting\Exceptions\MetadataException;
 
-class Oidc extends AbstractProvider implements IdentityProviderInterface
+class Oidc extends AbstractOidcProvider implements IdentityProviderInterface
 {
     public const METADATA_KEY_ENTITY_ID = 'issuer';
 
@@ -39,11 +37,6 @@ class Oidc extends AbstractProvider implements IdentityProviderInterface
         throw new MetadataException('OpenID VersionedDataProvider metadata does not contain entity ID.');
     }
 
-    public function getProtocol(): AuthenticationProtocolInterface
-    {
-        return new Authentication\Protocol\Oidc();
-    }
-
     protected function getProviderDescription(): string
     {
         return $this->getProtocol()->getDesignation() . ' OpenID Provider';
diff --git a/src/Entities/Providers/Service/Oidc.php b/src/Entities/Providers/Service/Oidc.php
index c683718..d074e77 100755
--- a/src/Entities/Providers/Service/Oidc.php
+++ b/src/Entities/Providers/Service/Oidc.php
@@ -4,13 +4,11 @@ declare(strict_types=1);
 
 namespace SimpleSAML\Module\accounting\Entities\Providers\Service;
 
-use SimpleSAML\Module\accounting\Entities\Authentication;
-use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider;
-use SimpleSAML\Module\accounting\Entities\Interfaces\AuthenticationProtocolInterface;
 use SimpleSAML\Module\accounting\Entities\Interfaces\ServiceProviderInterface;
+use SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider;
 use SimpleSAML\Module\accounting\Exceptions\MetadataException;
 
-class Oidc extends AbstractProvider implements ServiceProviderInterface
+class Oidc extends AbstractOidcProvider implements ServiceProviderInterface
 {
     public const METADATA_KEY_ENTITY_ID = 'id';
     public const METADATA_KEY_NAME = 'name';
@@ -41,11 +39,6 @@ class Oidc extends AbstractProvider implements ServiceProviderInterface
         throw new MetadataException('Relying VersionedDataProvider metadata does not contain entity ID.');
     }
 
-    public function getProtocol(): AuthenticationProtocolInterface
-    {
-        return new Authentication\Protocol\Oidc();
-    }
-
     protected function getProviderDescription(): string
     {
         return $this->getProtocol()->getDesignation() . ' Relying Party';
diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php
index 465e122..061461f 100755
--- a/src/Helpers/Arr.php
+++ b/src/Helpers/Arr.php
@@ -37,4 +37,33 @@ class Arr
         $keys = array_keys($array);
         return $keys !== array_keys($keys);
     }
+
+    /**
+     * @param array $array
+     * @param  string|int  ...$keys
+     * @return array|null
+     */
+    public function getNestedElementByKey(array $array, ...$keys): ?array
+    {
+        $element = $array;
+
+        foreach ($keys as $key) {
+            if (!is_array($element)) {
+                return null;
+            }
+
+            if (!isset($element[$key])) {
+                return null;
+            }
+
+            /** @var mixed $element */
+            $element = $element[$key];
+        }
+
+        if (is_array($element)) {
+            return $element;
+        }
+
+        return [$element];
+    }
 }
diff --git a/templates/user/connected-organizations.twig b/templates/user/connected-organizations.twig
index 6bbbf8a..3e8e3af 100755
--- a/templates/user/connected-organizations.twig
+++ b/templates/user/connected-organizations.twig
@@ -20,7 +20,14 @@
 
         {% for connectedServiceProvider in connectedServiceProviderBag.getAll %}
             <tr id="connected-service-provider-{{ loop.index }}">
-                <td>{{ connectedServiceProvider.getServiceProvider.getName|e }}</td>
+                <td>
+                    <img src="{{ connectedServiceProvider.serviceProvider.logoUrl ?? asset('css/src/icons/no-image.svg', 'accounting') }}"
+                         width="30"
+                         height="30"
+                         loading="lazy"
+                         alt="Service Logo"/>
+                    {{ connectedServiceProvider.getServiceProvider.getName|e }}
+                </td>
                 <td>{{ connectedServiceProvider.getNumberOfAuthentications|e }}</td>
                 <td>{{ connectedServiceProvider.getLastAuthenticationAt|date() }}</td>
             </tr>
diff --git a/tests/src/Constants/StateArrays.php b/tests/src/Constants/StateArrays.php
index 1833f18..3d844cd 100755
--- a/tests/src/Constants/StateArrays.php
+++ b/tests/src/Constants/StateArrays.php
@@ -44,7 +44,22 @@ final class StateArrays
             'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp',
             'metadata-set' => 'saml20-sp-remote',
             'name' => 'Test service',
-            'description' => 'Test service description'
+            'description' => 'Test service description',
+            'UIInfo' => [
+                'DisplayName' => [
+                    'en' => 'Test service UiInfo',
+                ],
+                'Description' => [
+                    'en' => 'Test service description UiInfo',
+                ],
+                'Logo' => [
+                    [
+                        'url' => 'https://placehold.co/100x80/orange/white?text=test',
+                        'height' => 80,
+                        'width' => 100,
+                    ],
+                ],
+            ],
         ],
         'saml:RelayState' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp',
         'saml:RequestId' => null,
@@ -117,6 +132,21 @@ final class StateArrays
             'entityid' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp',
             'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp',
             'metadata-set' => 'saml20-sp-remote',
+            'UIInfo' => [
+                'DisplayName' => [
+                    'en' => 'Test service UiInfo',
+                ],
+                'Description' => [
+                    'en' => 'Test service description UiInfo',
+                ],
+                'Logo' => [
+                    [
+                        'url' => 'https://placehold.co/100x80/orange/white?text=test',
+                        'height' => 80,
+                        'width' => 100,
+                    ],
+                ],
+            ],
         ],
         'Source' => [
             'host' => 'localhost.someone.from.hr',
diff --git a/tests/src/Entities/Bases/AbstractProviderTest.php b/tests/src/Entities/Bases/AbstractProviderTest.php
index e858379..aae51bd 100755
--- a/tests/src/Entities/Bases/AbstractProviderTest.php
+++ b/tests/src/Entities/Bases/AbstractProviderTest.php
@@ -117,6 +117,11 @@ class AbstractProviderTest extends TestCase
             {
                 return 'provider description';
             }
+
+            public function getLogoUrl(): ?string
+            {
+                return 'https://example.org/logo';
+            }
         };
     }
 }
diff --git a/tests/src/Entities/Providers/Identity/OidcTest.php b/tests/src/Entities/Providers/Identity/OidcTest.php
index 91ab3e2..9d1a6a3 100755
--- a/tests/src/Entities/Providers/Identity/OidcTest.php
+++ b/tests/src/Entities/Providers/Identity/OidcTest.php
@@ -11,6 +11,7 @@ use SimpleSAML\Module\accounting\Exceptions\MetadataException;
 /**
  * @covers \SimpleSAML\Module\accounting\Entities\Providers\Identity\Oidc
  * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider
+ * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider
  */
 class OidcTest extends TestCase
 {
diff --git a/tests/src/Entities/Providers/Service/OidcTest.php b/tests/src/Entities/Providers/Service/OidcTest.php
index db62ad4..a1445d4 100755
--- a/tests/src/Entities/Providers/Service/OidcTest.php
+++ b/tests/src/Entities/Providers/Service/OidcTest.php
@@ -11,6 +11,7 @@ use SimpleSAML\Module\accounting\Exceptions\MetadataException;
 /**
  * @covers \SimpleSAML\Module\accounting\Entities\Providers\Service\Oidc
  * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider
+ * @uses \SimpleSAML\Module\accounting\Entities\Providers\Bases\AbstractOidcProvider
  */
 class OidcTest extends TestCase
 {
diff --git a/tests/src/Helpers/ArrayTest.php b/tests/src/Helpers/ArrayTest.php
index dc35b09..c1a409a 100755
--- a/tests/src/Helpers/ArrayTest.php
+++ b/tests/src/Helpers/ArrayTest.php
@@ -70,4 +70,36 @@ class ArrayTest extends TestCase
         $this->assertTrue((new Arr())->isAssociative($associative));
         $this->assertTrue((new Arr())->isAssociative($mixed));
     }
+
+    public function testGetNestedElementByKey(): void
+    {
+        $simpleIndexed = [1, 2, 3];
+        $nestedIndexed = [[1, [2, [4, [5]]]], [3, 4]];
+        $nestedAssociative = ['a' => ['b' => 'c'], 'd' => 'e'];
+
+        $arrHelper = new Arr();
+
+        $this->assertSame($arrHelper->getNestedElementByKey($simpleIndexed, 0), [1]);
+        $this->assertNull($arrHelper->getNestedElementByKey($simpleIndexed, 3));
+        $this->assertNull($arrHelper->getNestedElementByKey($simpleIndexed, 'a'));
+
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0), [1, [2, [4, [5]]]]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0), [1, [2, [4, [5]]]]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 0), [1]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1), [2, [4, [5]]]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 0), [2]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1), [4, [5]]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1, 0), [4]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 0, 1, 1, 1), [5]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 1), [3, 4]);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedIndexed, 1, 0), [3]);
+        $this->assertNull($arrHelper->getNestedElementByKey($nestedIndexed, 3));
+        $this->assertNull($arrHelper->getNestedElementByKey($nestedIndexed, 'a'));
+
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'a'), ['b' => 'c']);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'a', 'b'), ['c']);
+        $this->assertSame($arrHelper->getNestedElementByKey($nestedAssociative, 'd'), ['e']);
+        $this->assertNull($arrHelper->getNestedElementByKey($nestedAssociative, 3));
+        $this->assertNull($arrHelper->getNestedElementByKey($nestedAssociative, 'f'));
+    }
 }
-- 
GitLab