getUpdateSubPagesRecursiveTriggerConfiguration()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.0156

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 12
ccs 3
cts 4
cp 0.75
rs 10
cc 1
nc 1
nop 0
crap 1.0156
1
<?php
2
namespace ApacheSolrForTypo3\Solr\IndexQueue;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2009-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\AbstractDataHandlerListener;
28
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\ConfigurationAwareRecordService;
29
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\MountPagesUpdater;
30
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\RootPageResolver;
31
use ApacheSolrForTypo3\Solr\FrontendEnvironment;
32
use ApacheSolrForTypo3\Solr\GarbageCollector;
33
use ApacheSolrForTypo3\Solr\System\Configuration\ExtensionConfiguration;
34
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
35
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
36
use ApacheSolrForTypo3\Solr\System\Records\Pages\PagesRepository;
37
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
38
use ApacheSolrForTypo3\Solr\Util;
39
use TYPO3\CMS\Backend\Utility\BackendUtility;
40
use TYPO3\CMS\Core\DataHandling\DataHandler;
41
use TYPO3\CMS\Core\Utility\GeneralUtility;
42
43
/**
44
 * A class that monitors changes to records so that the changed record gets
45
 * passed to the index queue to update the according index document.
46
 *
47
 * @author Ingo Renner <[email protected]>
48
 */
49
class RecordMonitor extends AbstractDataHandlerListener
50
{
51
    /**
52
     * Index Queue
53
     *
54
     * @var Queue
55
     */
56
    protected $indexQueue;
57
58
    /**
59
     * Mount Page Updater
60
     *
61
     * @var MountPagesUpdater
62
     */
63
    protected $mountPageUpdater;
64
65
    /**
66
     * TCA Service
67
     *
68
     * @var TCAService
69
     */
70
    protected $tcaService;
71
72
    /**
73
     * RootPageResolver
74
     *
75
     * @var RootPageResolver
76
     */
77
    protected $rootPageResolver;
78
79
    /**
80
     * @var PagesRepository
81
     */
82
    protected $pagesRepository;
83
84
    /**
85
     * @var SolrLogManager
86
     */
87
    protected $logger = null;
88
89 40
    /**
90
     * @var FrontendEnvironment
91 40
     */
92 40
    protected $frontendEnvironment = null;
93 40
94 40
    /**
95 40
     * RecordMonitor constructor.
96 40
     *
97 40
     * @param Queue|null $indexQueue
98
     * @param MountPagesUpdater|null $mountPageUpdater
99
     * @param TCAService|null $TCAService
100
     * @param RootPageResolver $rootPageResolver
101
     * @param PagesRepository|null $pagesRepository
102
     * @param SolrLogManager|null $solrLogManager
103
     * @param ConfigurationAwareRecordService|null $recordService
104
     */
105 27
    public function __construct(
106
        Queue $indexQueue = null,
107
        MountPagesUpdater $mountPageUpdater = null,
108
        TCAService $TCAService = null,
109 27
        RootPageResolver $rootPageResolver = null,
110
        PagesRepository $pagesRepository = null,
111
        SolrLogManager $solrLogManager = null,
112
        ConfigurationAwareRecordService $recordService = null,
113
        FrontendEnvironment $frontendEnvironment = null
114
    )
115
    {
116
        parent::__construct($recordService);
117
        $this->indexQueue = $indexQueue ?? GeneralUtility::makeInstance(Queue::class);
118
        $this->mountPageUpdater = $mountPageUpdater ?? GeneralUtility::makeInstance(MountPagesUpdater::class);
119
        $this->tcaService = $TCAService ?? GeneralUtility::makeInstance(TCAService::class);
120
        $this->rootPageResolver = $rootPageResolver ?? GeneralUtility::makeInstance(RootPageResolver::class);
121
        $this->pagesRepository = $pagesRepository ?? GeneralUtility::makeInstance(PagesRepository::class);
122
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
123
        $this->frontendEnvironment = $frontendEnvironment ?? GeneralUtility::makeInstance(FrontendEnvironment::class);
124
    }
125
126
    /**
127
     * @param SolrLogManager $logger
128
     */
129
    public function setLogger(SolrLogManager $logger)
130 2
    {
131
        $this->logger = $logger;
132
    }
133
134
    /**
135
     * Holds the configuration when a recursive page queing should be triggered.
136
     *
137
     * @var array
138 2
     * @return array
139
     */
140 1
    protected function getUpdateSubPagesRecursiveTriggerConfiguration()
141 1
    {
142
        return [
143 2
            // the current page has the field "extendToSubpages" enabled and the field "hidden" was set to 0 => requeue subpages
144
            'extendToSubpageEnabledAndHiddenFlagWasRemoved' => [
145
                'currentState' =>  ['extendToSubpages' => '1'],
146
                'changeSet' => ['hidden' => '0']
147
            ],
148
            // the current page has the field "hidden" enabled and the field "extendToSubpages" was set to 0 => requeue subpages
149
            'hiddenIsEnabledAndExtendToSubPagesWasRemoved' => [
150
                'currentState' =>  ['hidden' => '1'],
151
                'changeSet' => ['extendToSubpages' => '0']
152
            ]
153
        ];
154
    }
155 3
156
    /**
157
     * Hooks into TCE main and tracks record deletion commands.
158
     *
159
     * @param string $command The command.
160
     * @param string $table The table the record belongs to
161
     * @param int $uid The record's uid
162 3
     * @param string $value
163
     * @param DataHandler $tceMain TYPO3 Core Engine parent object
164
     */
165
    public function processCmdmap_preProcess(
166
        $command,
167
        $table,
168
        $uid,
169 3
        /** @noinspection PhpUnusedParameterInspection */
170
        $value,
171
        DataHandler $tceMain
172 1
    ) {
173
        if ($command === 'delete' && $table === 'tt_content' && $GLOBALS['BE_USER']->workspace == 0) {
174
            // skip workspaces: index only LIVE workspace
175 1
            $pid = $this->getValidatedPid($tceMain, $table, $uid);
176 1
            $this->indexQueue->updateItem('pages', $pid, time());
177 1
        }
178
    }
179 1
180 1
    /**
181 1
     * Hooks into TCE main and tracks workspace publish/swap events and
182
     * page move commands in LIVE workspace.
183
     *
184
     * @param string $command The command.
185
     * @param string $table The table the record belongs to
186
     * @param int $uid The record's uid
187
     * @param string $value
188 1
     * @param DataHandler $tceMain TYPO3 Core Engine parent object
189
     */
190
    public function processCmdmap_postProcess($command, $table, $uid, $value, DataHandler $tceMain)
191
    {
192
        if ($this->isDraftRecord($table, $uid)) {
193
            // skip workspaces: index only LIVE workspace
194
            return;
195
        }
196
197
        // track publish / swap events for records (workspace support)
198
        // command "version"
199
        if ($command === 'version' && $value['action'] === 'swap') {
200
            $this->applyVersionSwap($table, $uid, $tceMain);
201
        }
202
203
        // moving pages/records in LIVE workspace
204
        if ($command === 'move' && $GLOBALS['BE_USER']->workspace == 0) {
205
            if ($table === 'pages') {
206
                $this->applyPageChangesToQueue($uid);
207
            } else {
208
                $this->applyRecordChangesToQueue($table, $uid, $value);
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type integer expected by parameter $pid of ApacheSolrForTypo3\Solr\...yRecordChangesToQueue(). ( Ignorable by Annotation )

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

208
                $this->applyRecordChangesToQueue($table, $uid, /** @scrutinizer ignore-type */ $value);
Loading history...
209
            }
210
211
        }
212
    }
213
214
    /**
215
     * Apply's version swap to the IndexQueue.
216
     *
217 3
     * @param string $table
218
     * @param integer $uid
219
     * @param DataHandler $tceMain
220
     */
221
    protected function applyVersionSwap($table, $uid, DataHandler $tceMain)
222
    {
223
        $isPageRelatedRecord = $table === 'tt_content' || $table === 'pages';
224
        if($isPageRelatedRecord) {
225
            $uid = $table === 'tt_content' ? $this->getValidatedPid($tceMain, $table, $uid) : $uid;
226
            $this->applyPageChangesToQueue($uid);
227
        } else {
228
            $recordPageId = $this->getValidatedPid($tceMain, $table, $uid);
229
            $this->applyRecordChangesToQueue($table, $uid, $recordPageId);
230 3
        }
231
    }
232
233
    /**
234
     * Add's a page to the queue and updates mounts, when it is enabled, otherwise ensure that the page is removed
235
     * from the queue.
236
     *
237
     * @param integer $uid
238
     */
239
    protected function applyPageChangesToQueue($uid)
240
    {
241
        $solrConfiguration = $this->getSolrConfigurationFromPageId($uid);
242
        $record = $this->configurationAwareRecordService->getRecord('pages', $uid, $solrConfiguration);
243
        if (!empty($record) && $this->tcaService->isEnabledRecord('pages', $record)) {
244 35
            $this->mountPageUpdater->update($uid);
245
            $this->indexQueue->updateItem('pages', $uid);
246
        } else {
247
            // TODO should be moved to garbage collector
248
            $this->removeFromIndexAndQueueWhenItemInQueue('pages', $uid);
249
        }
250
    }
251 35
252 35
    /**
253
     * Add's a record to the queue if it is monitored and enabled, otherwise it removes the record from the queue.
254 35
     * 
255 1
     * @param string $table
256
     * @param integer $uid
257
     * @param integer $pid
258 34
     */
259 2
    protected function applyRecordChangesToQueue($table, $uid, $pid)
260
    {
261 34
        $solrConfiguration = $this->getSolrConfigurationFromPageId($pid);
262
        $isMonitoredTable = $solrConfiguration->getIndexQueueIsMonitoredTable($table);
263
264
        if ($isMonitoredTable) {
265
            $record = $this->configurationAwareRecordService->getRecord($table, $uid, $solrConfiguration);
266 34
267
            if (!empty($record) && $this->tcaService->isEnabledRecord($table, $record)) {
268
                $uid = $this->tcaService->getTranslationOriginalUidIfTranslated($table, $record, $uid);
269 33
                $this->indexQueue->updateItem($table, $uid);
270 6
            } else {
271 6
                // TODO should be moved to garbage collector
272
                $this->removeFromIndexAndQueueWhenItemInQueue($table, $uid);
273
            }
274 33
        }
275 33
    }
276
277
    /**
278
     * Hooks into TCE Main and watches all record creations and updates. If it
279
     * detects that the new/updated record belongs to a table configured for
280
     * indexing through Solr, we add the record to the index queue.
281
     *
282
     * @param string $status Status of the current operation, 'new' or 'update'
283 35
     * @param string $table The table the record belongs to
284
     * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...')
285 35
     * @param array $fields The record's data
286
     * @param DataHandler $tceMain TYPO3 Core Engine parent object
287 35
     * @return void
288 35
     */
289 35
    public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fields, DataHandler $tceMain) {
290
        $recordTable = $table;
291
        $recordUid = $uid;
292
293 35
        if ($this->skipMonitoringOfTable($table)) {
294 33
            return;
295
        }
296
297 2
        if ($status === 'new') {
298
            $recordUid = $tceMain->substNEWwithIDs[$recordUid];
299
        }
300
        if ($this->isDraftRecord($table, $recordUid)) {
301
            // skip workspaces: index only LIVE workspace
302
            return;
303
        }
304
305
        try {
306
            $recordPageId = $this->getRecordPageId($status, $recordTable, $recordUid, $uid, $fields, $tceMain);
307
308 33
            // when a content element changes we need to updated the page instead
309
            if ($recordTable === 'tt_content') {
310 33
                $recordTable = 'pages';
311
                $recordUid = $recordPageId;
312 33
            }
313
314
            $this->processRecord($recordTable, $recordPageId, $recordUid, $fields);
315
        } catch (NoPidException $e) {
316
            $message = 'Record without valid pid was processed ' . $table . ':' . $uid;
317
            $this->logger->log(SolrLogManager::WARNING, $message);
318 33
        }
319 33
    }
320
321 33
    /**
322
     * Check if the provided table is explicitly configured for monitoring
323
     *
324
     * @param string $table
325
     * @return bool
326 33
     */
327
    protected function skipMonitoringOfTable($table)
328 33
    {
329
        static $configurationMonitorTables;
330
331 1
        if (empty($configurationMonitorTables)) {
332 1
            $configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
333
            $configurationMonitorTables = $configuration->getIsUseConfigurationMonitorTables();
334 1
        }
335
336
        // No explicit configuration => all tables should be monitored
337
        if (empty($configurationMonitorTables)) {
338
            return false;
339
        }
340 32
341 32
        return !in_array($table, $configurationMonitorTables);
342 25
    }
343
344
    /**
345
     * Process the record located in processDatamap_afterDatabaseOperations
346 32
     *
347 32
     * @param string $recordTable The table the record belongs to
348
     * @param int $recordPageId pageid
349 3
     * @param mixed $recordUid The record's uid, [integer] or [string] (like 'NEW...')
350
     * @param array $fields The record's data
351
     */
352 3
    protected function processRecord($recordTable, $recordPageId, $recordUid, $fields)
353 3
    {
354
        if ($recordTable === 'pages') {
355
            $configurationPageId = $this->getConfigurationPageId($recordTable, $recordPageId, $recordUid);
356
            if ($configurationPageId === 0) {
357 32
                return;
358
            }
359 1
            $rootPageIds = [$configurationPageId];
360
        } else {
361
            try {
362 31
                $rootPageIds = $this->rootPageResolver->getResponsibleRootPageIds($recordTable, $recordUid);
363 28
                if (empty($rootPageIds)) {
364
                    $this->removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid);
365
                    return;
366 31
                }
367 27
            } catch ( \InvalidArgumentException $e) {
368
                $this->removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid);
369 31
                return;
370
            }
371
        }
372
        foreach ($rootPageIds as $configurationPageId) {
373
            $solrConfiguration = $this->getSolrConfigurationFromPageId($configurationPageId);
374
            $isMonitoredRecord = $solrConfiguration->getIndexQueueIsMonitoredTable($recordTable);
375
            if (!$isMonitoredRecord) {
376
                // when it is a non monitored record, we can skip it.
377
                continue;
378
            }
379 33
380
            $record = $this->configurationAwareRecordService->getRecord($recordTable, $recordUid, $solrConfiguration);
381 33
            if (empty($record)) {
382 33
                // TODO move this part to the garbage collector
383 31
                // check if the item should be removed from the index because it no longer matches the conditions
384
                $this->removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid);
385
                continue;
386 2
            }
387 2
            // Clear existing index queue items to prevent mount point duplicates.
388 2
            // This needs to be done before the overlay handling, because handling an overlay record should
389
            // not trigger a deletion.
390
            $isTranslation = !empty($record['sys_language_uid']) && $record['sys_language_uid'] !== 0;
391
            if ($recordTable === 'pages' && !$isTranslation) {
392
                $this->indexQueue->deleteItem('pages', $recordUid);
393
            }
394
395
            // only update/insert the item if we actually found a record
396
            $isLocalizedRecord = $this->tcaService->isLocalizedRecord($recordTable, $record);
397
            $recordUid = $this->tcaService->getTranslationOriginalUidIfTranslated($recordTable, $record, $recordUid);
398 3
399
            if ($isLocalizedRecord && !$this->getIsTranslationParentRecordEnabled($recordTable, $recordUid)) {
400 3
                // we have a localized record without a visible parent record. Nothing to do.
401 3
                continue;
402 3
            }
403
404
            if ($this->tcaService->isEnabledRecord($recordTable, $record)) {
405
                $this->indexQueue->updateItem($recordTable, $recordUid);
406
            }
407
408
            if ($recordTable === 'pages') {
409
                $this->doPagesPostUpdateOperations($fields, $recordUid);
410
            }
411 27
        }
412
    }
413 27
414 27
    /**
415
     * This method is used to determine the pageId that should be used to retrieve the index queue configuration.
416 27
     *
417 5
     * @param string $recordTable
418 5
     * @param integer $recordPageId
419
     * @param integer $recordUid
420 27
     * @return integer
421
     */
422
    protected function getConfigurationPageId($recordTable, $recordPageId, $recordUid)
423
    {
424
        $rootPageId = $this->rootPageResolver->getRootPageId($recordPageId);
425
        if ($this->rootPageResolver->getIsRootPageId($rootPageId)) {
426
            return $recordPageId;
427
        }
428
429
        $alternativeSiteRoots = $this->rootPageResolver->getAlternativeSiteRootPagesIds($recordTable, $recordUid, $recordPageId);
430
        $lastRootPage = array_pop($alternativeSiteRoots);
431
        return empty($lastRootPage) ? 0 : $lastRootPage;
432
    }
433
434 34
    /**
435
     * Checks if the parent record of the translated record is enabled.
436 34
     *
437
     * @param string $recordTable
438
     * @param integer $recordUid
439
     * @return bool
440 34
     */
441 22
    protected function getIsTranslationParentRecordEnabled($recordTable, $recordUid)
442 21
    {
443 7
        $l10nParentRecord = (array)BackendUtility::getRecord($recordTable, $recordUid, '*', '', false);
444
        return $this->tcaService->isEnabledRecord($recordTable, $l10nParentRecord);
445
    }
446 21
447
    /**
448
     * Applies needed updates when a pages record was processed by the RecordMonitor.
449 12
     *
450
     * @param array $fields
451
     * @param int $recordUid
452
     */
453
    protected function doPagesPostUpdateOperations(array $fields, $recordUid)
454
    {
455
        $this->updateCanonicalPages($recordUid);
456
        $this->mountPageUpdater->update($recordUid);
457 5
458
        if ($this->isRecursivePageUpdateRequired($recordUid, $fields)) {
459 5
            $treePageIds = $this->getSubPageIds($recordUid);
460 4
            $this->updatePageIdItems($treePageIds);
461
        }
462 5
    }
463
464
    /**
465
     * Determines the recordPageId (pid) of a record.
466
     *
467
     * @param string $status
468
     * @param string $recordTable
469
     * @param int $recordUid
470 1
     * @param int $originalUid
471
     * @param array $fields
472 1
     * @param DataHandler $tceMain
473 1
     *
474 1
     * @return int
475
     */
476
    protected function getRecordPageId($status, $recordTable, $recordUid, $originalUid, array $fields, DataHandler $tceMain)
477
    {
478
        if ($recordTable === 'pages' && isset($fields['l10n_parent']) && intval($fields['l10n_parent']) > 0) {
479
            return $fields['l10n_parent'];
480
        }
481
482
        if ($status === 'update' && !isset($fields['pid'])) {
483
            $recordPageId = $this->getValidatedPid($tceMain, $recordTable, $recordUid);
484 27
            if (($recordTable === 'pages') && ($this->rootPageResolver->getIsRootPageId($recordUid))) {
485
                $recordPageId = $originalUid;
486 27
            }
487 27
488
            return $recordPageId;
489
        }
490 27
491
        return $fields['pid'];
492
    }
493
494
    /**
495
     * Applies the updateItem instruction on a collection of pageIds.
496
     *
497
     * @param array $treePageIds
498
     */
499
    protected function updatePageIdItems(array $treePageIds)
500
    {
501 23
        foreach ($treePageIds as $treePageId) {
502
            $this->indexQueue->updateItem('pages', $treePageId);
503 23
        }
504 23
    }
505 1
506
    /**
507
     * Removes record from the index queue and from the solr index
508 22
     *
509 22
     * @param string $recordTable Name of table where the record lives
510
     * @param int $recordUid Id of record
511
     */
512
    protected function removeFromIndexAndQueue($recordTable, $recordUid)
513
    {
514
        $garbageCollector = GeneralUtility::makeInstance(GarbageCollector::class);
515
        $garbageCollector->collectGarbage($recordTable, $recordUid);
516
    }
517
518
    /**
519
     * Removes record from the index queue and from the solr index when the item is in the queue.
520
     *
521
     * @param string $recordTable Name of table where the record lives
522
     * @param int $recordUid Id of record
523
     */
524
    protected function removeFromIndexAndQueueWhenItemInQueue($recordTable, $recordUid)
525
    {
526
        if (!$this->indexQueue->containsItem($recordTable, $recordUid)) {
527
            return;
528
        }
529
530
        $this->removeFromIndexAndQueue($recordTable, $recordUid);
531
    }
532
533
    // Handle pages showing content from another page
534
535
    /**
536
     * Triggers Index Queue updates for other pages showing content from the
537
     * page currently being updated.
538
     *
539
     * @param int $pageId UID of the page currently being updated
540
     */
541
    protected function updateCanonicalPages($pageId)
542
    {
543
        $canonicalPages = $this->pagesRepository->findPageUidsWithContentsFromPid((int)$pageId);
544
        foreach ($canonicalPages as $page) {
545
            $this->indexQueue->updateItem('pages', $page['uid']);
546
        }
547
    }
548
549
    /**
550
     * Retrieves the pid of a record and throws an exception when getPid returns false.
551
     *
552
     * @param DataHandler $tceMain
553
     * @param string $table
554
     * @param integer $uid
555
     * @throws NoPidException
556
     * @return integer
557
     */
558
    protected function getValidatedPid(DataHandler $tceMain, $table, $uid)
559
    {
560
        $pid = $tceMain->getPID($table, $uid);
561
        if ($pid === false) {
562
            throw new NoPidException('Pid should not be false');
563
        }
564
565
        $pid = intval($pid);
566
        return $pid;
567
    }
568
569
    /**
570
     * Checks if the record is a draft record.
571
     *
572
     * @param string $table
573
     * @param int $uid
574
     * @return bool
575
     */
576
    protected function isDraftRecord($table, $uid)
577
    {
578
        return Util::isDraftRecord($table, $uid);
579
    }
580
581
    /**
582
     * @param $pageId
583
     * @param bool $initializeTsfe
584
     * @param int $language
585
     * @return TypoScriptConfiguration
586
     */
587
    protected function getSolrConfigurationFromPageId($pageId)
588
    {
589
        return $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId);
590
    }
591
}
592