Skip to content

Commit 850f5a6

Browse files
mateuszdebinskiMateusz Dębiński
andauthored
IBX-10425: Translated string is returned as XML (#29)
* IBX-10425: Translated string is returned as XML * Corrected phpstan for 7.4 and higher * Corrected CS * Added tests * Corrected CS * Corrected CS * Registered TextFieldCdataCleaner as service --------- Co-authored-by: Mateusz Dębiński <mateusz.debinski@ibexa.co>
1 parent 32d7ef4 commit 850f5a6

7 files changed

Lines changed: 217 additions & 49 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"require": {
3333
"php": "^7.4 || ^8.0",
3434
"ext-libxml": "*",
35+
"ext-dom": "*",
3536
"symfony/yaml": "^5.0",
3637
"symfony/routing": "^5.0",
3738
"symfony/console": "^5.0",

src/bundle/Resources/config/services.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ services:
6262
- { name: form.type_extension, extended_type: Ibexa\AdminUi\Form\Type\Language\LanguageCreateType }
6363

6464
Ibexa\Bundle\AutomatedTranslation\EventListener\ContentProxyTranslateListener: ~
65+
66+
Ibexa\AutomatedTranslation\TextFieldCdataCleaner: ~

src/lib/Encoder.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,18 @@ class Encoder
7979

8080
private FieldEncoderManager $fieldEncoderManager;
8181

82+
private TextFieldCdataCleaner $textFieldCdataCleaner;
83+
8284
public function __construct(
8385
ContentTypeService $contentTypeService,
8486
EventDispatcherInterface $eventDispatcher,
85-
FieldEncoderManager $fieldEncoderManager
87+
FieldEncoderManager $fieldEncoderManager,
88+
TextFieldCdataCleaner $textFieldCdataCleaner
8689
) {
8790
$this->contentTypeService = $contentTypeService;
8891
$this->eventDispatcher = $eventDispatcher;
8992
$this->fieldEncoderManager = $fieldEncoderManager;
93+
$this->textFieldCdataCleaner = $textFieldCdataCleaner;
9094
}
9195

9296
public function encode(Content $content): string
@@ -118,6 +122,8 @@ public function encode(Content $content): string
118122

119123
$encoder = new XmlEncoder();
120124
$payload = $encoder->encode($results, XmlEncoder::FORMAT);
125+
$payload = $this->textFieldCdataCleaner->clear($payload);
126+
121127
// here Encoder has decorated with CDATA, we don't want the CDATA
122128
return str_replace(
123129
['<![CDATA[', ']]>'],

src/lib/Encoder/Field/PageBuilderFieldEncoder.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use Ibexa\AutomatedTranslation\Encoder\BlockAttribute\BlockAttributeEncoderManager;
1212
use Ibexa\AutomatedTranslation\Exception\EmptyTranslatedAttributeException;
13+
use Ibexa\AutomatedTranslation\TextFieldCdataCleaner;
1314
use Ibexa\Contracts\AutomatedTranslation\Encoder\Field\FieldEncoderInterface;
1415
use Ibexa\Contracts\Core\Repository\Values\Content\Field;
1516
use Ibexa\Core\FieldType\Value as APIValue;
@@ -26,12 +27,16 @@ final class PageBuilderFieldEncoder implements FieldEncoderInterface
2627

2728
private BlockDefinitionFactoryInterface $blockDefinitionFactory;
2829

30+
private TextFieldCdataCleaner $textFieldCdataCleaner;
31+
2932
public function __construct(
3033
BlockAttributeEncoderManager $blockAttributeEncoderManager,
31-
BlockDefinitionFactoryInterface $blockDefinitionFactory
34+
BlockDefinitionFactoryInterface $blockDefinitionFactory,
35+
TextFieldCdataCleaner $textFieldCdataCleaner
3236
) {
3337
$this->blockAttributeEncoderManager = $blockAttributeEncoderManager;
3438
$this->blockDefinitionFactory = $blockDefinitionFactory;
39+
$this->textFieldCdataCleaner = $textFieldCdataCleaner;
3540
}
3641

3742
public function canEncode(Field $field): bool
@@ -85,12 +90,11 @@ public function encode(Field $field): string
8590
$payload = $encoder->encode($blocks, XmlEncoder::FORMAT, [
8691
XmlEncoder::ROOT_NODE_NAME => 'blocks',
8792
]);
88-
89-
$payload = str_replace('<?xml version="1.0"?>' . "\n", '', $payload);
93+
$payload = $this->textFieldCdataCleaner->clear($payload);
9094

9195
$payload = str_replace(
92-
['<![CDATA[', ']]>'],
93-
['<' . self::CDATA_FAKER_TAG . '>', '</' . self::CDATA_FAKER_TAG . '>'],
96+
['<?xml version="1.0"?>' . "\n", '<![CDATA[', ']]>'],
97+
['', '<' . self::CDATA_FAKER_TAG . '>', '</' . self::CDATA_FAKER_TAG . '>'],
9498
$payload
9599
);
96100

src/lib/TextFieldCdataCleaner.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\AutomatedTranslation;
10+
11+
use DOMCdataSection;
12+
use DOMDocument;
13+
use DOMElement;
14+
use DOMNode;
15+
use DOMXPath;
16+
use Ibexa\FieldTypeRichText\FieldType\RichText\Value as RichTextValue;
17+
use RuntimeException;
18+
19+
final class TextFieldCdataCleaner
20+
{
21+
public function clear(string $payload): string
22+
{
23+
$dom = $this->loadDocument($payload);
24+
$this->processCdataNodes($dom);
25+
26+
return $this->saveDocument($dom);
27+
}
28+
29+
private function loadDocument(string $payload): DOMDocument
30+
{
31+
$dom = new DOMDocument();
32+
$dom->loadXML($payload);
33+
34+
return $dom;
35+
}
36+
37+
private function processCdataNodes(DOMDocument $dom): void
38+
{
39+
$xpath = new DOMXPath($dom);
40+
$textNodes = $xpath->query('//text()');
41+
42+
if ($textNodes === false) {
43+
return;
44+
}
45+
46+
foreach ($textNodes as $textNode) {
47+
if (!$textNode instanceof DOMCdataSection) {
48+
continue;
49+
}
50+
51+
if ($this->shouldReplaceCdata($textNode)) {
52+
$this->replaceWithTextNode($dom, $textNode);
53+
}
54+
}
55+
}
56+
57+
private function shouldReplaceCdata(DOMNode $node): bool
58+
{
59+
$parent = $node->parentNode;
60+
if (!$parent instanceof DOMElement) {
61+
return false;
62+
}
63+
64+
return $parent->getAttribute('type') !== RichTextValue::class;
65+
}
66+
67+
private function replaceWithTextNode(DOMDocument $dom, DOMCdataSection $cdataNode): void
68+
{
69+
$newText = $dom->createTextNode($cdataNode->data);
70+
71+
if ($cdataNode->parentNode !== null) {
72+
$cdataNode->parentNode->replaceChild($newText, $cdataNode);
73+
}
74+
}
75+
76+
private function saveDocument(DOMDocument $dom): string
77+
{
78+
$result = $dom->saveXML();
79+
80+
if ($result === false) {
81+
throw new RuntimeException('Saving XML failed after removing CDATA.');
82+
}
83+
84+
return $result;
85+
}
86+
}

tests/lib/Encoder/Field/PageBuilderFieldEncoderTest.php

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use Ibexa\AutomatedTranslation\Encoder\BlockAttribute\BlockAttributeEncoderManager;
1212
use Ibexa\AutomatedTranslation\Encoder\Field\PageBuilderFieldEncoder;
13+
use Ibexa\AutomatedTranslation\TextFieldCdataCleaner;
1314
use Ibexa\Contracts\Core\Repository\Values\Content\Field;
1415
use Ibexa\Contracts\FieldTypePage\FieldType\LandingPage\Model\Attribute;
1516
use Ibexa\Contracts\FieldTypePage\FieldType\LandingPage\Model\BlockValue;
@@ -24,6 +25,7 @@
2425
final class PageBuilderFieldEncoderTest extends TestCase
2526
{
2627
public const ATTRIBUTE_VALUE = 'ibexa';
28+
public const ATTRIBUTE_VALUE_CDATA = 'ibexa & ibexa';
2729

2830
/** @var \Ibexa\AutomatedTranslation\Encoder\BlockAttribute\BlockAttributeEncoderManager&\PHPUnit\Framework\MockObject\MockObject */
2931
private BlockAttributeEncoderManager $blockAttributeEncoderManagerMock;
@@ -52,7 +54,8 @@ public function testEncode(): void
5254
$field = $this->getLandingPageField();
5355
$subject = new PageBuilderFieldEncoder(
5456
$this->blockAttributeEncoderManagerMock,
55-
$this->blockDefinitionFactoryMock
57+
$this->blockDefinitionFactoryMock,
58+
new TextFieldCdataCleaner()
5659
);
5760

5861
$result = $subject->encode($field);
@@ -75,7 +78,8 @@ public function testEncodeMissingAttribute(): void
7578
$field = $this->getLandingPageField();
7679
$subject = new PageBuilderFieldEncoder(
7780
$this->blockAttributeEncoderManagerMock,
78-
$this->blockDefinitionFactoryMock
81+
$this->blockDefinitionFactoryMock,
82+
new TextFieldCdataCleaner()
7983
);
8084

8185
$result = $subject->encode($field);
@@ -85,12 +89,38 @@ public function testEncodeMissingAttribute(): void
8589
self::assertEquals($expectedResult, $result);
8690
}
8791

92+
public function testEncodeCDATAInTextField(): void
93+
{
94+
$this->blockDefinitionFactoryMock
95+
->method('getBlockDefinition')
96+
->withAnyParameters()
97+
->willReturn($this->getBlockDefinition());
98+
99+
$this->blockAttributeEncoderManagerMock
100+
->method('encode')
101+
->withAnyParameters()
102+
->willReturn(self::ATTRIBUTE_VALUE_CDATA);
103+
104+
$field = $this->getLandingPageField();
105+
$subject = new PageBuilderFieldEncoder(
106+
$this->blockAttributeEncoderManagerMock,
107+
$this->blockDefinitionFactoryMock,
108+
new TextFieldCdataCleaner()
109+
);
110+
111+
$result = $subject->encode($field);
112+
$encodedValue = htmlspecialchars(self::ATTRIBUTE_VALUE_CDATA);
113+
114+
self::assertEquals($this->getEncodeResult($encodedValue), $result);
115+
}
116+
88117
public function testCanEncode(): void
89118
{
90119
$field = $this->getLandingPageField();
91120
$subject = new PageBuilderFieldEncoder(
92121
$this->blockAttributeEncoderManagerMock,
93-
$this->blockDefinitionFactoryMock
122+
$this->blockDefinitionFactoryMock,
123+
new TextFieldCdataCleaner()
94124
);
95125

96126
self::assertTrue($subject->canEncode($field));
@@ -107,7 +137,8 @@ public function testDecode(): void
107137
$field = $this->getLandingPageField();
108138
$subject = new PageBuilderFieldEncoder(
109139
$this->blockAttributeEncoderManagerMock,
110-
$this->blockDefinitionFactoryMock
140+
$this->blockDefinitionFactoryMock,
141+
new TextFieldCdataCleaner()
111142
);
112143

113144
$result = $subject->decode(
@@ -124,7 +155,8 @@ public function testCanDecode(): void
124155
$field = $this->getLandingPageField();
125156
$subject = new PageBuilderFieldEncoder(
126157
$this->blockAttributeEncoderManagerMock,
127-
$this->blockDefinitionFactoryMock
158+
$this->blockDefinitionFactoryMock,
159+
new TextFieldCdataCleaner()
128160
);
129161

130162
self::assertTrue($subject->canDecode(get_class($field->value)));
@@ -202,10 +234,10 @@ private function createBlockDefinition(): BlockDefinition
202234
return $blockDefinition;
203235
}
204236

205-
private function getEncodeResult(): string
237+
private function getEncodeResult(string $value = self::ATTRIBUTE_VALUE): string
206238
{
207239
return '<blocks><item key="1"><name>Code</name><attributes><content type="string">' .
208-
self::ATTRIBUTE_VALUE . '</content></attributes></item></blocks>
240+
$value . '</content></attributes></item></blocks>
209241
';
210242
}
211243
}

0 commit comments

Comments
 (0)