Add handling to token processor for double http in url tokens
authorEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 29 Nov 2022 06:24:35 +0000 (19:24 +1300)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Wed, 30 Nov 2022 00:49:15 +0000 (13:49 +1300)
Civi/Core/Container.php
Civi/Token/TidySubscriber.php [new file with mode: 0644]
Civi/Token/TokenProcessor.php
tests/phpunit/Civi/Token/TokenProcessorTest.php

index b7a5def8007e35da6fee3cfb033d12689c8d3579..2da5b7abd92e54fe53d311ad93fe0e6cae1f9be0 100644 (file)
@@ -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 (file)
index 0000000..ed9d944
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+namespace Civi\Token;
+
+use Civi\Token\Event\TokenRenderEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Class TokenCompatSubscriber
+ * @package Civi\Token
+ *
+ * This class handles the smarty processing of tokens.
+ */
+class TidySubscriber implements EventSubscriberInterface {
+
+  /**
+   * @inheritDoc
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      'civi.token.render' => ['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
+      );
+    }
+  }
+
+}
index a5434e138d19a5db8dd23fc8eb7dafce935b31d5..57761c7fdfcbccbf3cb1deb4065d796b44aa8c68 100644 (file)
@@ -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) {
index d3eff21d4c85e80989a26d9b453dbe1ea3ddce5e..c4b2c90da05b3f4652bb72243ab018e052c2d792 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace Civi\Token;
 
+use Civi\Api4\Website;
 use Civi\Token\Event\TokenRegisterEvent;
 use Civi\Token\Event\TokenValueEvent;
 use Civi\Core\CiviEventDispatcher;
@@ -261,32 +262,74 @@ class TokenProcessorTest extends \CiviUnitTestCase {
     $rowCount = 0;
     foreach ($tokenProcessor->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('<a href="https://example.com">blah</a>', $row->render('one'));
+    $this->assertEquals('<a href="https://example.com">blah</a>', $row->render('two'));
+
+    Website::update()->setValues(['url' => 'http://example.com'])->addWhere('id', '=', $websiteID)->execute();
+    $row = $this->renderUrlMessage($contactID);
+    $this->assertEquals('<a href="http://example.com">blah</a>', $row->render('one'));
+    $this->assertEquals('<a href="http://example.com">blah</a>', $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', '<a href="https://{contact.website_first.url}">blah</a>', 'text/html');
+    $tokenProcessor->addMessage('two', '<a href="http://{contact.website_first.url}">blah</a>', 'text/html');
+    return $tokenProcessor->evaluate()->getRow(0);
+  }
+
   public function testGetMessageTokens(): void {
-    $p = new TokenProcessor($this->dispatcher, [
-      'controller' => __CLASS__,
-    ]);
-    $p->addMessage('greeting_html', 'Good morning, <p>{contact.display_name}</p>. {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, <p>{contact.display_name}</p>. {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.