Skip to content

Commit 1c31b30

Browse files
committed
Merge branch '5.0' into 6.0
2 parents e39f896 + 758f13a commit 1c31b30

7 files changed

Lines changed: 155 additions & 35 deletions

File tree

phpstan-baseline.neon

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6954,18 +6954,6 @@ parameters:
69546954
count: 1
69556955
path: src/lib/FieldType/Selection/Type.php
69566956

6957-
-
6958-
message: '#^Parameter \#1 \$string of function mb_substr expects string, string\|false given\.$#'
6959-
identifier: argument.type
6960-
count: 1
6961-
path: src/lib/FieldType/TextBlock/SearchField.php
6962-
6963-
-
6964-
message: '#^Parameter \#1 \$string of method Ibexa\\Core\\FieldType\\TextBlock\\SearchField\:\:extractShortText\(\) expects string, array\|bool\|float\|int\|string\|null given\.$#'
6965-
identifier: argument.type
6966-
count: 1
6967-
path: src/lib/FieldType/TextBlock/SearchField.php
6968-
69696957
-
69706958
message: '#^Access to an undefined property Ibexa\\Contracts\\Core\\FieldType\\Value\:\:\$time\.$#'
69716959
identifier: property.notFound
@@ -18426,12 +18414,6 @@ parameters:
1842618414
count: 1
1842718415
path: src/lib/Search/Common/FieldValueMapper/MultipleStringMapper.php
1842818416

18429-
-
18430-
message: '#^Method Ibexa\\Core\\Search\\Common\\FieldValueMapper\\StringMapper\:\:convert\(\) should return string but returns string\|null\.$#'
18431-
identifier: return.type
18432-
count: 1
18433-
path: src/lib/Search/Common/FieldValueMapper/StringMapper.php
18434-
1843518417
-
1843618418
message: '#^Method Ibexa\\Core\\Search\\Common\\IncrementalIndexer\:\:purge\(\) has no return type specified\.$#'
1843718419
identifier: missingType.return

src/lib/FieldType/TextBlock/SearchField.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition)
2222
return [
2323
new Search\Field(
2424
'value',
25-
$this->extractShortText($field->value->data),
25+
$this->extractText($field->value->data),
2626
new Search\FieldType\StringField()
2727
),
2828
new Search\Field(
@@ -33,16 +33,15 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition)
3333
];
3434
}
3535

36-
/**
37-
* Extracts short snippet of the given $string.
38-
*
39-
* @param string $string
40-
*
41-
* @return string
42-
*/
43-
private function extractShortText($string): string
36+
private function extractText(mixed $string): string
4437
{
45-
return mb_substr(strtok(trim((string)$string), "\r\n"), 0, 255);
38+
if (!is_string($string)) {
39+
return '';
40+
}
41+
42+
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $string));
43+
44+
return implode(' ', array_map('trim', $lines));
4645
}
4746

4847
public function getIndexDefinition()

src/lib/Resources/settings/search_engines/field_value_mappers.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ services:
6262
- { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\PriceField }
6363

6464
Ibexa\Core\Search\Common\FieldValueMapper\StringMapper:
65+
autoconfigure: true
6566
class: Ibexa\Core\Search\Common\FieldValueMapper\StringMapper
6667
tags:
6768
- { name: ibexa.search.common.field_value.mapper, maps: Ibexa\Contracts\Core\Search\FieldType\StringField }

src/lib/Search/Common/FieldValueMapper/StringMapper.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,27 @@
1010
use Ibexa\Contracts\Core\Search\Field;
1111
use Ibexa\Contracts\Core\Search\FieldType;
1212
use Ibexa\Core\Search\Common\FieldValueMapper;
13+
use Psr\Log\LoggerAwareInterface;
14+
use Psr\Log\LoggerAwareTrait;
15+
use Psr\Log\LoggerInterface;
16+
use Psr\Log\NullLogger;
1317

1418
/**
1519
* Common string field value mapper implementation.
1620
*/
17-
class StringMapper extends FieldValueMapper
21+
class StringMapper extends FieldValueMapper implements LoggerAwareInterface
1822
{
23+
use LoggerAwareTrait;
24+
25+
public function __construct(
26+
?LoggerInterface $logger = null
27+
) {
28+
$this->logger = $logger ?? new NullLogger();
29+
}
30+
1931
public const REPLACE_WITH_SPACE_PATTERN = '([\x09\x0B\x0C]+)';
2032
public const REMOVE_PATTERN = '([\x00-\x08\x0E-\x1F]+)';
33+
public const MAX_TERM_LENGTH = 32766;
2134

2235
public function canMap(Field $field): bool
2336
{
@@ -44,10 +57,27 @@ protected function convert($value): string
4457
);
4558

4659
// Remove non-printable characters.
47-
return preg_replace(
60+
$value = preg_replace(
4861
self::REMOVE_PATTERN,
4962
'',
5063
(string)$value
5164
);
65+
66+
// Enforce Lucene's bytes MAX_TERM_LENGTH to avoid silent indexing failures
67+
$value = (string)$value;
68+
$truncated = mb_strcut($value, 0, self::MAX_TERM_LENGTH);
69+
70+
if (strlen($truncated) < strlen($value)) {
71+
$this->logger->warning(
72+
sprintf(
73+
'String field value was truncated from %d to %d bytes (max term length: %d).',
74+
strlen($value),
75+
strlen($truncated),
76+
self::MAX_TERM_LENGTH
77+
)
78+
);
79+
}
80+
81+
return $truncated;
5282
}
5383
}

tests/integration/Core/Repository/FieldType/TextBlockIntegrationTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,24 +287,24 @@ public function providerForTestIsNotEmptyValue()
287287

288288
protected function getValidSearchValueOne(): string
289289
{
290-
return 'caution is the " path to mediocrity' . PHP_EOL . 'something completely different';
290+
return 'caution is the " path to mediocrity something completely different';
291291
}
292292

293293
protected function getSearchTargetValueOne(): string
294294
{
295295
// ensure case-insensitivity
296-
return strtoupper('caution is the " path to mediocrity');
296+
return strtoupper('caution is the " path to mediocrity something completely different');
297297
}
298298

299299
protected function getValidSearchValueTwo(): string
300300
{
301-
return "truth suffers from ' too much analysis\n hello and goodbye";
301+
return "truth suffers from ' too much analysis hello and goodbye";
302302
}
303303

304304
protected function getSearchTargetValueTwo(): string
305305
{
306306
// ensure case-insensitivity
307-
return strtoupper("truth suffers from ' too much analysis");
307+
return strtoupper("truth suffers from ' too much analysis hello and goodbye");
308308
}
309309

310310
protected function getFullTextIndexedFieldData()

tests/lib/Search/Common/FieldValueMapper/RemoteIdentifierMapperTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Ibexa\Contracts\Core\Search\FieldType\StringField;
1616
use Ibexa\Core\Search\Common\FieldValueMapper\RemoteIdentifierMapper;
1717
use Ibexa\Tests\Core\Search\TestCase;
18+
use Psr\Log\LoggerInterface;
1819

1920
/**
2021
* @covers \Ibexa\Core\Search\Common\FieldValueMapper\RemoteIdentifierMapper
@@ -26,7 +27,9 @@ final class RemoteIdentifierMapperTest extends TestCase
2627

2728
protected function setUp(): void
2829
{
29-
$this->mapper = new RemoteIdentifierMapper();
30+
$this->mapper = new RemoteIdentifierMapper(
31+
$this->createMock(LoggerInterface::class)
32+
);
3033
}
3134

3235
/**
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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\Tests\Core\Search\Common\FieldValueMapper;
10+
11+
use Ibexa\Contracts\Core\Search\Field;
12+
use Ibexa\Contracts\Core\Search\FieldType\StringField;
13+
use Ibexa\Core\Search\Common\FieldValueMapper\StringMapper;
14+
use PHPUnit\Framework\TestCase;
15+
16+
/**
17+
* @covers \Ibexa\Core\Search\Common\FieldValueMapper\StringMapper
18+
*/
19+
final class StringMapperTest extends TestCase
20+
{
21+
private StringMapper $mapper;
22+
23+
protected function setUp(): void
24+
{
25+
$this->mapper = new StringMapper();
26+
}
27+
28+
public function testCanMap(): void
29+
{
30+
$field = $this->createFieldWithValue('hello', new StringField());
31+
32+
self::assertTrue($this->mapper->canMap($field));
33+
}
34+
35+
public function testMapsPlainString(): void
36+
{
37+
$field = $this->createFieldWithValue('hello world', new StringField());
38+
39+
self::assertSame('hello world', $this->mapper->map($field));
40+
}
41+
42+
public function testStripsNonPrintableCharacters(): void
43+
{
44+
$field = $this->createFieldWithValue("hello\x01\x02world", new StringField());
45+
46+
self::assertSame('helloworld', $this->mapper->map($field));
47+
}
48+
49+
public function testReplacesTabAndVerticalWhitespaceWithSpace(): void
50+
{
51+
$field = $this->createFieldWithValue("hello\x09world\x0Bfoo", new StringField());
52+
53+
self::assertSame('hello world foo', $this->mapper->map($field));
54+
}
55+
56+
public function testTruncatesToMaxTermLength(): void
57+
{
58+
$longValue = str_repeat('a', StringMapper::MAX_TERM_LENGTH + 100);
59+
$field = $this->createFieldWithValue($longValue, new StringField());
60+
$result = $this->mapper->map($field);
61+
62+
self::assertSame(StringMapper::MAX_TERM_LENGTH, strlen($result));
63+
}
64+
65+
public function testTruncatesMultibyteStringAtCharacterBoundary(): void
66+
{
67+
// Each UTF-8 character here is 3 bytes (€ = E2 82 AC).
68+
// Fill up to just past the limit so truncation must happen on a char boundary.
69+
$char = '';
70+
$charBytes = strlen($char);
71+
72+
self::assertSame(3, $charBytes);
73+
74+
$count = (int) ceil((StringMapper::MAX_TERM_LENGTH + $charBytes) / $charBytes);
75+
$longValue = str_repeat($char, $count);
76+
77+
$field = $this->createFieldWithValue($longValue, new StringField());
78+
$result = $this->mapper->map($field);
79+
80+
self::assertSame(StringMapper::MAX_TERM_LENGTH, strlen($result));
81+
// Result must be valid UTF-8 (no split mid-character).
82+
self::assertSame(1, preg_match('//u', $result));
83+
}
84+
85+
public function testValueWithinLimitIsNotTruncated(): void
86+
{
87+
$value = str_repeat('a', StringMapper::MAX_TERM_LENGTH);
88+
$field = $this->createFieldWithValue($value, new StringField());
89+
90+
self::assertSame($value, $this->mapper->map($field));
91+
}
92+
93+
private function createFieldWithValue(string $value, StringField $type): Field
94+
{
95+
$field = $this->createMock(Field::class);
96+
$field
97+
->method('getValue')
98+
->willReturn($value);
99+
$field
100+
->method('getType')
101+
->willReturn($type);
102+
103+
return $field;
104+
}
105+
}

0 commit comments

Comments
 (0)