Passed
Push — release-11.5.x ( 1b5a31...ef0664 )
by Markus
31:41
created

GarbageHandler   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Test Coverage

Coverage 93.85%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 29
eloc 65
c 1
b 0
f 0
dl 0
loc 237
ccs 61
cts 65
cp 0.9385
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A performRecordGarbageCheck() 0 24 4
B getIsGarbageRecord() 0 7 7
A isIndexablePageType() 0 3 1
A isInvisibleByStartOrEndtime() 0 6 3
A isPageExcludedFromSearch() 0 3 1
A collectGarbage() 0 4 1
A isRelatedQueueRecordMarkedAsIndexed() 0 10 2
A handlePageMovement() 0 12 4
A getRecordWithFieldRelevantForGarbageCollection() 0 17 3
A deleteSubEntriesWhenRecursiveTriggerIsRecognized() 0 14 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler;
19
20
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\GarbageRemover\StrategyFactory;
21
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
22
use PDO;
23
use Throwable;
24
use TYPO3\CMS\Backend\Utility\BackendUtility;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use UnexpectedValueException;
27
28
/**
29
 * Garbage handler
30
 *
31
 * Handles updates on potential relevant records and
32
 * collects the garbage, e.g. a deletion might require
33
 * index and index queue updates.
34
 */
35
class GarbageHandler extends AbstractUpdateHandler
36
{
37
    /**
38
     * Configuration used to check if recursive updates are required
39
     *
40
     * Holds the configuration when a recursive page queuing should be triggered, while processing record
41
     * updates
42
     *
43
     * Note: The SQL transaction is already committed, so the current state covers only "non"-changed fields.
44
     *
45
     * @var array
46
     */
47
    protected array $updateSubPagesRecursiveTriggerConfiguration = [
48
        // the current page has the field "extendToSubpages" enabled and the field "hidden" was set to 1
49
        // covers following scenarios:
50
        //   'currentState' =>  ['hidden' => '0', 'extendToSubpages' => '0|1'], 'changeSet' => ['hidden' => '1', (optional)'extendToSubpages' => '1']
51
        'extendToSubpageEnabledAndHiddenFlagWasAdded' => [
52
            'currentState' =>  ['extendToSubpages' => '1'],
53
            'changeSet' => ['hidden' => '1'],
54
        ],
55
        // the current page has the field "hidden" enabled and the field "extendToSubpages" was set to 1
56
        // covers following scenarios:
57
        //   'currentState' =>  ['hidden' => '0|1', 'extendToSubpages' => '0'], 'changeSet' => [(optional)'hidden' => '1', 'extendToSubpages' => '1']
58
        'hiddenIsEnabledAndExtendToSubPagesWasAdded' => [
59
            'currentState' =>  ['hidden' => '1'],
60
            'changeSet' => ['extendToSubpages' => '1'],
61
        ],
62
        // the field "no_search_sub_entries" of current page was set to 1
63
        'no_search_sub_entriesFlagWasAdded' => [
64
            'changeSet' => ['no_search_sub_entries' => '1'],
65
        ],
66
    ];
67
68
    /**
69
     * Tracks down index documents belonging to a particular record or page and
70
     * removes them from the index and the Index Queue.
71
     *
72
     * @param string $table The record's table name.
73
     * @param int $uid The record's uid.
74
     * @throws UnexpectedValueException if a hook object does not implement interface {@linkt \ApacheSolrForTypo3\Solr\GarbageCollectorPostProcessor::}
75
     */
76 36
    public function collectGarbage(string $table, int $uid): void
77
    {
78 36
        $garbageRemoverStrategy = StrategyFactory::getByTable($table);
79 36
        $garbageRemoverStrategy->removeGarbageOf($table, $uid);
80
    }
81
82
    /**
83
     * Handles moved pages
84
     *
85
     * As rootline and page slug might have changed on page movement,
86
     * document have to be removed from Solr. Reindexing is taken
87
     * care of by the DataUpdateHandler.
88
     *
89
     * @param int $uid
90
     * @param int|null $previousParentId
91
     */
92 7
    public function handlePageMovement(int $uid, ?int $previousParentId = null): void
93
    {
94 7
        $this->collectGarbage('pages', $uid);
95
96
        // collect garbage of subpages
97 7
        if ($previousParentId !== null) {
98 6
            $pageRecord = BackendUtility::getRecord('pages', $uid);
99 6
            if ($pageRecord !== null && (int)$pageRecord['pid'] !== $previousParentId) {
100 5
                $subPageIds = $this->getSubPageIds($uid);
101 5
                array_walk(
102 5
                    $subPageIds,
103 5
                    fn (int $subPageId) => $this->collectGarbage('pages', $subPageId)
104 5
                );
105
            }
106
        }
107
    }
108
109
    /**
110
     * Performs record garbage check
111
     *
112
     * @param int $uid
113
     * @param string $table
114
     * @param array $updatedFields
115
     * @param bool $frontendGroupsRemoved
116
     * @throws DBALDriverException
117
     */
118 19
    public function performRecordGarbageCheck(
119
        int $uid,
120
        string $table,
121
        array $updatedFields,
122
        bool $frontendGroupsRemoved
123
    ): void {
124 19
        $record = $this->getRecordWithFieldRelevantForGarbageCollection($table, $uid);
125
126
        // If no record could be found skip further processing
127 19
        if (empty($record)) {
128
            return;
129
        }
130
131 19
        if ($table === 'pages') {
132 10
            $this->deleteSubEntriesWhenRecursiveTriggerIsRecognized($table, $uid, $updatedFields);
133
        }
134
135 19
        $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
136 19
        $isGarbage = $this->getIsGarbageRecord($table, $record, $frontendGroupsRemoved);
137 19
        if (!$isGarbage) {
138 4
            return;
139
        }
140
141 15
        $this->collectGarbage($table, $uid);
142
    }
143
144
    /**
145
     * @param string $table
146
     * @param int $uid
147
     * @param array $updatedFields
148
     * @throws DBALDriverException
149
     */
150 10
    protected function deleteSubEntriesWhenRecursiveTriggerIsRecognized(
151
        string $table,
152
        int $uid,
153
        array $updatedFields
154
    ): void {
155 10
        if (!$this->isRecursivePageUpdateRequired($uid, $updatedFields)) {
156 5
            return;
157
        }
158
159
        // get affected subpages when "extendToSubpages" flag was set
160 5
        $pagesToDelete = $this->getSubPageIds($uid);
161
        // we need to at least remove this page
162 5
        foreach ($pagesToDelete as $pageToDelete) {
163 5
            $this->collectGarbage($table, $pageToDelete);
164
        }
165
    }
166
167
    /**
168
     * Determines if a record is garbage and can be deleted.
169
     *
170
     * @param string $table
171
     * @param array $record
172
     * @param bool $frontendGroupsRemoved
173
     * @return bool
174
     * @throws DBALDriverException
175
     */
176 19
    protected function getIsGarbageRecord(string $table, array $record, bool $frontendGroupsRemoved): bool
177
    {
178 19
        return $frontendGroupsRemoved
179 19
            || $this->tcaService->isHidden($table, $record)
180 19
            || $this->isInvisibleByStartOrEndtime($table, $record)
181 19
            || ($table === 'pages' && $this->isPageExcludedFromSearch($record))
182 19
            || ($table === 'pages' && !$this->isIndexablePageType($record));
183
    }
184
185
    /**
186
     * Checks whether a page has a page type that can be indexed.
187
     * Currently, standard pages and mount pages can be indexed.
188
     *
189
     * @param array $record A page record
190
     * @return bool TRUE if the page can be indexed according to its page type, FALSE otherwise
191
     * @throws DBALDriverException
192
     */
193 3
    protected function isIndexablePageType(array $record): bool
194
    {
195 3
        return $this->frontendEnvironment->isAllowedPageType($record);
196
    }
197
198
    /**
199
     * Checks whether the page has been excluded from searching.
200
     *
201
     * @param array $record An array with record fields that may affect visibility.
202
     * @return bool True if the page has been excluded from searching, FALSE otherwise
203
     */
204 3
    protected function isPageExcludedFromSearch(array $record): bool
205
    {
206 3
        return (bool)$record['no_search'];
207
    }
208
209
    /**
210
     * Check if a record is getting invisible due to changes in start or endtime. In addition, it is checked that the related
211
     * queue item was marked as indexed.
212
     *
213
     * @param string $table
214
     * @param array $record
215
     * @return bool
216
     */
217 9
    protected function isInvisibleByStartOrEndtime(string $table, array $record): bool
218
    {
219 9
        return
220 9
            ($this->tcaService->isStartTimeInFuture($table, $record)
221 9
                || $this->tcaService->isEndTimeInPast($table, $record))
222 9
            && $this->isRelatedQueueRecordMarkedAsIndexed($table, $record)
223 9
        ;
224
    }
225
226
    /**
227
     * Checks if the related index queue item is indexed.
228
     *
229
     * * For tt_content the page from the pid is checked
230
     * * For all other records the table it's self is checked
231
     *
232
     * @param string $table The table name.
233
     * @param array $record An array with record fields that may affect visibility.
234
     * @return bool True if the record is marked as being indexed
235
     */
236 4
    protected function isRelatedQueueRecordMarkedAsIndexed(string $table, array $record): bool
237
    {
238 4
        if ($table === 'tt_content') {
239 4
            $table = 'pages';
240 4
            $uid = $record['pid'];
241
        } else {
242
            $uid = $record['uid'];
243
        }
244
245 4
        return $this->indexQueue->containsIndexedItem($table, $uid);
246
    }
247
248
    /**
249
     * Returns a record with all visibility affecting fields.
250
     *
251
     * @param string $table
252
     * @param int $uid
253
     * @return array|null
254
     */
255 20
    public function getRecordWithFieldRelevantForGarbageCollection(string $table, int $uid): ?array
256
    {
257 20
        $garbageCollectionRelevantFields = $this->tcaService->getVisibilityAffectingFieldsByTable($table);
258
        try {
259 20
            $queryBuilder = $this->getQueryBuilderForTable($table);
260 20
            $queryBuilder->getRestrictions()->removeAll();
261 20
            $row = $queryBuilder
262 20
                ->select(...GeneralUtility::trimExplode(',', $garbageCollectionRelevantFields, true))
263 20
                ->from($table)
264 20
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, PDO::PARAM_INT)))
265 20
                ->executeQuery()
266 20
                ->fetchAssociative();
267
        } catch (Throwable $e) {
268
            $row = false;
269
        }
270
271 20
        return is_array($row) ? $row : null;
272
    }
273
}
274