Skip to content

Commit fced056

Browse files
kevin-appeltbmack
authored andcommitted
[TASK] Dynamically limit livesearch to searchFields used in subtypes
Previously, the bodytext field, for example, was only searched if the CType was text, textpic or textmedia. This condition is unknown to practically all developers which has been spoken to. The only exception so far seems to be the `EXT:mask` extension, which already dynamically extends this condition. It was introduced with TYPO3 v4.6 with #26829 and was pretty much untouched ever since. With the introduction of the TCA Schema API, we can handle the issue much more generically for all tables with subtypes. We can thus make the search much more precise and only search in fields that actually exist in the respective subtype like the CType for example. The purpose of this search restriction is to ensure that, for example, the content in the bodytext field is no longer found if it was filled as a text element and then changed to a plugin. Resolves: #104539 Related: #26829 Releases: main, 13.4 Change-Id: I490a0b25568ae1919c825553215123d73a959b44 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/85489 Tested-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: core-ci <typo3@b13.com>
1 parent 2047d9d commit fced056

File tree

4 files changed

+259
-17
lines changed

4 files changed

+259
-17
lines changed

typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent;
3131
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
3232
use TYPO3\CMS\Backend\Routing\UriBuilder;
33+
use TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider;
3334
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
3435
use TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton;
3536
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
@@ -56,6 +57,7 @@
5657
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
5758
use TYPO3\CMS\Core\Schema\Field\NumberFieldType;
5859
use TYPO3\CMS\Core\Schema\SearchableSchemaFieldsCollector;
60+
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
5961
use TYPO3\CMS\Core\Service\DependencyOrderingService;
6062
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
6163
use TYPO3\CMS\Core\Type\Bitmask\Permission;
@@ -412,6 +414,7 @@ public function __construct(
412414
protected readonly BackendViewFactory $backendViewFactory,
413415
protected readonly ModuleProvider $moduleProvider,
414416
protected readonly SearchableSchemaFieldsCollector $searchableSchemaFieldsCollector,
417+
protected readonly TcaSchemaFactory $tcaSchemaFactory,
415418
) {
416419
$this->calcPerms = new Permission();
417420
$this->spaceIcon = '<span class="btn btn-default disabled" aria-hidden="true">' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '</span>';
@@ -2599,37 +2602,59 @@ protected function makeSearchString(string $table, int $currentPid, QueryBuilder
25992602
}
26002603

26012604
$searchableFields = $this->searchableSchemaFieldsCollector->getFields($table);
2605+
[$subSchemaDivisorFieldName, $fieldsSubSchemaTypes] = $this->getSchemaFieldSubSchemaTypes($table);
26022606
// Get fields from ctrl section of TCA first
26032607
if (MathUtility::canBeInterpretedAsInteger($this->searchString)) {
26042608
$constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString);
26052609
foreach ($searchableFields as $field) {
26062610
$fieldConfig = $field->getConfiguration();
2611+
$searchConstraint = null;
26072612
if ($field instanceof NumberFieldType || $field instanceof DateTimeFieldType) {
26082613
if (!isset($fieldConfig['search']['pidonly'])
26092614
|| ($fieldConfig['search']['pidonly'] && $currentPid > 0)
26102615
) {
2611-
$constraints[] = $expressionBuilder->and(
2616+
$searchConstraint = $expressionBuilder->and(
26122617
$expressionBuilder->eq($field->getName(), (int)$this->searchString),
26132618
$expressionBuilder->eq($tablePidField, $currentPid)
26142619
);
2620+
} else {
2621+
continue;
26152622
}
26162623
} else {
2617-
$constraints[] = $expressionBuilder->like(
2624+
$searchConstraint = $expressionBuilder->like(
26182625
$field->getName(),
26192626
$queryBuilder->quote('%' . $this->searchString . '%')
26202627
);
26212628
}
2629+
2630+
// If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain
2631+
// e.g. "bodytext" in their list of fields, to search through them. This is important when a field
2632+
// is filled but its type has been changed.
2633+
if ($subSchemaDivisorFieldName !== ''
2634+
&& isset($fieldsSubSchemaTypes[$field->getName()])
2635+
&& $fieldsSubSchemaTypes[$field->getName()] !== []
2636+
) {
2637+
// Using `IN()` with a string-value quoted list is fine for all database systems, even when
2638+
// used on integer-typed fields and no additional work required here to mitigate something.
2639+
$searchConstraint = $queryBuilder->expr()->and(
2640+
$searchConstraint,
2641+
$queryBuilder->expr()->in(
2642+
$subSchemaDivisorFieldName,
2643+
$queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$field->getName()])
2644+
),
2645+
);
2646+
}
2647+
2648+
$constraints[] = $searchConstraint;
26222649
}
26232650
} elseif ($searchableFields->count() > 0) {
26242651
$like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%');
26252652
foreach ($searchableFields as $field) {
26262653
$fieldConfig = $field->getConfiguration();
2627-
$searchConstraint = $expressionBuilder->and(
2628-
$expressionBuilder->comparison(
2629-
'LOWER(' . $queryBuilder->castFieldToTextType($field->getName()) . ')',
2630-
'LIKE',
2631-
'LOWER(' . $like . ')'
2632-
)
2654+
$searchConstraint = $expressionBuilder->comparison(
2655+
'LOWER(' . $queryBuilder->castFieldToTextType($field->getName()) . ')',
2656+
'LIKE',
2657+
'LOWER(' . $like . ')'
26332658
);
26342659
if (is_array($fieldConfig['search'] ?? null)) {
26352660
$searchConfig = $fieldConfig['search'];
@@ -2638,14 +2663,37 @@ protected function makeSearchString(string $table, int $currentPid, QueryBuilder
26382663
$searchConstraint = $expressionBuilder->and($expressionBuilder->like($field->getName(), $like));
26392664
}
26402665
if (($searchConfig['pidonly'] ?? false) && $currentPid > 0) {
2641-
$searchConstraint = $searchConstraint->with($expressionBuilder->eq($tablePidField, (int)$currentPid));
2666+
$searchConstraint = $expressionBuilder->and(
2667+
$searchConstraint,
2668+
$expressionBuilder->eq($tablePidField, (int)$currentPid),
2669+
);
26422670
}
26432671
if ($searchConfig['andWhere'] ?? false) {
2644-
$searchConstraint = $searchConstraint->with(
2672+
$searchConstraint = $expressionBuilder->and(
2673+
$searchConstraint,
26452674
QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere']))
26462675
);
26472676
}
26482677
}
2678+
2679+
// If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain
2680+
// e.g. "bodytext" in their list of fields, to search through them. This is important when a field
2681+
// is filled but its type has been changed.
2682+
if ($subSchemaDivisorFieldName !== ''
2683+
&& isset($fieldsSubSchemaTypes[$field->getName()])
2684+
&& $fieldsSubSchemaTypes[$field->getName()] !== []
2685+
) {
2686+
// Using `IN()` with a string-value quoted list is fine for all database systems, even when
2687+
// used on integer-typed fields and no additional work required here to mitigate something.
2688+
$searchConstraint = $queryBuilder->expr()->and(
2689+
$searchConstraint,
2690+
$queryBuilder->expr()->in(
2691+
$subSchemaDivisorFieldName,
2692+
$queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$field->getName()])
2693+
),
2694+
);
2695+
}
2696+
26492697
$constraints[] = $searchConstraint;
26502698
}
26512699
}
@@ -3420,4 +3468,42 @@ protected function addDividerToCellGroup(array &$cells): void
34203468
$this->addActionToCellGroup($cells, '<hr class="dropdown-divider">', 'divider');
34213469
}
34223470
}
3471+
3472+
/**
3473+
* Returns table subschema divisor field name and a list of fields not included in all subSchemas along with
3474+
* the list of subSchemas they are included.
3475+
*
3476+
* @param string $tableName
3477+
* @return array{0: string, 1: array<string, list<string>>}
3478+
* @todo Consider to move this to {@see SearchableSchemaFieldsCollector}, a dedicated trait or a shared place to
3479+
* mitigate code duplication (and maintenance in different places).
3480+
* - {@see PageRecordProvider::getSchemaFieldSubSchemaTypes()}
3481+
* - {@see DatabaseRecordProvider::getSchemaFieldSubSchemaTypes()}
3482+
*/
3483+
protected function getSchemaFieldSubSchemaTypes(string $tableName): array
3484+
{
3485+
$result = [
3486+
0 => '',
3487+
1 => [],
3488+
];
3489+
if (!$this->tcaSchemaFactory->has($tableName)) {
3490+
return $result;
3491+
}
3492+
$schema = $this->tcaSchemaFactory->get($tableName);
3493+
if ($schema->getSubSchemaDivisorField() === null) {
3494+
return $result;
3495+
}
3496+
$result[0] = $schema->getSubSchemaDivisorField()->getName();
3497+
foreach ($schema->getSubSchemata() as $recordType => $subSchemata) {
3498+
foreach ($subSchemata->getFields() as $fieldInSubschema => $fieldConfig) {
3499+
$result[1][$fieldInSubschema] ??= [];
3500+
$result[1][$fieldInSubschema][] = $recordType;
3501+
}
3502+
}
3503+
// Remove all fields which are contained in all sub-schemas, determined by
3504+
// comparing each field types count with table types count.
3505+
$subSchemaCount = count($schema->getSubSchemata());
3506+
$result[1] = array_filter($result[1], static fn($value) => count($value) < $subSchemaCount);
3507+
return $result;
3508+
}
34233509
}

typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
4545
use TYPO3\CMS\Core\Schema\Field\NumberFieldType;
4646
use TYPO3\CMS\Core\Schema\SearchableSchemaFieldsCollector;
47+
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
4748
use TYPO3\CMS\Core\Type\Bitmask\Permission;
4849
use TYPO3\CMS\Core\Utility\GeneralUtility;
4950
use TYPO3\CMS\Core\Utility\MathUtility;
@@ -68,6 +69,7 @@ public function __construct(
6869
protected readonly UriBuilder $uriBuilder,
6970
protected readonly QueryParser $queryParser,
7071
protected readonly SearchableSchemaFieldsCollector $searchableSchemaFieldsCollector,
72+
protected readonly TcaSchemaFactory $tcaSchemaFactory,
7173
) {
7274
$this->languageService = $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser());
7375
$this->userPermissions = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
@@ -343,6 +345,7 @@ protected function buildConstraintsForTable(string $queryString, QueryBuilder $q
343345
$platform = $queryBuilder->getConnection()->getDatabasePlatform();
344346
$isPostgres = $platform instanceof DoctrinePostgreSQLPlatform;
345347
$fieldsToSearchWithin = $this->searchableSchemaFieldsCollector->getFields($tableName);
348+
[$subSchemaDivisorFieldName, $fieldsSubSchemaTypes] = $this->getSchemaFieldSubSchemaTypes($tableName);
346349
$constraints = [];
347350
// If the search string is a simple integer, assemble an equality comparison
348351
if (MathUtility::canBeInterpretedAsInteger($queryString)) {
@@ -358,19 +361,39 @@ protected function buildConstraintsForTable(string $queryString, QueryBuilder $q
358361
foreach ($fieldsToSearchWithin as $fieldName => $field) {
359362
// Assemble the search condition only if the field is an integer
360363
if ($field instanceof NumberFieldType || $field instanceof DateTimeFieldType) {
361-
$constraints[] = $queryBuilder->expr()->eq(
364+
$searchConstraint = $queryBuilder->expr()->eq(
362365
$fieldName,
363366
$queryBuilder->createNamedParameter($queryString, Connection::PARAM_INT)
364367
);
365368
} else {
366369
// Otherwise assemble a like condition
367-
$constraints[] = $queryBuilder->expr()->like(
370+
$searchConstraint = $queryBuilder->expr()->like(
368371
$fieldName,
369372
$queryBuilder->createNamedParameter(
370373
'%' . $queryBuilder->escapeLikeWildcards($queryString) . '%'
371374
)
372375
);
373376
}
377+
378+
// If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain
379+
// e.g. "bodytext" in their list of fields, to search through them. This is important when a field
380+
// is filled but its type has been changed.
381+
if ($subSchemaDivisorFieldName !== ''
382+
&& isset($fieldsSubSchemaTypes[$fieldName])
383+
&& $fieldsSubSchemaTypes[$fieldName] !== []
384+
) {
385+
// Using `IN()` with a string-value quoted list is fine for all database systems, even when
386+
// used on integer-typed fields and no additional work required here to mitigate something.
387+
$searchConstraint = $queryBuilder->expr()->and(
388+
$searchConstraint,
389+
$queryBuilder->expr()->in(
390+
$subSchemaDivisorFieldName,
391+
$queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$fieldName])
392+
),
393+
);
394+
}
395+
396+
$constraints[] = $searchConstraint;
374397
}
375398
} else {
376399
$like = '%' . $queryBuilder->escapeLikeWildcards($queryString) . '%';
@@ -413,6 +436,24 @@ protected function buildConstraintsForTable(string $queryString, QueryBuilder $q
413436
}
414437
}
415438

439+
// If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain
440+
// e.g. "bodytext" in their list of fields, to search through them. This is important when a field
441+
// is filled but its type has been changed.
442+
if ($subSchemaDivisorFieldName !== ''
443+
&& isset($fieldsSubSchemaTypes[$fieldName])
444+
&& $fieldsSubSchemaTypes[$fieldName] !== []
445+
) {
446+
// Using `IN()` with a string-value quoted list is fine for all database systems, even when
447+
// used on integer-typed fields and no additional work required here to mitigate something.
448+
$searchConstraint = $queryBuilder->expr()->and(
449+
$searchConstraint,
450+
$queryBuilder->expr()->in(
451+
$subSchemaDivisorFieldName,
452+
$queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$fieldName])
453+
),
454+
);
455+
}
456+
416457
$constraints[] = $searchConstraint;
417458
}
418459
}
@@ -478,6 +519,44 @@ protected function getEditLink(string $tableName, array $row): string
478519
return $editLink;
479520
}
480521

522+
/**
523+
* Returns table subschema divisor field name and a list of fields not included in all subSchemas along with
524+
* the list of subSchemas they are included.
525+
*
526+
* @param string $tableName
527+
* @return array{0: string, 1: array<string, list<string>>}
528+
* @todo Consider to move this to {@see SearchableSchemaFieldsCollector}, a dedicated trait or a shared place to
529+
* mitigate code duplication (and maintenance in different places).
530+
* - {@see PageRecordProvider::getSchemaFieldSubSchemaTypes()}
531+
* - {@see DatabaseRecordList::getSchemaFieldSubSchemaTypes()}
532+
*/
533+
protected function getSchemaFieldSubSchemaTypes(string $tableName): array
534+
{
535+
$result = [
536+
0 => '',
537+
1 => [],
538+
];
539+
if (!$this->tcaSchemaFactory->has($tableName)) {
540+
return $result;
541+
}
542+
$schema = $this->tcaSchemaFactory->get($tableName);
543+
if ($schema->getSubSchemaDivisorField() === null) {
544+
return $result;
545+
}
546+
$result[0] = $schema->getSubSchemaDivisorField()->getName();
547+
foreach ($schema->getSubSchemata() as $recordType => $subSchemata) {
548+
foreach ($subSchemata->getFields() as $fieldInSubschema => $fieldConfig) {
549+
$result[1][$fieldInSubschema] ??= [];
550+
$result[1][$fieldInSubschema][] = $recordType;
551+
}
552+
}
553+
// Remove all fields which are contained in all sub-schemas, determined by
554+
// comparing each field types count with table types count.
555+
$subSchemaCount = count($schema->getSubSchemata());
556+
$result[1] = array_filter($result[1], static fn($value) => count($value) < $subSchemaCount);
557+
return $result;
558+
}
559+
481560
protected function getBackendUser(): BackendUserAuthentication
482561
{
483562
return $GLOBALS['BE_USER'];

0 commit comments

Comments
 (0)