Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dev/core#2898 - Add handling to token processor for double http in url tokens #25078

Merged
merged 1 commit into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Civi/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ public function createContainer() {
'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) {
Expand Down
51 changes: 51 additions & 0 deletions Civi/Token/TidySubscriber.php
Original file line number Diff line number Diff line change
@@ -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
);
}
}

}
2 changes: 1 addition & 1 deletion Civi/Token/TokenProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
82 changes: 69 additions & 13 deletions tests/phpunit/Civi/Token/TokenProcessorTest.php
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -261,32 +262,74 @@ public function testRenderLocalizedHookToken(): void {
$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());
}

/**
Expand Down Expand Up @@ -638,6 +681,19 @@ public function testSmartyTokenAlias_Contribution(): void {
$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.
Expand Down