Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
final class DeviceTrackingServiceClearCookiesTest extends MauticMysqlTestCase
{
/**
* @return array<string, array{bool}>
*/
public static function blockedTrackingCookieDataProvider(): array
{
return [
'with blocked tracking cookie' => [true],
'without blocked tracking cookie' => [false],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('blockedTrackingCookieDataProvider')]
public function testClearTrackingCookiesBehavior(bool $shouldClearCookies): void
{
$this->logoutUser();
$page = new Page();
$page->setIsPublished(true);
$page->setTitle('Test Page for Clear Tracking Cookies');
$page->setAlias('test-clear-cookies');
$page->setCustomHtml('<html><body><h1>Test Page</h1></body></html>');
$this->em->persist($page);
$this->em->flush();
if ($shouldClearCookies) {
$this->client->getCookieJar()->set(new \Symfony\Component\BrowserKit\Cookie('Blocked-Tracking', '1'));
}
$this->client->request(Request::METHOD_GET, '/test-clear-cookies');
$this->assertResponseIsSuccessful();
$deviceIdCookieCleared = false;
$mtcIdCookieCleared = false;
foreach ($this->client->getResponse()->headers->getCookies() as $cookie) {
// Check if tracking cookies are being deleted (empty value + past expiration)
$cookieIsDeleted = '' === $cookie->getValue() && $cookie->getExpiresTime() < time();
if ('mautic_device_id' === $cookie->getName() && $cookieIsDeleted) {
$deviceIdCookieCleared = true;
}
if ('mtc_id' === $cookie->getName() && $cookieIsDeleted) {
$mtcIdCookieCleared = true;
}
}
Assert::assertSame($shouldClearCookies, $deviceIdCookieCleared);
Assert::assertSame($shouldClearCookies, $mtcIdCookieCleared);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class NotFoundFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testCustom404Page(): void
{
// Create a custom 404 page:
$notFoundPage = new Page();
$notFoundPage->setTitle('404 Not Found');
$notFoundPage->setAlias('404-not-found');
$notFoundPage->setCustomHtml('<html><body>Custom 404 Not Found Page</body></html>');
$this->em->persist($notFoundPage);
$this->em->flush();
// Configure the 404 page:
$this->configParams['404_page'] = $notFoundPage->getId();
parent::setUpSymfony($this->configParams);
// Test the custom 404 page:
$crawler = $this->client->request(Request::METHOD_GET, '/page-that-does-not-exist');
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
Assert::assertStringContainsString('Custom 404 Not Found Page', $crawler->text());
Assert::assertFalse($this->client->getResponse()->isRedirection(), 'The response should not be a redirect.');
Assert::assertSame('/page-that-does-not-exist', $this->client->getRequest()->getRequestUri(), 'The request URI should be the same as the original URI.');
}
}

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\DynamicContentBundle\Entity\DynamicContent;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\PageBundle\Entity\Page;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class PageControllerFunctionalTest extends MauticMysqlTestCase
{
public function testPagePreview(): void
{
$segment = $this->createSegment();
$filter = [
[
'glue' => 'and',
'field' => 'leadlist',
'object' => 'lead',
'type' => 'leadlist',
'filter' => [$segment->getId()],
'display' => null,
'operator' => 'in',
],
];
$dynamicContent = $this->createDynamicContentWithSegmentFilter($filter);
$dynamicContentToken = sprintf('{dwc=%s}', $dynamicContent->getSlotName());
$page = $this->createPage($dynamicContentToken);
$this->client->request(Request::METHOD_GET, sprintf('/%s', $page->getAlias()));
$response = $this->client->getResponse();
$this->assertSame(200, $response->getStatusCode());
$this->assertStringContainsString('Test Html', $response->getContent());
}
private function createSegment(): LeadList
{
$segment = new LeadList();
$segment->setName('Segment 1');
$segment->setPublicName('Segment 1');
$segment->setAlias('segment_1');
$this->em->persist($segment);
$this->em->flush();
return $segment;
}
/**
* @param mixed[] $filters
*/
private function createDynamicContentWithSegmentFilter(array $filters = []): DynamicContent
{
$dynamicContent = new DynamicContent();
$dynamicContent->setName('DC 1');
$dynamicContent->setDescription('Customised value');
$dynamicContent->setFilters($filters);
$dynamicContent->setIsCampaignBased(false);
$dynamicContent->setSlotName('Segment1_Slot');
$this->em->persist($dynamicContent);
$this->em->flush();
return $dynamicContent;
}
private function createPage(string $token = ''): Page
{
$page = new Page();
$page->setIsPublished(true);
$page->setTitle('Page Title');
$page->setAlias('page-alias');
$page->setTemplate('blank');
$page->setCustomHtml('Test Html'.$token);
$this->em->persist($page);
$this->em->flush();
return $page;
}
public function testPageWithProject(): void
{
$page = $this->createPage();
$project = new Project();
$project->setName('Test Project');
$this->em->persist($project);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', '/s/pages/edit/'.$page->getId());
$form = $crawler->selectButton('Save')->form();
$form['page[projects]']->setValue((string) $project->getId());
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$savedPage = $this->em->find(Page::class, $page->getId());
$this->assertSame($project->getId(), $savedPage->getProjects()->first()->getId());
}
public function testPageWithNullCustomHtmlIsUpdated(): void
{
$page = new Page();
$page->setTitle('Page A');
$page->setAlias('page-a');
$page->setTemplate('mautic_code_mode');
$this->em->persist($page);
$this->em->flush();
$pageId = $page->getId();
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/edit/'.$pageId);
$buttonCrawler = $crawler->selectButton('Save & Close');
$form = $buttonCrawler->form();
$form['page[title]']->setValue('New Page');
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$this->em->clear();
Assert::assertEquals('New Page', $this->em->find(Page::class, $pageId)->getTitle());
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\Redirect;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class PageControllerSqlRollbackFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testRedirectNotPersistClickthrough(): void
{
$lead = new Lead();
$lead->setEmail('test@example.com');
$this->em->persist($lead);
$this->em->flush();
$redirectUrl = 'https://mautic.org/';
$redirect = new Redirect();
$redirectHash = uniqid('', true);
$redirect->setRedirectId($redirectHash);
$redirect->setUrl($redirectUrl);
$this->em->persist($redirect);
$this->em->flush();
$email = new Email();
$email->setName('Test email');
$this->em->persist($email);
$this->em->flush();
$statHash = uniqid('', true);
$stat = new Stat();
$stat->setEmail($email);
$stat->setEmailAddress($lead->getEmail());
$stat->setDateSent(new \DateTime());
$stat->setLead($lead);
$stat->setTrackingHash($statHash);
$this->em->persist($stat);
$this->em->flush();
$ct = [
'source' => ['email', $email->getId()],
'email' => $email->getId(),
'stat' => $statHash,
'lead' => '1',
'channel' => ['email' => $email->getId()],
];
$encodedCt = base64_encode(serialize($ct));
$this->setUpSymfony($this->configParams);
$this->client->followRedirects(false);
$this->client->request(Request::METHOD_GET, "/r/{$redirectHash}?ct={$encodedCt}");
$response = $this->client->getResponse();
Assert::assertTrue($response->isRedirect($redirectUrl), (string) $response);
// Re-enable redirect following for subsequent tests.
$this->client->followRedirects();
$hitRepository = $this->em->getRepository(Hit::class);
/** @var Hit|null $hit */
$hit = $hitRepository->findOneBy(['lead' => $lead]);
Assert::assertNotNull($hit, 'A Hit entity should have been created.');
Assert::assertSame('email', $hit->getSource(), 'The hit source should be email.');
Assert::assertSame($email->getId(), $hit->getSourceId(), 'The hit source ID should be the email ID.');
Assert::assertSame($redirect->getId(), $hit->getRedirect()->getId(), 'The hit should be associated with the correct redirect.');
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Tests\Traits\ControllerTrait;
use Mautic\LeadBundle\Entity\UtmTag;
use Mautic\PageBundle\DataFixtures\ORM\LoadPageCategoryData;
use Mautic\PageBundle\DataFixtures\ORM\LoadPageData;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Model\PageModel;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PageControllerTest extends MauticMysqlTestCase
{
use ControllerTrait;
/**
* @var string
*/
private $prefix;
/**
* @var int
*/
private $id;
/**
* @throws \Exception
*/
protected function setUp(): void
{
parent::setUp();
$this->prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
$pageData = [
'title' => 'Test Page',
'template' => 'blank',
];
$model = static::getContainer()->get('mautic.page.model.page');
$page = new Page();
$page->setTitle($pageData['title'])
->setTemplate($pageData['template']);
$model->saveEntity($page);
$this->id = $page->getId();
}
/**
* Index should return status code 200.
*/
public function testIndexAction(): void
{
$urlAlias = 'pages';
$routeAlias = 'page';
$column = 'dateModified';
$column2 = 'title';
$tableAlias = 'p.';
$this->getControllerColumnTests($urlAlias, $routeAlias, $column, $tableAlias, $column2);
}
public function testLandingPageTracking(): void
{
$this->logoutUser();
$this->connection->insert($this->prefix.'pages', [
'is_published' => true,
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
'title' => 'Page:Page:LandingPageTracking',
'alias' => 'page-page-landingPageTracking',
'template' => 'blank',
'custom_html' => 'some content',
'hits' => 0,
'unique_hits' => 0,
'variant_hits' => 0,
'revision' => 0,
'lang' => 'en',
]);
$leadsBeforeTest = $this->connection->fetchAllAssociative('SELECT `id` FROM `'.$this->prefix.'leads`;');
$leadIdsBeforeTest = array_column($leadsBeforeTest, 'id');
$this->client->request('GET', '/page-page-landingPageTracking');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
$sql = 'SELECT `id` FROM `'.$this->prefix.'leads`';
if (!empty($leadIdsBeforeTest)) {
$sql .= ' WHERE `id` NOT IN ('.implode(',', $leadIdsBeforeTest).');';
}
$newLeads = $this->connection->fetchAllAssociative($sql);
$this->assertCount(1, $newLeads);
$leadId = reset($newLeads)['id'];
$leadEventLogs = $this->connection->fetchAllAssociative('
SELECT `id`, `action`
FROM `'.$this->prefix.'lead_event_log`
WHERE `lead_id` = :leadId
AND `bundle` = "page" AND `object` = "page";', ['leadId' => $leadId]
);
$this->assertCount(1, $leadEventLogs);
$this->assertSame('created_contact', reset($leadEventLogs)['action']);
}
/**
* Skipped for now.
*/
public function LandingPageTrackingSecondVisit(): void
{
$this->connection->insert($this->prefix.'pages', [
'is_published' => true,
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
'title' => 'Page:Page:LandingPageTrackingSecondVisit',
'alias' => 'page-page-landingPageTrackingSecondVisit',
'template' => 'blank',
'hits' => 0,
'unique_hits' => 0,
'variant_hits' => 0,
'revision' => 0,
'lang' => 'en',
]);
$leadsBeforeTest = $this->connection->fetchAllAssociative('SELECT `id` FROM `'.$this->prefix.'leads`;');
$leadIdsBeforeTest = array_column($leadsBeforeTest, 'id');
$this->client->request('GET', '/page-page-landingPageTrackingSecondVisit');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$sql = 'SELECT `id` FROM `'.$this->prefix.'leads`';
if (!empty($leadIdsBeforeTest)) {
$sql .= ' WHERE `id` NOT IN ('.implode(',', $leadIdsBeforeTest).');';
}
$newLeadsAfterFirstVisit = $this->connection->fetchAllAssociative($sql);
$this->assertCount(1, $newLeadsAfterFirstVisit);
$leadId = reset($newLeadsAfterFirstVisit)['id'];
$eventLogsAfterFirstVisit = $this->connection->fetchAllAssociative('
SELECT `id`, `action`
FROM `'.$this->prefix.'lead_event_log`
WHERE `lead_id` = :leadId
AND `bundle` = "page" AND `object` = "page";', ['leadId' => $leadId]
);
$this->assertCount(1, $eventLogsAfterFirstVisit);
$this->assertSame('created_contact', reset($eventLogsAfterFirstVisit)['action']);
$this->client->request('GET', '/page-page-landingPageTrackingSecondVisit');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$eventLogsAfterSecondVisit = $this->connection->fetchAllAssociative('
SELECT `id`, `action`
FROM `'.$this->prefix.'lead_event_log`
WHERE `lead_id` = :leadId
AND `bundle` = "page" AND `object` = "page";', ['leadId' => $leadId]
);
$this->assertCount(1, $eventLogsAfterSecondVisit);
$this->assertSame(reset($eventLogsAfterFirstVisit)['id'], reset($eventLogsAfterSecondVisit)['id']);
}
/**
* Test tracking of a first visit with UTM Tags.
*/
public function testLandingPageWithUtmTracking(): void
{
$this->logoutUser();
$timestamp = \time();
$page = $this->createTestPage();
$this->client->request('GET', "/{$page->getAlias()}?utm_source=linkedin&utm_medium=social&utm_campaign=mautic&utm_content=".$timestamp);
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode(), $clientResponse->getContent());
$allUtmTags = $this->em->getRepository(UtmTag::class)->getEntities();
$this->assertNotCount(0, $allUtmTags);
foreach ($allUtmTags as $utmTag) {
$this->assertSame('linkedin', $utmTag->getUtmSource(), 'utm_source does not match');
$this->assertSame('social', $utmTag->getUtmMedium(), 'utm_medium does not match');
$this->assertSame('mautic', $utmTag->getUtmCampaign(), 'utm_campaign does not match');
$this->assertSame(strval($timestamp), $utmTag->getUtmContent(), 'utm_content does not match');
}
}
/**
* Create a page for testing.
*/
protected function createTestPage($pageParams = []): Page
{
$page = new Page();
$title = $pageParams['title'] ?? 'Page:Page:LandingPageTracking';
$alias = $pageParams['alias'] ?? 'page-page-landingPageTracking';
$isPublished = $pageParams['isPublished'] ?? true;
$template = $pageParams['template'] ?? 'blank';
$page->setTitle($title);
$page->setAlias($alias);
$page->setIsPublished($isPublished);
$page->setTemplate($template);
$page->setCustomHtml('some content');
$this->em->persist($page);
$this->em->flush();
return $page;
}
/*
* Get page's view.
*/
public function testViewActionPage(): void
{
$this->client->request('GET', '/s/pages/view/'.$this->id);
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$model = static::getContainer()->get('mautic.page.model.page');
$page = $model->getEntity($this->id);
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
$this->assertStringContainsString($page->getTitle(), $clientResponseContent, 'The return must contain the title of page');
}
/**
* Get landing page's create page.
*/
public function testNewActionPage(): void
{
$this->client->request('GET', '/s/pages/new/');
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
}
/* Get landing page's submissions list */
public function testListLandingPageSubmissions(): void
{
$this->client->request('GET', 's/pages/results/'.$this->id);
$clientResponse = $this->client->getResponse();
$clientResponseContent = $clientResponse->getContent();
$model = static::getContainer()->get('mautic.page.model.page');
$page = $model->getEntity($this->id);
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
}
/**
* Only tests if an actual CSV file is returned.
*/
public function testCsvIsExportedCorrectly(): void
{
$this->loadFixtures([LoadPageCategoryData::class, LoadPageData::class]);
ob_start();
$this->client->request(Request::METHOD_GET, '/s/pages/results/'.$this->id.'/export');
$content = ob_get_contents();
ob_end_clean();
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
$this->assertEquals($this->client->getInternalResponse()->getHeader('content-type'), 'text/csv; charset=UTF-8');
}
/**
* Only tests if an actual Excel file is returned.
*/
public function testExcelIsExportedCorrectly(): void
{
$this->loadFixtures([LoadPageCategoryData::class, LoadPageData::class]);
ob_start();
$this->client->request(Request::METHOD_GET, '/s/pages/results/'.$this->id.'/export/xlsx');
$content = ob_get_contents();
ob_end_clean();
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
$this->assertEquals($this->client->getInternalResponse()->getHeader('content-type'), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
}
/**
* Only tests if an actual HTML file is returned.
*/
public function testHTMLIsExportedCorrectly(): void
{
$this->loadFixtures([LoadPageCategoryData::class, LoadPageData::class]);
ob_start();
$this->client->request(Request::METHOD_GET, '/s/pages/results/'.$this->id.'/export/html');
$content = ob_get_contents();
ob_end_clean();
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
$this->assertEquals($this->client->getInternalResponse()->getHeader('content-type'), 'text/html; charset=UTF-8');
}
public function testSavePageAliasWithUnderscores(): void
{
/** @var PageModel $pageModel */
$pageModel = static::getContainer()->get('mautic.page.model.page');
$parentPage = new Page();
$parentPage->setTitle('This is My Page');
$parentPage->setAlias('This_Is_My_Page');
$parentPage->setTemplate('blank');
$parentPage->setCustomHtml('This is My Page');
$pageModel->saveEntity($parentPage);
$this->client->request(Request::METHOD_GET, '/this_is_my_page');
$response = $this->client->getResponse();
Assert::assertTrue($response->isOk());
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Entity\PageDraft;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
final class PageDraftFunctionalTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->configParams['page_draft_enabled'] = 'testPageDraftNotConfigured' !== $this->name();
parent::setUp();
}
public function testPageDraftNotConfigured(): void
{
$page = $this->createNewPage();
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/edit/{$page->getId()}");
Assert::assertEquals(0, $crawler->selectButton('Save as Draft')->count());
Assert::assertEquals(0, $crawler->selectButton('Apply Draft')->count());
Assert::assertEquals(0, $crawler->selectButton('Discard Draft')->count());
}
public function testPageDraftConfigured(): void
{
$page = $this->createNewPage();
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/edit/{$page->getId()}");
Assert::assertEquals(1, $crawler->selectButton('Save as Draft')->count());
Assert::assertEquals(0, $crawler->selectButton('Apply Draft')->count());
Assert::assertEquals(0, $crawler->selectButton('Discard Draft')->count());
}
public function testCheckDraftInList(): void
{
$page = $this->createNewPage();
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages');
$this->assertStringNotContainsString('Has Draft', $crawler->filter('#app-content a[href="/s/pages/view/'.$page->getId().'"]')->html());
$this->saveDraft($page);
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages');
$this->assertStringContainsString('Has Draft', $crawler->filter('#app-content a[href="/s/pages/view/'.$page->getId().'"]')->html());
}
public function testPreviewDraft(): void
{
$page = $this->createNewPage();
$this->saveDraft($page);
$crawler = $this->client->request(Request::METHOD_GET, "/page/preview/{$page->getId()}");
$this->assertEquals('Test html', $crawler->filter('body')->text());
$crawler = $this->client->request(Request::METHOD_GET, "/page/preview/{$page->getId()}/draft");
$this->assertEquals('Test html Draft', $crawler->filter('body')->text());
}
public function testSaveDraftAndApplyDraft(): void
{
$page = $this->createNewPage();
$this->saveDraft($page);
$this->applyDraft($page);
}
public function testDiscardDraft(): void
{
$page = $this->createNewPage();
$this->saveDraft($page);
$this->discardDraft($page);
}
private function applyDraft(Page $page): void
{
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/edit/{$page->getId()}");
$form = $crawler->selectButton('Apply Draft')->form();
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk());
$pageDraft = $this->em->getRepository(PageDraft::class)->findOneBy(['page' => $page]);
Assert::assertNull($pageDraft);
Assert::assertSame('Test html Draft', $page->getCustomHtml());
}
private function discardDraft(Page $page): void
{
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/edit/{$page->getId()}");
$form = $crawler->selectButton('Discard Draft')->form();
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk());
$pageDraft = $this->em->getRepository(PageDraft::class)->findOneBy(['page' => $page]);
Assert::assertNull($pageDraft);
Assert::assertSame('Test html', $page->getCustomHtml());
}
private function saveDraft(Page $page): void
{
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/edit/{$page->getId()}");
$form = $crawler->selectButton('Save as Draft')->form();
$form['page[customHtml]'] = 'Test html Draft';
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk());
$pageDraft = $this->em->getRepository(PageDraft::class)->findOneBy(['page' => $page]);
Assert::assertEquals('Test html Draft', $pageDraft->getHtml());
Assert::assertSame('Test html', $page->getCustomHtml());
}
private function createNewPage(): Page
{
$pageObject = new Page();
$pageObject->setIsPublished(true);
$pageObject->setDateAdded(new \DateTime());
$pageObject->setTitle('Page Test');
$pageObject->setAlias('Page Test');
$pageObject->setTemplate('blank');
$pageObject->setCustomHtml('Test html');
$pageObject->setLanguage('en');
$this->em->persist($pageObject);
$this->em->flush();
return $pageObject;
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\PageBundle\Entity\Page;
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
final class PageProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
{
$projectOne = $this->createProject('Project One');
$projectTwo = $this->createProject('Project Two');
$projectThree = $this->createProject('Project Three');
$pageAlpha = $this->createPage('Page Alpha');
$pageBeta = $this->createPage('Page Beta');
$this->createPage('Page Gamma');
$this->createPage('Page Delta');
$pageAlpha->addProject($projectOne);
$pageAlpha->addProject($projectTwo);
$pageBeta->addProject($projectTwo);
$pageBeta->addProject($projectThree);
$this->em->flush();
$this->em->clear();
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/pages', '/s/pages']);
}
/**
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
*/
public static function searchDataProvider(): \Generator
{
yield 'search by one project' => [
'searchTerm' => 'project:"Project Two"',
'expectedEntities' => ['Page Alpha', 'Page Beta'],
'unexpectedEntities' => ['Page Gamma', 'Page Delta'],
];
yield 'search by one project AND page name' => [
'searchTerm' => 'project:"Project Two" AND Beta',
'expectedEntities' => ['Page Beta'],
'unexpectedEntities' => ['Page Alpha', 'Page Gamma', 'Page Delta'],
];
yield 'search by one project OR page name' => [
'searchTerm' => 'project:"Project Two" OR Gamma',
'expectedEntities' => ['Page Alpha', 'Page Beta', 'Page Gamma'],
'unexpectedEntities' => ['Page Delta'],
];
yield 'search by NOT one project' => [
'searchTerm' => '!project:"Project Two"',
'expectedEntities' => ['Page Gamma', 'Page Delta'],
'unexpectedEntities' => ['Page Alpha', 'Page Beta'],
];
yield 'search by two projects with AND' => [
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
'expectedEntities' => ['Page Beta'],
'unexpectedEntities' => ['Page Alpha', 'Page Gamma', 'Page Delta'],
];
yield 'search by two projects with NOT AND' => [
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
'expectedEntities' => ['Page Gamma', 'Page Delta'],
'unexpectedEntities' => ['Page Alpha', 'Page Beta'],
];
yield 'search by two projects with OR' => [
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
'expectedEntities' => ['Page Alpha', 'Page Beta'],
'unexpectedEntities' => ['Page Gamma', 'Page Delta'],
];
yield 'search by two projects with NOT OR' => [
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
'expectedEntities' => ['Page Alpha', 'Page Gamma', 'Page Delta'],
'unexpectedEntities' => ['Page Beta'],
];
}
private function createPage(string $name): Page
{
$page = new Page();
$page->setTitle($name);
$page->setAlias($name);
$this->em->persist($page);
return $page;
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\DynamicContentBundle\Entity\DynamicContent;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PageBundle\Entity\Page;
use Mautic\UserBundle\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PreviewFunctionalTest extends MauticMysqlTestCase
{
public function testPreviewPageWithContact(): void
{
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
$lead = $this->createLead();
$dynamicContent = $this->createDynamicContent($lead);
$defaultContent = 'Default web content';
// Create non public landing page.
$page = $this->createPage($dynamicContent, $defaultContent, true, false);
$this->em->flush();
$this->em->clear();
$url = "/page/preview/{$page->getId()}";
// Anonymous visitor is not allowed to access preview if not public
$this->logoutUser();
$this->client->request(Request::METHOD_GET, $url);
self::assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
$this->loginUser($user);
// Admin user is allowed to access preview
$this->assertPageContent($url, $defaultContent);
// Check DWC replacement for the given lead
$this->assertPageContent("{$url}?contactId={$lead->getId()}", $dynamicContent->getContent());
// Check there is no DWC replacement for a non-existent lead
$this->assertPageContent("{$url}?contactId=987", $defaultContent);
$this->logoutUser();
// Anonymous visitor is not allowed to access preview
$this->client->request(Request::METHOD_GET, $url);
self::assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
}
public function testPreviewPageUrlIsValid(): void
{
$page = $this->createPage();
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for correct preview URL.
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/view/'.$pageId);
self::assertStringContainsString('/page/preview/'.$pageId, $crawler->filter('#content_preview_url')->attr('value'));
}
public function testPreviewPagePublicToggle(): void
{
$page = $this->createPage();
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for public preview ON.
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/view/'.$pageId);
$toggleElem = $crawler->filter('i.ri-toggle-fill');
self::assertEquals(1, $toggleElem->count());
// Toggle public preview.
$parameters = [
'action' => 'togglePublishStatus',
'model' => 'page',
'id' => $pageId,
'customToggle' => 'publicPreview',
];
$this->client->request(Request::METHOD_POST, '/s/ajax', $parameters);
// Check for public preview OFF.
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/view/'.$pageId);
$toggleElem = $crawler->filter('i.ri-toggle-line');
self::assertEquals(1, $toggleElem->count());
// Create landing page with public preview OFF.
$page = $this->createPage(null, '', true, false);
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for public preview OFF.
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/view/'.$pageId);
self::assertEquals(1, $crawler->filter('i.ri-toggle-line')->count());
// Toggle public preview.
$parameters['id'] = $pageId;
$this->client->request(Request::METHOD_POST, '/s/ajax', $parameters);
// Check for public preview ON.
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/view/'.$pageId);
$toggleElem = $crawler->filter('i.ri-toggle-fill');
self::assertEquals(1, $toggleElem->count());
}
public function testPreviewPageWithPublishAndPublicOptions(): void
{
$page = $this->createPage();
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for public preview ON.
$this->client->request(Request::METHOD_GET, '/s/logout');
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
self::assertEquals('Hello', $crawler->filter('body')->text());
// Create landing page with public preview OFF.
$page = $this->createPage(null, '', true, false);
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check public preview without login.
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString(
'Unauthorized access to requested URL: /page/preview/'.$pageId,
$crawler->text()
);
// Create page with publish OFF.
$page = $this->createPage(null, '', false);
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for public preview ON.
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString(
'Unauthorized access to requested URL: /page/preview/'.$pageId,
$crawler->text()
);
// Create landing page with publish and public preview OFF.
$page = $this->createPage(null, '', false, false);
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check for public preview ON.
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString(
'Unauthorized access to requested URL: /page/preview/'.$pageId,
$crawler->text()
);
}
public function testPreviewPageNotFound(): void
{
// Check for non existing landing page preview.
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/20000');
self::assertEquals(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString('404 Not Found', $crawler->text());
}
public function testPreviewPageAccess(): void
{
// Create non published, non public landing page.
$page = $this->createPage(null, '', false, false);
$this->em->flush();
$this->em->clear();
$pageId = $page->getId();
// Check public preview with login.
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
$this->loginUser($user);
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
self::assertEquals('Hello', $crawler->filter('body')->text());
// Check public preview without login.
$this->client->request(Request::METHOD_GET, '/s/logout');
$crawler = $this->client->request(Request::METHOD_GET, '/page/preview/'.$pageId);
self::assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString(
'Unauthorized access to requested URL: /page/preview/'.$pageId,
$crawler->text()
);
// Check public preview access without permissions
$this->loginUser($user);
$security = $this->createMock(CorePermissions::class);
$security->method('isAnonymous')->willReturn(false);
$security->method('hasEntityAccess')->with(
'page:pages:viewown',
'page:pages:viewother',
$page->getCreatedBy()
)->willReturn(false);
$this->getContainer()->set('mautic.security', $security);
self::assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString(
'Unauthorized access to requested URL: /page/preview/'.$pageId,
$crawler->text()
);
}
private function assertPageContent(string $url, string $expectedContent): void
{
$crawler = $this->client->request(Request::METHOD_GET, $url);
self::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
self::assertSame($expectedContent, $crawler->filter('body')->text());
}
private function createPage(
?DynamicContent $dynamicContent = null,
string $defaultContent = '',
bool $isPublished = true,
bool $publicPreview = true,
): Page {
if (null === $dynamicContent) {
$customHtml = '<html lang="en"><body>Hello</body></html>';
} else {
$customHtml = sprintf('<div data-slot="dwc" data-param-slot-name="%s"><span>%s</span></div>', $dynamicContent->getSlotName(), $defaultContent);
}
$page = new Page();
$page->setIsPublished($isPublished);
$page->setDateAdded(new \DateTime());
$page->setTitle('Preview settings test - main page');
$page->setAlias('page-main');
$page->setTemplate('Blank');
$page->setCustomHtml($customHtml);
$page->setPublicPreview($publicPreview);
$this->em->persist($page);
return $page;
}
private function createLead(): Lead
{
$lead = new Lead();
$lead->setEmail('test@domain.tld');
$this->em->persist($lead);
return $lead;
}
private function createDynamicContent(Lead $lead): DynamicContent
{
$dynamicContent = new DynamicContent();
$dynamicContent->setName('Test DWC');
$dynamicContent->setIsCampaignBased(false);
$dynamicContent->setContent('DWC content');
$dynamicContent->setSlotName('test');
$dynamicContent->setFilters([
[
'glue' => 'and',
'field' => 'email',
'object' => 'lead',
'type' => 'email',
'filter' => $lead->getEmail(),
'display' => null,
'operator' => '=',
],
]);
$this->em->persist($dynamicContent);
return $dynamicContent;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use Symfony\Component\HttpFoundation\Request;
class PreviewSettingsFunctionalTest extends MauticMysqlTestCase
{
public function testPreviewSettingsAllEnabled(): void
{
$pageMain = new Page();
$pageMain->setIsPublished(true);
$pageMain->setDateAdded(new \DateTime());
$pageMain->setTitle('Preview settings test - main page');
$pageMain->setAlias('page-main');
$pageMain->setTemplate('Blank');
$pageMain->setCustomHtml('Test Html');
$pageMain->setLanguage('en');
$this->em->persist($pageMain);
$this->em->flush();
$mainPageId = $pageMain->getId();
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages');
self::assertStringContainsString($pageMain->getTitle(), $crawler->text());
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/view/{$mainPageId}");
// Translation choice is not visible
self::assertCount(
0,
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]')
);
// Variant choice is not visible
self::assertCount(
0,
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]')
);
// Contact lookup is not visible
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_contact"]')
);
$pageTranslated = new Page();
$pageTranslated->setIsPublished(true);
$pageTranslated->setDateAdded(new \DateTime());
$pageTranslated->setTitle('Preview settings test - NL translation');
$pageTranslated->setAlias('page-trans-nl');
$pageTranslated->setTemplate('Blank');
$pageTranslated->setCustomHtml('Test Html');
$pageTranslated->setLanguage('nl_CW');
// Add translation relationship to main page
$pageMain->addTranslationChild($pageTranslated);
$pageTranslated->setTranslationParent($pageMain);
$pageVariant = new Page();
$pageVariant->setIsPublished(true);
$pageVariant->setDateAdded(new \DateTime());
$pageVariant->setTitle('Preview settings test - B variant');
$pageVariant->setAlias('page-variant-b');
$pageVariant->setTemplate('Blank');
$pageVariant->setCustomHtml('Test Html');
$pageVariant->setLanguage('en');
// Add variant relationship to main page
$pageMain->addVariantChild($pageVariant);
$this->em->persist($pageMain);
$this->em->persist($pageTranslated);
$this->em->persist($pageVariant);
$this->em->flush();
$crawler = $this->client->request(Request::METHOD_GET, "/s/pages/view/{$mainPageId}");
// Translation choice is visible
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]')
);
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_translation"]/option[@value="'.$pageTranslated->getId().'"]')
);
// Variant choice is visible
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]')
);
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_variant"]/option[@value="'.$pageVariant->getId().'"]')
);
// Contact lookup is visible
self::assertCount(
1,
$crawler->filterXPath('//*[@id="content_preview_settings_contact"]')
);
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Tag;
use Mautic\PageBundle\Entity\Page;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class PublicControllerFunctionalTest extends MauticMysqlTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('xssPayloadsProvider')]
public function testContactTrackingTagsXss(string $payload, ?string $expectedSanitized): void
{
$this->logoutUser();
$page = new Page();
$page->setIsPublished(true);
$page->setTitle('XSS Test');
$page->setAlias('xss-test');
$page->setCustomHtml('xss-test');
$this->em->persist($page);
$this->em->flush();
$encodedPayload = urlencode($payload);
$this->client->request(Request::METHOD_GET, "/xss-test?tags={$encodedPayload}");
Assert::assertTrue($this->client->getResponse()->isOk());
$tagRepository = $this->em->getRepository(Tag::class);
$tags = $tagRepository->findAll();
if ($expectedSanitized) {
// Assert that a tag was created
Assert::assertCount(1, $tags);
// Get the created tag
$tag = $tags[0];
// Assert that the tag name does not contain the malicious script
Assert::assertStringNotContainsString('<script>', $tag->getTag());
Assert::assertStringNotContainsString('</script>', $tag->getTag());
// Assert that the tag name has been properly sanitized
Assert::assertEquals($expectedSanitized, $tag->getTag());
} else {
// Assert that a tag was NOT created
Assert::assertCount(0, $tags);
}
// Check the response content to ensure no script is present
$content = $this->client->getResponse()->getContent();
Assert::assertStringNotContainsString($payload, $content);
}
/**
* @return array<string, array<int, string|null>>
*/
public static function xssPayloadsProvider(): array
{
return [
'Basic script tag' => [
'<script>alert(1)</script>',
'alert(1)',
],
'Script tag with attributes' => [
'<script src="http://example.com/evil.js"></script>',
null,
],
'Encoded script tag' => [
'&#60;script&#62;alert(1)&#60;/script&#62;',
'alert(1)',
],
'On-event handler' => [
'<img src="x" onerror="alert(1)">',
null,
],
'JavaScript protocol in URL' => [
'<a href="javascript:alert(1)">Click me</a>',
'Click me',
],
'SVG with embedded script' => [
'<svg><script>alert(1)</script></svg>',
'alert(1)',
],
'CSS expression' => [
'<div style="background:url(javascript:alert(1))">',
null,
],
'Malformed tag' => [
'<img """><script>alert("XSS")</script>"<',
'alert("XSS")"',
],
'Malformed tag2' => [
'<IMG SRC="jav&#x09;ascript:alert(\'XSS\');">',
null,
],
'Unicode escape' => [
'<script>\u0061lert(1)</script>',
'\u0061lert(1)',
],
];
}
public function testMtcEventCompanyXss(): void
{
$this->logoutUser();
$this->client->request('POST', '/mtc/event', [
'page_url' => 'https://example.com?Company=%3Cimg+src+onerror%3Dalert%28%27Company%27%29%3E',
]);
$this->assertResponseIsSuccessful();
$this->loginUser($this->em->getRepository(User::class)->findOneBy(['username' => 'admin']));
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->client->request('GET', sprintf('/s/contacts/view/%d', $response['id']));
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
$crawler = $this->client->request('GET', sprintf('/s/contacts/edit/%d', $response['id']));
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
$buttonCrawlerNode = $crawler->selectButton('Save & Close');
Assert::assertCount(1, $buttonCrawlerNode, $crawler->html());
$form = $buttonCrawlerNode->form();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
Assert::assertStringNotContainsString('<img src onerror=alert(\'Company\')>', $content);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Entity\Redirect;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PublicControllerRedirectTest extends MauticMysqlTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('redirectTypeOptions')]
public function testValidationRedirectWithoutUrl(string $redirectUrl, string $expectedMessage): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/pages/new');
$saveButton = $crawler->selectButton('Save');
$form = $saveButton->form();
$form['page[title]']->setValue('Redirect test');
$form['page[redirectType]']->setValue((string) Response::HTTP_MOVED_PERMANENTLY);
$form['page[redirectUrl]']->setValue($redirectUrl);
$form['page[template]']->setValue('mautic_code_mode');
$this->client->submit($form);
Assert::assertStringContainsString($expectedMessage, $this->client->getResponse()->getContent());
}
/**
* @return iterable<string, array{string, string}>
*/
public static function redirectTypeOptions(): iterable
{
yield 'redirect set, empty redirect URL' => ['', 'A value is required.'];
yield 'redirect set, invalid redirect URL' => ['invalid.url', 'This value is not a valid URL.'];
yield 'redirect set, valid redirect URL' => ['https://valid.url', 'Edit Page - Redirect test'];
}
public function testCreateRedirectWithNoUrlForExistingPages(): void
{
$page = new Page();
$page->setTitle('Page A');
$page->setAlias('page-a');
$page->setIsPublished(false);
$page->setRedirectType((string) Response::HTTP_MOVED_PERMANENTLY);
$this->em->persist($page);
$this->em->flush();
$this->logoutUser();
$this->client->request(Request::METHOD_GET, '/page-a');
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
}
public function testRedirectWithSpacesInQuery(): void
{
$url = 'https://google.com?q=this%20has%20spaces';
$redirect = new Redirect();
$redirect->setUrl($url);
$redirect->setRedirectId('57cf5a66a9f9414f301082cf0');
$this->em->persist($redirect);
$this->em->flush();
$this->client->followRedirects(false);
$this->client->request(Request::METHOD_GET, sprintf('/r/%s', $redirect->getRedirectId()));
$response = $this->client->getResponse();
\assert($response instanceof RedirectResponse);
Assert::assertSame(Response::HTTP_FOUND, $response->getStatusCode());
Assert::assertSame($url, $response->getTargetUrl(), 'The spaces in the query part must not be encoded with plus signs.');
}
}

View File

@@ -0,0 +1,702 @@
<?php
namespace Mautic\PageBundle\Tests\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Exception\InvalidDecodedStringException;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\CookieHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\ThemeHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Service\FlashBag;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\CoreBundle\Twig\Helper\AnalyticsHelper;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Helper\ContactRequestHelper;
use Mautic\LeadBundle\Helper\PrimaryCompanyHelper;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
use Mautic\PageBundle\Controller\PublicController;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Entity\Redirect;
use Mautic\PageBundle\Event\TrackingEvent;
use Mautic\PageBundle\Helper\TrackingHelper;
use Mautic\PageBundle\Model\PageModel;
use Mautic\PageBundle\Model\RedirectModel;
use Mautic\PageBundle\Model\Tracking404Model;
use Mautic\PageBundle\PageEvents;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Asset\Packages;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
#[\PHPUnit\Framework\Attributes\CoversClass(TrackingEvent::class)]
class PublicControllerTest extends MauticMysqlTestCase
{
/**
* @var MockObject|Container
*/
private MockObject $internalContainer;
/**
* @var \Psr\Log\LoggerInterface
*/
private MockObject $logger;
/**
* @var ModelFactory<object>&MockObject
*/
private MockObject $modelFactory;
/**
* @var RedirectModel
*/
private MockObject $redirectModel;
/**
* @var Redirect
*/
private MockObject $redirect;
private Request $request;
/**
* @var IpLookupHelper
*/
private MockObject $ipLookupHelper;
/**
* @var IpAddress
*/
private MockObject $ipAddress;
/**
* @var LeadModel
*/
private MockObject $leadModel;
/**
* @var PageModel
*/
private MockObject $pageModel;
/**
* @var PrimaryCompanyHelper
*/
private MockObject $primaryCompanyHelper;
/**
* @var ContactRequestHelper&MockObject
*/
private MockObject $contactRequestHelper;
protected function setUp(): void
{
$this->request = new Request();
$this->internalContainer = $this->createMock(Container::class);
$this->logger = $this->createMock(\Psr\Log\LoggerInterface::class);
$this->modelFactory = $this->createMock(ModelFactory::class);
$this->redirectModel = $this->createMock(RedirectModel::class);
$this->redirect = $this->createMock(Redirect::class);
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
$this->ipAddress = $this->createMock(IpAddress::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->pageModel = $this->createMock(PageModel::class);
$this->primaryCompanyHelper = $this->createMock(PrimaryCompanyHelper::class);
$this->contactRequestHelper = $this->createMock(ContactRequestHelper::class);
parent::setUp();
}
/**
* Test that the appropriate variant is displayed based on hit counts and variant weights.
*/
public function testVariantPageWeightsAreAppropriate(): void
{
// Each of these should return the one with the greatest weight deficit based on
// A = 50%
// B = 25%
// C = 25%
// A = 0/50; B = 0/25; C = 0/25
$this->assertEquals('pageA', $this->getVariantContent(0, 0, 0));
// A = 100/50; B = 0/25; C = 0/25
$this->assertEquals('pageB', $this->getVariantContent(1, 0, 0));
// A = 50/50; B = 50/25; C = 0/25;
$this->assertEquals('pageC', $this->getVariantContent(1, 1, 0));
// A = 33/50; B = 33/25; C = 33/25;
$this->assertEquals('pageA', $this->getVariantContent(1, 1, 1));
// A = 66/50; B = 33/25; C = 0/25
$this->assertEquals('pageC', $this->getVariantContent(2, 1, 0));
// A = 50/50; B = 25/25; C = 25/25
$this->assertEquals('pageA', $this->getVariantContent(2, 1, 1));
// A = 33/50; B = 66/50; C = 0/25
$this->assertEquals('pageC', $this->getVariantContent(1, 2, 0));
// A = 25/50; B = 50/50; C = 25/25
$this->assertEquals('pageA', $this->getVariantContent(1, 2, 1));
// A = 55/50; B = 18/25; C = 27/25
$this->assertEquals('pageB', $this->getVariantContent(6, 2, 3));
// A = 50/50; B = 25/25; C = 25/25
$this->assertEquals('pageA', $this->getVariantContent(6, 3, 3));
}
/**
* @return string
*/
private function getVariantContent($aCount, $bCount, $cCount)
{
$pageEntityB = $this->createMock(Page::class);
$pageEntityB->method('getId')
->willReturn(2);
$pageEntityB->method('isPublished')
->willReturn(true);
$pageEntityB->method('getVariantHits')
->willReturn($bCount);
$pageEntityB->method('getTranslations')
->willReturn([]);
$pageEntityB->method('isTranslation')
->willReturn(false);
$pageEntityB->method('getContent')
->willReturn(null);
$pageEntityB->method('getCustomHtml')
->willReturn('pageB');
$pageEntityB->method('getVariantSettings')
->willReturn(['weight' => '25']);
$pageEntityC = $this->createMock(Page::class);
$pageEntityC->method('getId')
->willReturn(3);
$pageEntityC->method('isPublished')
->willReturn(true);
$pageEntityC->method('getVariantHits')
->willReturn($cCount);
$pageEntityC->method('getTranslations')
->willReturn([]);
$pageEntityC->method('isTranslation')
->willReturn(false);
$pageEntityC->method('getContent')
->willReturn(null);
$pageEntityC->method('getCustomHtml')
->willReturn('pageC');
$pageEntityC->method('getVariantSettings')
->willReturn(['weight' => '25']);
$pageEntityA = $this->createMock(Page::class);
$pageEntityA->method('getId')
->willReturn(1);
$pageEntityA->method('isPublished')
->willReturn(true);
$pageEntityA->method('getVariants')
->willReturn([$pageEntityA, [2 => $pageEntityB, 3 => $pageEntityC]]);
$pageEntityA->method('getVariantHits')
->willReturn($aCount);
$pageEntityA->method('getTranslations')
->willReturn([]);
$pageEntityA->method('isTranslation')
->willReturn(false);
$pageEntityA->method('getContent')
->willReturn(null);
$pageEntityA->method('getCustomHtml')
->willReturn('pageA');
$pageEntityA->method('getVariantSettings')
->willReturn(['weight' => '50']);
$cookieHelper = $this->createMock(CookieHelper::class);
/** @var Packages&MockObject $packagesMock */
$packagesMock = $this->createMock(Packages::class);
/** @var CoreParametersHelper&MockObject $coreParametersHelper */
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$assetHelper = new AssetsHelper($packagesMock);
$mauticSecurity = $this->createMock(CorePermissions::class);
$mauticSecurity->method('hasEntityAccess')
->willReturn(false);
$analyticsHelper = new AnalyticsHelper($coreParametersHelper);
$pageModel = $this->createMock(PageModel::class);
$pageModel->method('getHitQuery')
->willReturn([]);
$pageModel->method('getEntityBySlugs')
->willReturn($pageEntityA);
$pageModel->method('hitPage')
->willReturn(true);
$this->contactRequestHelper->method('getContactFromQuery')
->willReturn(new Lead());
$router = $this->createMock(Router::class);
$dispatcher = new EventDispatcher();
$modelFactory = $this->createMock(ModelFactory::class);
$modelFactory->method('getModel')
->willReturnMap(
[
['page', $pageModel],
['lead', $this->leadModel],
]
);
$container = $this->createMock(Container::class);
$container->expects(self::never())
->method('has');
$container->expects(self::never())
->method('get');
$this->request->attributes->set('ignore_mismatch', true);
$router = $this->createMock(RouterInterface::class);
$doctrine = $this->createMock(ManagerRegistry::class);
$userHelper = $this->createMock(UserHelper::class);
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$translator = $this->createMock(Translator::class);
$flashBag = $this->createMock(FlashBag::class);
$themeHelper = $this->createMock(ThemeHelper::class);
$themeHelper->expects(self::never())
->method('checkForTwigTemplate');
$requestStack = new RequestStack();
$controller = new PublicController(
$doctrine,
$modelFactory,
$userHelper,
$coreParametersHelper,
$dispatcher,
$translator,
$flashBag,
$requestStack,
$mauticSecurity
);
$controller->setContainer($container);
$response = $controller->indexAction(
$this->request,
$this->contactRequestHelper,
$cookieHelper,
$analyticsHelper,
$assetHelper,
$themeHelper,
$this->createMock(Tracking404Model::class),
$router,
$this->createMock(DeviceTrackingServiceInterface::class),
'/page/a',
);
return $response->getContent();
}
public function testThatInvalidClickTroughGetsProcessed(): void
{
$redirectId = 'someRedirectId';
$clickTrough = 'someClickTroughValue';
$redirectUrl = 'https://someurl.test/';
$this->redirectModel->expects(self::once())
->method('getRedirectById')
->with($redirectId)
->willReturn($this->redirect);
$matcher = self::exactly(2);
$this->modelFactory->expects($matcher)
->method('getModel')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame(RedirectModel::class, $parameters[0]);
return $this->redirectModel;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame(PageModel::class, $parameters[0]);
return $this->pageModel;
}
self::fail('The index '.$matcher->numberOfInvocations().' is not set.');
});
$this->redirect->expects(self::once())
->method('isPublished')
->with(false)
->willReturn(true);
$this->redirect->expects(self::once())
->method('getUrl')
->willReturn($redirectUrl);
$this->ipLookupHelper->expects(self::once())
->method('getIpAddress')
->willReturn($this->ipAddress);
$this->ipAddress->expects(self::once())
->method('isTrackable')
->willReturn(true);
$getContactFromRequestCallback = function ($queryFields) use ($clickTrough) {
if (empty($queryFields)) {
return null;
}
throw new InvalidDecodedStringException($clickTrough);
};
$this->contactRequestHelper->expects(self::exactly(2))
->method('getContactFromQuery')
->willReturnCallback($getContactFromRequestCallback);
$routerMock = $this->createMock(Router::class);
$routerMock->expects(self::once())
->method('generate')
->willReturn('/asset/');
$this->internalContainer
->expects(self::once())
->method('get')
->willReturnMap([
['router', Container::EXCEPTION_ON_INVALID_REFERENCE, $routerMock],
]);
$this->request->query->set('ct', $clickTrough);
$doctrine = $this->createMock(ManagerRegistry::class);
$userHelper = $this->createMock(UserHelper::class);
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$translator = $this->createMock(Translator::class);
$flashBag = $this->createMock(FlashBag::class);
$requestStack = new RequestStack();
$mauticSecurity = $this->createMock(CorePermissions::class);
$controller = new PublicController(
$doctrine,
$this->modelFactory,
$userHelper,
$coreParametersHelper,
$dispatcher,
$translator,
$flashBag,
$requestStack,
$mauticSecurity
);
$controller->setContainer($this->internalContainer);
$response = $controller->redirectAction(
$this->request,
$this->contactRequestHelper,
$this->primaryCompanyHelper,
$this->ipLookupHelper,
$this->logger,
$redirectId
);
self::assertSame('https://someurl.test/', $response->getTargetUrl());
}
/**
* @throws \Exception
*/
#[DataProvider('provideRedirectUrls')]
public function testAssetRedirectUrlWithClickThrough(string $redirectUrl, string $targetUrl): void
{
$redirectId = 'dummy_redirect_id';
$clickThrough = 'dummy_click_through';
$this->redirectModel->expects(self::once())
->method('getRedirectById')
->with($redirectId)
->willReturn($this->redirect);
$matcher = self::exactly(2);
$this->modelFactory->expects($matcher)
->method('getModel')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame(RedirectModel::class, $parameters[0]);
return $this->redirectModel;
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame(PageModel::class, $parameters[0]);
return $this->pageModel;
}
self::fail('Unknown invocation.');
});
$this->redirect->expects(self::once())
->method('isPublished')
->with(false)
->willReturn(true);
$this->redirect->expects(self::once())
->method('getUrl')
->willReturn($redirectUrl);
$this->ipLookupHelper->expects(self::once())
->method('getIpAddress')
->willReturn($this->ipAddress);
$this->ipAddress->expects(self::once())
->method('isTrackable')
->willReturn(true);
$getContactFromRequestCallback = function ($queryFields) use ($clickThrough) {
if (empty($queryFields)) {
return null;
}
throw new InvalidDecodedStringException($clickThrough);
};
$this->contactRequestHelper->expects(self::exactly(2))
->method('getContactFromQuery')
->willReturnCallback($getContactFromRequestCallback);
$routerMock = $this->createMock(Router::class);
$routerMock->expects(self::once())
->method('generate')
->with('mautic_asset_download')
->willReturn('/asset');
$this->internalContainer
->expects(self::once())
->method('get')
->willReturnMap([
['router', Container::EXCEPTION_ON_INVALID_REFERENCE, $routerMock],
]);
$this->request->query->set('ct', $clickThrough);
$doctrine = $this->createMock(ManagerRegistry::class);
$userHelper = $this->createMock(UserHelper::class);
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$translator = $this->createMock(Translator::class);
$flashBag = $this->createMock(FlashBag::class);
$requestStack = new RequestStack();
$mauticSecurity = $this->createMock(CorePermissions::class);
$controller = new PublicController(
$doctrine,
$this->modelFactory,
$userHelper,
$coreParametersHelper,
$dispatcher,
$translator,
$flashBag,
$requestStack,
$mauticSecurity
);
$controller->setContainer($this->internalContainer);
$response = $controller->redirectAction(
$this->request,
$this->contactRequestHelper,
$this->primaryCompanyHelper,
$this->ipLookupHelper,
$this->logger,
$redirectId
);
self::assertSame($targetUrl, $response->getTargetUrl());
self::assertSame(Response::HTTP_FOUND, $response->getStatusCode());
}
public static function provideRedirectUrls(): \Generator
{
yield 'No query parameters' => [
'https://some.test.url/asset/1:examplefilejpg',
'https://some.test.url/asset/1:examplefilejpg?ct=dummy_click_through',
];
yield 'With query parameter' => [
'https://some.test.url/asset/1:examplefilejpg?param=value',
'https://some.test.url/asset/1:examplefilejpg?param=value&ct=dummy_click_through',
];
yield 'With click-through parameter' => [
'https://some.test.url/asset/1:examplefilejpg?ct=parameter',
'https://some.test.url/asset/1:examplefilejpg?ct=dummy_click_through',
];
}
/**
* @throws \Exception
*/
public function testMtcTrackingEvent(): void
{
$request = new Request(
[
'foo' => 'bar',
]
);
$contact = new Lead();
$contact->setEmail('foo@bar.com');
$mtcSessionEventArray = ['mtc' => 'foobar'];
$event = new TrackingEvent($contact, $request, $mtcSessionEventArray);
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->once())
->method('dispatch')
->with($event, PageEvents::ON_CONTACT_TRACKED)
->willReturnCallback(
function (TrackingEvent $event) {
$contact = $event->getContact()->getEmail();
$request = $event->getRequest();
$response = $event->getResponse();
$response->set('tracking', $contact);
$response->set('foo', $request->get('foo'));
return $event;
}
);
$security = $this->createMock(CorePermissions::class);
$security->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$pageModel = $this->createMock(PageModel::class);
$modelFactory = $this->createMock(ModelFactory::class);
$modelFactory->expects($this->once())
->method('getModel')
->with('page')
->willReturn($pageModel);
$deviceTrackingService = $this->createMock(DeviceTrackingServiceInterface::class);
$trackingHelper = $this->createMock(TrackingHelper::class);
$trackingHelper->expects($this->once())
->method('getCacheItem')
->willReturn($mtcSessionEventArray);
$contactTracker = $this->createMock(ContactTracker::class);
$contactTracker->method('getContact')
->willReturn($contact);
$doctrine = $this->createMock(ManagerRegistry::class);
$userHelper = $this->createMock(UserHelper::class);
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$translator = $this->createMock(Translator::class);
$flashBag = $this->createMock(FlashBag::class);
$requestStack = new RequestStack();
$publicController = new PublicController(
$doctrine,
$modelFactory,
$userHelper,
$coreParametersHelper,
$eventDispatcher,
$translator,
$flashBag,
$requestStack,
$security
);
$response = $publicController->trackingAction(
$request,
$deviceTrackingService,
$trackingHelper,
$contactTracker
);
$json = json_decode($response->getContent(), true);
$this->assertEquals(
[
'mtc' => 'foobar',
'tracking' => 'foo@bar.com',
'foo' => 'bar',
],
$json['events']
);
}
public function testTrackingActionWithInvalidCt(): void
{
$request = new Request();
$pageModel = $this->createMock(PageModel::class);
$pageModel->expects($this->once())->method('hitPage')->willReturnCallback(
function (): void {
throw new InvalidDecodedStringException();
}
);
$modelFactory = $this->createMock(ModelFactory::class);
$modelFactory->expects($this->once())
->method('getModel')
->with('page')
->willReturn($pageModel);
$security = $this->createMock(CorePermissions::class);
$security->expects($this->once())
->method('isAnonymous')
->willReturn(true);
$doctrine = $this->createMock(ManagerRegistry::class);
$userHelper = $this->createMock(UserHelper::class);
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$translator = $this->createMock(Translator::class);
$flashBag = $this->createMock(FlashBag::class);
$requestStack = new RequestStack();
$publicController = new PublicController(
$doctrine,
$modelFactory,
$userHelper,
$coreParametersHelper,
$dispatcher,
$translator,
$flashBag,
$requestStack,
$security
);
$response = $publicController->trackingAction(
$request,
$this->createMock(DeviceTrackingServiceInterface::class),
$this->createMock(TrackingHelper::class),
$this->createMock(ContactTracker::class)
);
$this->assertEquals(
['success' => 0],
json_decode($response->getContent(), true)
);
}
public function testTrackingImageAction(): void
{
$this->client->request('GET', '/mtracking.gif?url=http%3A%2F%2Fmautic.org');
$this->assertResponseStatusCodeSame(200);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\HitRepository;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class VisitPageWitIpAnonymizationOffFunctionalTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->configParams['anonymize_ip'] = false;
parent::setUp();
}
public function testPageWithIpAnonymizationOff(): void
{
// create landing page
$pageObject = new Page();
$pageObject->setIsPublished(true);
$pageObject->setDateAdded(new \DateTime());
$pageObject->setTitle('Page:Page:Anonymization:Off');
$pageObject->setAlias('page-page-anonymizaiton-off');
$pageObject->setTemplate('Blank');
$pageObject->setCustomHtml('Test Html');
$pageObject->setLanguage('en');
$this->em->persist($pageObject);
$this->em->flush();
$this->logoutUser();
$pageContent = $this->client->request(Request::METHOD_GET, '/page-page-anonymizaiton-off');
Assert::assertTrue($this->client->getResponse()->isOk(), $pageContent->text());
Assert::assertStringContainsString('Test Html', $pageContent->text());
/** @var HitRepository $hitRepository */
$hitRepository = $this->em->getRepository(Hit::class);
/** @var Hit[] $hits */
$hits = $hitRepository->findBy(['page' => $pageObject->getId()]);
Assert::assertCount(1, $hits);
Assert::assertSame('127.0.0.1', $hits[0]->getIpAddress()->getIpAddress());
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Mautic\PageBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PageBundle\Entity\Hit;
use Mautic\PageBundle\Entity\HitRepository;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class VisitPageWitIpAnonymizationOnFunctionalTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->configParams['anonymize_ip'] = true;
parent::setUp();
}
public function testPageWithIpAnonymizationOn(): void
{
// create landing page
$pageObject = new Page();
$pageObject->setIsPublished(true);
$pageObject->setDateAdded(new \DateTime());
$pageObject->setTitle('Page:Page:Anonymization:On');
$pageObject->setAlias('page-page-anonymizaiton-on');
$pageObject->setTemplate('Blank');
$pageObject->setCustomHtml('Test Html');
$pageObject->setLanguage('en');
$this->em->persist($pageObject);
$this->em->flush();
$this->logoutUser();
$pageContent = $this->client->request(Request::METHOD_GET, '/page-page-anonymizaiton-on');
Assert::assertTrue($this->client->getResponse()->isOk(), $pageContent->text());
Assert::assertStringContainsString('Test Html', $pageContent->text());
/** @var HitRepository $hitRepository */
$hitRepository = $this->em->getRepository(Hit::class);
/** @var Hit[] $hits */
$hits = $hitRepository->findBy(['page' => $pageObject->getId()]);
Assert::assertCount(1, $hits);
Assert::assertSame('*.*.*.*', $hits[0]->getIpAddress()->getIpAddress());
}
}