Completed
Push — master ( aa6910...722d75 )
by Timo
16:42
created

Queue::updateItem()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0072

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 12
cts 13
cp 0.9231
rs 9.0534
cc 4
eloc 13
nc 4
nop 3
crap 4.0072
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 2 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\RecordMonitor\Helper\ConfigurationAwareRecordService;
28
use ApacheSolrForTypo3\Solr\Domain\Index\Queue\RecordMonitor\Helper\RootPageResolver;
29
use ApacheSolrForTypo3\Solr\Site;
30
use ApacheSolrForTypo3\Solr\System\Cache\TwoLevelCache;
31
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
32
use ApacheSolrForTypo3\Solr\Util;
33
use ApacheSolrForTypo3\Solr\Utility\DatabaseUtility;
34
use TYPO3\CMS\Backend\Utility\BackendUtility;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
37
/**
38
 * The Indexing Queue. It allows us to decouple from frontend indexing and
39
 * reacting to changes faster.
40
 *
41
 * @author Ingo Renner <[email protected]>
42
 */
43
class Queue
44
{
45
    /**
46
     * @var RootPageResolver
47
     */
48
    protected $rootPageResolver;
49
50
    /**
51
     * @var ConfigurationAwareRecordService
52
     */
53
    protected $recordService;
54
55
    /**
56
     * @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager
57
     */
58
    protected $logger = null;
59
60
    /**
61
     * Queue constructor.
62
     * @param RootPageResolver|null $rootPageResolver
63
     * @param ConfigurationAwareRecordService|null $recordService
64
     */
65 68
    public function __construct(RootPageResolver $rootPageResolver = null, ConfigurationAwareRecordService $recordService = null)
66
    {
67 68
        $this->logger = GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__);
68 68
        $this->rootPageResolver = isset($rootPageResolver) ? $rootPageResolver : GeneralUtility::makeInstance(RootPageResolver::class);
69 68
        $this->recordService = isset($recordService) ? $recordService : GeneralUtility::makeInstance(ConfigurationAwareRecordService::class);
70 68
    }
71
72
    // FIXME some of the methods should be renamed to plural forms
73
    // FIXME singular form methods should deal with exactly one item only
74
75
    /**
76
     * Returns the timestamp of the last indexing run.
77
     *
78
     * @param int $rootPageId The root page uid for which to get
79
     *      the last indexed item id
80
     * @return int Timestamp of last index run.
81
     */
82
    public function getLastIndexTime($rootPageId)
83
    {
84
        $lastIndexTime = 0;
85
86
        $lastIndexedRow = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
87
            'indexed',
88
            'tx_solr_indexqueue_item',
89
            'root = ' . (int)$rootPageId,
90
            '',
91
            'indexed DESC',
92
            1
93
        );
94
95
        if ($lastIndexedRow[0]['indexed']) {
96
            $lastIndexTime = $lastIndexedRow[0]['indexed'];
97
        }
98
99
        return $lastIndexTime;
100
    }
101
102
    /**
103
     * Returns the uid of the last indexed item in the queue
104
     *
105
     * @param int $rootPageId The root page uid for which to get
106
     *      the last indexed item id
107
     * @return int The last indexed item's ID.
108
     */
109
    public function getLastIndexedItemId($rootPageId)
110
    {
111
        $lastIndexedItemId = 0;
112
113
        $lastIndexedItemRow = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
114
            'uid',
115
            'tx_solr_indexqueue_item',
116
            'root = ' . (int)$rootPageId,
117
            '',
118
            'indexed DESC',
119
            1
120
        );
121
        if ($lastIndexedItemRow[0]['uid']) {
122
            $lastIndexedItemId = $lastIndexedItemRow[0]['uid'];
123
        }
124
125
        return $lastIndexedItemId;
126
    }
127
128
    /**
129
     * Truncate and rebuild the tx_solr_indexqueue_item table. This is the most
130
     * complete way to force reindexing, or to build the Index Queue for the
131
     * first time. The Index Queue initialization is site-specific.
132
     *
133
     * @param Site $site The site to initialize
134
     * @param string $indexingConfigurationName Name of a specific
135
     *      indexing configuration
136
     * @return array An array of booleans, each representing whether the
137
     *      initialization for an indexing configuration was successful
138
     */
139 6
    public function initialize(Site $site, $indexingConfigurationName = '')
140
    {
141 6
        $indexingConfigurations = [];
142 6
        $initializationStatus = [];
143
144 6
        if (empty($indexingConfigurationName)) {
145 1
            $solrConfiguration = $site->getSolrConfiguration();
146 1
            $indexingConfigurations = $solrConfiguration->getEnabledIndexQueueConfigurationNames();
147
        } else {
148 5
            $indexingConfigurations[] = $indexingConfigurationName;
149
        }
150
151 6
        foreach ($indexingConfigurations as $indexingConfigurationName) {
152 6
            $initializationStatus[$indexingConfigurationName] = $this->initializeIndexingConfiguration(
153
                $site,
154
                $indexingConfigurationName
155
            );
156
        }
157
158 6
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessIndexQueueInitialization'])) {
159
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessIndexQueueInitialization'] as $classReference) {
160
                $indexQueueInitializationPostProcessor = GeneralUtility::getUserObj($classReference);
161
162
                if ($indexQueueInitializationPostProcessor instanceof InitializationPostProcessor) {
163
                    $indexQueueInitializationPostProcessor->postProcessIndexQueueInitialization(
164
                        $site,
165
                        $indexingConfigurations,
166
                        $initializationStatus
167
                    );
168
                } else {
169
                    throw new \UnexpectedValueException(
170
                        get_class($indexQueueInitializationPostProcessor) .
171
                        ' must implement interface ' . InitializationPostProcessor::class,
172
                        1345815561
173
                    );
174
                }
175
            }
176
        }
177
178 6
        return $initializationStatus;
179
    }
180
181
    /**
182
     * Initializes the Index Queue for a specific indexing configuration.
183
     *
184
     * @param Site $site The site to initialize
185
     * @param string $indexingConfigurationName name of a specific
186
     *      indexing configuration
187
     * @return bool TRUE if the initialization was successful, FALSE otherwise
188
     */
189 6
    protected function initializeIndexingConfiguration(
190
        Site $site,
191
        $indexingConfigurationName
192
    ) {
193
        // clear queue
194 6
        $this->deleteItemsBySite($site, $indexingConfigurationName);
195
196 6
        $solrConfiguration = $site->getSolrConfiguration();
197
198 6
        $tableToIndex = $solrConfiguration->getIndexQueueTableNameOrFallbackToConfigurationName($indexingConfigurationName);
199 6
        $initializerClass = $solrConfiguration->getIndexQueueInitializerClassByConfigurationName($indexingConfigurationName);
200
201 6
        $initializer = GeneralUtility::makeInstance($initializerClass);
202
        /** @var $initializer \ApacheSolrForTypo3\Solr\IndexQueue\Initializer\AbstractInitializer */
203 6
        $initializer->setSite($site);
204 6
        $initializer->setType($tableToIndex);
205 6
        $initializer->setIndexingConfigurationName($indexingConfigurationName);
206
207 6
        $indexConfiguration = $solrConfiguration->getIndexQueueConfigurationByName($indexingConfigurationName);
208 6
        $initializer->setIndexingConfiguration($indexConfiguration);
209
210 6
        return $initializer->initialize();
211
    }
212
213
    /**
214
     * Gets the indexing configuration to use for an item.
215
     * Sometimes, when there are multiple configurations for a certain item type
216
     * (table) it can be hard or even impossible to find which one to use
217
     * though.
218
     * Currently selects the first indexing configuration where the name matches
219
     * the itemType or where the configured tbale is the same as the itemType.
220
     *
221
     * !!! Might return incorrect results for complex configurations !!!
222
     * Try to set the indexingConfiguration directly when using the updateItem()
223
     * method in such situations.
224
     *
225
     * @param string $itemType The item's type, usually a table name.
226
     * @param string $itemUid The item's uid, usually an integer uid, could be a
227
     *      different value for non-database-record types.
228
     * @param int $rootPageId The configuration's page tree's root page id.
229
     *      Optional, not needed for all types.
230
     * @return string The indexing configuration's name to use when indexing
231
     * @deprecated since 6.1 will be removed in 7.0
232
     * Use getIndexingConfigurationsByItem() now, which behaves
233
     * almost the same way but returns an array of configurations
234
     */
235
    protected function getIndexingConfigurationByItem(
236
        $itemType,
237
        $itemUid,
0 ignored issues
show
Unused Code introduced by
The parameter $itemUid is not used and could be removed.

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

Loading history...
238
        $rootPageId = null
239
    ) {
240
        GeneralUtility::logDeprecatedFunction();
241
        $indexingConfigurationName = '';
242
243
        $configurations = $this->getIndexingConfigurationsByItem($itemType, $rootPageId);
0 ignored issues
show
Deprecated Code introduced by
The method ApacheSolrForTypo3\Solr\...gConfigurationsByItem() has been deprecated with message: since 6.1 will be removed in 7.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
244
        if (count($configurations) > 0) {
245
            $indexingConfigurationName = $configurations[0];
246
        }
247
248
        return $indexingConfigurationName;
249
    }
250
251
    /**
252
     * Gets the indexing configurations to use for an item.
253
     * Multiple configurations for a certain item type (table) might be available.
254
     *
255
     * @param string $itemType The item's type, usually a table name.
256
     * @param int $rootPageId The configuration's page tree's root page id.
257
     *      Optional, not needed for all types.
258
     * @return array<string> The indexing configurations names to use when indexing
259
     * @deprecated since 6.1 will be removed in 7.0
260
     */
261
    protected function getIndexingConfigurationsByItem(
262
        $itemType,
263
        $rootPageId = null
264
    ) {
265
        GeneralUtility::logDeprecatedFunction();
266
267
        $possibleIndexingConfigurationNames = [];
268
269
        if (!is_null($rootPageId)) {
270
            // get configuration for the root's branch
271
            $solrConfiguration = Util::getSolrConfigurationFromPageId($rootPageId);
272
            $possibleIndexingConfigurationNames = $solrConfiguration->getIndexQueueConfigurationNamesByTableName($itemType);
273
        }
274
275
        return $possibleIndexingConfigurationNames;
276
    }
277
278
    /**
279
     * Marks an item as needing (re)indexing.
280
     *
281
     * Like with Solr itself, there's no add method, just a simple update method
282
     * that handles the adds, too.
283
     *
284
     * The method creates or updates the index queue items for all related rootPageIds.
285
     *
286
     * @param string $itemType The item's type, usually a table name.
287
     * @param string $itemUid The item's uid, usually an integer uid, could be a
288
     *      different value for non-database-record types.
289
     * @param int $forcedChangeTime The change time for the item if set, otherwise
290
     *          value from getItemChangedTime() is used.
291
     */
292 47
    public function updateItem($itemType, $itemUid, $forcedChangeTime = 0)
293
    {
294 47
        $rootPageIds = $this->rootPageResolver->getResponsibleRootPageIds($itemType, $itemUid);
295 46
        foreach ($rootPageIds as $rootPageId) {
296 46
            $skipInvalidRootPage = $rootPageId === 0;
297 46
            if ($skipInvalidRootPage) {
298
                continue;
299
            }
300
301 46
            $solrConfiguration = Util::getSolrConfigurationFromPageId($rootPageId);
302 46
            $indexingConfiguration = $this->recordService->getIndexingConfigurationName($itemType, $itemUid, $solrConfiguration);
303 46
            $itemInQueueForRootPage = $this->containsItemWithRootPageId($itemType, $itemUid, $rootPageId);
304 46
            if ($itemInQueueForRootPage) {
305
                // update the existing queue item
306 10
                $this->updateExistingItem($itemType, $itemUid, $indexingConfiguration, $rootPageId, $forcedChangeTime);
307
            } else {
308
                // add the item since it's not in the queue yet
309 46
                $this->addNewItem($itemType, $itemUid, $indexingConfiguration, $rootPageId);
310
            }
311
        }
312 46
    }
313
314
    /**
315
     * Updates an existing queue entry by $itemType $itemUid and $rootPageId.
316
     *
317
     * @param string $itemType  The item's type, usually a table name.
318
     * @param int $itemUid The item's uid, usually an integer uid, could be a
319
     *      different value for non-database-record types.
320
     * @param string $indexingConfiguration The name of the related indexConfiguration
321
     * @param int $rootPageId The uid of the rootPage
322
     * @param int $forcedChangeTime The forced change time that should be used for updating
323
     */
324 10
    protected function updateExistingItem($itemType, $itemUid, $indexingConfiguration, $rootPageId, $forcedChangeTime)
325
    {
326
        // update if that item is in the queue already
327
        $changes = [
328 10
            'changed' => ($forcedChangeTime > 0) ? $forcedChangeTime : $this->getItemChangedTime($itemType, $itemUid)
329
        ];
330
331 10
        if (!empty($indexingConfiguration)) {
332 10
            $changes['indexing_configuration'] = $indexingConfiguration;
333
        }
334
335 10
        $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
336 10
            'tx_solr_indexqueue_item',
337 10
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType, 'tx_solr_indexqueue_item') .
338 10
            ' AND item_uid = ' . (int)$itemUid . ' AND root = ' . (int)$rootPageId,
339
            $changes);
340 10
    }
341
342
    /**
343
     * Adds an item to the index queue.
344
     *
345
     * Not meant for public use.
346
     *
347
     * @param string $itemType The item's type, usually a table name.
348
     * @param string $itemUid The item's uid, usually an integer uid, could be a
349
     *      different value for non-database-record types.
350
     * @param string $indexingConfiguration The item's indexing configuration to use.
351
     *      Optional, overwrites existing / determined configuration.
352
     * @return void
353
     */
354 40
    private function addNewItem($itemType, $itemUid, $indexingConfiguration, $rootPageId)
355
    {
356 40
        $additionalRecordFields = '';
357 40
        if ($itemType == 'pages') {
358 27
            $additionalRecordFields = ', doktype, uid';
359
        }
360
361 40
        $record = $this->getRecordCached($itemType, $itemUid, $additionalRecordFields);
362
363 40
        if (empty($record) || ($itemType == 'pages' && !Util::isAllowedPageType($record, $indexingConfiguration))) {
364 1
            return;
365
        }
366
367
        $item = [
368 39
            'root' => $rootPageId,
369 39
            'item_type' => $itemType,
370 39
            'item_uid' => $itemUid,
371 39
            'changed' => $this->getItemChangedTime($itemType, $itemUid),
372 39
            'errors' => ''
373
        ];
374
375
        // make a backup of the current item
376 39
        $item['indexing_configuration'] = $indexingConfiguration;
377 39
        $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_solr_indexqueue_item', $item);
378 39
    }
379
380
    /**
381
     * Get record to be added in addNewItem
382
     *
383
     * @param string $itemType The item's type, usually a table name.
384
     * @param string $itemUid The item's uid, usually an integer uid, could be a
385
     *      different value for non-database-record types.
386
     * @param string $additionalRecordFields for sql-query
387
     *
388
     * @return array|NULL
389
     */
390 40
    protected function getRecordCached($itemType, $itemUid, $additionalRecordFields)
391
    {
392 40
        $cache = GeneralUtility::makeInstance(TwoLevelCache::class, 'cache_runtime');
393 40
        $cacheId = md5('Queue' . ':' . 'getRecordCached' . ':' . $itemType . ':' . $itemUid . ':' . 'pid' . $additionalRecordFields);
394
395 40
        $record = $cache->get($cacheId);
396 40
        if (empty($record)) {
397 40
            $record = BackendUtility::getRecord($itemType, $itemUid, 'pid' . $additionalRecordFields);
398 40
            $cache->set($cacheId, $record);
399
        }
400
401 40
        return $record;
402
    }
403
404
    /**
405
     * Determines the time for when an item should be indexed. This timestamp
406
     * is then stored in the changed column in the Index Queue.
407
     *
408
     * The changed timestamp usually is now - time(). For records which are set
409
     * to published at a later time, this timestamp is the start time. So if a
410
     * future start time has been set, that will be used to delay indexing
411
     * of an item.
412
     *
413
     * @param string $itemType The item's table name.
414
     * @param string $itemUid The item's uid, usually an integer uid, could be a
415
     *      different value for non-database-record types.
416
     * @return int Timestamp of the item's changed time or future start time
417
     */
418 45
    protected function getItemChangedTime($itemType, $itemUid)
419
    {
420 45
        $itemTypeHasStartTimeColumn = false;
421 45
        $changedTimeColumns = $GLOBALS['TCA'][$itemType]['ctrl']['tstamp'];
422 45
        $startTime = 0;
423 45
        $pageChangedTime = 0;
424
425 45
        if (!empty($GLOBALS['TCA'][$itemType]['ctrl']['enablecolumns']['starttime'])) {
426 45
            $itemTypeHasStartTimeColumn = true;
427 45
            $changedTimeColumns .= ', ' . $GLOBALS['TCA'][$itemType]['ctrl']['enablecolumns']['starttime'];
428
        }
429 45
        if ($itemType == 'pages') {
430
            // does not carry time information directly, but needed to support
431
            // canonical pages
432 32
            $changedTimeColumns .= ', content_from_pid';
433
        }
434
435 45
        $record = BackendUtility::getRecord($itemType, $itemUid, $changedTimeColumns);
436 45
        $itemChangedTime = $record[$GLOBALS['TCA'][$itemType]['ctrl']['tstamp']];
437
438 45
        if ($itemTypeHasStartTimeColumn) {
439 45
            $startTime = $record[$GLOBALS['TCA'][$itemType]['ctrl']['enablecolumns']['starttime']];
440
        }
441
442 45
        if ($itemType == 'pages') {
443 32
            $record['uid'] = $itemUid;
444
            // overrule the page's last changed time with the most recent
445
            //content element change
446 32
            $pageChangedTime = $this->getPageItemChangedTime($record);
447
        }
448
449 45
        $localizationsChangedTime = $this->getLocalizableItemChangedTime($itemType, $itemUid);
450
451
        // if start time exists and start time is higher than last changed timestamp
452
        // then set changed to the future start time to make the item
453
        // indexed at a later time
454 45
        $changedTime = max(
455
            $itemChangedTime,
456
            $pageChangedTime,
457
            $localizationsChangedTime,
458
            $startTime
459
        );
460
461 45
        return $changedTime;
462
    }
463
464
    /**
465
     * Gets the most recent changed time of a page's content elements
466
     *
467
     * @param array $page Partial page record
468
     * @return int Timestamp of the most recent content element change
469
     */
470 32
    protected function getPageItemChangedTime(array $page)
471
    {
472 32
        if (!empty($page['content_from_pid'])) {
473
            // canonical page, get the original page's last changed time
474
            $pageContentLastChangedTime = $this->getPageItemChangedTime(['uid' => $page['content_from_pid']]);
475
        } else {
476 32
            $pageContentLastChangedTime = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
477 32
                'MAX(tstamp) AS changed_time',
478 32
                'tt_content',
479 32
                'pid = ' . (int)$page['uid']
480
            );
481 32
            $pageContentLastChangedTime = $pageContentLastChangedTime['changed_time'];
482
        }
483
484 32
        return $pageContentLastChangedTime;
485
    }
486
487
    /**
488
     * Gets the most recent changed time for an item taking into account
489
     * localized records.
490
     *
491
     * @param string $itemType The item's type, usually a table name.
492
     * @param string $itemUid The item's uid, usually an integer uid, could be a
493
     *      different value for non-database-record types.
494
     * @return int Timestamp of the most recent content element change
495
     */
496 45
    protected function getLocalizableItemChangedTime($itemType, $itemUid)
497
    {
498 45
        $localizedChangedTime = 0;
499
500 45
        if (isset($GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'])) {
501
            // table is localizable
502 13
            $translationOriginalPointerField = $GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'];
503
504 13
            $itemUid = intval($itemUid);
505 13
            $localizedChangedTime = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
506 13
                'MAX(tstamp) AS changed_time',
507
                $itemType,
508 13
                "uid = $itemUid OR $translationOriginalPointerField = $itemUid"
509
            );
510 13
            $localizedChangedTime = $localizedChangedTime['changed_time'];
511
        }
512
513 45
        return $localizedChangedTime;
514
    }
515
516
    /**
517
     * Checks whether the Index Queue contains a specific item.
518
     *
519
     * @param string $itemType The item's type, usually a table name.
520
     * @param string $itemUid The item's uid, usually an integer uid, could be a
521
     *      different value for non-database-record types.
522
     * @return bool TRUE if the item is found in the queue, FALSE otherwise
523
     */
524 3
    public function containsItem($itemType, $itemUid)
525
    {
526 3
        $itemIsInQueue = (boolean)$GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
527 3
            'uid',
528 3
            'tx_solr_indexqueue_item',
529 3
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType,
530 3
                'tx_solr_indexqueue_item') .
531 3
            ' AND item_uid = ' . (int)$itemUid
532
        );
533
534 3
        return $itemIsInQueue;
535
    }
536
537
    /**
538
     * Checks whether the Index Queue contains a specific item.
539
     *
540
     * @param string $itemType The item's type, usually a table name.
541
     * @param string $itemUid The item's uid, usually an integer uid, could be a
542
     *      different value for non-database-record types.
543
     * @param integer $rootPageId
544
     * @return bool TRUE if the item is found in the queue, FALSE otherwise
545
     */
546 46
    public function containsItemWithRootPageId($itemType, $itemUid, $rootPageId)
547
    {
548 46
        $itemIsInQueue = (boolean)$GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
549 46
            'uid',
550 46
            'tx_solr_indexqueue_item',
551 46
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType,
552 46
                'tx_solr_indexqueue_item') .
553 46
            ' AND item_uid = ' . (int)$itemUid . ' AND root = ' . (int)$rootPageId
554
        );
555
556 46
        return $itemIsInQueue;
557
    }
558
559
    /**
560
     * Checks whether the Index Queue contains a specific item that has been
561
     * marked as indexed.
562
     *
563
     * @param string $itemType The item's type, usually a table name.
564
     * @param string $itemUid The item's uid, usually an integer uid, could be a
565
     *      different value for non-database-record types.
566
     * @return bool TRUE if the item is found in the queue and marked as
567
     *      indexed, FALSE otherwise
568
     */
569 3
    public function containsIndexedItem($itemType, $itemUid)
570
    {
571 3
        $itemIsInQueue = (boolean)$GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
572 3
            'uid',
573 3
            'tx_solr_indexqueue_item',
574 3
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType,
575 3
                'tx_solr_indexqueue_item') .
576 3
            ' AND item_uid = ' . (int)$itemUid .
577 3
            ' AND indexed > 0'
578
        );
579
580 3
        return $itemIsInQueue;
581
    }
582
583
    /**
584
     * Removes an item from the Index Queue.
585
     *
586
     * @param string $itemType The type of the item to remove, usually a table name.
587
     * @param int $itemUid The uid of the item to remove
588
     */
589 26
    public function deleteItem($itemType, $itemUid)
590
    {
591 26
        $uidList = [];
592
593
        // get the item uids to use them in the deletes afterwards
594 26
        $items = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
595 26
            'uid',
596 26
            'tx_solr_indexqueue_item',
597 26
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType,
598 26
                'tx_solr_indexqueue_item') .
599 26
            ' AND item_uid = ' . intval($itemUid)
600
        );
601
602 26
        if (count($items)) {
603 9
            foreach ($items as $item) {
604 9
                $uidList[] = $item['uid'];
605
            }
606
607 9
            $GLOBALS['TYPO3_DB']->exec_DELETEquery(
608 9
                'tx_solr_indexqueue_item',
609 9
                'uid IN(' . implode(',', $uidList) . ')'
610
            );
611 9
            $GLOBALS['TYPO3_DB']->exec_DELETEquery(
612 9
                'tx_solr_indexqueue_indexing_property',
613 9
                'item_id IN(' . implode(',', $uidList) . ')'
614
            );
615
        }
616 26
    }
617
618
    /**
619
     * Removes all items of a certain type from the Index Queue.
620
     *
621
     * @param string $itemType The type of items to remove, usually a table name.
622
     */
623 1
    public function deleteItemsByType($itemType)
624
    {
625 1
        $uidList = [];
626
627
        // get the item uids to use them in the deletes afterwards
628 1
        $items = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
629 1
            'uid',
630 1
            'tx_solr_indexqueue_item',
631 1
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr(
632
                $itemType,
633 1
                'tx_solr_indexqueue_item'
634
            )
635
        );
636
637 1
        if (count($items)) {
638 1
            foreach ($items as $item) {
639 1
                $uidList[] = $item['uid'];
640
            }
641
642 1
            $GLOBALS['TYPO3_DB']->exec_DELETEquery(
643 1
                'tx_solr_indexqueue_item',
644 1
                'uid IN(' . implode(',', $uidList) . ')'
645
            );
646 1
            $GLOBALS['TYPO3_DB']->exec_DELETEquery(
647 1
                'tx_solr_indexqueue_indexing_property',
648 1
                'item_id IN(' . implode(',', $uidList) . ')'
649
            );
650
        }
651 1
    }
652
653
    /**
654
     * Removes all items of a certain site from the Index Queue. Accepts an
655
     * optional parameter to limit the deleted items by indexing configuration.
656
     *
657
     * @param Site $site The site to remove items for.
658
     * @param string $indexingConfigurationName Name of a specific indexing
659
     *      configuration
660
     */
661 6
    public function deleteItemsBySite(
662
        Site $site,
663
        $indexingConfigurationName = ''
664
    ) {
665 6
        $rootPageConstraint = 'tx_solr_indexqueue_item.root = ' . $site->getRootPageId();
666
667 6
        $indexingConfigurationConstraint = '';
668 6
        if (!empty($indexingConfigurationName)) {
669
            $indexingConfigurationConstraint =
670
                ' AND tx_solr_indexqueue_item.indexing_configuration = \'' .
671 6
                $indexingConfigurationName . '\'';
672
        }
673
674 6
        DatabaseUtility::transactionStart();
675
        try {
676
            // reset Index Queue
677 6
            $result = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
678 6
                'tx_solr_indexqueue_item',
679 6
                $rootPageConstraint . $indexingConfigurationConstraint
680
            );
681 6
            if (!$result) {
682
                throw new \RuntimeException(
683
                    'Failed to reset Index Queue for site ' . $site->getLabel(),
684
                    1412986560
685
                );
686
            }
687
688
            // reset Index Queue Properties
689
            $indexQueuePropertyResetQuery = '
690
                DELETE tx_solr_indexqueue_indexing_property.*
691
                FROM tx_solr_indexqueue_indexing_property
692
                INNER JOIN tx_solr_indexqueue_item
693
                    ON tx_solr_indexqueue_item.uid = tx_solr_indexqueue_indexing_property.item_id
694
                    AND ' .
695 6
                $rootPageConstraint .
696 6
                $indexingConfigurationConstraint;
697
698 6
            $result = $GLOBALS['TYPO3_DB']->sql_query($indexQueuePropertyResetQuery);
699 6
            if (!$result) {
700
                throw new \RuntimeException(
701
                    'Failed to reset Index Queue properties for site ' . $site->getLabel(),
702
                    1412986604
703
                );
704
            }
705
706 6
            DatabaseUtility::transactionCommit();
707
        } catch (\RuntimeException $e) {
708
            DatabaseUtility::transactionRollback();
709
        }
710 6
    }
711
712
    /**
713
     * Removes all items from the Index Queue.
714
     *
715
     */
716 1
    public function deleteAllItems()
717
    {
718 1
        $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery('tx_solr_indexqueue_item', '');
719 1
    }
720
721
    /**
722
     * Gets a single Index Queue item by its uid.
723
     *
724
     * @param int $itemId Index Queue item uid
725
     * @return Item The request Index Queue item or NULL
726
     *      if no item with $itemId was found
727
     */
728 11
    public function getItem($itemId)
729
    {
730 11
        $item = null;
731
732 11
        $indexQueueItemRecord = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
733 11
            '*',
734 11
            'tx_solr_indexqueue_item',
735 11
            'uid = ' . intval($itemId)
736
        );
737
738 11
        if (count($indexQueueItemRecord) == 1) {
739 11
            $indexQueueItemRecord = $indexQueueItemRecord[0];
740
741 11
            $item = GeneralUtility::makeInstance(
742 11
                Item::class,
743
                $indexQueueItemRecord
744
            );
745
        }
746
747 11
        return $item;
748
    }
749
750
    /**
751
     * Gets Index Queue items by type and uid.
752
     *
753
     * @param string $itemType item type, usually  the table name
754
     * @param int $itemUid item uid
755
     * @return Item[] An array of items matching $itemType and $itemUid
756
     */
757 21
    public function getItems($itemType, $itemUid)
758
    {
759 21
        $indexQueueItemRecords = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
760 21
            '*',
761 21
            'tx_solr_indexqueue_item',
762 21
            'item_type = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($itemType,
763 21
                'tx_solr_indexqueue_item') .
764 21
            ' AND item_uid = ' . intval($itemUid)
765
        );
766
767 21
        return $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
768
    }
769
770
    /**
771
     * Gets number of Index Queue items for a specific site / indexing configuration
772
     * optional parameter to limit the counted items by indexing configuration.
773
     *
774
     * @param Site $site The site to search for.
775
     * @param string $indexingConfigurationName name of a specific indexing
776
     *      configuration
777
     * @return int Number of items
778
     */
779 2
    public function getItemsCountBySite(Site $site, $indexingConfigurationName = '')
780
    {
781 2
        $indexingConfigurationConstraint = $this->buildIndexConfigurationConstraint($indexingConfigurationName);
782 2
        $where = 'root = ' . $site->getRootPageId() . $indexingConfigurationConstraint;
783 2
        return (int)$this->getItemCount($where);
784
    }
785
786
    /**
787
     * Gets number of unprocessed Index Queue items for a specific site / indexing configuration
788
     * optional parameter to limit the counted items by indexing configuration.
789
     *
790
     * @param Site $site The site to search for.
791
     * @param string $indexingConfigurationName name of a specific indexing
792
     *      configuration
793
     * @return int Number of items.
794
     */
795 2
    public function getRemainingItemsCountBySite(Site $site, $indexingConfigurationName = '')
796
    {
797 2
        $indexingConfigurationConstraint = $this->buildIndexConfigurationConstraint($indexingConfigurationName);
798 2
        $where = 'changed > indexed AND root = ' . $site->getRootPageId() . $indexingConfigurationConstraint;
799 2
        return (int)$this->getItemCount($where);
800
    }
801
802
    /**
803
     * Returns the number of items for all queues.
804
     *
805
     * @return int
806
     */
807 47
    public function getAllItemsCount()
808
    {
809 47
        return $this->getItemCount();
810
    }
811
812
    /**
813
     * @param string $where
814
     * @return int
815
     */
816 50
    private function getItemCount($where = '1=1')
817
    {
818
        /**  @var $db \TYPO3\CMS\Core\Database\DatabaseConnection */
819 50
        $db = $GLOBALS['TYPO3_DB'];
820
821 50
        return (int)$db->exec_SELECTcountRows('uid', 'tx_solr_indexqueue_item', $where);
822
    }
823
824
    /**
825
     * Build a database constraint that limits to a certain indexConfigurationName
826
     *
827
     * @param string $indexingConfigurationName
828
     * @return string
829
     */
830 3
    protected function buildIndexConfigurationConstraint($indexingConfigurationName)
831
    {
832 3
        $indexingConfigurationConstraint = '';
833 3
        if (!empty($indexingConfigurationName)) {
834
            $indexingConfigurationConstraint = ' AND indexing_configuration = \'' . $indexingConfigurationName . '\'';
835
            return $indexingConfigurationConstraint;
836
        }
837 3
        return $indexingConfigurationConstraint;
838
    }
839
840
    /**
841
     * Gets $limit number of items to index for a particular $site.
842
     *
843
     * @param Site $site TYPO3 site
844
     * @param int $limit Number of items to get from the queue
845
     * @return Item[] Items to index to the given solr server
846
     */
847 7
    public function getItemsToIndex(Site $site, $limit = 50)
848
    {
849 7
        $itemsToIndex = [];
850
851
        // determine which items to index with this run
852 7
        $indexQueueItemRecords = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
853 7
            '*',
854 7
            'tx_solr_indexqueue_item',
855 7
            'root = ' . $site->getRootPageId() .
856 7
            ' AND changed > indexed' .
857 7
            ' AND changed <= ' . time() .
858 7
            ' AND errors = \'\'',
859 7
            '',
860 7
            'indexing_priority DESC, changed DESC, uid DESC',
861
            intval($limit)
862
        );
863 7
        if (!empty($indexQueueItemRecords)) {
864
            // convert queued records to index queue item objects
865 7
            $itemsToIndex = $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
866
        }
867
868 7
        return $itemsToIndex;
869
    }
870
871
    /**
872
     * Creates an array of ApacheSolrForTypo3\Solr\IndexQueue\Item objects from an array of
873
     * index queue records.
874
     *
875
     * @param array $indexQueueItemRecords Array of plain index queue records
876
     * @return array Array of ApacheSolrForTypo3\Solr\IndexQueue\Item objects
877
     */
878 26
    protected function getIndexQueueItemObjectsFromRecords(
879
        array $indexQueueItemRecords
880
    ) {
881 26
        $indexQueueItems = [];
882 26
        $tableUids = [];
883 26
        $tableRecords = [];
884
885
        // grouping records by table
886 26
        foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
887 26
            $tableUids[$indexQueueItemRecord['item_type']][] = $indexQueueItemRecord['item_uid'];
888
        }
889
890
        // fetching records by table, saves us a lot of single queries
891 26
        foreach ($tableUids as $table => $uids) {
892 26
            $uidList = implode(',', $uids);
893 26
            $records = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
894 26
                '*',
895
                $table,
896 26
                'uid IN(' . $uidList . ')',
897 26
                '', '', '', // group, order, limit
898 26
                'uid'
899
            );
900 26
            $tableRecords[$table] = $records;
901
902 26
            if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'])) {
903
                $params = ['table' => $table, 'uids' => $uids, 'tableRecords' => &$tableRecords];
904
                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'] as $reference) {
905
                    GeneralUtility::callUserFunction($reference, $params, $this);
906
                }
907 26
                unset($params);
908
            }
909
        }
910
911
        // creating index queue item objects and assigning / mapping
912
        // records to index queue items
913 26
        foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
914 26
            if (isset($tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']])) {
915 25
                $indexQueueItems[] = GeneralUtility::makeInstance(
916 25
                    Item::class,
917
                    $indexQueueItemRecord,
918 25
                    $tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']]
919
                );
920
            } else {
921 1
                $this->logger->log(
922 1
                    SolrLogManager::ERROR,
923 1
                    'Record missing for Index Queue item. Item removed.',
924
                    [
925 1
                        $indexQueueItemRecord
926
                    ]
927
                );
928 1
                $this->deleteItem($indexQueueItemRecord['item_type'],
929 26
                    $indexQueueItemRecord['item_uid']);
930
            }
931
        }
932
933 26
        return $indexQueueItems;
934
    }
935
936
    /**
937
     * Marks an item as failed and causes the indexer to skip the item in the
938
     * next run.
939
     *
940
     * @param int|Item $item Either the item's Index Queue
941
     *      uid or the complete item
942
     * @param string $errorMessage Error message
943
     */
944
    public function markItemAsFailed($item, $errorMessage = '')
945
    {
946
        if ($item instanceof Item) {
947
            $itemUid = $item->getIndexQueueUid();
948
        } else {
949
            $itemUid = (int)$item;
950
        }
951
952
        if (empty($errorMessage)) {
953
            // simply set to "TRUE"
954
            $errorMessage = '1';
955
        }
956
957
        $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
958
            'tx_solr_indexqueue_item',
959
            'uid = ' . $itemUid,
960
            [
961
                'errors' => $errorMessage
962
            ]
963
        );
964
    }
965
}
966