From c54909f2a3188ead30e50a669e1459b1b3554b0e Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Tue, 15 Oct 2024 14:37:42 +0200 Subject: [PATCH 01/14] Fix comments for doc --- application/applicationextension.inc.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 7177c3606..29b56621b 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -64,13 +64,13 @@ interface iLoginFSMExtension extends iLoginExtension * If a page is displayed, the action must exit at this point * if LoginWebPage::LOGIN_FSM_RETURN_ERROR is returned $iErrorCode must be set * if LoginWebPage::LOGIN_FSM_RETURN_OK is returned then the login is OK and terminated - * if LoginWebPage::LOGIN_FSM_RETURN_IGNORE is returned then the FSM will proceed to next plugin or state + * if LoginWebPage::LOGIN_FSM_CONTINUE is returned then the FSM will proceed to next plugin or state * * @api * @param string $sLoginState (see LoginWebPage::LOGIN_STATE_...) * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ public function LoginAction($sLoginState, &$iErrorCode); } @@ -83,7 +83,7 @@ interface iLoginFSMExtension extends iLoginExtension * * If a page is displayed, the action must exit at this point * * if LoginWebPage::LOGIN_FSM_RETURN_ERROR is returned $iErrorCode must be set * * if LoginWebPage::LOGIN_FSM_RETURN_OK is returned then the login is OK and terminated - * * if LoginWebPage::LOGIN_FSM_RETURN_IGNORE is returned then the FSM will proceed to next plugin or to next state + * * if LoginWebPage::LOGIN_FSM_CONTINUE is returned then the FSM will proceed to next plugin or to next state * * @api * @package LoginExtensibilityAPI @@ -136,7 +136,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnStart(&$iErrorCode) { @@ -150,7 +150,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnModeDetection(&$iErrorCode) { @@ -167,7 +167,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnReadCredentials(&$iErrorCode) { @@ -181,7 +181,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnCheckCredentials(&$iErrorCode) { @@ -192,7 +192,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnCredentialsOK(&$iErrorCode) { @@ -203,7 +203,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnUsersOK(&$iErrorCode) { @@ -214,7 +214,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnConnected(&$iErrorCode) { @@ -225,7 +225,7 @@ abstract class AbstractLoginFSMExtension implements iLoginFSMExtension * @api * @param int $iErrorCode (see LoginWebPage::EXIT_CODE_...) * - * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_RETURN_IGNORE + * @return int LoginWebPage::LOGIN_FSM_RETURN_ERROR, LoginWebPage::LOGIN_FSM_RETURN_OK or LoginWebPage::LOGIN_FSM_CONTINUE */ protected function OnError(&$iErrorCode) { From 7a4d29d561c76d9f292a6b6441acdc16464f72f6 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Wed, 2 Oct 2024 17:01:06 +0200 Subject: [PATCH 02/14] =?UTF-8?q?N=C2=B07870=20:white=5Fcheck=5Fmark:=20Pr?= =?UTF-8?q?erequisites=20to=20test=20portal=20services=20(instantiate=20a?= =?UTF-8?q?=20symfony=20service)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/config/packages/web_profiler.yaml | 5 - lib/composer/autoload_classmap.php | 10 + lib/composer/autoload_static.php | 10 + lib/composer/installed.php | 4 +- .../Test/BrowserKitAssertionsTrait.php | 197 ++++++++++++++++++ .../Test/DomCrawlerAssertionsTrait.php | 151 ++++++++++++++ .../Test/HttpClientAssertionsTrait.php | 134 ++++++++++++ .../framework-bundle/Test/KernelTestCase.php | 137 ++++++++++++ .../Test/MailerAssertionsTrait.php | 138 ++++++++++++ .../Test/NotificationAssertionsTrait.php | 100 +++++++++ .../Test/TestBrowserToken.php | 58 ++++++ .../framework-bundle/Test/TestContainer.php | 126 +++++++++++ .../Test/WebTestAssertionsTrait.php | 19 ++ .../framework-bundle/Test/WebTestCase.php | 59 ++++++ .../Dependencies/Composer/iTopComposer.php | 2 +- .../module_integration.xml.dist | 2 +- tests/php-unit-tests/phpunit.xml.dist | 2 +- .../postbuild_integration.xml.dist | 2 +- .../src/BaseTestCase/ItopTestCase.php | 39 +++- .../php-unit-tests/BootSymfonyKernelTest.php | 49 +++++ tests/php-unit-tests/unittestautoload.php | 6 +- 21 files changed, 1233 insertions(+), 17 deletions(-) create mode 100644 lib/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/KernelTestCase.php create mode 100644 lib/symfony/framework-bundle/Test/MailerAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/NotificationAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/TestBrowserToken.php create mode 100644 lib/symfony/framework-bundle/Test/TestContainer.php create mode 100644 lib/symfony/framework-bundle/Test/WebTestAssertionsTrait.php create mode 100644 lib/symfony/framework-bundle/Test/WebTestCase.php create mode 100644 tests/php-unit-tests/unitary-tests/tests/php-unit-tests/BootSymfonyKernelTest.php diff --git a/datamodels/2.x/itop-portal-base/portal/config/packages/web_profiler.yaml b/datamodels/2.x/itop-portal-base/portal/config/packages/web_profiler.yaml index 7676f2292..4c93561e8 100644 --- a/datamodels/2.x/itop-portal-base/portal/config/packages/web_profiler.yaml +++ b/datamodels/2.x/itop-portal-base/portal/config/packages/web_profiler.yaml @@ -4,9 +4,4 @@ when@dev: toolbar: true intercept_redirects: false -when@test: - web_profiler: - toolbar: false - intercept_redirects: false - diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 6c8ee08cc..e30582e5f 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -1778,6 +1778,16 @@ return array( 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\AbstractVault' => $vendorDir . '/symfony/framework-bundle/Secrets/AbstractVault.php', 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\DotenvVault' => $vendorDir . '/symfony/framework-bundle/Secrets/DotenvVault.php', 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\SodiumVault' => $vendorDir . '/symfony/framework-bundle/Secrets/SodiumVault.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\BrowserKitAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\DomCrawlerAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\HttpClientAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase' => $vendorDir . '/symfony/framework-bundle/Test/KernelTestCase.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/MailerAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\NotificationAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/NotificationAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken' => $vendorDir . '/symfony/framework-bundle/Test/TestBrowserToken.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\TestContainer' => $vendorDir . '/symfony/framework-bundle/Test/TestContainer.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestAssertionsTrait' => $vendorDir . '/symfony/framework-bundle/Test/WebTestAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase' => $vendorDir . '/symfony/framework-bundle/Test/WebTestCase.php', 'Symfony\\Bundle\\FrameworkBundle\\Translation\\Translator' => $vendorDir . '/symfony/framework-bundle/Translation/Translator.php', 'Symfony\\Bundle\\TwigBundle\\CacheWarmer\\TemplateCacheWarmer' => $vendorDir . '/symfony/twig-bundle/CacheWarmer/TemplateCacheWarmer.php', 'Symfony\\Bundle\\TwigBundle\\Command\\LintCommand' => $vendorDir . '/symfony/twig-bundle/Command/LintCommand.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 3b2a23078..ac3a90c8e 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -2158,6 +2158,16 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\AbstractVault' => __DIR__ . '/..' . '/symfony/framework-bundle/Secrets/AbstractVault.php', 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\DotenvVault' => __DIR__ . '/..' . '/symfony/framework-bundle/Secrets/DotenvVault.php', 'Symfony\\Bundle\\FrameworkBundle\\Secrets\\SodiumVault' => __DIR__ . '/..' . '/symfony/framework-bundle/Secrets/SodiumVault.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\BrowserKitAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\DomCrawlerAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\HttpClientAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/KernelTestCase.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/MailerAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\NotificationAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/NotificationAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/TestBrowserToken.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\TestContainer' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/TestContainer.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestAssertionsTrait' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/WebTestAssertionsTrait.php', + 'Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase' => __DIR__ . '/..' . '/symfony/framework-bundle/Test/WebTestCase.php', 'Symfony\\Bundle\\FrameworkBundle\\Translation\\Translator' => __DIR__ . '/..' . '/symfony/framework-bundle/Translation/Translator.php', 'Symfony\\Bundle\\TwigBundle\\CacheWarmer\\TemplateCacheWarmer' => __DIR__ . '/..' . '/symfony/twig-bundle/CacheWarmer/TemplateCacheWarmer.php', 'Symfony\\Bundle\\TwigBundle\\Command\\LintCommand' => __DIR__ . '/..' . '/symfony/twig-bundle/Command/LintCommand.php', diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 4ceb53402..b30a7b375 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'd53d4970f40cbc0a8c5cd6e1d5cc3ca06ac4719e', + 'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'd53d4970f40cbc0a8c5cd6e1d5cc3ca06ac4719e', + 'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/lib/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php b/lib/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php new file mode 100644 index 000000000..a6d4fed33 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/BrowserKitAssertionsTrait.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use PHPUnit\Framework\Constraint\LogicalAnd; +use PHPUnit\Framework\Constraint\LogicalNot; +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\BrowserKit\AbstractBrowser; +use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint; + +/** + * Ideas borrowed from Laravel Dusk's assertions. + * + * @see https://laravel.com/docs/5.7/dusk#available-assertions + */ +trait BrowserKitAssertionsTrait +{ + public static function assertResponseIsSuccessful(string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful(), $message); + } + + public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message); + } + + public static function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat), $message); + } + + public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void + { + $constraint = new ResponseConstraint\ResponseIsRedirected(); + if ($expectedLocation) { + if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) { + $locationConstraint = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation); + } else { + $locationConstraint = new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation); + } + + $constraint = LogicalAnd::fromConstraints($constraint, $locationConstraint); + } + if ($expectedCode) { + $constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode)); + } + + self::assertThatForResponse($constraint, $message); + } + + public static function assertResponseHasHeader(string $headerName, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseHasHeader($headerName), $message); + } + + public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void + { + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message); + } + + public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message); + } + + public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message); + } + + public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message); + } + + public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message); + } + + public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForResponse(LogicalAnd::fromConstraints( + new ResponseConstraint\ResponseHasCookie($name, $path, $domain), + new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain) + ), $message); + } + + public static function assertResponseIsUnprocessable(string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable(), $message); + } + + public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForClient(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); + } + + public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); + } + + public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThatForClient(LogicalAnd::fromConstraints( + new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), + new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain) + ), $message); + } + + public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void + { + self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message); + } + + public static function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void + { + $constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute); + $constraints = []; + foreach ($parameters as $key => $value) { + $constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value); + } + if ($constraints) { + $constraint = LogicalAnd::fromConstraints($constraint, ...$constraints); + } + + self::assertThat(self::getRequest(), $constraint, $message); + } + + public static function assertThatForResponse(Constraint $constraint, string $message = ''): void + { + try { + self::assertThat(self::getResponse(), $constraint, $message); + } catch (ExpectationFailedException $exception) { + if (($serverExceptionMessage = self::getResponse()->headers->get('X-Debug-Exception')) + && ($serverExceptionFile = self::getResponse()->headers->get('X-Debug-Exception-File'))) { + $serverExceptionFile = explode(':', $serverExceptionFile); + $exception->__construct($exception->getMessage(), $exception->getComparisonFailure(), new \ErrorException(rawurldecode($serverExceptionMessage), 0, 1, rawurldecode($serverExceptionFile[0]), $serverExceptionFile[1]), $exception->getPrevious()); + } + + throw $exception; + } + } + + public static function assertThatForClient(Constraint $constraint, string $message = ''): void + { + self::assertThat(self::getClient(), $constraint, $message); + } + + protected static function getClient(AbstractBrowser $newClient = null): ?AbstractBrowser + { + static $client; + + if (0 < \func_num_args()) { + return $client = $newClient; + } + + if (!$client instanceof AbstractBrowser) { + static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__)); + } + + return $client; + } + + private static function getResponse(): Response + { + if (!$response = self::getClient()->getResponse()) { + static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?'); + } + + return $response; + } + + private static function getRequest(): Request + { + if (!$request = self::getClient()->getRequest()) { + static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?'); + } + + return $request; + } +} diff --git a/lib/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php b/lib/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php new file mode 100644 index 000000000..a16709461 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/DomCrawlerAssertionsTrait.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\Constraint\LogicalAnd; +use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorExists; + +/** + * Ideas borrowed from Laravel Dusk's assertions. + * + * @see https://laravel.com/docs/5.7/dusk#available-assertions + */ +trait DomCrawlerAssertionsTrait +{ + public static function assertSelectorExists(string $selector, string $message = ''): void + { + self::assertThat(self::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorExists($selector), $message); + } + + public static function assertSelectorNotExists(string $selector, string $message = ''): void + { + self::assertThat(self::getCrawler(), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorExists($selector)), $message); + } + + public static function assertSelectorCount(int $expectedCount, string $selector, string $message = ''): void + { + self::assertThat(self::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorCount($expectedCount, $selector), $message); + } + + public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text) + ), $message); + } + + public static function assertAnySelectorTextContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text) + ), $message); + } + + public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerSelectorTextSame($selector, $text) + ), $message); + } + + public static function assertAnySelectorTextSame(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerAnySelectorTextSame($selector, $text) + ), $message); + } + + public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text)) + ), $message); + } + + public static function assertAnySelectorTextNotContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new LogicalNot(new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text)) + ), $message); + } + + public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void + { + self::assertSelectorTextSame('title', $expectedTitle, $message); + } + + public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void + { + self::assertSelectorTextContains('title', $expectedTitle, $message); + } + + public static function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) + ), $message); + } + + public static function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)) + ), $message); + } + + public static function assertCheckboxChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked"), $message); + } + + public static function assertCheckboxNotChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), new LogicalNot(new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked")), $message); + } + + public static function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayHasKey($fieldName, $values, $message ?: sprintf('Field "%s" not found in form "%s".', $fieldName, $formSelector)); + self::assertSame($value, $values[$fieldName]); + } + + public static function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayNotHasKey($fieldName, $values, $message ?: sprintf('Field "%s" has a value in form "%s".', $fieldName, $formSelector)); + } + + private static function getCrawler(): Crawler + { + if (!$crawler = self::getClient()->getCrawler()) { + static::fail('A client must have a crawler to make assertions. Did you forget to make an HTTP request?'); + } + + return $crawler; + } +} diff --git a/lib/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php b/lib/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php new file mode 100644 index 000000000..bed835fa1 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/HttpClientAssertionsTrait.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; + +/* + * @author Mathieu Santostefano + */ + +trait HttpClientAssertionsTrait +{ + public static function assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client'): void + { + /** @var KernelBrowser $client */ + $client = static::getClient(); + + if (!($profile = $client->getProfile())) { + static::fail('The Profiler must be enabled for the current request. Please ensure to call "$client->enableProfiler()" before making the request.'); + } + + /** @var HttpClientDataCollector $httpClientDataCollector */ + $httpClientDataCollector = $profile->getCollector('http_client'); + $expectedRequestHasBeenFound = false; + + if (!\array_key_exists($httpClientId, $httpClientDataCollector->getClients())) { + static::fail(sprintf('HttpClient "%s" is not registered.', $httpClientId)); + } + + foreach ($httpClientDataCollector->getClients()[$httpClientId]['traces'] as $trace) { + if (($expectedUrl !== $trace['info']['url'] && $expectedUrl !== $trace['url']) + || $expectedMethod !== $trace['method'] + ) { + continue; + } + + if (null !== $expectedBody) { + $actualBody = null; + + if (null !== $trace['options']['body'] && null === $trace['options']['json']) { + $actualBody = \is_string($trace['options']['body']) ? $trace['options']['body'] : $trace['options']['body']->getValue(true); + } + + if (null === $trace['options']['body'] && null !== $trace['options']['json']) { + $actualBody = $trace['options']['json']->getValue(true); + } + + if (!$actualBody) { + continue; + } + + if ($expectedBody === $actualBody) { + $expectedRequestHasBeenFound = true; + + if (!$expectedHeaders) { + break; + } + } + } + + if ($expectedHeaders) { + $actualHeaders = $trace['options']['headers'] ?? []; + + foreach ($actualHeaders as $headerKey => $actualHeader) { + if (\array_key_exists($headerKey, $expectedHeaders) + && $expectedHeaders[$headerKey] === $actualHeader->getValue(true) + ) { + $expectedRequestHasBeenFound = true; + break 2; + } + } + } + + $expectedRequestHasBeenFound = true; + break; + } + + self::assertTrue($expectedRequestHasBeenFound, 'The expected request has not been called: "'.$expectedMethod.'" - "'.$expectedUrl.'"'); + } + + public function assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client'): void + { + /** @var KernelBrowser $client */ + $client = static::getClient(); + + if (!$profile = $client->getProfile()) { + static::fail('The Profiler must be enabled for the current request. Please ensure to call "$client->enableProfiler()" before making the request.'); + } + + /** @var HttpClientDataCollector $httpClientDataCollector */ + $httpClientDataCollector = $profile->getCollector('http_client'); + $unexpectedUrlHasBeenFound = false; + + if (!\array_key_exists($httpClientId, $httpClientDataCollector->getClients())) { + static::fail(sprintf('HttpClient "%s" is not registered.', $httpClientId)); + } + + foreach ($httpClientDataCollector->getClients()[$httpClientId]['traces'] as $trace) { + if (($unexpectedUrl === $trace['info']['url'] || $unexpectedUrl === $trace['url']) + && $expectedMethod === $trace['method'] + ) { + $unexpectedUrlHasBeenFound = true; + break; + } + } + + self::assertFalse($unexpectedUrlHasBeenFound, sprintf('Unexpected URL called: "%s" - "%s"', $expectedMethod, $unexpectedUrl)); + } + + public static function assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client'): void + { + /** @var KernelBrowser $client */ + $client = static::getClient(); + + if (!($profile = $client->getProfile())) { + static::fail('The Profiler must be enabled for the current request. Please ensure to call "$client->enableProfiler()" before making the request.'); + } + + /** @var HttpClientDataCollector $httpClientDataCollector */ + $httpClientDataCollector = $profile->getCollector('http_client'); + + self::assertCount($count, $httpClientDataCollector->getClients()[$httpClientId]['traces']); + } +} diff --git a/lib/symfony/framework-bundle/Test/KernelTestCase.php b/lib/symfony/framework-bundle/Test/KernelTestCase.php new file mode 100644 index 000000000..8d27b757f --- /dev/null +++ b/lib/symfony/framework-bundle/Test/KernelTestCase.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * KernelTestCase is the base class for tests needing a Kernel. + * + * @author Fabien Potencier + */ +abstract class KernelTestCase extends TestCase +{ + use MailerAssertionsTrait; + use NotificationAssertionsTrait; + + protected static $class; + + /** + * @var KernelInterface + */ + protected static $kernel; + + protected static $booted = false; + + protected function tearDown(): void + { + static::ensureKernelShutdown(); + static::$class = null; + static::$kernel = null; + static::$booted = false; + } + + /** + * @throws \RuntimeException + * @throws \LogicException + */ + protected static function getKernelClass(): string + { + if (!isset($_SERVER['KERNEL_CLASS']) && !isset($_ENV['KERNEL_CLASS'])) { + throw new \LogicException(sprintf('You must set the KERNEL_CLASS environment variable to the fully-qualified class name of your Kernel in phpunit.xml / phpunit.xml.dist or override the "%1$s::createKernel()" or "%1$s::getKernelClass()" method.', static::class)); + } + + if (!class_exists($class = $_ENV['KERNEL_CLASS'] ?? $_SERVER['KERNEL_CLASS'])) { + throw new \RuntimeException(sprintf('Class "%s" doesn\'t exist or cannot be autoloaded. Check that the KERNEL_CLASS value in phpunit.xml matches the fully-qualified class name of your Kernel or override the "%s::createKernel()" method.', $class, static::class)); + } + + return $class; + } + + /** + * Boots the Kernel for this test. + */ + protected static function bootKernel(array $options = []): KernelInterface + { + static::ensureKernelShutdown(); + + $kernel = static::createKernel($options); + $kernel->boot(); + static::$kernel = $kernel; + static::$booted = true; + + return static::$kernel; + } + + /** + * Provides a dedicated test container with access to both public and private + * services. The container will not include private services that have been + * inlined or removed. Private services will be removed when they are not + * used by other services. + * + * Using this method is the best way to get a container from your test code. + * + * @return Container + */ + protected static function getContainer(): ContainerInterface + { + if (!static::$booted) { + static::bootKernel(); + } + + try { + return self::$kernel->getContainer()->get('test.service_container'); + } catch (ServiceNotFoundException $e) { + throw new \LogicException('Could not find service "test.service_container". Try updating the "framework.test" config to "true".', 0, $e); + } + } + + /** + * Creates a Kernel. + * + * Available options: + * + * * environment + * * debug + */ + protected static function createKernel(array $options = []): KernelInterface + { + static::$class ??= static::getKernelClass(); + + $env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test'; + $debug = $options['debug'] ?? $_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true; + + return new static::$class($env, $debug); + } + + /** + * Shuts the kernel down if it was used in the test - called by the tearDown method by default. + */ + protected static function ensureKernelShutdown() + { + if (null !== static::$kernel) { + static::$kernel->boot(); + $container = static::$kernel->getContainer(); + static::$kernel->shutdown(); + static::$booted = false; + + if ($container instanceof ResetInterface) { + $container->reset(); + } + } + } +} diff --git a/lib/symfony/framework-bundle/Test/MailerAssertionsTrait.php b/lib/symfony/framework-bundle/Test/MailerAssertionsTrait.php new file mode 100644 index 000000000..83643421e --- /dev/null +++ b/lib/symfony/framework-bundle/Test/MailerAssertionsTrait.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Component\Mailer\Test\Constraint as MailerConstraint; +use Symfony\Component\Mime\RawMessage; +use Symfony\Component\Mime\Test\Constraint as MimeConstraint; + +trait MailerAssertionsTrait +{ + public static function assertEmailCount(int $count, string $transport = null, string $message = ''): void + { + self::assertThat(self::getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport), $message); + } + + public static function assertQueuedEmailCount(int $count, string $transport = null, string $message = ''): void + { + self::assertThat(self::getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport, true), $message); + } + + public static function assertEmailIsQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new MailerConstraint\EmailIsQueued(), $message); + } + + public static function assertEmailIsNotQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new LogicalNot(new MailerConstraint\EmailIsQueued()), $message); + } + + public static function assertEmailAttachmentCount(RawMessage $email, int $count, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailAttachmentCount($count), $message); + } + + public static function assertEmailTextBodyContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailTextBodyContains($text), $message); + } + + public static function assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailTextBodyContains($text)), $message); + } + + public static function assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHtmlBodyContains($text), $message); + } + + public static function assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text)), $message); + } + + public static function assertEmailHasHeader(RawMessage $email, string $headerName, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHasHeader($headerName), $message); + } + + public static function assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName)), $message); + } + + public static function assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHeaderSame($headerName, $expectedValue), $message); + } + + public static function assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)), $message); + } + + public static function assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue), $message); + } + + public static function assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailSubjectContains($expectedValue), $message); + } + + public static function assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailSubjectContains($expectedValue)), $message); + } + + /** + * @return MessageEvent[] + */ + public static function getMailerEvents(string $transport = null): array + { + return self::getMessageMailerEvents()->getEvents($transport); + } + + public static function getMailerEvent(int $index = 0, string $transport = null): ?MessageEvent + { + return self::getMailerEvents($transport)[$index] ?? null; + } + + /** + * @return RawMessage[] + */ + public static function getMailerMessages(string $transport = null): array + { + return self::getMessageMailerEvents()->getMessages($transport); + } + + public static function getMailerMessage(int $index = 0, string $transport = null): ?RawMessage + { + return self::getMailerMessages($transport)[$index] ?? null; + } + + private static function getMessageMailerEvents(): MessageEvents + { + $container = static::getContainer(); + if ($container->has('mailer.message_logger_listener')) { + return $container->get('mailer.message_logger_listener')->getEvents(); + } + + static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); + } +} diff --git a/lib/symfony/framework-bundle/Test/NotificationAssertionsTrait.php b/lib/symfony/framework-bundle/Test/NotificationAssertionsTrait.php new file mode 100644 index 000000000..53d24cb12 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/NotificationAssertionsTrait.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\Notifier\Event\MessageEvent; +use Symfony\Component\Notifier\Event\NotificationEvents; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Test\Constraint as NotifierConstraint; + +/* + * @author Smaïne Milianni + */ +trait NotificationAssertionsTrait +{ + public static function assertNotificationCount(int $count, string $transportName = null, string $message = ''): void + { + self::assertThat(self::getNotificationEvents(), new NotifierConstraint\NotificationCount($count, $transportName), $message); + } + + public static function assertQueuedNotificationCount(int $count, string $transportName = null, string $message = ''): void + { + self::assertThat(self::getNotificationEvents(), new NotifierConstraint\NotificationCount($count, $transportName, true), $message); + } + + public static function assertNotificationIsQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new NotifierConstraint\NotificationIsQueued(), $message); + } + + public static function assertNotificationIsNotQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new LogicalNot(new NotifierConstraint\NotificationIsQueued()), $message); + } + + public static function assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = ''): void + { + self::assertThat($notification, new NotifierConstraint\NotificationSubjectContains($text), $message); + } + + public static function assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = ''): void + { + self::assertThat($notification, new LogicalNot(new NotifierConstraint\NotificationSubjectContains($text)), $message); + } + + public static function assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName = null, string $message = ''): void + { + self::assertThat($notification, new NotifierConstraint\NotificationTransportIsEqual($transportName), $message); + } + + public static function assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName = null, string $message = ''): void + { + self::assertThat($notification, new LogicalNot(new NotifierConstraint\NotificationTransportIsEqual($transportName)), $message); + } + + /** + * @return MessageEvent[] + */ + public static function getNotifierEvents(string $transportName = null): array + { + return self::getNotificationEvents()->getEvents($transportName); + } + + public static function getNotifierEvent(int $index = 0, string $transportName = null): ?MessageEvent + { + return self::getNotifierEvents($transportName)[$index] ?? null; + } + + /** + * @return MessageInterface[] + */ + public static function getNotifierMessages(string $transportName = null): array + { + return self::getNotificationEvents()->getMessages($transportName); + } + + public static function getNotifierMessage(int $index = 0, string $transportName = null): ?MessageInterface + { + return self::getNotifierMessages($transportName)[$index] ?? null; + } + + public static function getNotificationEvents(): NotificationEvents + { + $container = static::getContainer(); + if ($container->has('notifier.notification_logger_listener')) { + return $container->get('notifier.notification_logger_listener')->getEvents(); + } + + static::fail('A client must have Notifier enabled to make notifications assertions. Did you forget to require symfony/notifier?'); + } +} diff --git a/lib/symfony/framework-bundle/Test/TestBrowserToken.php b/lib/symfony/framework-bundle/Test/TestBrowserToken.php new file mode 100644 index 000000000..8bf365eb0 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/TestBrowserToken.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * A very limited token that is used to login in tests using the KernelBrowser. + * + * @author Wouter de Jong + */ +class TestBrowserToken extends AbstractToken +{ + private string $firewallName; + + public function __construct(array $roles = [], UserInterface $user = null, string $firewallName = 'main') + { + parent::__construct($roles); + + if (null !== $user) { + $this->setUser($user); + } + + $this->firewallName = $firewallName; + } + + public function getFirewallName(): string + { + return $this->firewallName; + } + + public function getCredentials(): mixed + { + return null; + } + + public function __serialize(): array + { + return [$this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->firewallName, $parentData] = $data; + + parent::__unserialize($parentData); + } +} diff --git a/lib/symfony/framework-bundle/Test/TestContainer.php b/lib/symfony/framework-bundle/Test/TestContainer.php new file mode 100644 index 000000000..e1e7a8592 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/TestContainer.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * A special container used in tests. This gives access to both public and + * private services. The container will not include private services that have + * been inlined or removed. Private services will be removed when they are not + * used by other services. + * + * @author Nicolas Grekas + * + * @internal + */ +class TestContainer extends Container +{ + public function __construct( + private KernelInterface $kernel, + private string $privateServicesLocatorId, + private array $renamedIds = [], + ) { + } + + public function compile(): void + { + $this->getPublicContainer()->compile(); + } + + public function isCompiled(): bool + { + return $this->getPublicContainer()->isCompiled(); + } + + public function getParameterBag(): ParameterBagInterface + { + return $this->getPublicContainer()->getParameterBag(); + } + + public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null + { + return $this->getPublicContainer()->getParameter($name); + } + + public function hasParameter(string $name): bool + { + return $this->getPublicContainer()->hasParameter($name); + } + + public function setParameter(string $name, mixed $value): void + { + $this->getPublicContainer()->setParameter($name, $value); + } + + public function set(string $id, mixed $service): void + { + $container = $this->getPublicContainer(); + $renamedId = $this->renamedIds[$id] ?? $id; + + try { + $container->set($renamedId, $service); + } catch (InvalidArgumentException $e) { + if (!str_starts_with($e->getMessage(), "The \"$renamedId\" service is private")) { + throw $e; + } + if (isset($container->privates[$renamedId])) { + throw new InvalidArgumentException(sprintf('The "%s" service is already initialized, you cannot replace it.', $id)); + } + $container->privates[$renamedId] = $service; + } + } + + public function has(string $id): bool + { + return $this->getPublicContainer()->has($id) || $this->getPrivateContainer()->has($id); + } + + public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object + { + return $this->getPrivateContainer()->has($id) ? $this->getPrivateContainer()->get($id) : $this->getPublicContainer()->get($id, $invalidBehavior); + } + + public function initialized(string $id): bool + { + return $this->getPublicContainer()->initialized($id); + } + + public function reset(): void + { + // ignore the call + } + + public function getServiceIds(): array + { + return $this->getPublicContainer()->getServiceIds(); + } + + public function getRemovedIds(): array + { + return $this->getPublicContainer()->getRemovedIds(); + } + + private function getPublicContainer(): Container + { + return $this->kernel->getContainer() ?? throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); + } + + private function getPrivateContainer(): ContainerInterface + { + return $this->getPublicContainer()->get($this->privateServicesLocatorId); + } +} diff --git a/lib/symfony/framework-bundle/Test/WebTestAssertionsTrait.php b/lib/symfony/framework-bundle/Test/WebTestAssertionsTrait.php new file mode 100644 index 000000000..aebd4577b --- /dev/null +++ b/lib/symfony/framework-bundle/Test/WebTestAssertionsTrait.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +trait WebTestAssertionsTrait +{ + use BrowserKitAssertionsTrait; + use DomCrawlerAssertionsTrait; + use HttpClientAssertionsTrait; +} diff --git a/lib/symfony/framework-bundle/Test/WebTestCase.php b/lib/symfony/framework-bundle/Test/WebTestCase.php new file mode 100644 index 000000000..de31d4ba9 --- /dev/null +++ b/lib/symfony/framework-bundle/Test/WebTestCase.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +/** + * WebTestCase is the base class for functional tests. + * + * @author Fabien Potencier + */ +abstract class WebTestCase extends KernelTestCase +{ + use WebTestAssertionsTrait; + + protected function tearDown(): void + { + parent::tearDown(); + self::getClient(null); + } + + /** + * Creates a KernelBrowser. + * + * @param array $options An array of options to pass to the createKernel method + * @param array $server An array of server parameters + */ + protected static function createClient(array $options = [], array $server = []): KernelBrowser + { + if (static::$booted) { + throw new \LogicException(sprintf('Booting the kernel before calling "%s()" is not supported, the kernel should only be booted once.', __METHOD__)); + } + + $kernel = static::bootKernel($options); + + try { + $client = $kernel->getContainer()->get('test.client'); + } catch (ServiceNotFoundException) { + if (class_exists(KernelBrowser::class)) { + throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.'); + } + throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit".'); + } + + $client->setServerParameters($server); + + return self::getClient($client); + } +} diff --git a/sources/Dependencies/Composer/iTopComposer.php b/sources/Dependencies/Composer/iTopComposer.php index 4db223284..2578b06ad 100644 --- a/sources/Dependencies/Composer/iTopComposer.php +++ b/sources/Dependencies/Composer/iTopComposer.php @@ -40,6 +40,7 @@ class iTopComposer extends AbstractFolderAnalyzer { return [ 'twig/twig/src/Node/Expression/Test', + 'symfony/framework-bundle/Test', // Tools for testing Symfony applications ]; } @@ -87,7 +88,6 @@ class iTopComposer extends AbstractFolderAnalyzer 'symfony/http-foundation/Test', 'symfony/http-kernel/Tests', 'symfony/service-contracts/Test', - 'symfony/framework-bundle/Test', 'symfony/mime/Test', 'symfony/routing/Tests', 'symfony/stopwatch/Tests', diff --git a/tests/php-unit-tests/module_integration.xml.dist b/tests/php-unit-tests/module_integration.xml.dist index ad7ff7a99..f13e34082 100644 --- a/tests/php-unit-tests/module_integration.xml.dist +++ b/tests/php-unit-tests/module_integration.xml.dist @@ -2,7 +2,7 @@ LoadRequiredItopFiles(); $this->LoadRequiredTestFiles(); } @@ -212,7 +220,7 @@ abstract class ItopTestCase extends TestCase protected function LoadRequiredItopFiles(): void { // At least make sure that the autoloader will be loaded, and that the APPROOT constant is defined - require_once __DIR__.'/../../../../approot.inc.php'; + require_once __DIR__.'/../../../../approot.inc.php'; } /** @@ -541,4 +549,27 @@ abstract class ItopTestCase extends TestCase $this->AssertArraysHaveSameItems($aExpected, $aFiles, $sMessage); } + + /** + * Control which Kernel will be loaded when invoking the bootKernel method + * + * @see static::bootKernel(), static::getContainer() + * @see \Combodo\iTop\Kernel, \Combodo\iTop\Portal\Kernel + * + * @param string $sKernelClass + * + * @since 3.2.1 + */ + static protected function SetKernelClass(string $sKernelClass): void + { + $_SERVER['KERNEL_CLASS'] = $sKernelClass; + } + + static protected function bootKernel(array $options = []): KernelInterface + { + if (!array_key_exists('KERNEL_CLASS', $_SERVER)) { + throw new \LogicException('static::SetKernelClass() must be called before booting the kernel.'); + } + return parent::bootKernel($options); + } } diff --git a/tests/php-unit-tests/unitary-tests/tests/php-unit-tests/BootSymfonyKernelTest.php b/tests/php-unit-tests/unitary-tests/tests/php-unit-tests/BootSymfonyKernelTest.php new file mode 100644 index 000000000..9d3dc8e33 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/tests/php-unit-tests/BootSymfonyKernelTest.php @@ -0,0 +1,49 @@ +SetKernelClass(\Combodo\iTop\Portal\Kernel::class); + self::bootKernel(); + $controller = static::getContainer()->get(AggregatePageBrickController::class); + + $this->assertInstanceOf(AggregatePageBrickController::class, $controller); + } + + public function testInstantiateServiceWithAnAutomaticKernelBoot() + { + $this->SetKernelClass(\Combodo\iTop\Portal\Kernel::class); + $controller = static::getContainer()->get(AggregatePageBrickController::class); + + $this->assertInstanceOf(AggregatePageBrickController::class, $controller); + } + + public function testUnspecifiedKernelClassThrowsAnException() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('static::SetKernelClass() must be called before booting the kernel'); + + static::getContainer(); + } + + public function testTwoDifferentKernelsCanBeStartedConsecutively() + { + self::markTestSkipped('This test is still failing: the second kernel container does not find the requested service'); + + $this->SetKernelClass(\Combodo\iTop\Kernel::class); + self::bootKernel(); + + $this->SetKernelClass(\Combodo\iTop\Portal\Kernel::class); + self::bootKernel(); + $controller = static::getContainer()->get(AggregatePageBrickController::class); + + $this->assertInstanceOf(AggregatePageBrickController::class, $controller); + } +} diff --git a/tests/php-unit-tests/unittestautoload.php b/tests/php-unit-tests/unittestautoload.php index 8b720f250..73ec676c8 100644 --- a/tests/php-unit-tests/unittestautoload.php +++ b/tests/php-unit-tests/unittestautoload.php @@ -2,6 +2,8 @@ // Main autoload, this is the one to use in the PHPUnit configuration // -// It was previously used to include both the vendor autoloader and our custom base test case classes, but these are now autoloaded from ./src/BasetestCase -// This file should then no longer be necessary, but we have to keep it until projects / branches / modules have been corrected. +// This file was previously mentioned as deprecated, and now it HAS to be used (see phpunit.xml/bootstrap attribute) require_once 'vendor/autoload.php'; + +// Required to benefit from symfony/framework-bundle's KernelTestCase, which is in a package which is a mix of runtime and test tools +require_once __DIR__.'/../../lib/autoload.php'; From 7254bb7a2f9bdd8ab48e5b80e2e0e369e58721ec Mon Sep 17 00:00:00 2001 From: odain-cbd <56586767+odain-cbd@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:41:58 +0200 Subject: [PATCH 03/14] =?UTF-8?q?N=C2=B07433=20-=20Allow=20MFA=20configura?= =?UTF-8?q?tion=20in=20the=20portal=20(#670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * N°7433 - Extension for MFA * Portal user profile brick * fix mfa tab display * Portal user profile brick * Portal user profile brick * mfa portal action * extensibility on portal to make mfa recovery mode work * Portal user profile brick extension * Portal user profile brick extension * display alert section in user portal profile layout * protect mfa via transaction_id mecanism * Fix transaction ids * Fix when no MFA * N°7433 - Allow MFA configuration in the portal (refactor interfaces) * N°7433 - Allow MFA configuration in the portal (revert unnecessary changes) * N°7433 - Allow MFA configuration in the portal (finer style) * N°7433 - Allow MFA configuration in the portal (PHPDoc update) * N°7433 - PR review --------- Co-authored-by: Eric Espie --- .../portal/src/Brick/UserProfileBrick.php | 184 ++++++----- .../Controller/UserProfileBrickController.php | 114 ++++--- .../portal/src/Helper/ExtensibilityHelper.php | 96 ++++++ .../iAbstractPortalTabContentExtension.php | 73 +++++ .../src/Hook/iAbstractPortalTabExtension.php | 62 ++++ .../Hook/iUserProfileTabContentExtension.php | 18 ++ .../src/Hook/iUserProfileTabExtension.php | 18 ++ .../portal/src/Twig/PortalBlockExtension.php | 71 +++++ .../portal/src/Twig/PortalTwigContext.php | 61 ++++ .../bricks/user-profile/layout.html.twig | 288 +++++------------- .../bricks/user-profile/user_info.html.twig | 119 ++++++++ .../user-profile/user_info.ready.js.twig | 100 ++++++ .../portal/templates/helpers/macros.twig | 13 + .../portal/templates/layout.html.twig | 6 +- .../vendor/composer/autoload_classmap.php | 65 ++++ .../portal/vendor/composer/autoload_real.php | 1 + .../vendor/composer/autoload_static.php | 65 ++++ 17 files changed, 1006 insertions(+), 348 deletions(-) create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Helper/ExtensibilityHelper.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Hook/iAbstractPortalTabContentExtension.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Hook/iAbstractPortalTabExtension.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Hook/iUserProfileTabContentExtension.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Hook/iUserProfileTabExtension.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Twig/PortalBlockExtension.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/Twig/PortalTwigContext.php create mode 100644 datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.html.twig create mode 100644 datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.ready.js.twig create mode 100644 datamodels/2.x/itop-portal-base/portal/templates/helpers/macros.twig diff --git a/datamodels/2.x/itop-portal-base/portal/src/Brick/UserProfileBrick.php b/datamodels/2.x/itop-portal-base/portal/src/Brick/UserProfileBrick.php index ac5716533..6b4fdb691 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Brick/UserProfileBrick.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Brick/UserProfileBrick.php @@ -19,33 +19,33 @@ namespace Combodo\iTop\Portal\Brick; -use DOMFormatException; use Combodo\iTop\DesignElement; +use DOMFormatException; /** * Description of UserProfileBrick - * - * @package Combodo\iTop\Portal\Brick - * @since 2.7.0 + * * @author Guillaume Lajarige + * @since 2.7.0 + * @package Combodo\iTop\Portal\Brick */ class UserProfileBrick extends PortalBrick { // Overloaded constants - const DEFAULT_PAGE_TEMPLATE_PATH = 'itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig'; - const DEFAULT_TILE_TEMPLATE_PATH = 'itop-portal-base/portal/templates/bricks/user-profile/tile.html.twig'; - const DEFAULT_VISIBLE_NAVIGATION_MENU = false; - const DEFAULT_VISIBLE_HOME = false; - const DEFAUT_TITLE = 'Brick:Portal:UserProfile:Title'; - const DEFAULT_DECORATION_CLASS_HOME = 'glyphicon glyphicon-user'; + const DEFAULT_PAGE_TEMPLATE_PATH = 'itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig'; + const DEFAULT_TILE_TEMPLATE_PATH = 'itop-portal-base/portal/templates/bricks/user-profile/tile.html.twig'; + const DEFAULT_VISIBLE_NAVIGATION_MENU = false; + const DEFAULT_VISIBLE_HOME = false; + const DEFAUT_TITLE = 'Brick:Portal:UserProfile:Title'; + const DEFAULT_DECORATION_CLASS_HOME = 'glyphicon glyphicon-user'; const DEFAULT_DECORATION_CLASS_NAVIGATION_MENU = 'glyphicon glyphicon-user'; /** @var bool DEFAULT_SHOW_PICTURE_FORM */ const DEFAULT_SHOW_PICTURE_FORM = true; /** @var bool DEFAULT_SHOW_PREFERENCES_FORM */ - const DEFAULT_SHOW_PREFERENCES_FORM = true; - /** @var bool DEFAULT_SHOW_PASSWORD_FORM */ - const DEFAULT_SHOW_PASSWORD_FORM = true; + const DEFAULT_SHOW_PREFERENCES_FORM = true; + /** @var bool DEFAULT_SHOW_PASSWORD_FORM */ + const DEFAULT_SHOW_PASSWORD_FORM = true; // Overloaded variables static $sRouteName = 'p_user_profile_brick'; @@ -67,8 +67,8 @@ class UserProfileBrick extends PortalBrick parent::__construct(); $this->aForm = array( - 'id' => 'default-user-profile', - 'type' => 'zlist', + 'id' => 'default-user-profile', + 'type' => 'zlist', 'fields' => 'details', 'layout' => null, ); @@ -89,67 +89,75 @@ class UserProfileBrick extends PortalBrick /** * * @param array $aForm + * * @return \Combodo\iTop\Portal\Brick\UserProfileBrick */ public function SetForm($aForm) { $this->aForm = $aForm; + return $this; } - /** - * @return bool - */ - public function GetShowPictureForm() - { - return $this->bShowPictureForm; - } + /** + * @return bool + */ + public function GetShowPictureForm() + { + return $this->bShowPictureForm; + } - /** - * @param $bShowPictureForm - * @return \Combodo\iTop\Portal\Brick\UserProfileBrick - */ - public function SetShowPictureForm($bShowPictureForm) - { - $this->bShowPictureForm = $bShowPictureForm; - return $this; - } + /** + * @param $bShowPictureForm + * + * @return \Combodo\iTop\Portal\Brick\UserProfileBrick + */ + public function SetShowPictureForm($bShowPictureForm) + { + $this->bShowPictureForm = $bShowPictureForm; - /** - * @return bool - */ - public function GetShowPreferencesForm() - { - return $this->bShowPreferencesForm; - } + return $this; + } - /** - * @param $bShowPreferencesForm - * @return \Combodo\iTop\Portal\Brick\UserProfileBrick - */ - public function SetShowPreferencesForm($bShowPreferencesForm) - { - $this->bShowPreferencesForm = $bShowPreferencesForm; - return $this; - } + /** + * @return bool + */ + public function GetShowPreferencesForm() + { + return $this->bShowPreferencesForm; + } - /** - * @return bool - */ - public function GetShowPasswordForm() - { - return $this->bShowPasswordForm; - } + /** + * @param $bShowPreferencesForm + * + * @return \Combodo\iTop\Portal\Brick\UserProfileBrick + */ + public function SetShowPreferencesForm($bShowPreferencesForm) + { + $this->bShowPreferencesForm = $bShowPreferencesForm; - /** - * @param $bShowPasswordForm - * @return \Combodo\iTop\Portal\Brick\UserProfileBrick - */ - public function SetShowPasswordForm($bShowPasswordForm) - { - $this->bShowPasswordForm = $bShowPasswordForm; - return $this; - } + return $this; + } + + /** + * @return bool + */ + public function GetShowPasswordForm() + { + return $this->bShowPasswordForm; + } + + /** + * @param $bShowPasswordForm + * + * @return \Combodo\iTop\Portal\Brick\UserProfileBrick + */ + public function SetShowPasswordForm($bShowPasswordForm) + { + $this->bShowPasswordForm = $bShowPasswordForm; + + return $this; + } /** * Load the brick's data from the xml passed as a ModuleDesignElement. @@ -167,50 +175,39 @@ class UserProfileBrick extends PortalBrick // Checking specific elements /** @var \Combodo\iTop\DesignElement $oBrickSubNode */ - foreach ($oMDElement->GetNodes('./*') as $oBrickSubNode) - { - switch ($oBrickSubNode->nodeName) - { + foreach ($oMDElement->GetNodes('./*') as $oBrickSubNode) { + switch ($oBrickSubNode->nodeName) { case 'form': // Note : This is inspired by Combodo\iTop\Portal\Helper\ApplicationHelper::LoadFormsConfiguration() // Enumerating fields - if ($oBrickSubNode->GetOptionalElement('fields') !== null) - { + if ($oBrickSubNode->GetOptionalElement('fields') !== null) { $this->aForm['type'] = 'custom_list'; $this->aForm['fields'] = array(); /** @var \Combodo\iTop\DesignElement $oFieldNode */ - foreach ($oBrickSubNode->GetOptionalElement('fields')->GetNodes('field') as $oFieldNode) - { + foreach ($oBrickSubNode->GetOptionalElement('fields')->GetNodes('field') as $oFieldNode) { $sFieldId = $oFieldNode->getAttribute('id'); - if ($sFieldId !== '') - { + if ($sFieldId !== '') { $aField = array(); // Parsing field options like read_only, hidden and mandatory - if ($oFieldNode->GetOptionalElement('read_only')) - { + if ($oFieldNode->GetOptionalElement('read_only')) { $aField['readonly'] = ($oFieldNode->GetOptionalElement('read_only')->GetText('true') === 'true') ? true : false; } - if ($oFieldNode->GetOptionalElement('mandatory')) - { + if ($oFieldNode->GetOptionalElement('mandatory')) { $aField['mandatory'] = ($oFieldNode->GetOptionalElement('mandatory')->GetText('true') === 'true') ? true : false; } - if ($oFieldNode->GetOptionalElement('hidden')) - { + if ($oFieldNode->GetOptionalElement('hidden')) { $aField['hidden'] = ($oFieldNode->GetOptionalElement('hidden')->GetText('true') === 'true') ? true : false; } $this->aForm['fields'][$sFieldId] = $aField; - } - else - { + } else { throw new DOMFormatException('Field tag must have an id attribute', null, null, $oFieldNode); } } } // Parsing presentation - if ($oBrickSubNode->GetOptionalElement('twig') !== null) - { + if ($oBrickSubNode->GetOptionalElement('twig') !== null) { // Extracting the twig template and removing the first and last lines (twig tags) $sXml = $oBrickSubNode->GetOptionalElement('twig')->Dump(true); //$sXml = $oMDElement->saveXML($oBrickSubNode->GetOptionalElement('twig')); @@ -218,25 +215,24 @@ class UserProfileBrick extends PortalBrick $sXml = preg_replace('/\n.+$/', '', $sXml); $this->aForm['layout'] = array( - 'type' => (preg_match('/\{\{|\{\#|\{\%/', $sXml) === 1) ? 'twig' : 'xhtml', + 'type' => (preg_match('/\{\{|\{\#|\{\%/', $sXml) === 1) ? 'twig' : 'xhtml', 'content' => $sXml, ); } break; - case 'show_picture_form': - case 'show_preferences_form': - case 'show_password_form': - $sConstName = 'DEFAULT_'.strtoupper($oBrickSubNode->nodeName); - $sSetterName = 'Set'.str_replace('_', '', ucwords($oBrickSubNode->nodeName, '_')); + case 'show_picture_form': + case 'show_preferences_form': + case 'show_password_form': + $sConstName = 'DEFAULT_'.strtoupper($oBrickSubNode->nodeName); + $sSetterName = 'Set'.str_replace('_', '', ucwords($oBrickSubNode->nodeName, '_')); - $bNodeValue = ($oBrickSubNode->GetText(constant('static::'.$sConstName)) === 'true') ? true : false; - $this->$sSetterName($bNodeValue); - break; + $bNodeValue = ($oBrickSubNode->GetText(constant('static::'.$sConstName)) === 'true') ? true : false; + $this->$sSetterName($bNodeValue); + break; } } return $this; } - } diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php index 75055eb2a..3d5e3ca23 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/UserProfileBrickController.php @@ -24,8 +24,13 @@ use Combodo\iTop\Portal\Brick\BrickCollection; use Combodo\iTop\Portal\Brick\UserProfileBrick; use Combodo\iTop\Portal\Form\PasswordFormManager; use Combodo\iTop\Portal\Form\PreferencesFormManager; +use Combodo\iTop\Portal\Helper\ExtensibilityHelper; use Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper; use Combodo\iTop\Portal\Helper\RequestManipulatorHelper; +use Combodo\iTop\Portal\Hook\iAbstractPortalTabContentExtension; +use Combodo\iTop\Portal\Hook\iAbstractPortalTabExtension; +use Combodo\iTop\Portal\Hook\iUserProfileTabContentExtension; +use Combodo\iTop\Portal\Hook\iUserProfileTabExtension; use Combodo\iTop\Portal\Routing\UrlGenerator; use Combodo\iTop\Renderer\Bootstrap\BsFormRenderer; use Exception; @@ -106,56 +111,60 @@ class UserProfileBrickController extends BrickController $oBrick = $this->oBrickCollection->GetBrickById($sBrickId); } - $aData = array(); + $aData = []; // Setting form mode regarding the demo mode parameter $bDemoMode = MetaModel::GetConfig()->Get('demo_mode'); $sFormMode = ($bDemoMode) ? ObjectFormHandlerHelper::ENUM_MODE_VIEW : ObjectFormHandlerHelper::ENUM_MODE_EDIT; + $sTab = $this->oRequestManipulatorHelper->ReadParam('sTab', 'user-info', FILTER_UNSAFE_RAW, FILTER_FLAG_EMPTY_STRING_NULL); + // If this is ajax call, we are just submitting preferences or password forms if ($oRequest->isXmlHttpRequest()) { - $aCurrentValues = $this->oRequestManipulatorHelper->ReadParam('current_values', array(), FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY); - $sFormType = $aCurrentValues['form_type']; - if ($sFormType === PreferencesFormManager::FORM_TYPE) - { - $aData['form'] = $this->HandlePreferencesForm($oRequest, $sFormMode); - } - elseif ($sFormType === PasswordFormManager::FORM_TYPE) - { - $aData['form'] = $this->HandlePasswordForm($oRequest, $sFormMode); - } - elseif ($sFormType === static::ENUM_FORM_TYPE_PICTURE) - { - $aData['form'] = $this->HandlePictureForm($oRequest); - } - else - { - throw new Exception('Unknown form type.'); + if ($sTab === "user-info") { + $aCurrentValues = $this->oRequestManipulatorHelper->ReadParam('current_values', array(), FILTER_UNSAFE_RAW, + FILTER_REQUIRE_ARRAY); + $sFormType = $aCurrentValues['form_type']; + if ($sFormType === PreferencesFormManager::FORM_TYPE) { + $aData['form'] = $this->HandlePreferencesForm($oRequest, $sFormMode); + } elseif ($sFormType === PasswordFormManager::FORM_TYPE) { + $aData['form'] = $this->HandlePasswordForm($oRequest, $sFormMode); + } elseif ($sFormType === static::ENUM_FORM_TYPE_PICTURE) { + $aData['form'] = $this->HandlePictureForm($oRequest); + } else { + throw new Exception('Unknown form type.'); + } } + $oResponse = new JsonResponse($aData); } // Else, we are displaying page for first time else { - // Retrieving current contact - /** @var \DBObject $oCurContact */ - $oCurContact = UserRights::GetContactObject(); - $sCurContactClass = get_class($oCurContact); - $sCurContactId = $oCurContact->GetKey(); + if ($sTab === "user-info") { + // Retrieving current contact + /** @var \DBObject $oCurContact */ + $oCurContact = UserRights::GetContactObject(); + $sCurContactClass = get_class($oCurContact); + $sCurContactId = $oCurContact->GetKey(); - // Preparing forms - $aData['forms']['contact'] = $this->ObjectFormHandlerHelper->HandleForm($oRequest, $sFormMode, $sCurContactClass, $sCurContactId, - $oBrick->GetForm()); - $aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode); - // - If user can change password, we display the form - $aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $sFormMode) : null; + // Preparing forms + $aData['forms']['contact'] = $this->ObjectFormHandlerHelper->HandleForm($oRequest, $sFormMode, $sCurContactClass, + $sCurContactId, + $oBrick->GetForm()); + $aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode); + // - If user can change password, we display the form + $aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $sFormMode) : null; + } - $aData = $aData + array( + $aData = $aData + [ 'oBrick' => $oBrick, 'sFormMode' => $sFormMode, 'bDemoMode' => $bDemoMode, - ); + ]; + + $this->ManageUserProfileBrickExtensibility($sTab, $aData); $oResponse = $this->render($oBrick->GetPageTemplatePath(), $aData); } @@ -163,6 +172,46 @@ class UserProfileBrickController extends BrickController return $oResponse; } + + private function ManageUserProfileBrickExtensibility(string $sTab, array &$aData): void + { + $aData['sTab'] = $sTab; + + // Read the tabs From iPortalTabExtension + $aTabExtensions = ExtensibilityHelper::GetInstance()->GetPortalTabExtensions(iUserProfileTabExtension::class); + + /** @var iAbstractPortalTabExtension $oPortalTabExtension */ + foreach ($aTabExtensions as $oPortalTabExtension) { + $aData['aTabsValues'][] = [ + 'code' => $oPortalTabExtension->GetTabCode(), + 'label' => $oPortalTabExtension->GetTabLabel(), + ]; + } + + // Read the current tab content From iPortalTabSectionExtension + $aTabSectionExtensions = ExtensibilityHelper::GetInstance()->GetPortalTabContentExtensions(iUserProfileTabContentExtension::class, $sTab); + if (count($aTabSectionExtensions) !== 0 && count($_POST) !== 0){ + $sTransactionId = utils::ReadPostedParam('transaction_id', null, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID); + IssueLog::Debug(__FUNCTION__.": transaction [$sTransactionId]"); + if (utils::IsNullOrEmptyString($sTransactionId) || !utils::IsTransactionValid($sTransactionId, false)) { + throw new Exception(\Dict::S('iTopUpdate:Error:InvalidToken')); + } + } + + $aData['sTransactionId'] = utils::GetNewTransactionId(); + + /** @var iAbstractPortalTabContentExtension $oPortalTabSectionExtension */ + foreach ($aTabSectionExtensions as $oPortalTabSectionExtension) { + $oPortalTabSectionExtension->HandlePortalForm($aData); + } + + $aData['aPluginFormData'] = []; + foreach ($aTabSectionExtensions as $oPortalTabSectionExtension) { + $aData['aPluginFormData'][] = $oPortalTabSectionExtension->GetPortalTabContentTwigs(); + } + } + + /** * @param \Symfony\Component\HttpFoundation\Request $oRequest * @param string $sFormMode @@ -173,8 +222,6 @@ class UserProfileBrickController extends BrickController */ public function HandlePreferencesForm(Request $oRequest, $sFormMode) { - - $aFormData = array(); // Handling form @@ -393,5 +440,4 @@ class UserProfileBrickController extends BrickController return $aFormData; } - } diff --git a/datamodels/2.x/itop-portal-base/portal/src/Helper/ExtensibilityHelper.php b/datamodels/2.x/itop-portal-base/portal/src/Helper/ExtensibilityHelper.php new file mode 100644 index 000000000..dc32ad9fc --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/Helper/ExtensibilityHelper.php @@ -0,0 +1,96 @@ +FindItopClasses($sPortalTabExtensionInterface) as $sPortalTabExtension) { + $oPortalTabExtension = new $sPortalTabExtension(); + if ($oPortalTabExtension->IsTabPresent()) { + $aTabExtensions[] = $oPortalTabExtension; + } + } + usort($aTabExtensions, function (iAbstractPortalTabExtension $a, iAbstractPortalTabExtension $b) { + return $a->GetTabRank() - $b->GetTabRank(); + }); + + return $aTabExtensions; + } + + /** + * Instantiate all the classes implementing the given interface for the given tab + * + * @param string $sPortalTabSectionExtensionInterface Extensibility interface to search for (derived from iAbstractPortalTabContentExtension) + * @param string $sTab Tab code + * + * @return array[iPortalTabContentExtension] array of objects implementing the given interface + */ + public function GetPortalTabContentExtensions(string $sPortalTabSectionExtensionInterface, string $sTab): array + { + $aTabSectionExtensions = []; + foreach (InterfaceDiscovery::GetInstance()->FindItopClasses($sPortalTabSectionExtensionInterface) as $sPortalTabSectionExtension) { + $oPortalTabSectionExtension = new $sPortalTabSectionExtension(); + if (!$oPortalTabSectionExtension->IsActive()) { + continue; + } + + if ($oPortalTabSectionExtension->GetTabCode() !== $sTab) { + continue; + } + $aTabSectionExtensions[] = $oPortalTabSectionExtension; + } + + usort($aTabSectionExtensions, function (iAbstractPortalTabContentExtension $a, iAbstractPortalTabContentExtension $b) { + return $a->GetSectionRank() - $b->GetSectionRank(); + }); + + return $aTabSectionExtensions; + } +} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/Hook/iAbstractPortalTabContentExtension.php b/datamodels/2.x/itop-portal-base/portal/src/Hook/iAbstractPortalTabContentExtension.php new file mode 100644 index 000000000..8fa7027e1 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/Hook/iAbstractPortalTabContentExtension.php @@ -0,0 +1,73 @@ +sTwig = $sTwig; + $this->aData = $aData; + } + + /** + * Used by twig templates to get the name of the template to render. + * + * @return string twig template to render + * + * @api + * + * @since iTop 3.2.1 + */ + public function GetTwig(): string + { + return $this->sTwig; + } + + /** + * Used by twig templates to get the data for the template to render. + * + * @return array Data used to render the template + * + * @api + * + * @since iTop 3.2.1 + */ + public function GetData(): array + { + $this->aData['sTransactionId'] = utils::GetNewTransactionId(); + return $this->aData; + } +} diff --git a/datamodels/2.x/itop-portal-base/portal/src/Twig/PortalTwigContext.php b/datamodels/2.x/itop-portal-base/portal/src/Twig/PortalTwigContext.php new file mode 100644 index 000000000..978a12166 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/Twig/PortalTwigContext.php @@ -0,0 +1,61 @@ +aBlockExtension = []; + } + + /** + * Add a Twig block extension. + * This method is used by extensions to provide templates. + * + * @api + * + * @param PortalBlockExtension $oBlockExtension Entity containing a twig template and associated data + * @param string $sBlockName Name of the block where to add the twig + * + * @since iTop 3.2.1 + */ + function AddBlockExtension(string $sBlockName, PortalBlockExtension $oBlockExtension): void + { + $this->aBlockExtension[$sBlockName] = $oBlockExtension; + } + + /** + * Get all the templates to render for a given block. + * This method is used by twig templates to render extensions. + * + * @api + * + * @param string $sBlockName Name of the block currently rendered + * + * @return \Combodo\iTop\Portal\Twig\PortalBlockExtension|null + * + * @since iTop 3.2.1 + */ + public function GetBlockExtension(string $sBlockName): ?PortalBlockExtension + { + /** @var PortalBlockExtension $oBlockExtension */ + $oBlockExtension = $this->aBlockExtension[$sBlockName] ?? null; + return $oBlockExtension; + } +} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig index e482db0b6..f27c5d559 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/layout.html.twig @@ -2,9 +2,9 @@ {# User profile brick base layout #} {% extends 'itop-portal-base/portal/templates/bricks/layout.html.twig' %} -{% set oContactForm = forms.contact %} -{% set oPreferencesForm = forms.preferences %} -{% set oPasswordForm = forms.password %} +{% if sTab == "" %} + {% set sTab = "user-info" %} +{% endif %} {% block pPageBodyClass %}{{ parent() }} page_user-profile_brick{% endblock %} @@ -20,228 +20,82 @@ {% endif %} -
- {% block pUserProfileWrapper %} -
-
-
-
-

{{ 'Brick:Portal:UserProfile:PersonalInformations:Title'|dict_s }}

-
-
-
- -
- - - -
-
- {{ oContactForm.renderer.GetBaseLayout()|raw }} -
-
-
-
-
-
- {% if oBrick.GetShowPictureForm() %} - {% block pUserProfilePictureFormContainer %} - - {% endblock %} - {% endif %} +
+ {% if sMessage is defined %} + + {% else %} + + {% endif %} - {% if oBrick.GetShowPreferencesForm() %} - {% block pUserProfilePreferencesFormContainer %} -
-
-

{{ 'Class:appUserPreferences/Attribute:preferences'|dict_s }}

-
-
-
-
- - - -
-
- {{ oPreferencesForm.renderer.GetBaseLayout()|raw }} -
-
-
-
- {% endblock %} - {% endif %} + {% if sError is defined %} + + {% else %} + + {% endif %} +
+ + + + {% if sTab == "user-info" %} + {% set oContactForm = forms.contact %} + {% set oPreferencesForm = forms.preferences %} + {% set oPasswordForm = forms.password %} + {% include 'itop-portal-base/portal/templates/bricks/user-profile/user_info.html.twig' %} + {% else %} +
+
+ {% import "itop-portal-base/portal/templates/helpers/macros.twig" as Macro %} + {{ Macro.BlockExtension(aPluginFormData, 'html', oBrick) }} +
+
+ {% endif %} - {% if oBrick.GetShowPasswordForm() %} - {% block pUserProfilePasswordFormContainer %} -
-
-

{{ 'Brick:Portal:UserProfile:Password:Title'|dict_s }}

-
-
- {% if oPasswordForm is not null %} -
-
- - - -
-
- {{ oPasswordForm.renderer.GetBaseLayout()|raw }} -
-
- {% else %} - {{ 'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator'|dict_format(constant('ITOP_APPLICATION_SHORT')) }} - {% endif %} -
-
- {% endblock %} - {% endif %} -
-
-
- {% block pUserProfileFormButtons %} -
- {% if sFormMode == constant('\\Combodo\\iTop\\Portal\\Helper\\ObjectFormHandlerHelper::ENUM_MODE_EDIT') %} - - {% endif %} -
- {% endblock %} -
- {% endblock %} -
{% endblock %} {% block pPageReadyScripts %} {{ parent() }} + {% import "itop-portal-base/portal/templates/helpers/macros.twig" as Macro %} + {{ Macro.BlockExtension(aPluginFormData, 'ready_script', oBrick) }} - // Personal informations form - var oContactFormFieldSet = $('#{{ oContactForm.id }} > .form_fields').field_set({{ oContactForm.fieldset|json_encode()|raw }}); - $('#{{ oContactForm.id }}').portal_form_handler({ - formmanager_class: "{{ oContactForm.formmanager_class|escape('js') }}", - formmanager_data: {{ oContactForm.formmanager_data|json_encode()|raw }}, - field_set: oContactFormFieldSet, - endpoint: "{{ oContactForm.renderer.GetEndpoint()|raw }}" - }); - - // Preferences form - var oPreferencesFormFieldSet = $('#{{ oPreferencesForm.id }} > .form_fields').field_set({{ oPreferencesForm.fieldset|json_encode()|raw }}); - $('#{{ oPreferencesForm.id }}').portal_form_handler({ - formmanager_class: "{{ oPreferencesForm.formmanager_class|escape('js') }}", - formmanager_data: {{ oPreferencesForm.formmanager_data|json_encode()|raw }}, - field_set: oPreferencesFormFieldSet, - endpoint: "{{ oPreferencesForm.renderer.GetEndpoint()|raw }}" - }); - - {% if oPasswordForm is not null %} - // Password form - var oPasswordFormFieldSet = $('#{{ oPasswordForm.id }} > .form_fields').field_set({{ oPasswordForm.fieldset|json_encode()|raw }}); - $('#{{ oPasswordForm.id }}').portal_form_handler({ - formmanager_class: "{{ oPasswordForm.formmanager_class|escape('js') }}", - formmanager_data: {{ oPasswordForm.formmanager_data|json_encode()|raw }}, - field_set: oPasswordFormFieldSet, - endpoint: "{{ oPasswordForm.renderer.GetEndpoint()|raw }}" - }); + {% if sTab == "" %} + {% set sTab = "user-info" %} {% endif %} - // Picture form - // - JQuery upload widget - $('#picture-form #picture').fileupload({ - dataType: 'json', - acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, - disableImageResize: /Android(?!.*Chrome)|Opera/.test(window.navigator.userAgent) - }) - .on('fileuploadsend', function(oEvent, oData){ - $('.user_profile_picture .form_alerts .alert-error').hide() - $('#page_overlay .overlay_content .content_loader').clone().prependTo('.user_profile_picture .panel-body'); - }) - .on('fileuploadalways', function(oEvent, oData){ - $('.user_profile_picture .content_loader').remove(); - }) - .on('fileuploaddone', function(oEvent, oData){ - if( (oData._response.result.form !== undefined) && (oData._response.result.form.validation.valid === true) ) - { - // Retrieving picture url - var sPictureUrl = oData._response.result.form.picture_url; - // Replacing form preview image - $('#picture-form .preview img').attr('src', sPictureUrl); - // Replacing menu image - $('#topbar .user_photo, #sidebar .user_photo').css('background-image', 'url("' + sPictureUrl + '")'); - } - }) - .on('fileuploadfail', function(oEvent, oData){ - if( (oData._response.jqXHR.responseJSON !== undefined) && (oData._response.jqXHR.responseJSON.error_message !== undefined) ) - { - $('.user_profile_picture .form_alerts .alert-error').show().text(oData._response.jqXHR.responseJSON.error_message); - } - }); - // - Undo button - /*$('#user-profile-wrapper .actions .btn_undo').on('click', function(oEvent){ - //console.log('Picture undo trigger'); - });*/ - // - Reset button - $('#user-profile-wrapper .actions .btn_reset').on('click', function(oEvent){ - //console.log('Picture reset trigger'); - }); + {% if sTab == "user-info" %} + {% set oContactForm = forms.contact %} + {% set oPreferencesForm = forms.preferences %} + {% set oPasswordForm = forms.password %} + {% include 'itop-portal-base/portal/templates/bricks/user-profile/user_info.ready.js.twig' %} + {% endif %} +{% endblock %} - // Submit button - $('#user-profile-wrapper .form_buttons .form_btn_submit').off('click').on('click', function(oEvent){ - oEvent.preventDefault(); +{% block pPageLiveScriptHelpers %} + {{ parent() }} + {% import "itop-portal-base/portal/templates/helpers/macros.twig" as Macro %} + {{ Macro.BlockExtension(aPluginFormData, 'script', oBrick) }} +{% endblock %} - // Resetting feedback - $('#user-profile-wrapper .form_alerts .alert').hide(); - $('#user-profile-wrapper .form_alerts .alert > p').remove(); - $('#user-profile-wrapper .form_field').removeClass('has-error'); - $('#user-profile-wrapper .form_field .help-block > p').remove(); - - // Submiting contact form through AJAX - $('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent); - - // Submiting preferences form through AJAX - $('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent); - - {% if oPasswordForm is not null %} - // Submiting password form through AJAX - // Only if fields are filled - $('#{{ oPasswordForm.id }} :password').each(function(iIndex, oElem){ - if($(oElem).val() !== '') - { - $('#{{ oPasswordForm.id }}').portal_form_handler('submit', oEvent); - return false; - } - }); - {% endif %} - }); -{% endblock %} \ No newline at end of file +{% block pStyleinline %} + {{ parent() }} + +{% endblock %} diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.html.twig new file mode 100644 index 000000000..93c530265 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.html.twig @@ -0,0 +1,119 @@ +{# @copyright Copyright (C) 2010-2024 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +
+ {% block pUserProfileWrapper %} +
+
+
+
+

{{ 'Brick:Portal:UserProfile:PersonalInformations:Title'|dict_s }}

+
+
+
+ +
+ + + +
+
+ {{ oContactForm.renderer.GetBaseLayout()|raw }} +
+
+
+
+
+
+ {% if oBrick.GetShowPictureForm() %} + {% block pUserProfilePictureFormContainer %} + + {% endblock %} + {% endif %} + + {% if oBrick.GetShowPreferencesForm() %} + {% block pUserProfilePreferencesFormContainer %} +
+
+

{{ 'Class:appUserPreferences/Attribute:preferences'|dict_s }}

+
+
+
+
+ + + +
+
+ {{ oPreferencesForm.renderer.GetBaseLayout()|raw }} +
+
+
+
+ {% endblock %} + {% endif %} + + {% if oBrick.GetShowPasswordForm() %} + {% block pUserProfilePasswordFormContainer %} +
+
+

{{ 'Brick:Portal:UserProfile:Password:Title'|dict_s }}

+
+
+ {% if oPasswordForm is not null %} +
+
+ + + +
+
+ {{ oPasswordForm.renderer.GetBaseLayout()|raw }} +
+
+ {% else %} + {{ 'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator'|dict_format(constant('ITOP_APPLICATION_SHORT')) }} + {% endif %} +
+
+ {% endblock %} + {% endif %} +
+
+
+ {% block pUserProfileFormButtons %} +
+ {% if sFormMode == constant('\\Combodo\\iTop\\Portal\\Helper\\ObjectFormHandlerHelper::ENUM_MODE_EDIT') %} + + {% endif %} +
+ {% endblock %} +
+ {% endblock %} +
diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.ready.js.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.ready.js.twig new file mode 100644 index 000000000..dcc0d32d0 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/user-profile/user_info.ready.js.twig @@ -0,0 +1,100 @@ +{# @copyright Copyright (C) 2010-2024 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +// Personal informations form +var oContactFormFieldSet = $('#{{ oContactForm.id }} > .form_fields').field_set({{ oContactForm.fieldset|json_encode()|raw }}); +$('#{{ oContactForm.id }}').portal_form_handler({ + formmanager_class: "{{ oContactForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oContactForm.formmanager_data|json_encode()|raw }}, + field_set: oContactFormFieldSet, + endpoint: "{{ oContactForm.renderer.GetEndpoint()|raw }}" +}); + +// Preferences form +var oPreferencesFormFieldSet = $('#{{ oPreferencesForm.id }} > .form_fields').field_set({{ oPreferencesForm.fieldset|json_encode()|raw }}); +$('#{{ oPreferencesForm.id }}').portal_form_handler({ + formmanager_class: "{{ oPreferencesForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oPreferencesForm.formmanager_data|json_encode()|raw }}, + field_set: oPreferencesFormFieldSet, + endpoint: "{{ oPreferencesForm.renderer.GetEndpoint()|raw }}" +}); + +{% if oPasswordForm is not null %} +// Password form +var oPasswordFormFieldSet = $('#{{ oPasswordForm.id }} > .form_fields').field_set({{ oPasswordForm.fieldset|json_encode()|raw }}); +$('#{{ oPasswordForm.id }}').portal_form_handler({ + formmanager_class: "{{ oPasswordForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oPasswordForm.formmanager_data|json_encode()|raw }}, + field_set: oPasswordFormFieldSet, + endpoint: "{{ oPasswordForm.renderer.GetEndpoint()|raw }}" +}); +{% endif %} + +// Picture form +// - JQuery upload widget +$('#picture-form #picture').fileupload({ + dataType: 'json', + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + disableImageResize: /Android(?!.*Chrome)|Opera/.test(window.navigator.userAgent) + }) + .on('fileuploadsend', function(oEvent, oData){ + $('.user_profile_picture .form_alerts .alert-error').hide() + $('#page_overlay .overlay_content .content_loader').clone().prependTo('.user_profile_picture .panel-body'); + }) + .on('fileuploadalways', function(oEvent, oData){ + $('.user_profile_picture .content_loader').remove(); + }) + .on('fileuploaddone', function(oEvent, oData){ + if( (oData._response.result.form !== undefined) && (oData._response.result.form.validation.valid === true) ) + { + // Retrieving picture url + var sPictureUrl = oData._response.result.form.picture_url; + // Replacing form preview image + $('#picture-form .preview img').attr('src', sPictureUrl); + // Replacing menu image + $('#topbar .user_photo, #sidebar .user_photo').css('background-image', 'url("' + sPictureUrl + '")'); + } + }) + .on('fileuploadfail', function(oEvent, oData){ + if( (oData._response.jqXHR.responseJSON !== undefined) && (oData._response.jqXHR.responseJSON.error_message !== undefined) ) + { + $('.user_profile_picture .form_alerts .alert-error').show().text(oData._response.jqXHR.responseJSON.error_message); + } + }); +// - Undo button +/*$('#user-profile-wrapper .actions .btn_undo').on('click', function(oEvent){ + //console.log('Picture undo trigger'); +});*/ +// - Reset button +$('#user-profile-wrapper .actions .btn_reset').on('click', function(oEvent){ + //console.log('Picture reset trigger'); +}); + +// Submit button +$('#user-profile-wrapper .form_buttons .form_btn_submit').off('click').on('click', function(oEvent){ + oEvent.preventDefault(); + + // Resetting feedback + $('#user-profile-wrapper .form_alerts .alert').hide(); + $('#user-profile-wrapper .form_alerts .alert > p').remove(); + $('#user-profile-wrapper .form_field').removeClass('has-error'); + $('#user-profile-wrapper .form_field .help-block > p').remove(); + + // Submiting contact form through AJAX + $('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent); + + // Submiting preferences form through AJAX + $('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent); + + {% if oPasswordForm is not null %} + // Submiting password form through AJAX + // Only if fields are filled + $('#{{ oPasswordForm.id }} :password').each(function(iIndex, oElem){ + if($(oElem).val() !== '') + { + $('#{{ oPasswordForm.id }}').portal_form_handler('submit', oEvent); + return false; + } + }); + {% endif %} +}); \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/templates/helpers/macros.twig b/datamodels/2.x/itop-portal-base/portal/templates/helpers/macros.twig new file mode 100644 index 000000000..92b91fe67 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/templates/helpers/macros.twig @@ -0,0 +1,13 @@ +{# @copyright Copyright (C) 2010-2024 Combodo SAS #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +{% macro BlockExtension(aPluginFormData, sBlockName, oBrick) %} + {% for oPortalData in aPluginFormData %} + {% if (oPortalData is defined and oPortalData.GetBlockExtension(sBlockName)) %} + {% set oBlockExtension = oPortalData.GetBlockExtension(sBlockName) %} + {% set sTwig = oBlockExtension.GetTwig() %} + {% set aData = oBlockExtension.GetData() %} + {% include sTwig ignore missing %} + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig index 0414c4cb1..12bae2f56 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig @@ -78,9 +78,9 @@ {% block pStyleinline %} {# UI Extensions inline CSS #} {% if app['ui_extensions_helper'].css_inline is not null %} - + {% endif %} {% endblock %} diff --git a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_classmap.php b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_classmap.php index 0fb0a2c19..127eefa4a 100644 --- a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_classmap.php +++ b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_classmap.php @@ -6,5 +6,70 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'Combodo\\iTop\\Portal\\Brick\\AbstractBrick' => $baseDir . '/src/Brick/AbstractBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\AggregatePageBrick' => $baseDir . '/src/Brick/AggregatePageBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\BrickCollection' => $baseDir . '/src/Brick/BrickCollection.php', + 'Combodo\\iTop\\Portal\\Brick\\BrickNotFoundException' => $baseDir . '/src/Brick/BrickNotFoundException.php', + 'Combodo\\iTop\\Portal\\Brick\\BrowseBrick' => $baseDir . '/src/Brick/BrowseBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\CreateBrick' => $baseDir . '/src/Brick/CreateBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\FilterBrick' => $baseDir . '/src/Brick/FilterBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\ManageBrick' => $baseDir . '/src/Brick/ManageBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\PortalBrick' => $baseDir . '/src/Brick/PortalBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\PropertyNotFoundException' => $baseDir . '/src/Brick/PropertyNotFoundException.php', + 'Combodo\\iTop\\Portal\\Brick\\UserProfileBrick' => $baseDir . '/src/Brick/UserProfileBrick.php', + 'Combodo\\iTop\\Portal\\Controller\\AbstractController' => $baseDir . '/src/Controller/AbstractController.php', + 'Combodo\\iTop\\Portal\\Controller\\AggregatePageBrickController' => $baseDir . '/src/Controller/AggregatePageBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\BrickController' => $baseDir . '/src/Controller/BrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\BrowseBrickController' => $baseDir . '/src/Controller/BrowseBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\CreateBrickController' => $baseDir . '/src/Controller/CreateBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\DefaultController' => $baseDir . '/src/Controller/DefaultController.php', + 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController' => $baseDir . '/src/Controller/ManageBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\ObjectController' => $baseDir . '/src/Controller/ObjectController.php', + 'Combodo\\iTop\\Portal\\Controller\\SessionMessageController' => $baseDir . '/src/Controller/SessionMessageController.php', + 'Combodo\\iTop\\Portal\\Controller\\UserProfileBrickController' => $baseDir . '/src/Controller/UserProfileBrickController.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\AbstractConfiguration' => $baseDir . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/AbstractConfiguration.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Basic' => $baseDir . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Basic.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Forms' => $baseDir . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Forms.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Lists' => $baseDir . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Lists.php', + 'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetPluginPropertyClass' => $baseDir . '/src/EventListener/ApplicationContextSetPluginPropertyClass.php', + 'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetUrlMakerClass' => $baseDir . '/src/EventListener/ApplicationContextSetUrlMakerClass.php', + 'Combodo\\iTop\\Portal\\EventListener\\CssFromSassCompiler' => $baseDir . '/src/EventListener/CssFromSassCompiler.php', + 'Combodo\\iTop\\Portal\\EventListener\\ExceptionListener' => $baseDir . '/src/EventListener/ExceptionListener.php', + 'Combodo\\iTop\\Portal\\EventListener\\UserProvider' => $baseDir . '/src/EventListener/UserProvider.php', + 'Combodo\\iTop\\Portal\\Form\\ObjectFormManager' => $baseDir . '/src/Form/ObjectFormManager.php', + 'Combodo\\iTop\\Portal\\Form\\PasswordFormManager' => $baseDir . '/src/Form/PasswordFormManager.php', + 'Combodo\\iTop\\Portal\\Form\\PreferencesFormManager' => $baseDir . '/src/Form/PreferencesFormManager.php', + 'Combodo\\iTop\\Portal\\Helper\\ApplicationHelper' => $baseDir . '/src/Helper/ApplicationHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\BrickControllerHelper' => $baseDir . '/src/Helper/BrickControllerHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\BrowseBrickHelper' => $baseDir . '/src/Helper/BrowseBrickHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ContextManipulatorHelper' => $baseDir . '/src/Helper/ContextManipulatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ExtensibilityHelper' => $baseDir . '/src/Helper/ExtensibilityHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\LifecycleValidatorHelper' => $baseDir . '/src/Helper/LifecycleValidatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\NavigationRuleHelper' => $baseDir . '/src/Helper/NavigationRuleHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ObjectFormHandlerHelper' => $baseDir . '/src/Helper/ObjectFormHandlerHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\RequestManipulatorHelper' => $baseDir . '/src/Helper/RequestManipulatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ScopeValidatorHelper' => $baseDir . '/src/Helper/ScopeValidatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\SecurityHelper' => $baseDir . '/src/Helper/SecurityHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\SessionMessageHelper' => $baseDir . '/src/Helper/SessionMessageHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\UIExtensionsHelper' => $baseDir . '/src/Helper/UIExtensionsHelper.php', + 'Combodo\\iTop\\Portal\\Hook\\iAbstractPortalTabContentExtension' => $baseDir . '/src/Hook/iAbstractPortalTabContentExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iAbstractPortalTabExtension' => $baseDir . '/src/Hook/iAbstractPortalTabExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iUserProfileTabContentExtension' => $baseDir . '/src/Hook/iUserProfileTabContentExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iUserProfileTabExtension' => $baseDir . '/src/Hook/iUserProfileTabExtension.php', + 'Combodo\\iTop\\Portal\\Kernel' => $baseDir . '/src/Kernel.php', + 'Combodo\\iTop\\Portal\\Routing\\ItopExtensionsExtraRoutes' => $baseDir . '/src/Routing/ItopExtensionsExtraRoutes.php', + 'Combodo\\iTop\\Portal\\Routing\\UrlGenerator' => $baseDir . '/src/Routing/UrlGenerator.php', + 'Combodo\\iTop\\Portal\\Twig\\AppExtension' => $baseDir . '/src/Twig/AppExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\AppGlobal' => $baseDir . '/src/Twig/AppGlobal.php', + 'Combodo\\iTop\\Portal\\Twig\\AppVariable' => $baseDir . '/src/Twig/AppVariable.php', + 'Combodo\\iTop\\Portal\\Twig\\CKEditorExtension' => $baseDir . '/src/Twig/CKEditorExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\CurrentUserAccessor' => $baseDir . '/src/Twig/CurrentUserAccessor.php', + 'Combodo\\iTop\\Portal\\Twig\\PortalBlockExtension' => $baseDir . '/src/Twig/PortalBlockExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\PortalTwigContext' => $baseDir . '/src/Twig/PortalTwigContext.php', + 'Combodo\\iTop\\Portal\\UrlMaker\\AbstractPortalUrlMaker' => $baseDir . '/src/UrlMaker/AbstractPortalUrlMaker.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\AbstractStringVariableAccessor' => $baseDir . '/src/VariableAccessor/AbstractStringVariableAccessor.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\AbstractVariableAccessor' => $baseDir . '/src/VariableAccessor/AbstractVariableAccessor.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\CombodoCurrentContactPhotoUrl' => $baseDir . '/src/VariableAccessor/CombodoCurrentContactPhotoUrl.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\CombodoPortalInstanceConf' => $baseDir . '/src/VariableAccessor/CombodoPortalInstanceConf.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_real.php b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_real.php index 0bd9d0feb..be4042c68 100644 --- a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_real.php +++ b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_real.php @@ -29,6 +29,7 @@ class ComposerAutoloaderInitdf408f3f8ea034d298269cdf7647358b require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitdf408f3f8ea034d298269cdf7647358b::getInitializer($loader)); + $loader->setClassMapAuthoritative(true); $loader->register(true); return $loader; diff --git a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_static.php b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_static.php index 5655fafc7..b9c0d723d 100644 --- a/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_static.php +++ b/datamodels/2.x/itop-portal-base/portal/vendor/composer/autoload_static.php @@ -26,6 +26,71 @@ class ComposerStaticInitdf408f3f8ea034d298269cdf7647358b ); public static $classMap = array ( + 'Combodo\\iTop\\Portal\\Brick\\AbstractBrick' => __DIR__ . '/../..' . '/src/Brick/AbstractBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\AggregatePageBrick' => __DIR__ . '/../..' . '/src/Brick/AggregatePageBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\BrickCollection' => __DIR__ . '/../..' . '/src/Brick/BrickCollection.php', + 'Combodo\\iTop\\Portal\\Brick\\BrickNotFoundException' => __DIR__ . '/../..' . '/src/Brick/BrickNotFoundException.php', + 'Combodo\\iTop\\Portal\\Brick\\BrowseBrick' => __DIR__ . '/../..' . '/src/Brick/BrowseBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\CreateBrick' => __DIR__ . '/../..' . '/src/Brick/CreateBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\FilterBrick' => __DIR__ . '/../..' . '/src/Brick/FilterBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\ManageBrick' => __DIR__ . '/../..' . '/src/Brick/ManageBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\PortalBrick' => __DIR__ . '/../..' . '/src/Brick/PortalBrick.php', + 'Combodo\\iTop\\Portal\\Brick\\PropertyNotFoundException' => __DIR__ . '/../..' . '/src/Brick/PropertyNotFoundException.php', + 'Combodo\\iTop\\Portal\\Brick\\UserProfileBrick' => __DIR__ . '/../..' . '/src/Brick/UserProfileBrick.php', + 'Combodo\\iTop\\Portal\\Controller\\AbstractController' => __DIR__ . '/../..' . '/src/Controller/AbstractController.php', + 'Combodo\\iTop\\Portal\\Controller\\AggregatePageBrickController' => __DIR__ . '/../..' . '/src/Controller/AggregatePageBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\BrickController' => __DIR__ . '/../..' . '/src/Controller/BrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\BrowseBrickController' => __DIR__ . '/../..' . '/src/Controller/BrowseBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\CreateBrickController' => __DIR__ . '/../..' . '/src/Controller/CreateBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\DefaultController' => __DIR__ . '/../..' . '/src/Controller/DefaultController.php', + 'Combodo\\iTop\\Portal\\Controller\\ManageBrickController' => __DIR__ . '/../..' . '/src/Controller/ManageBrickController.php', + 'Combodo\\iTop\\Portal\\Controller\\ObjectController' => __DIR__ . '/../..' . '/src/Controller/ObjectController.php', + 'Combodo\\iTop\\Portal\\Controller\\SessionMessageController' => __DIR__ . '/../..' . '/src/Controller/SessionMessageController.php', + 'Combodo\\iTop\\Portal\\Controller\\UserProfileBrickController' => __DIR__ . '/../..' . '/src/Controller/UserProfileBrickController.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\AbstractConfiguration' => __DIR__ . '/../..' . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/AbstractConfiguration.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Basic' => __DIR__ . '/../..' . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Basic.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Forms' => __DIR__ . '/../..' . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Forms.php', + 'Combodo\\iTop\\Portal\\DependencyInjection\\SilexCompatBootstrap\\PortalXmlConfiguration\\Lists' => __DIR__ . '/../..' . '/src/DependencyInjection/SilexCompatBootstrap/PortalXmlConfiguration/Lists.php', + 'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetPluginPropertyClass' => __DIR__ . '/../..' . '/src/EventListener/ApplicationContextSetPluginPropertyClass.php', + 'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetUrlMakerClass' => __DIR__ . '/../..' . '/src/EventListener/ApplicationContextSetUrlMakerClass.php', + 'Combodo\\iTop\\Portal\\EventListener\\CssFromSassCompiler' => __DIR__ . '/../..' . '/src/EventListener/CssFromSassCompiler.php', + 'Combodo\\iTop\\Portal\\EventListener\\ExceptionListener' => __DIR__ . '/../..' . '/src/EventListener/ExceptionListener.php', + 'Combodo\\iTop\\Portal\\EventListener\\UserProvider' => __DIR__ . '/../..' . '/src/EventListener/UserProvider.php', + 'Combodo\\iTop\\Portal\\Form\\ObjectFormManager' => __DIR__ . '/../..' . '/src/Form/ObjectFormManager.php', + 'Combodo\\iTop\\Portal\\Form\\PasswordFormManager' => __DIR__ . '/../..' . '/src/Form/PasswordFormManager.php', + 'Combodo\\iTop\\Portal\\Form\\PreferencesFormManager' => __DIR__ . '/../..' . '/src/Form/PreferencesFormManager.php', + 'Combodo\\iTop\\Portal\\Helper\\ApplicationHelper' => __DIR__ . '/../..' . '/src/Helper/ApplicationHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\BrickControllerHelper' => __DIR__ . '/../..' . '/src/Helper/BrickControllerHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\BrowseBrickHelper' => __DIR__ . '/../..' . '/src/Helper/BrowseBrickHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ContextManipulatorHelper' => __DIR__ . '/../..' . '/src/Helper/ContextManipulatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ExtensibilityHelper' => __DIR__ . '/../..' . '/src/Helper/ExtensibilityHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\LifecycleValidatorHelper' => __DIR__ . '/../..' . '/src/Helper/LifecycleValidatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\NavigationRuleHelper' => __DIR__ . '/../..' . '/src/Helper/NavigationRuleHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ObjectFormHandlerHelper' => __DIR__ . '/../..' . '/src/Helper/ObjectFormHandlerHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\RequestManipulatorHelper' => __DIR__ . '/../..' . '/src/Helper/RequestManipulatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\ScopeValidatorHelper' => __DIR__ . '/../..' . '/src/Helper/ScopeValidatorHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\SecurityHelper' => __DIR__ . '/../..' . '/src/Helper/SecurityHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\SessionMessageHelper' => __DIR__ . '/../..' . '/src/Helper/SessionMessageHelper.php', + 'Combodo\\iTop\\Portal\\Helper\\UIExtensionsHelper' => __DIR__ . '/../..' . '/src/Helper/UIExtensionsHelper.php', + 'Combodo\\iTop\\Portal\\Hook\\iAbstractPortalTabContentExtension' => __DIR__ . '/../..' . '/src/Hook/iAbstractPortalTabContentExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iAbstractPortalTabExtension' => __DIR__ . '/../..' . '/src/Hook/iAbstractPortalTabExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iUserProfileTabContentExtension' => __DIR__ . '/../..' . '/src/Hook/iUserProfileTabContentExtension.php', + 'Combodo\\iTop\\Portal\\Hook\\iUserProfileTabExtension' => __DIR__ . '/../..' . '/src/Hook/iUserProfileTabExtension.php', + 'Combodo\\iTop\\Portal\\Kernel' => __DIR__ . '/../..' . '/src/Kernel.php', + 'Combodo\\iTop\\Portal\\Routing\\ItopExtensionsExtraRoutes' => __DIR__ . '/../..' . '/src/Routing/ItopExtensionsExtraRoutes.php', + 'Combodo\\iTop\\Portal\\Routing\\UrlGenerator' => __DIR__ . '/../..' . '/src/Routing/UrlGenerator.php', + 'Combodo\\iTop\\Portal\\Twig\\AppExtension' => __DIR__ . '/../..' . '/src/Twig/AppExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\AppGlobal' => __DIR__ . '/../..' . '/src/Twig/AppGlobal.php', + 'Combodo\\iTop\\Portal\\Twig\\AppVariable' => __DIR__ . '/../..' . '/src/Twig/AppVariable.php', + 'Combodo\\iTop\\Portal\\Twig\\CKEditorExtension' => __DIR__ . '/../..' . '/src/Twig/CKEditorExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\CurrentUserAccessor' => __DIR__ . '/../..' . '/src/Twig/CurrentUserAccessor.php', + 'Combodo\\iTop\\Portal\\Twig\\PortalBlockExtension' => __DIR__ . '/../..' . '/src/Twig/PortalBlockExtension.php', + 'Combodo\\iTop\\Portal\\Twig\\PortalTwigContext' => __DIR__ . '/../..' . '/src/Twig/PortalTwigContext.php', + 'Combodo\\iTop\\Portal\\UrlMaker\\AbstractPortalUrlMaker' => __DIR__ . '/../..' . '/src/UrlMaker/AbstractPortalUrlMaker.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\AbstractStringVariableAccessor' => __DIR__ . '/../..' . '/src/VariableAccessor/AbstractStringVariableAccessor.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\AbstractVariableAccessor' => __DIR__ . '/../..' . '/src/VariableAccessor/AbstractVariableAccessor.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\CombodoCurrentContactPhotoUrl' => __DIR__ . '/../..' . '/src/VariableAccessor/CombodoCurrentContactPhotoUrl.php', + 'Combodo\\iTop\\Portal\\VariableAccessor\\CombodoPortalInstanceConf' => __DIR__ . '/../..' . '/src/VariableAccessor/CombodoPortalInstanceConf.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); From 69fd9048fd84d813e662c09b7b71a20417057ac6 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Thu, 17 Oct 2024 17:14:30 +0200 Subject: [PATCH 04/14] =?UTF-8?q?N=C2=B07903=20-=20On-going=20demand=20bri?= =?UTF-8?q?ck=20broken=20when=20adding=20Global=20Demand=201.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml index 5596b1a0b..06cb8c874 100755 --- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml +++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml @@ -1072,7 +1072,7 @@ fc fc-ongoing-request fc-2x - + @@ -1138,7 +1138,7 @@ fc fc-closed-request fc-2x - + From 80e413c3706b05a20b09f699e9b488559df124fa Mon Sep 17 00:00:00 2001 From: v-dumas Date: Thu, 17 Oct 2024 17:43:18 +0200 Subject: [PATCH 05/14] =?UTF-8?q?N=C2=B07906=20-=20User=20Preference:=20mi?= =?UTF-8?q?ssing=20list,=20search=20criteria=20and=20reconciliation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/user.preferences.class.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/user.preferences.class.inc.php b/application/user.preferences.class.inc.php index 560beb655..ed1325d2a 100644 --- a/application/user.preferences.class.inc.php +++ b/application/user.preferences.class.inc.php @@ -251,7 +251,7 @@ class appUserPreferences extends DBObject "key_type" => "autoincrement", "name_attcode" => "userid", "state_attcode" => "", - "reconc_keys" => array(), + "reconc_keys" => array("userid"), "db_table" => "priv_app_preferences", "db_key_field" => "id", "db_finalclass_field" => "", @@ -260,6 +260,8 @@ class appUserPreferences extends DBObject MetaModel::Init_Params($aParams); MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributePropertySet("preferences", array("allowed_values"=>null, "sql"=>"preferences", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_SetZListItems('list', array('preferences',)); + MetaModel::Init_SetZListItems('default_search', array('userid')); } /** From e6a3a95ff98dfb43a694f2fde083cc989b1b3ad5 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Thu, 17 Oct 2024 18:16:48 +0200 Subject: [PATCH 06/14] =?UTF-8?q?N=C2=B07859=20-=20French=20translation=20?= =?UTF-8?q?missing=20on=20newsroom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictionaries/fr.dictionary.itop.ui.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 0406f7178..27f6be35a 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -1061,7 +1061,7 @@ Elle s\'applique à tous les objets dans le périmètre de sa catégorie d\'audi 'UI:Newsroom:ResetCache' => 'Ràz du cache', 'UI:Newsroom:ResetCache:Success:Message' => 'Le cache de la newsroom a été réinitialisé avec succès', 'UI:Newsroom:ViewAllMessages' => 'Voir tous les messages', - 'UI:Newsroom:XNewMessage' => '%1$s new message(s)~~', + 'UI:Newsroom:XNewMessage' => '%1$s nouveau(x) message(s)', 'UI:NoInlineImage' => 'Il n\'y a aucune image de disponible sur le serveur. Utilisez le bouton "Parcourir" (ci-dessus) pour sélectionner une image sur votre ordinateur et la télécharger sur le serveur.', 'UI:NoObjectToDisplay' => 'Aucun objet à afficher.', 'UI:NoObject_Class_ToDisplay' => 'Aucun objet %1$s à afficher', From 7b3023ccce0d0d22925e35c734089b7af7a2c4e0 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Thu, 17 Oct 2024 18:24:08 +0200 Subject: [PATCH 07/14] =?UTF-8?q?N=C2=B07820=20-=20Tooltip=20Newsroom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictionaries/fr.dictionary.itop.core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 79e02080b..1e6dbae70 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -144,7 +144,7 @@ Les mots-clés sous la forme :this->attcode spécifiant un champ de l\'objet aya - Sinon, si l\'objet déclencheur a une icône de classe définie dans le datamodel, elle sera utilisée - Sinon, le logo compact de l\'application sera utilisé', 'Class:ActionNewsroom/Attribute:priority' => 'Priorité', - 'Class:ActionNewsroom/Attribute:priority+' => 'Les news sont affichés par priorité décroissante.', + 'Class:ActionNewsroom/Attribute:priority+' => 'Les news sont affichées par priorité décroissante.', 'Class:ActionNewsroom/Attribute:priority/Value:1' => 'Critique', 'Class:ActionNewsroom/Attribute:priority/Value:1+' => '', 'Class:ActionNewsroom/Attribute:priority/Value:2' => 'Urgent', From bb16f4ad57973d2bdf9ae17eefc12dc5b8e92abd Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 18 Oct 2024 11:10:27 +0200 Subject: [PATCH 08/14] =?UTF-8?q?N=C2=B07903=20-=20On-going=20demand=20bri?= =?UTF-8?q?ck=20broken=20when=20adding=20Global=20Demand=201.5.0=20(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml index 06cb8c874..294c9fcf2 100755 --- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml +++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml @@ -1138,7 +1138,7 @@ fc fc-closed-request fc-2x - + From 9b651c245198882e4da614d457794ddb2c4c6c23 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 18 Oct 2024 17:23:46 +0200 Subject: [PATCH 09/14] =?UTF-8?q?N=C2=B07803=20:white=5Fcheck=5Fmark:=20Te?= =?UTF-8?q?st=20iTop=20Hub=20move=20to=20production=20(compilation=20phase?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itop-hub-connector/AjaxPageTest.php | 68 +++++++++++++++++++ .../src/BaseTestCase/ItopDataTestCase.php | 24 +++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php new file mode 100644 index 000000000..e69150660 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php @@ -0,0 +1,68 @@ +GivenUserInDB(self::AUTHENTICATION_PASSWORD, ['Administrator']); + + $iLastCompilation = filemtime(APPROOT.'env-production'); + + // When + $sOutput = $this->CallItopUrl( + "/pages/exec.php?exec_module=itop-hub-connector&exec_page=ajax.php", + [ + 'auth_user' => $sLogin, + 'auth_pwd' => self::AUTHENTICATION_PASSWORD, + 'operation' => "compile", + 'authent' => self::AUTHENTICATION_TOKEN, + ] + ); + + // Then + $aRes = json_decode($sOutput, true); + $this->assertNotNull($aRes, "Response should be a valid json, found instead:" . PHP_EOL . $sOutput); + $this->assertEquals( + [ + 'code' => 0, + 'message' => 'Ok', + 'fields' => [] + ], + $aRes + ); + + clearstatcache(); + $this->assertGreaterThan($iLastCompilation, filemtime(APPROOT.'env-production'), 'The env-production directory should have been rebuilt'); + } + + protected function CallItopUrl($sUri, ?array $aPostFields = null, bool $bXDebugEnabled = false) + { + $ch = curl_init(); + if ($bXDebugEnabled) { + curl_setopt($ch, CURLOPT_COOKIE, 'XDEBUG_SESSION=phpstorm'); + } + + $sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri"; + var_dump($sUrl); + curl_setopt($ch, CURLOPT_URL, $sUrl); + curl_setopt($ch, CURLOPT_POST, 1);// set post data to true + curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + $sOutput = curl_exec($ch); + //echo "$sUrl error code:".curl_error($ch); + curl_close($ch); + + return $sOutput; + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 4c4f756e5..3d5aba722 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1368,4 +1368,28 @@ abstract class ItopDataTestCase extends ItopTestCase ]); return $sLogin; } + + /** + * @param string $sPassword + * @param array $aProfiles Profile names Example: ['Administrator'] + * + * @return string The unique login + * @throws \Exception + */ + protected function GivenUserInDB(string $sPassword, array $aProfiles): string + { + $sLogin = 'demo_test_'.uniqid(__CLASS__, true); + + $aProfileList = array_map(function($sProfileId) { + return 'profileid:'.self::$aURP_Profiles[$sProfileId]; + }, $aProfiles); + + $iUser = $this->GivenObjectInDB('UserLocal', [ + 'login' => $sLogin, + 'password' => $sPassword, + 'language' => 'EN US', + 'profile_list' => $aProfileList, + ]); + return $sLogin; + } } From 015ff8f179ed4021702ba132a7f0450f2cb54c76 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 18 Oct 2024 17:29:50 +0200 Subject: [PATCH 10/14] :white_check_mark: A better illustration to show that the impact is propagated in one way --- .../2.x/itop-tickets/UpdateImpactedItemsTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateImpactedItemsTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateImpactedItemsTest.php index 74d33a7b0..a764f3e46 100644 --- a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateImpactedItemsTest.php +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateImpactedItemsTest.php @@ -79,21 +79,18 @@ class UpdateImpactedItemsTest extends ItopDataTestCase { /** * Server1 +----> Hypervisor1 - * Server2 +----> Hypervisor1 */ $this->GivenCITreeInDB(<< Server_1 - Hypervisor_1 -> Server_2 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ - 'Server_1' => 'manual' + 'Hypervisor_1' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ - 'Server_1' => 'manual', - 'Hypervisor_1' => 'computed' + 'Hypervisor_1' => 'manual', ]); } From 764eddd9f8ce95147211814ef264b808b77dcd10 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Mon, 21 Oct 2024 10:45:26 +0200 Subject: [PATCH 11/14] =?UTF-8?q?N=C2=B07803=20:white=5Fcheck=5Fmark:=20Fi?= =?UTF-8?q?x=20test=20not=20working=20on=20a=20clean=20installation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-tests/itop-hub-connector/AjaxPageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php index e69150660..3e54cc0af 100644 --- a/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php @@ -12,6 +12,7 @@ class AjaxPageTest extends ItopDataTestCase { public function testCompileOperation() { // Given + static::RecurseMkdir(APPROOT.'data/hub'); file_put_contents(APPROOT.'data/hub/compile_authent', self::AUTHENTICATION_TOKEN); $sLogin = $this->GivenUserInDB(self::AUTHENTICATION_PASSWORD, ['Administrator']); From 04bd8cc5ceafdb19cd2d0e0ee72fab353c16900d Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Tue, 22 Oct 2024 16:07:47 +0200 Subject: [PATCH 12/14] :rocket: Update GitHub actions to improve PR classification --- .github/workflows/action.yml | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 46095c66c..70cda2c53 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -10,7 +10,34 @@ jobs: name: Add PR to Combodo Project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.2 + - name: Check if author is a member of the organization + id: check-membership + run: | + ORG="Combodo" + AUTHOR=$(jq -r .pull_request.user.login "$GITHUB_EVENT_PATH") + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \ + "https://api.github.com/orgs/$ORG/members/$AUTHOR") + if [ "$RESPONSE" == "404" ]; then + echo "project_url=https://github.com/orgs/Combodo/projects/5" >> $GITHUB_ENV + echo "is_member=false" >> $GITHUB_ENV + else + echo "project_url=https://github.com/orgs/Combodo/projects/4" >> $GITHUB_ENV + echo "is_member=true" >> $GITHUB_ENV + + fi + + - name: Add internal tag if member + if: env.is_member == 'true' + run: | + curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/Combodo/combodo-wikit-integration/issues/${{ github.event.pull_request.number }}/labels \ + -d '{"labels":["internal"]}' + env: + is_member: ${{ env.is_member }} + + - name: Add PR to the appropriate project + uses: actions/add-to-project@v1.0.2 with: - project-url: https://github.com/orgs/Combodo/projects/5 - github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }} + project-url: ${{ env.project_url }} + github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }} \ No newline at end of file From 674dfebb0d2f291ef8c96eeb5b0211c509fb9552 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Wed, 23 Oct 2024 15:46:44 +0200 Subject: [PATCH 13/14] =?UTF-8?q?N=C2=B07803=20:white=5Fcheck=5Fmark:=20Sk?= =?UTF-8?q?ip=20test=20if=20itop-hub-connector=20is=20not=20installed=20(n?= =?UTF-8?q?ew=20helper=20to=20handle=20this=20in=20any=20other=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itop-hub-connector/AjaxPageTest.php | 7 +++++++ .../src/BaseTestCase/ItopDataTestCase.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php index 3e54cc0af..f9d8af0d2 100644 --- a/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/AjaxPageTest.php @@ -3,12 +3,19 @@ namespace Combodo\iTop\Test\UnitTest\HubConnector; use Combodo\iTop\Application\Helper\Session; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; +use PHPUnit\Framework\SkippedTestCase; class AjaxPageTest extends ItopDataTestCase { const USE_TRANSACTION = false; const AUTHENTICATION_TOKEN = '14b5da9d092f84044187421419a0347e7317bc8cd2b486fdda631be06b959269'; const AUTHENTICATION_PASSWORD = "tagada-Secret,007"; + protected function setUp(): void + { + $this->SkipIfModuleNotPresent('itop-hub-connector'); + parent::setUp(); + } + public function testCompileOperation() { // Given diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 3d5aba722..cde0f101a 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1392,4 +1392,18 @@ abstract class ItopDataTestCase extends ItopTestCase ]); return $sLogin; } + + /** + * Can be invoked in setUp or any test method to skip the test if the module is not present + * + * @param string $sModule e.g: itop-hub-connector + * + * @return void + */ + protected function SkipIfModuleNotPresent(string $sModule): void + { + if (!file_exists(\utils::GetAbsoluteModulePath($sModule))) { + self::markTestSkipped("Test skipped: module '$sModule' is not present"); + } + } } From 4886e2a7dcf780f58be8bdb21ba6917ba657a967 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 23 Oct 2024 17:54:26 +0200 Subject: [PATCH 14/14] :memo: Update itop-version-history.md --- .doc/itop-version-history.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.doc/itop-version-history.md b/.doc/itop-version-history.md index 26c501ee4..c8f5252ea 100644 --- a/.doc/itop-version-history.md +++ b/.doc/itop-version-history.md @@ -79,17 +79,20 @@ gitGraph commit id: "2023-08-10" tag: "2.7.9" checkout support/3.1 commit id: "2023-12-20" tag: "3.1.1" + checkout develop + commit id: "2024-01-15" tag: "Start 3.2" type: HIGHLIGHT + branch support/3.2 order: 830 checkout support/2.7 commit id: "2024-01-17a" tag: "2.7.10" checkout support/3.0 commit id: "2024-01-17b" tag: "3.0.4" - checkout develop - commit id: "2024-06-25" tag: "3.2.0-beta1" type: REVERSE - commit id: "2024-08-07" tag: "3.2.0" type: HIGHLIGHT - branch support/3.2 order: 830 - checkout support/3.2 checkout support/2.7 - commit id: "2024-09-26" tag: "2.7.11" + commit id: "2024-09-28" tag: "2.7.11" + checkout support/3.1 + commit id: "2024-09-27" tag: "3.1.2" + checkout support/3.2 + commit id: "2024-06-25" tag: "3.2.0-beta1" type: REVERSE + commit id: "2024-08-07" tag: "3.2.0" ``` To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).