From 14ac1afd4c4c7df3c5a3a4e45674c068768cfbf7 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Tue, 29 Nov 2022 19:24:35 +1300 Subject: [PATCH] Add handling to token processor for double http in url tokens --- Civi/Core/Container.php | 4 + Civi/Token/TidySubscriber.php | 51 ++++++++++++ Civi/Token/TokenProcessor.php | 2 +- .../phpunit/Civi/Token/TokenProcessorTest.php | 82 ++++++++++++++++--- 4 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 Civi/Token/TidySubscriber.php diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index b7a5def800..2da5b7abd9 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -371,6 +371,10 @@ class Container { 'CRM_Core_DomainTokens', [] ))->addTag('kernel.event_subscriber')->setPublic(TRUE); + $container->setDefinition('crm_token_tidy', new Definition( + '\Civi\Token\TidySubscriber', + [] + ))->addTag('kernel.event_subscriber')->setPublic(TRUE); $dispatcherDefn = $container->getDefinition('dispatcher'); foreach (\CRM_Core_DAO_AllCoreTables::getBaoClasses() as $baoEntity => $baoClass) { diff --git a/Civi/Token/TidySubscriber.php b/Civi/Token/TidySubscriber.php new file mode 100644 index 0000000000..ed9d944912 --- /dev/null +++ b/Civi/Token/TidySubscriber.php @@ -0,0 +1,51 @@ + ['tidyHtml', 1000], + ]; + } + + /** + * Cleanup html issues. + * + * Currently we only clean up double https as can be generated by ckeditor + * in conjunction with a url token - eg https://{action.url} results in + * https:://https:://example.com. + * + * @param \Civi\Token\Event\TokenRenderEvent $e + * + * @noinspection HttpUrlsUsage + * @noinspection PhpUnused + */ + public function tidyHtml(TokenRenderEvent $e): void { + if (strpos($e->string, 'http') !== FALSE) { + $e->string = str_replace( + [ + 'https://https://', + 'http://https://', + 'http://http://', + 'https://http://', + ], + ['https://', 'https://', 'http://', 'http://'], + $e->string + ); + } + } + +} diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php index a5434e138d..57761c7fdf 100644 --- a/Civi/Token/TokenProcessor.php +++ b/Civi/Token/TokenProcessor.php @@ -116,7 +116,7 @@ class TokenProcessor { protected $next = 0; /** - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * @param \Civi\Core\CiviEventDispatcher $dispatcher * @param array $context */ public function __construct($dispatcher, $context) { diff --git a/tests/phpunit/Civi/Token/TokenProcessorTest.php b/tests/phpunit/Civi/Token/TokenProcessorTest.php index d3eff21d4c..c4b2c90da0 100644 --- a/tests/phpunit/Civi/Token/TokenProcessorTest.php +++ b/tests/phpunit/Civi/Token/TokenProcessorTest.php @@ -1,6 +1,7 @@ evaluate()->getRows() as $key => $row) { /** @var TokenRow */ - $this->assertTrue($row instanceof TokenRow); + $this->assertInstanceOf(TokenRow::class, $row); $this->assertEquals($expectText[$key], $row->render('text')); $rowCount++; } $this->assertEquals(3, $rowCount); } + /** + * Test that double urls created by https:// followed by a token are cleaned up. + * + * The ckeditor UI makes it easy to put https:// in the html when adding links, + * but they in the website url already. + * + * @throws \CRM_Core_Exception + * + * @noinspection HttpUrlsUsage + */ + public function testRenderDoubleUrl(): void { + $this->dispatcher->addSubscriber(new \CRM_Contact_Tokens()); + $this->dispatcher->addSubscriber(new TidySubscriber()); + $contactID = $this->individualCreate(); + $websiteID = Website::create()->setValues(['contact_id' => $contactID, 'url' => 'https://example.com'])->execute()->first()['id']; + $row = $this->renderUrlMessage($contactID); + $this->assertEquals('blah', $row->render('one')); + $this->assertEquals('blah', $row->render('two')); + + Website::update()->setValues(['url' => 'http://example.com'])->addWhere('id', '=', $websiteID)->execute(); + $row = $this->renderUrlMessage($contactID); + $this->assertEquals('blah', $row->render('one')); + $this->assertEquals('blah', $row->render('two')); + } + + /** + * Render a message with double url potential. + * + * @param int $contactID + * + * @return \Civi\Token\TokenRow + * + * @noinspection HttpUrlsUsage + */ + protected function renderUrlMessage(int $contactID): TokenRow { + $tokenProcessor = $this->getTokenProcessor(['schema' => ['contactId']]); + $tokenProcessor->addRow(['contactId' => $contactID]); + $tokenProcessor->addMessage('one', 'blah', 'text/html'); + $tokenProcessor->addMessage('two', 'blah', 'text/html'); + return $tokenProcessor->evaluate()->getRow(0); + } + public function testGetMessageTokens(): void { - $p = new TokenProcessor($this->dispatcher, [ - 'controller' => __CLASS__, - ]); - $p->addMessage('greeting_html', 'Good morning,

{contact.display_name}

. {custom.foobar}!', 'text/html'); - $p->addMessage('greeting_text', 'Good morning, {contact.display_name}. {custom.whizbang}, {contact.first_name}!', 'text/plain'); + $tokenProcessor = $this->getTokenProcessor(); + $tokenProcessor->addMessage('greeting_html', 'Good morning,

{contact.display_name}

. {custom.foobar}!', 'text/html'); + $tokenProcessor->addMessage('greeting_text', 'Good morning, {contact.display_name}. {custom.whiz_bang}, {contact.first_name}!', 'text/plain'); + $expected = [ 'contact' => ['display_name', 'first_name'], - 'custom' => ['foobar', 'whizbang'], + 'custom' => ['foobar', 'whiz_bang'], ]; - $this->assertEquals($expected, $p->getMessageTokens()); + $this->assertEquals($expected, $tokenProcessor->getMessageTokens()); } + /** + * Test getting available tokens. + */ public function testListTokens(): void { - $p = new TokenProcessor($this->dispatcher, [ - 'controller' => __CLASS__, - ]); - $p->addToken(['entity' => 'MyEntity', 'field' => 'myField', 'label' => 'My Label']); - $this->assertEquals(['{MyEntity.myField}' => 'My Label'], $p->listTokens()); + $tokenProcessor = $this->getTokenProcessor(); + $tokenProcessor->addToken(['entity' => 'MyEntity', 'field' => 'myField', 'label' => 'My Label']); + $this->assertEquals(['{MyEntity.myField}' => 'My Label'], $tokenProcessor->listTokens()); } /** @@ -638,6 +681,19 @@ class TokenProcessorTest extends \CiviUnitTestCase { $this->assertEquals('Invoice #200!', $outputs[1]); } + /** + * Get a token processor instance. + * + * @param array $context + * + * @return \Civi\Token\TokenProcessor + */ + protected function getTokenProcessor(array $context = []): TokenProcessor { + return new TokenProcessor($this->dispatcher, array_merge([ + 'controller' => __CLASS__, + ], $context)); + } + ///** // * This defines a compatibility mechanism wherein an old Smarty expression can // * be evaluated based on a newer token expression. -- 2.25.1