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

GarbageCollector::getGarbageHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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;
19
20
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler\Events\PageMovedEvent;
21
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler\Events\RecordDeletedEvent;
22
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler\Events\RecordGarbageCheckEvent;
23
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\UpdateHandler\GarbageHandler;
24
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
25
use Psr\EventDispatcher\EventDispatcherInterface;
26
use TYPO3\CMS\Backend\Utility\BackendUtility;
27
use TYPO3\CMS\Core\DataHandling\DataHandler;
28
use TYPO3\CMS\Core\SingletonInterface;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
use UnexpectedValueException;
31
32
/**
33
 * Garbage Collector, removes related documents from the index when a record is
34
 * set to hidden, is deleted or is otherwise made invisible to website visitors.
35
 *
36
 * Garbage collection will happen for online/LIVE workspaces only.
37
 *
38
 * @author Ingo Renner <[email protected]>
39
 * @author Timo Schmidt <[email protected]>
40
 */
41
class GarbageCollector implements SingletonInterface
42
{
43
    /**
44
     * @var array
45
     */
46
    protected array $trackedRecords = [];
47
48
    /**
49
     * @var TCAService
50
     */
51
    protected TCAService $tcaService;
52
53
    /**
54
     * @var EventDispatcherInterface
55
     */
56
    protected EventDispatcherInterface $eventDispatcher;
57
58
    /**
59
     * GarbageCollector constructor.
60
     *
61
     * @param TCAService|null $TCAService
62
     * @param EventDispatcherInterface|null $eventDispatcher
63
     */
64 41
    public function __construct(TCAService $TCAService = null, EventDispatcherInterface $eventDispatcher = null)
65
    {
66 41
        $this->tcaService = $TCAService ?? GeneralUtility::makeInstance(TCAService::class);
67 41
        $this->eventDispatcher = $eventDispatcher ?? GeneralUtility::makeInstance(EventDispatcherInterface::class);
68
    }
69
70
    /**
71
     * Hooks into TCE main and tracks record deletion commands.
72
     *
73
     * @param string $command The command.
74
     * @param string $table The table the record belongs to
75
     * @param int $uid The record's uid
76
     * @param string $value Not used
77
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
78
     * @noinspection PhpMissingParamTypeInspection
79
     * @noinspection PhpUnusedParameterInspection
80
     */
81 17
    public function processCmdmap_preProcess($command, $table, $uid, $value, DataHandler $tceMain): void
82
    {
83
        // workspaces: process command map only for LIVE workspace
84 17
        if (($GLOBALS['BE_USER']->workspace ?? null) != 0) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $GLOBALS['BE_USER']->workspace ?? null of type mixed|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
85 1
            return;
86
        }
87
88 16
        if ($command === 'delete') {
89 10
            $this->eventDispatcher->dispatch(
90 10
                new RecordDeletedEvent((int)$uid, (string)$table)
91 10
            );
92 6
        } elseif ($command === 'move' && $table === 'pages') {
93 6
            $pageRow = BackendUtility::getRecord('pages', $uid);
94 6
            $this->trackedRecords['pages'][$uid] = $pageRow;
95
        }
96
    }
97
98
    /**
99
     * Tracks down index documents belonging to a particular record or page and
100
     * removes them from the index and the Index Queue.
101
     *
102
     * @param string $table The record's table name.
103
     * @param int $uid The record's uid.
104
     * @throws UnexpectedValueException if a hook object does not implement interface \ApacheSolrForTypo3\Solr\GarbageCollectorPostProcessor
105
     */
106 2
    public function collectGarbage(string $table, int $uid): void
107
    {
108 2
        $this->getGarbageHandler()->collectGarbage($table, $uid);
109
    }
110
111
    /**
112
     * Hooks into TCE main and tracks page move commands.
113
     *
114
     * @param string $command The command.
115
     * @param string $table The table the record belongs to
116
     * @param int $uid The record's uid
117
     * @param string $value Not used
118
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
119
     * @noinspection PhpMissingParamTypeInspection
120
     * @noinspection PhpUnusedParameterInspection
121
     */
122 17
    public function processCmdmap_postProcess($command, $table, $uid, $value, DataHandler $tceMain)
123
    {
124
        // workspaces: collect garbage only for LIVE workspace
125 17
        if ($command === 'move' && $table === 'pages' && ($GLOBALS['BE_USER']->workspace ?? null) == 0) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $GLOBALS['BE_USER']->workspace ?? null of type mixed|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
126 7
            $event = new PageMovedEvent((int)$uid);
127 7
            if (($this->trackedRecords['pages'][$uid] ?? null) !== null) {
128 6
                $event->setPreviousParentId((int)$this->trackedRecords['pages'][$uid]['pid']);
129
            }
130 7
            $this->eventDispatcher->dispatch($event);
131
        }
132
    }
133
134
    /**
135
     * Hooks into TCE main and tracks changed records. In this case the current
136
     * record's values are stored to do a change comparison later on for fields
137
     * like fe_group.
138
     *
139
     * @param array $incomingFields An array of incoming fields, new or changed, not used
140
     * @param string $table The table the record belongs to
141
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
142
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
143
     * @noinspection PhpMissingParamTypeInspection
144
     * @noinspection PhpUnusedParameterInspection
145
     */
146 13
    public function processDatamap_preProcessFieldArray($incomingFields, $table, $uid, DataHandler $tceMain): void
147
    {
148 13
        if (!is_int($uid)) {
149
            // a newly created record, skip
150 2
            return;
151
        }
152
153 12
        $uid = (int)$uid;
154 12
        $table = (string)$table;
155 12
        if (Util::isDraftRecord($table, $uid)) {
156
            // skip workspaces: collect garbage only for LIVE workspace
157
            return;
158
        }
159
160 12
        $hasConfiguredEnableColumnForFeGroup = $this->tcaService->isEnableColumn($table, 'fe_group');
161 12
        if (!$hasConfiguredEnableColumnForFeGroup) {
162
            return;
163
        }
164
165 12
        $record = $this->getGarbageHandler()->getRecordWithFieldRelevantForGarbageCollection($table, $uid);
166
        // If no record could be found skip further processing
167 12
        if (empty($record)) {
168
            return;
169
        }
170
171 12
        $record = $this->tcaService->normalizeFrontendGroupField($table, $record);
172
173
        // keep previous state of important fields for later comparison
174 12
        $this->trackedRecords[$table][$uid] = $record;
175
    }
176
177
    /**
178
     * Hooks into TCE Main and watches all record updates. If a change is
179
     * detected that would remove the record from the website, we try to find
180
     * related documents and remove them from the index.
181
     *
182
     * @param string $status Status of the current operation, 'new' or 'update'
183
     * @param string $table The table the record belongs to
184
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
185
     * @param array $fields The record's data, not used
186
     * @param DataHandler $tceMain TYPO3 Core Engine parent object, not used
187
     * @noinspection PhpMissingParamTypeInspection
188
     * @noinspection PhpUnusedParameterInspection
189
     */
190 19
    public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fields, DataHandler $tceMain): void
191
    {
192 19
        if ($status === 'new') {
193
            // a newly created record, skip
194 2
            return;
195
        }
196
197 18
        $uid = (int)$uid;
198 18
        $table = (string)$table;
199 18
        if (Util::isDraftRecord($table, $uid)) {
200
            // skip workspaces: collect garbage only for LIVE workspace
201
            return;
202
        }
203
204 18
        $updatedRecord = $this->getGarbageHandler()->getRecordWithFieldRelevantForGarbageCollection($table, $uid);
205 18
        if (empty($updatedRecord)) {
206
            return;
207
        }
208
209 18
        $this->eventDispatcher->dispatch(
210 18
            new RecordGarbageCheckEvent(
211 18
                $uid,
212 18
                $table,
213 18
                $fields,
214 18
                $this->hasFrontendGroupsRemoved($table, $updatedRecord)
215 18
            )
216 18
        );
217
    }
218
219
    /**
220
     * Checks whether the frontend group field exists for the record and if so
221
     * whether groups have been removed from accessing the record thus making
222
     * the record invisible to at least some people.
223
     *
224
     * @param string $table The table name.
225
     * @param array $updatedRecord An array with fields of the updated record that may affect visibility.
226
     * @return bool TRUE if frontend groups have been removed from access to the record, FALSE otherwise.
227
     */
228 18
    protected function hasFrontendGroupsRemoved(string $table, array $updatedRecord): bool
229
    {
230 18
        if (!isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
231
            return false;
232
        }
233
234 18
        $frontendGroupsField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
235
236 18
        $previousGroups = GeneralUtility::intExplode(',', (string)$this->trackedRecords[$table][$updatedRecord['uid']][$frontendGroupsField]);
237 18
        $currentGroups = GeneralUtility::intExplode(',', (string)$updatedRecord[$frontendGroupsField]);
238 18
        $removedGroups = array_diff($previousGroups, $currentGroups);
239
240 18
        return !empty($removedGroups);
241
    }
242
243
    /**
244
     * Returns the GarbageHandler
245
     *
246
     * @return GarbageHandler
247
     */
248 21
    protected function getGarbageHandler(): GarbageHandler
249
    {
250 21
        return GeneralUtility::makeInstance(GarbageHandler::class);
251
    }
252
}
253