Failed Conditions
Push — master ( 5f60a5...9b80eb )
by Rafael
21:42
created

GarbageCollector::collectGarbage()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 28.9113

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 4
cts 17
cp 0.2353
rs 8.2222
c 0
b 0
f 0
cc 7
eloc 14
nc 8
nop 2
crap 28.9113
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\IndexQueue\Queue;
28
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
29
use TYPO3\CMS\Backend\Utility\BackendUtility;
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Backend\Utility\BackendUtility was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use TYPO3\CMS\Core\DataHandling\DataHandler;
31
use TYPO3\CMS\Core\SingletonInterface;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
34
/**
35
 * Garbage Collector, removes related documents from the index when a record is
36
 * set to hidden, is deleted or is otherwise made invisible to website visitors.
37
 *
38
 * Garbage collection will happen for online/LIVE workspaces only.
39
 *
40
 * @author Ingo Renner <[email protected]>
41
 * @author Timo Schmidt <[email protected]>
42
 */
43
class GarbageCollector extends AbstractDataHandlerListener implements SingletonInterface
44
{
45
    /**
46
     * @var array
47
     */
48
    protected $trackedRecords = [];
49
50
    /**
51
     * @var TCAService
52
     */
53
    protected $tcaService;
54
55
    /**
56
     * GarbageCollector constructor.
57
     * @param TCAService|null $TCAService
58
     */
59 11
    public function __construct(TCAService $TCAService = null)
60
    {
61 11
        parent::__construct();
62 11
        $this->tcaService = is_null($TCAService) ? GeneralUtility::makeInstance(TCAService::class) : $TCAService;
63 11
    }
64
65
    /**
66
     * Hooks into TCE main and tracks record deletion commands.
67
     *
68
     * @param string $command The command.
69
     * @param string $table The table the record belongs to
70
     * @param int $uid The record's uid
71
     * @param string $value Not used
72
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
73
     * @return void
74
     */
75 2
    public function processCmdmap_preProcess(
76
        $command,
77
        $table,
78
        $uid,
79
        /** @noinspection PhpUnusedParameterInspection */
80
        $value,
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

80
        /** @scrutinizer ignore-unused */ $value,

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...
81
        /** @noinspection PhpUnusedParameterInspection */
82
        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

82
        /** @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...
83
    ) {
84
        // workspaces: collect garbage only for LIVE workspace
85 2
        if ($command === 'delete' && $GLOBALS['BE_USER']->workspace == 0) {
86 2
            $this->collectGarbage($table, $uid);
87
88 2
            if ($table === 'pages') {
89 1
                $this->getIndexQueue()->deleteItem($table, $uid);
90
            }
91
        }
92 2
    }
93
94
    /**
95
     * Holds the configuration when a recursive page queing should be triggered.
96
     *
97
     * @var array
98
     * @return array
99
     */
100 3
    protected function getUpdateSubPagesRecursiveTriggerConfiguration()
101
    {
102
        return [
103
            // the current page has the field "extendToSubpages" enabled and the field "hidden" was set to 1
104 3
            'extendToSubpageEnabledAndHiddenFlagWasAdded' => [
105
                'currentState' =>  ['extendToSubpages' => '1'],
106
                'changeSet' => ['hidden' => '1']
107
            ],
108
            // the current page has the field "hidden" enabled and the field "extendToSubpages" was set to 1
109
            'hiddenIsEnabledAndExtendToSubPagesWasAdded' => [
110
                'currentState' =>  ['hidden' => '1'],
111
                'changeSet' => ['extendToSubpages' => '1']
112
            ]
113
        ];
114
    }
115
116
    /**
117
     * Tracks down index documents belonging to a particular record or page and
118
     * removes them from the index and the Index Queue.
119
     *
120
     * @param string $table The record's table name.
121
     * @param int $uid The record's uid.
122
     * @throws \UnexpectedValueException if a hook object does not implement interface \ApacheSolrForTypo3\Solr\GarbageCollectorPostProcessor
123
     */
124 10
    public function collectGarbage($table, $uid)
125
    {
126 10
        if ($table === 'tt_content' || $table === 'pages' || $table === 'pages_language_overlay') {
127 10
            $this->collectPageGarbage($table, $uid);
128
        } else {
129
            $this->collectRecordGarbage($table, $uid);
130
        }
131
132 10
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessGarbageCollector'])) {
133
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessGarbageCollector'] as $classReference) {
134
                $garbageCollectorPostProcessor = GeneralUtility::makeInstance($classReference);
135
136
                if ($garbageCollectorPostProcessor instanceof GarbageCollectorPostProcessor) {
137
                    $garbageCollectorPostProcessor->postProcessGarbageCollector($table,
138
                        $uid);
139
                } else {
140
                    throw new \UnexpectedValueException(
141
                        get_class($garbageCollectorPostProcessor) . ' must implement interface ' . GarbageCollectorPostProcessor::class,
142
                        1345807460
143
                    );
144
                }
145
            }
146
        }
147 10
    }
148
149
    /**
150
     * Tracks down index documents belonging to a particular page and
151
     * removes them from the index and the Index Queue.
152
     *
153
     * @param string $table The record's table name.
154
     * @param int $uid The record's uid.
155
     */
156 10
    protected function collectPageGarbage($table, $uid)
157
    {
158
        switch ($table) {
159 10
            case 'tt_content':
160 4
                $contentElement = BackendUtility::getRecord('tt_content', $uid, 'uid, pid', '', false);
161
162 4
                $table = 'pages';
163 4
                $uid = $contentElement['pid'];
164
165 4
                $this->deleteIndexDocuments($table, $uid);
166
                // only a content element was removed, now update/re-index the page
167 4
                $this->getIndexQueue()->updateItem($table, $uid);
168 4
                break;
169
            // @todo This case can be deleted when TYPO3 8 compatibility is dropped
170 6
            case 'pages_language_overlay':
171
                $pageOverlayRecord = BackendUtility::getRecord('pages_language_overlay', $uid, 'uid, pid', '', false);
172
173
                $table = 'pages';
174
                $uid = $pageOverlayRecord['pid'];
175
176
                $this->deleteIndexDocuments($table, $uid);
177
                // only a page overlay was removed, now update/re-index the page
178
                $this->getIndexQueue()->updateItem($table, $uid);
179
                break;
180 6
            case 'pages':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
181
182
                // @todo The content of this if statement can allways be executed when TYPO3 8 support is dropped
183 6
                if (!Util::getIsTYPO3VersionBelow9()) {
184
                    $pageOverlay = BackendUtility::getRecord('pages', $uid, 'l10n_parent', '', false);
185
                    $uid = empty($pageOverlay['l10n_parent']) ? $uid : $pageOverlay['l10n_parent'];
186
                }
187
188 6
                $this->deleteIndexDocuments($table, $uid);
189 6
                $this->getIndexQueue()->deleteItem($table, $uid);
190
191 6
                break;
192
        }
193 10
    }
194
195
    /**
196
     * @param string $table
197
     * @param int $uid
198
     * @param array $changedFields
199
     */
200 3
    protected function deleteSubpagesWhenExtendToSubpagesIsSet($table, $uid, $changedFields)
201
    {
202 3
        if (!$this->isRecursivePageUpdateRequired($uid, $changedFields)) {
203 1
            return;
204
        }
205
206 2
        $indexQueue = $this->getIndexQueue();
207
        // get affected subpages when "extendToSubpages" flag was set
208 2
        $pagesToDelete = $this->getSubPageIds($uid);
209
        // we need to at least remove this page
210 2
        foreach ($pagesToDelete as $pageToDelete) {
211 2
            $this->deleteIndexDocuments($table, $pageToDelete);
212 2
            $indexQueue->deleteItem($table, $pageToDelete);
213
        }
214 2
    }
215
216
    /**
217
     * Deletes index documents for a given record identification.
218
     *
219
     * @param string $table The record's table name.
220
     * @param int $uid The record's uid.
221
     */
222 10
    protected function deleteIndexDocuments($table, $uid)
223
    {
224
        /** @var $connectionManager ConnectionManager */
225 10
        $connectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
226
227
        // record can be indexed for multiple sites
228 10
        $indexQueueItems = $this->getIndexQueue()->getItems($table, $uid);
229 10
        foreach ($indexQueueItems as $indexQueueItem) {
230 9
            $site = $indexQueueItem->getSite();
231 9
            $solrConfiguration = $site->getSolrConfiguration();
232 9
            $enableCommitsSetting = $solrConfiguration->getEnableCommits();
233
234
            // a site can have multiple connections (cores / languages)
235 9
            $solrConnections = $connectionManager->getConnectionsBySite($site);
236 9
            foreach ($solrConnections as $solr) {
237 9
                $solr->getWriteService()->deleteByQuery('type:' . $table . ' AND uid:' . intval($uid));
238 9
                if ($enableCommitsSetting) {
239 9
                    $solr->getWriteService()->commit(false, false, false);
240
                }
241
            }
242
        }
243 10
    }
244
245
    /**
246
     * Tracks down index documents belonging to a particular record and
247
     * removes them from the index and the Index Queue.
248
     *
249
     * @param string $table The record's table name.
250
     * @param int $uid The record's uid.
251
     */
252
    protected function collectRecordGarbage($table, $uid)
253
    {
254
        $this->deleteIndexDocuments($table, $uid);
255
        $this->getIndexQueue()->deleteItem($table, $uid);
256
    }
257
258
    // methods checking whether to trigger garbage collection
259
260
    /**
261
     * Hooks into TCE main and tracks page move commands.
262
     *
263
     * @param string $command The command.
264
     * @param string $table The table the record belongs to
265
     * @param int $uid The record's uid
266
     * @param string $value Not used
267
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
268
     */
269 2
    public function processCmdmap_postProcess(
270
        $command,
271
        $table,
272
        $uid,
273
        /** @noinspection PhpUnusedParameterInspection */
274
        $value,
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

274
        /** @scrutinizer ignore-unused */ $value,

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...
275
        /** @noinspection PhpUnusedParameterInspection */
276
        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

276
        /** @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...
277
    ) {
278
        // workspaces: collect garbage only for LIVE workspace
279 2
        if ($command === 'move' && $table === 'pages' && $GLOBALS['BE_USER']->workspace == 0) {
280
            // TODO the below comment is not valid anymore, pid has been removed from doc ID
281
            // ...still needed?
282
283
            // must be removed from index since the pid changes and
284
            // is part of the Solr document ID
285
            $this->collectGarbage($table, $uid);
286
287
            // now re-index with new properties
288
            $this->getIndexQueue()->updateItem($table, $uid);
289
        }
290 2
    }
291
292
    /**
293
     * Hooks into TCE main and tracks changed records. In this case the current
294
     * record's values are stored to do a change comparison later on for fields
295
     * like fe_group.
296
     *
297
     * @param array $incomingFields An array of incoming fields, new or changed, not used
298
     * @param string $table The table the record belongs to
299
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
300
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
301
     */
302 5
    public function processDatamap_preProcessFieldArray(
303
        /** @noinspection PhpUnusedParameterInspection */
304
        $incomingFields,
0 ignored issues
show
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

304
        /** @scrutinizer ignore-unused */ $incomingFields,

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...
305
        $table,
306
        $uid,
307
        /** @noinspection PhpUnusedParameterInspection */
308
        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

308
        /** @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...
309
    ) {
310 5
        if (!is_int($uid)) {
311
            // a newly created record, skip
312
            return;
313
        }
314
315 5
        if (Util::isDraftRecord($table, $uid)) {
316
            // skip workspaces: collect garbage only for LIVE workspace
317
            return;
318
        }
319
320 5
        $hasConfiguredEnableColumnForFeGroup = $this->tcaService->isEnableColumn($table, 'fe_group');
321
322 5
        if ($hasConfiguredEnableColumnForFeGroup) {
323 5
            $visibilityAffectingFields = $this->tcaService->getVisibilityAffectingFieldsByTable($table);
324 5
            $record = (array)BackendUtility::getRecord(
325 5
                $table,
326 5
                $uid,
327 5
                $visibilityAffectingFields,
328 5
                '',
329 5
                false
330
            );
331
332
            // If no record could be found skip further processing
333 5
            if (empty($record)) {
334
                return;
335
            }
336
337 5
            $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
338
339
            // keep previous state of important fields for later comparison
340 5
            $this->trackedRecords[$table][$uid] = $record;
341
        }
342 5
    }
343
344
    /**
345
     * Hooks into TCE Main and watches all record updates. If a change is
346
     * detected that would remove the record from the website, we try to find
347
     * related documents and remove them from the index.
348
     *
349
     * @param string $status Status of the current operation, 'new' or 'update'
350
     * @param string $table The table the record belongs to
351
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
352
     * @param array $fields The record's data, not used
353
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
354
     */
355 7
    public function processDatamap_afterDatabaseOperations(
356
        $status,
357
        $table,
358
        $uid,
359
        array $fields,
360
        /** @noinspection PhpUnusedParameterInspection */
361
        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

361
        /** @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...
362
    ) {
363 7
        if ($status === 'new') {
364
            // a newly created record, skip
365
            return;
366
        }
367
368 7
        if (Util::isDraftRecord($table, $uid)) {
369
            // skip workspaces: collect garbage only for LIVE workspace
370
            return;
371
        }
372
373 7
        $garbageCollectionRelevantFields = $this->tcaService->getVisibilityAffectingFieldsByTable($table);
374
375 7
        $record = (array)BackendUtility::getRecord($table, $uid, $garbageCollectionRelevantFields, '', false);
376
377
        // If no record could be found skip further processing
378 7
        if (empty($record)) {
379
            return;
380
        }
381
382 7
        $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
383
384 7
        if ($this->tcaService->isHidden($table, $record)
385 3
            || $this->isInvisibleByStartOrEndtime($table, $record)
386 1
            || $this->hasFrontendGroupsRemoved($table, $record)
387 1
            || ($table === 'pages' && $this->isPageExcludedFromSearch($record))
388 7
            || ($table === 'pages' && !$this->isIndexablePageType($record))
389
        ) {
390 6
            $this->collectGarbage($table, $uid);
391
392 6
            if ($table === 'pages') {
393 3
                $this->deleteSubpagesWhenExtendToSubpagesIsSet($table, $uid, $fields);
394
            }
395
        }
396 7
    }
397
398
    /**
399
     * Check if a record is getting invisible due to changes in start or endtime. In addition it is checked that the related
400
     * queue item was marked as indexed.
401
     *
402
     * @param string $table
403
     * @param array $record
404
     * @return bool
405
     */
406 3
    protected function isInvisibleByStartOrEndtime($table, $record)
407
    {
408
        return (
409 3
            ($this->tcaService->isStartTimeInFuture($table, $record) || $this->tcaService->isEndTimeInPast($table, $record)) &&
410 3
            $this->isRelatedQueueRecordMarkedAsIndexed($table, $record)
411
        );
412
    }
413
414
    /**
415
     * Checks if the related index queue item is indexed.
416
     *
417
     * * For tt_content and pages_language_overlay the page from the pid is checked
418
     * * For all other records the table it's self is checked
419
     *
420
     * @param string $table The table name.
421
     * @param array $record An array with record fields that may affect visibility.
422
     * @return bool True if the record is marked as being indexed
423
     */
424 2
    protected function isRelatedQueueRecordMarkedAsIndexed($table, $record)
425
    {
426
        //@todo check for pages_language_overlay can be dropped when TYPO3 8 compatibility is dropped.
427 2
        if ($table === 'tt_content' || $table === 'pages_language_overlay') {
428 2
            $table = 'pages';
429 2
            $uid = $record['pid'];
430
        } else {
431
            $uid = $record['uid'];
432
        }
433
434 2
        return $this->getIndexQueue()->containsIndexedItem($table, $uid);
435
    }
436
437
    /**
438
     * @return Queue
439
     */
440 10
    private function getIndexQueue()
441
    {
442 10
        return GeneralUtility::makeInstance(Queue::class);
443
    }
444
445
    /**
446
     * Checks whether the a frontend group field exists for the record and if so
447
     * whether groups have been removed from accessing the record thus making
448
     * the record invisible to at least some people.
449
     *
450
     * @param string $table The table name.
451
     * @param array $record An array with record fields that may affect visibility.
452
     * @return bool TRUE if frontend groups have been removed from access to the record, FALSE otherwise.
453
     */
454 1
    protected function hasFrontendGroupsRemoved($table, $record)
455
    {
456 1
        $frontendGroupsRemoved = false;
457
458 1
        if (isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
459 1
            $frontendGroupsField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
460
461 1
            $previousGroups = explode(',',
462 1
                (string)$this->trackedRecords[$table][$record['uid']][$frontendGroupsField]);
463 1
            $currentGroups = explode(',',
464 1
                (string)$record[$frontendGroupsField]);
465
466 1
            $removedGroups = array_diff($previousGroups, $currentGroups);
467
468 1
            $frontendGroupsRemoved = (boolean)count($removedGroups);
469
        }
470
471 1
        return $frontendGroupsRemoved;
472
    }
473
474
    /**
475
     * Checks whether the page has been excluded from searching.
476
     *
477
     * @param array $record An array with record fields that may affect visibility.
478
     * @return bool True if the page has been excluded from searching, FALSE otherwise
479
     */
480
    protected function isPageExcludedFromSearch($record)
481
    {
482
        return (boolean)$record['no_search'];
483
    }
484
485
    /**
486
     * Checks whether a page has a page type that can be indexed.
487
     * Currently standard pages and mount pages can be indexed.
488
     *
489
     * @param array $record A page record
490
     * @return bool TRUE if the page can be indexed according to its page type, FALSE otherwise
491
     */
492
    protected function isIndexablePageType(array $record)
493
    {
494
        return Util::isAllowedPageType($record);
495
    }
496
}
497