Passed
Push — upcoming-feature/Introduce_Rou... ( e466c2...d478f4 )
by Rafael
67:01 queued 31:07
created

GarbageCollector::hasFrontendGroupsRemoved()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.108

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
ccs 7
cts 10
cp 0.7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2.108
1
<?php
2
namespace ApacheSolrForTypo3\Solr;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2010-2015 Ingo Renner <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 3 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\GarbageRemover\StrategyFactory;
28
use ApacheSolrForTypo3\Solr\IndexQueue\Queue;
29
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
30
use TYPO3\CMS\Backend\Utility\BackendUtility;
31
use TYPO3\CMS\Core\DataHandling\DataHandler;
32
use TYPO3\CMS\Core\SingletonInterface;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
/**
36
 * Garbage Collector, removes related documents from the index when a record is
37
 * set to hidden, is deleted or is otherwise made invisible to website visitors.
38
 *
39
 * Garbage collection will happen for online/LIVE workspaces only.
40
 *
41
 * @author Ingo Renner <[email protected]>
42
 * @author Timo Schmidt <[email protected]>
43
 */
44
class GarbageCollector extends AbstractDataHandlerListener implements SingletonInterface
45
{
46
    /**
47
     * @var array
48
     */
49
    protected $trackedRecords = [];
50
51
    /**
52
     * @var TCAService
53
     */
54
    protected $tcaService;
55
56
    /**
57
     * GarbageCollector constructor.
58
     * @param TCAService|null $TCAService
59
     */
60 15
    public function __construct(TCAService $TCAService = null)
61
    {
62 15
        parent::__construct();
63 15
        $this->tcaService = $TCAService ?? GeneralUtility::makeInstance(TCAService::class);
64 15
    }
65
66
    /**
67
     * Hooks into TCE main and tracks record deletion commands.
68
     *
69
     * @param string $command The command.
70
     * @param string $table The table the record belongs to
71
     * @param int $uid The record's uid
72
     * @param string $value Not used
73
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
74
     * @return void
75
     */
76 3
    public function processCmdmap_preProcess($command, $table, $uid, $value, DataHandler $tceMain)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

76
    public function processCmdmap_preProcess($command, $table, $uid, /** @scrutinizer ignore-unused */ $value, DataHandler $tceMain)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $tceMain is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

76
    public function processCmdmap_preProcess($command, $table, $uid, $value, /** @scrutinizer ignore-unused */ DataHandler $tceMain)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
77
    {
78
        // workspaces: collect garbage only for LIVE workspace
79 3
        if ($command === 'delete' && $GLOBALS['BE_USER']->workspace == 0) {
80 3
            $this->collectGarbage($table, $uid);
81
82 3
            if ($table === 'pages') {
83 1
                $this->getIndexQueue()->deleteItem($table, $uid);
84
            }
85
        }
86 3
    }
87
88
    /**
89
     * Holds the configuration when a recursive page deletion should be triggered.
90
     *
91
     * Note: The SQL transaction is already committed, so the current state covers only "non"-changed fields.
92
     *
93
     * @var array
94
     * @return array
95
     */
96 4
    protected function getUpdateSubPagesRecursiveTriggerConfiguration()
97
    {
98
        return [
99
            // the current page has the field "extendToSubpages" enabled and the field "hidden" was set to 1
100
            // covers following scenarios:
101
            //   'currentState' =>  ['hidden' => '0', 'extendToSubpages' => '0|1'], 'changeSet' => ['hidden' => '1', (optional)'extendToSubpages' => '1']
102 4
            'extendToSubpageEnabledAndHiddenFlagWasAdded' => [
103
                'currentState' =>  ['extendToSubpages' => '1'],
104
                'changeSet' => ['hidden' => '1']
105
            ],
106
            // the current page has the field "hidden" enabled and the field "extendToSubpages" was set to 1
107
            // covers following scenarios:
108
            //   'currentState' =>  ['hidden' => '0|1', 'extendToSubpages' => '0'], 'changeSet' => [(optional)'hidden' => '1', 'extendToSubpages' => '1']
109
            'hiddenIsEnabledAndExtendToSubPagesWasAdded' => [
110
                'currentState' =>  ['hidden' => '1'],
111
                'changeSet' => ['extendToSubpages' => '1']
112
            ],
113
            // the field "no_search_sub_entries" of current page was set to 1
114
            'no_search_sub_entriesFlagWasAdded' => [
115
                'changeSet' => ['no_search_sub_entries' => '1']
116
            ],
117
        ];
118
    }
119
120
    /**
121
     * Tracks down index documents belonging to a particular record or page and
122
     * removes them from the index and the Index Queue.
123
     *
124
     * @param string $table The record's table name.
125
     * @param int $uid The record's uid.
126
     * @throws \UnexpectedValueException if a hook object does not implement interface \ApacheSolrForTypo3\Solr\GarbageCollectorPostProcessor
127
     */
128 12
    public function collectGarbage($table, $uid)
129
    {
130 12
        $garbageRemoverStrategy = StrategyFactory::getByTable($table);
131 12
        $garbageRemoverStrategy->removeGarbageOf($table, $uid);
132 12
    }
133
134
    /**
135
     * @param string $table
136
     * @param int $uid
137
     * @param array $changedFields
138
     */
139 4
    protected function deleteSubEntriesWhenRecursiveTriggerIsRecognized($table, $uid, $changedFields)
140
    {
141 4
        if (!$this->isRecursivePageUpdateRequired($uid, $changedFields)) {
142 2
            return;
143
        }
144
145
        // get affected subpages when "extendToSubpages" flag was set
146 2
        $pagesToDelete = $this->getSubPageIds($uid);
147
        // we need to at least remove this page
148 2
        foreach ($pagesToDelete as $pageToDelete) {
149 2
            $this->collectGarbage($table, $pageToDelete);
150
        }
151 2
    }
152
153
    // methods checking whether to trigger garbage collection
154
155
    /**
156
     * Hooks into TCE main and tracks page move commands.
157
     *
158
     * @param string $command The command.
159
     * @param string $table The table the record belongs to
160
     * @param int $uid The record's uid
161
     * @param string $value Not used
162
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
163
     */
164 3
    public function processCmdmap_postProcess($command, $table, $uid, $value, DataHandler $tceMain) {
0 ignored issues
show
Unused Code introduced by
The parameter $tceMain is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

164
    public function processCmdmap_postProcess($command, $table, $uid, $value, /** @scrutinizer ignore-unused */ DataHandler $tceMain) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

164
    public function processCmdmap_postProcess($command, $table, $uid, /** @scrutinizer ignore-unused */ $value, DataHandler $tceMain) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
165
        // workspaces: collect garbage only for LIVE workspace
166 3
        if ($command === 'move' && $table === 'pages' && $GLOBALS['BE_USER']->workspace == 0) {
167
            // TODO the below comment is not valid anymore, pid has been removed from doc ID
168
            // ...still needed?
169
170
            // must be removed from index since the pid changes and
171
            // is part of the Solr document ID
172
            $this->collectGarbage($table, $uid);
173
174
            // now re-index with new properties
175
            $this->getIndexQueue()->updateItem($table, $uid);
176
        }
177 3
    }
178
179
    /**
180
     * Hooks into TCE main and tracks changed records. In this case the current
181
     * record's values are stored to do a change comparison later on for fields
182
     * like fe_group.
183
     *
184
     * @param array $incomingFields An array of incoming fields, new or changed, not used
185
     * @param string $table The table the record belongs to
186
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
187
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
188
     */
189 7
    public function processDatamap_preProcessFieldArray($incomingFields, $table, $uid, DataHandler $tceMain)
0 ignored issues
show
Unused Code introduced by
The parameter $tceMain is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

189
    public function processDatamap_preProcessFieldArray($incomingFields, $table, $uid, /** @scrutinizer ignore-unused */ DataHandler $tceMain)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $incomingFields is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

189
    public function processDatamap_preProcessFieldArray(/** @scrutinizer ignore-unused */ $incomingFields, $table, $uid, DataHandler $tceMain)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
190
    {
191 7
        if (!is_int($uid)) {
192
            // a newly created record, skip
193 2
            return;
194
        }
195
196 5
        if (Util::isDraftRecord($table, $uid)) {
197
            // skip workspaces: collect garbage only for LIVE workspace
198
            return;
199
        }
200
201 5
        $hasConfiguredEnableColumnForFeGroup = $this->tcaService->isEnableColumn($table, 'fe_group');
202 5
        if (!$hasConfiguredEnableColumnForFeGroup) {
203
            return;
204
        }
205
206 5
        $visibilityAffectingFields = $this->tcaService->getVisibilityAffectingFieldsByTable($table);
207 5
        $record = (array)BackendUtility::getRecord($table, $uid, $visibilityAffectingFields, '', false);
208
        // If no record could be found skip further processing
209 5
        if (empty($record)) {
210
            return;
211
        }
212
213 5
        $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
214
215
        // keep previous state of important fields for later comparison
216 5
        $this->trackedRecords[$table][$uid] = $record;
217 5
    }
218
219
    /**
220
     * Hooks into TCE Main and watches all record updates. If a change is
221
     * detected that would remove the record from the website, we try to find
222
     * related documents and remove them from the index.
223
     *
224
     * @param string $status Status of the current operation, 'new' or 'update'
225
     * @param string $table The table the record belongs to
226
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
227
     * @param array $fields The record's data, not used
228
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
229
     */
230 10
    public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fields, DataHandler $tceMain)
0 ignored issues
show
Unused Code introduced by
The parameter $tceMain is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

230
    public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fields, /** @scrutinizer ignore-unused */ DataHandler $tceMain)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
231
    {
232 10
        if ($status === 'new') {
233
            // a newly created record, skip
234 2
            return;
235
        }
236
237 8
        if (Util::isDraftRecord($table, $uid)) {
238
            // skip workspaces: collect garbage only for LIVE workspace
239
            return;
240
        }
241
242 8
        $record = $this->getRecordWithFieldRelevantForGarbageCollection($table, $uid);
243
244
        // If no record could be found skip further processing
245 8
        if (empty($record)) {
246
            return;
247
        }
248
249 8
        if ($table === 'pages') {
250 4
            $this->deleteSubEntriesWhenRecursiveTriggerIsRecognized($table, $uid, $fields);
251
        }
252
253 8
        $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
254 8
        $isGarbage = $this->getIsGarbageRecord($table, $record);
255 8
        if (!$isGarbage) {
256 1
            return;
257
        }
258
259 7
        $this->collectGarbage($table, $uid);
260 7
    }
261
262
    /**
263
     * Check if a record is getting invisible due to changes in start or endtime. In addition it is checked that the related
264
     * queue item was marked as indexed.
265
     *
266
     * @param string $table
267
     * @param array $record
268
     * @return bool
269
     */
270 4
    protected function isInvisibleByStartOrEndtime($table, $record)
271
    {
272
        return (
273 4
            ($this->tcaService->isStartTimeInFuture($table, $record) || $this->tcaService->isEndTimeInPast($table, $record)) &&
274 4
            $this->isRelatedQueueRecordMarkedAsIndexed($table, $record)
275
        );
276
    }
277
278
    /**
279
     * Checks if the related index queue item is indexed.
280
     *
281
     * * For tt_content the page from the pid is checked
282
     * * For all other records the table it's self is checked
283
     *
284
     * @param string $table The table name.
285
     * @param array $record An array with record fields that may affect visibility.
286
     * @return bool True if the record is marked as being indexed
287
     */
288 2
    protected function isRelatedQueueRecordMarkedAsIndexed($table, $record)
289
    {
290 2
        if ($table === 'tt_content') {
291 2
            $table = 'pages';
292 2
            $uid = $record['pid'];
293
        } else {
294
            $uid = $record['uid'];
295
        }
296
297 2
        return $this->getIndexQueue()->containsIndexedItem($table, $uid);
298
    }
299
300
    /**
301
     * @return Queue
302
     */
303 3
    private function getIndexQueue()
304
    {
305 3
        return GeneralUtility::makeInstance(Queue::class);
306
    }
307
308
    /**
309
     * Checks whether the a frontend group field exists for the record and if so
310
     * whether groups have been removed from accessing the record thus making
311
     * the record invisible to at least some people.
312
     *
313
     * @param string $table The table name.
314
     * @param array $record An array with record fields that may affect visibility.
315
     * @return bool TRUE if frontend groups have been removed from access to the record, FALSE otherwise.
316
     */
317 2
    protected function hasFrontendGroupsRemoved($table, $record)
318
    {
319 2
        if (!isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
320
            return false;
321
        }
322
323 2
        $frontendGroupsField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
324
325 2
        $previousGroups = explode(',', (string)$this->trackedRecords[$table][$record['uid']][$frontendGroupsField]);
326 2
        $currentGroups = explode(',', (string)$record[$frontendGroupsField]);
327 2
        $removedGroups = array_diff($previousGroups, $currentGroups);
328
329 2
        return (boolean)count($removedGroups);
330
    }
331
332
    /**
333
     * Checks whether the page has been excluded from searching.
334
     *
335
     * @param array $record An array with record fields that may affect visibility.
336
     * @return bool True if the page has been excluded from searching, FALSE otherwise
337
     */
338
    protected function isPageExcludedFromSearch($record)
339
    {
340
        return (boolean)$record['no_search'];
341
    }
342
343
    /**
344
     * Checks whether a page has a page type that can be indexed.
345
     * Currently standard pages and mount pages can be indexed.
346
     *
347
     * @param array $record A page record
348
     * @return bool TRUE if the page can be indexed according to its page type, FALSE otherwise
349
     */
350
    protected function isIndexablePageType(array $record)
351
    {
352
        return $this->frontendEnvironment->isAllowedPageType($record);
353
    }
354
355
    /**
356
     * Determines if a record is garbage and can be deleted.
357
     *
358
     * @param string $table
359
     * @param array $record
360
     * @return bool
361
     */
362 8
    protected function getIsGarbageRecord($table, $record):bool
363
    {
364 8
        return $this->tcaService->isHidden($table, $record) ||
365 4
                $this->isInvisibleByStartOrEndtime($table, $record) ||
366 2
                $this->hasFrontendGroupsRemoved($table, $record) ||
367 1
                ($table === 'pages' && $this->isPageExcludedFromSearch($record)) ||
368 8
                ($table === 'pages' && !$this->isIndexablePageType($record));
369
    }
370
371
    /**
372
     * Returns a record with all visibility affecting fields.
373
     *
374
     * @param string $table
375
     * @param int $uid
376
     * @return array
377
     */
378 8
    protected function getRecordWithFieldRelevantForGarbageCollection($table, $uid):array
379
    {
380 8
        $garbageCollectionRelevantFields = $this->tcaService->getVisibilityAffectingFieldsByTable($table);
381 8
        $record = (array)BackendUtility::getRecord($table, $uid, $garbageCollectionRelevantFields, '', false);
382 8
        return $record;
383
    }
384
}
385