Passed
Pull Request — master (#672)
by Tomas Norre
09:21 queued 06:09
created

CrawlerController::urlListFromUrlArray()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 8.2037

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 39
c 1
b 0
f 0
nc 8
nop 9
dl 0
loc 68
ccs 29
cts 34
cp 0.8529
crap 8.2037
rs 8.0515

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace AOE\Crawler\Controller;
6
7
/***************************************************************
8
 *  Copyright notice
9
 *
10
 *  (c) 2020 AOE GmbH <[email protected]>
11
 *
12
 *  All rights reserved
13
 *
14
 *  This script is part of the TYPO3 project. The TYPO3 project is
15
 *  free software; you can redistribute it and/or modify
16
 *  it under the terms of the GNU General Public License as published by
17
 *  the Free Software Foundation; either version 3 of the License, or
18
 *  (at your option) any later version.
19
 *
20
 *  The GNU General Public License can be found at
21
 *  http://www.gnu.org/copyleft/gpl.html.
22
 *
23
 *  This script is distributed in the hope that it will be useful,
24
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 *  GNU General Public License for more details.
27
 *
28
 *  This copyright notice MUST APPEAR in all copies of the script!
29
 ***************************************************************/
30
31
use AOE\Crawler\Configuration\ExtensionConfigurationProvider;
32
use AOE\Crawler\Converter\JsonCompatibilityConverter;
33
use AOE\Crawler\Crawler;
34
use AOE\Crawler\CrawlStrategy\CrawlStrategyFactory;
35
use AOE\Crawler\Domain\Model\Process;
36
use AOE\Crawler\Domain\Repository\ConfigurationRepository;
37
use AOE\Crawler\Domain\Repository\ProcessRepository;
38
use AOE\Crawler\Domain\Repository\QueueRepository;
39
use AOE\Crawler\QueueExecutor;
40
use AOE\Crawler\Service\UrlService;
41
use AOE\Crawler\Utility\SignalSlotUtility;
42
use AOE\Crawler\Value\QueueFilter;
43
use PDO;
44
use Psr\Http\Message\UriInterface;
45
use Psr\Log\LoggerAwareInterface;
46
use Psr\Log\LoggerAwareTrait;
47
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
48
use TYPO3\CMS\Backend\Utility\BackendUtility;
49
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
50
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
51
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
52
use TYPO3\CMS\Core\Core\Bootstrap;
53
use TYPO3\CMS\Core\Core\Environment;
54
use TYPO3\CMS\Core\Database\Connection;
55
use TYPO3\CMS\Core\Database\ConnectionPool;
56
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
57
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
58
use TYPO3\CMS\Core\Database\QueryGenerator;
59
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
60
use TYPO3\CMS\Core\Imaging\Icon;
61
use TYPO3\CMS\Core\Imaging\IconFactory;
62
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
63
use TYPO3\CMS\Core\Site\Entity\Site;
64
use TYPO3\CMS\Core\Type\Bitmask\Permission;
65
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
66
use TYPO3\CMS\Core\Utility\DebugUtility;
67
use TYPO3\CMS\Core\Utility\GeneralUtility;
68
use TYPO3\CMS\Core\Utility\MathUtility;
69
use TYPO3\CMS\Extbase\Object\ObjectManager;
70
use TYPO3\CMS\Frontend\Page\PageRepository;
71
72
/**
73
 * Class CrawlerController
74
 *
75
 * @package AOE\Crawler\Controller
76
 */
77
class CrawlerController implements LoggerAwareInterface
78
{
79
    use LoggerAwareTrait;
80
    use PublicMethodDeprecationTrait;
81
    use PublicPropertyDeprecationTrait;
82
83
    public const CLI_STATUS_NOTHING_PROCCESSED = 0;
84
85
    //queue not empty
86
    public const CLI_STATUS_REMAIN = 1;
87
88
    //(some) queue items where processed
89
    public const CLI_STATUS_PROCESSED = 2;
90
91
    //instance didn't finish
92
    public const CLI_STATUS_ABORTED = 4;
93
94
    public const CLI_STATUS_POLLABLE_PROCESSED = 8;
95
96
    /**
97
     * @var integer
98
     */
99
    public $setID = 0;
100
101
    /**
102
     * @var string
103
     */
104
    public $processID = '';
105
106
    /**
107
     * @var array
108
     */
109
    public $duplicateTrack = [];
110
111
    /**
112
     * @var array
113
     */
114
    public $downloadUrls = [];
115
116
    /**
117
     * @var array
118
     */
119
    public $incomingProcInstructions = [];
120
121
    /**
122
     * @var array
123
     */
124
    public $incomingConfigurationSelection = [];
125
126
    /**
127
     * @var bool
128
     */
129
    public $registerQueueEntriesInternallyOnly = false;
130
131
    /**
132
     * @var array
133
     */
134
    public $queueEntries = [];
135
136
    /**
137
     * @var array
138
     */
139
    public $urlList = [];
140
141
    /**
142
     * @var array
143
     */
144
    public $extensionSettings = [];
145
146
    /**
147
     * Mount Point
148
     *
149
     * @var bool
150
     * Todo: Check what this is used for and adjust the type hint or code, as bool doesn't match the current code.
151
     */
152
    public $MP = false;
153
154
    /**
155
     * @var string
156
     * @deprecated
157
     */
158
    protected $processFilename;
159
160
    /**
161
     * Holds the internal access mode can be 'gui','cli' or 'cli_im'
162
     *
163
     * @var string
164
     * @deprecated
165
     */
166
    protected $accessMode;
167
168
    /**
169
     * @var QueueRepository
170
     */
171
    protected $queueRepository;
172
173
    /**
174
     * @var ProcessRepository
175
     */
176
    protected $processRepository;
177
178
    /**
179
     * @var ConfigurationRepository
180
     */
181
    protected $configurationRepository;
182
183
    /**
184
     * @var string
185
     */
186
    protected $tableName = 'tx_crawler_queue';
187
188
    /**
189
     * @var QueueExecutor
190
     */
191
    protected $queueExecutor;
192
193
    /**
194
     * @var int
195
     */
196
    protected $maximumUrlsToCompile = 10000;
197
198
    /**
199
     * @var IconFactory
200
     */
201
    protected $iconFactory;
202
203
    /**
204
     * @var string[]
205
     */
206
    private $deprecatedPublicMethods = [
0 ignored issues
show
introduced by
The private property $deprecatedPublicMethods is not used, and could be removed.
Loading history...
207
        'cleanUpOldQueueEntries' => 'Using CrawlerController::cleanUpOldQueueEntries() is deprecated since 9.0.1 and will be removed in v11.x, please use QueueRepository->cleanUpOldQueueEntries() instead.',
208
        'CLI_debug' => 'Using CrawlerController->CLI_debug() is deprecated since 9.1.3 and will be removed in v11.x',
209
        'getAccessMode' => 'Using CrawlerController->getAccessMode() is deprecated since 9.1.3 and will be removed in v11.x',
210
        'getLogEntriesForSetId' => 'Using crawlerController::getLogEntriesForSetId() is deprecated since 9.0.1 and will be removed in v11.x',
211
        'flushQueue' => 'Using CrawlerController::flushQueue() is deprecated since 9.0.1 and will be removed in v11.x, please use QueueRepository->flushQueue() instead.',
212
        'setAccessMode' => 'Using CrawlerController->setAccessMode() is deprecated since 9.1.3 and will be removed in v11.x',
213
        'getDisabled' => 'Using CrawlerController->getDisabled() is deprecated since 9.1.3 and will be removed in v11.x, please use Crawler->isDisabled() instead',
214
        'setDisabled' => 'Using CrawlerController->setDisabled() is deprecated since 9.1.3 and will be removed in v11.x, please use Crawler->setDisabled() instead',
215
        'getProcessFilename' => 'Using CrawlerController->getProcessFilename() is deprecated since 9.1.3 and will be removed in v11.x',
216
        'setProcessFilename' => 'Using CrawlerController->setProcessFilename() is deprecated since 9.1.3 and will be removed in v11.x',
217
        'getDuplicateRowsIfExist' => 'Using CrawlerController->getDuplicateRowsIfExist() is deprecated since 9.1.4 and will be remove in v11.x, please use QueueRepository->getDuplicateQueueItemsIfExists() instead',
218
219
    ];
220
221
    /**
222
     * @var string[]
223
     */
224
    private $deprecatedPublicProperties = [
1 ignored issue
show
introduced by
The private property $deprecatedPublicProperties is not used, and could be removed.
Loading history...
225
        'accessMode' => 'Using CrawlerController->accessMode is deprecated since 9.1.3 and will be removed in v11.x',
226
        'processFilename' => 'Using CrawlerController->accessMode is deprecated since 9.1.3 and will be removed in v11.x',
227
    ];
228
229
    /**
230
     * @var BackendUserAuthentication|null
231
     */
232
    private $backendUser;
233
234
    /**
235
     * @var integer
236
     */
237
    private $scheduledTime = 0;
238
239
    /**
240
     * @var integer
241
     */
242
    private $reqMinute = 0;
243
244
    /**
245
     * @var bool
246
     */
247
    private $submitCrawlUrls = false;
248
249
    /**
250
     * @var bool
251
     */
252
    private $downloadCrawlUrls = false;
253
254
    /**
255
     * @var PageRepository
256
     */
257
    private $pageRepository;
258
259
    /**
260
     * @var Crawler
261
     */
262
    private $crawler;
263
264
    /************************************
265
     *
266
     * Getting URLs based on Page TSconfig
267
     *
268
     ************************************/
269
270 36
    public function __construct()
271
    {
272 36
        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
273 36
        $crawlStrategyFactory = GeneralUtility::makeInstance(CrawlStrategyFactory::class);
274 36
        $this->queueRepository = $objectManager->get(QueueRepository::class);
275 36
        $this->processRepository = $objectManager->get(ProcessRepository::class);
276 36
        $this->configurationRepository = $objectManager->get(ConfigurationRepository::class);
277 36
        $this->pageRepository = $objectManager->get(PageRepository::class);
278 36
        $this->queueExecutor = GeneralUtility::makeInstance(QueueExecutor::class, $crawlStrategyFactory);
279 36
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
280 36
        $this->crawler = GeneralUtility::makeInstance(Crawler::class);
281
282 36
        $this->processFilename = Environment::getVarPath() . '/lock/tx_crawler.proc';
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

282
        /** @scrutinizer ignore-deprecated */ $this->processFilename = Environment::getVarPath() . '/lock/tx_crawler.proc';
Loading history...
283
284
        /** @var ExtensionConfigurationProvider $configurationProvider */
285 36
        $configurationProvider = GeneralUtility::makeInstance(ExtensionConfigurationProvider::class);
286 36
        $settings = $configurationProvider->getExtensionConfiguration();
287 36
        $this->extensionSettings = is_array($settings) ? $settings : [];
0 ignored issues
show
introduced by
The condition is_array($settings) is always true.
Loading history...
288
289
        // set defaults:
290 36
        if (MathUtility::convertToPositiveInteger($this->extensionSettings['countInARun']) === 0) {
291
            $this->extensionSettings['countInARun'] = 100;
292
        }
293
294 36
        $this->extensionSettings['processLimit'] = MathUtility::forceIntegerInRange($this->extensionSettings['processLimit'], 1, 99, 1);
295 36
        $this->maximumUrlsToCompile = MathUtility::forceIntegerInRange($this->extensionSettings['maxCompileUrls'], 1, 1000000000, 10000);
296 36
    }
297
298
    /**
299
     * Method to set the accessMode can be gui, cli or cli_im
300
     *
301
     * @return string
302
     * @deprecated
303
     */
304 1
    public function getAccessMode()
305
    {
306 1
        return $this->accessMode;
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...Controller::$accessMode has been deprecated. ( Ignorable by Annotation )

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

306
        return /** @scrutinizer ignore-deprecated */ $this->accessMode;
Loading history...
307
    }
308
309
    /**
310
     * @param string $accessMode
311
     * @deprecated
312
     */
313 1
    public function setAccessMode($accessMode): void
314
    {
315 1
        $this->accessMode = $accessMode;
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...Controller::$accessMode has been deprecated. ( Ignorable by Annotation )

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

315
        /** @scrutinizer ignore-deprecated */ $this->accessMode = $accessMode;
Loading history...
316 1
    }
317
318
    /**
319
     * Set disabled status to prevent processes from being processed
320
     *
321
     * @param bool $disabled (optional, defaults to true)
322
     * @deprecated
323
     */
324 2
    public function setDisabled($disabled = true): void
325
    {
326 2
        if ($disabled) {
327 1
            GeneralUtility::writeFile($this->processFilename, '');
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

327
            GeneralUtility::writeFile(/** @scrutinizer ignore-deprecated */ $this->processFilename, '');
Loading history...
328
        } else {
329 1
            if (is_file($this->processFilename)) {
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

329
            if (is_file(/** @scrutinizer ignore-deprecated */ $this->processFilename)) {
Loading history...
330 1
                unlink($this->processFilename);
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

330
                unlink(/** @scrutinizer ignore-deprecated */ $this->processFilename);
Loading history...
331
            }
332
        }
333 2
    }
334
335
    /**
336
     * Get disable status
337
     *
338
     * @return bool true if disabled
339
     * @deprecated
340
     */
341 2
    public function getDisabled()
342
    {
343 2
        return is_file($this->processFilename);
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

343
        return is_file(/** @scrutinizer ignore-deprecated */ $this->processFilename);
Loading history...
344
    }
345
346
    /**
347
     * @param string $filenameWithPath
348
     * @deprecated
349
     */
350 3
    public function setProcessFilename($filenameWithPath): void
351
    {
352 3
        $this->processFilename = $filenameWithPath;
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

352
        /** @scrutinizer ignore-deprecated */ $this->processFilename = $filenameWithPath;
Loading history...
353 3
    }
354
355
    /**
356
     * @return string
357
     * @deprecated
358
     */
359 1
    public function getProcessFilename()
360
    {
361 1
        return $this->processFilename;
0 ignored issues
show
Deprecated Code introduced by
The property AOE\Crawler\Controller\C...oller::$processFilename has been deprecated. ( Ignorable by Annotation )

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

361
        return /** @scrutinizer ignore-deprecated */ $this->processFilename;
Loading history...
362
    }
363
364
    /**
365
     * Sets the extensions settings (unserialized pendant of $TYPO3_CONF_VARS['EXT']['extConf']['crawler']).
366
     */
367 14
    public function setExtensionSettings(array $extensionSettings): void
368
    {
369 14
        $this->extensionSettings = $extensionSettings;
370 14
    }
371
372
    /**
373
     * Check if the given page should be crawled
374
     *
375
     * @return false|string false if the page should be crawled (not excluded), true / skipMessage if it should be skipped
376
     */
377 12
    public function checkIfPageShouldBeSkipped(array $pageRow)
378
    {
379
        // if page is hidden
380 12
        if (! $this->extensionSettings['crawlHiddenPages'] && $pageRow['hidden']) {
381 1
            return 'Because page is hidden';
382
        }
383
384 11
        if (GeneralUtility::inList('3,4,199,254,255', $pageRow['doktype'])) {
385 3
            return 'Because doktype is not allowed';
386
        }
387
388 8
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['excludeDoktype'] ?? [] as $key => $doktypeList) {
389 1
            if (GeneralUtility::inList($doktypeList, $pageRow['doktype'])) {
390 1
                return 'Doktype was excluded by "' . $key . '"';
391
            }
392
        }
393
394
        // veto hook
395 7
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pageVeto'] ?? [] as $key => $func) {
396
            $params = [
397 2
                'pageRow' => $pageRow,
398
            ];
399
            // expects "false" if page is ok and "true" or a skipMessage if this page should _not_ be crawled
400 2
            $veto = GeneralUtility::callUserFunction($func, $params, $this);
401 2
            if ($veto !== false) {
402 2
                if (is_string($veto)) {
403 1
                    return $veto;
404
                }
405 1
                return 'Veto from hook "' . htmlspecialchars($key) . '"';
406
            }
407
        }
408
409 5
        return false;
410
    }
411
412
    /**
413
     * Wrapper method for getUrlsForPageId()
414
     * It returns an array of configurations and no urls!
415
     *
416
     * @param array $pageRow Page record with at least dok-type and uid columns.
417
     * @param string $skipMessage
418
     * @return array
419
     * @see getUrlsForPageId()
420
     */
421 6
    public function getUrlsForPageRow(array $pageRow, &$skipMessage = '')
422
    {
423 6
        $message = $this->checkIfPageShouldBeSkipped($pageRow);
424 6
        if ($message === false) {
425 5
            $res = $this->getUrlsForPageId($pageRow['uid']);
426 5
            $skipMessage = '';
427
        } else {
428 1
            $skipMessage = $message;
429 1
            $res = [];
430
        }
431
432 6
        return $res;
433
    }
434
435
    /**
436
     * Creates a list of URLs from input array (and submits them to queue if asked for)
437
     * See Web > Info module script + "indexed_search"'s crawler hook-client using this!
438
     *
439
     * @param array $vv Information about URLs from pageRow to crawl.
440
     * @param array $pageRow Page row
441
     * @param int $scheduledTime Unix time to schedule indexing to, typically time()
442
     * @param int $reqMinute Number of requests per minute (creates the interleave between requests)
443
     * @param bool $submitCrawlUrls If set, submits the URLs to queue
444
     * @param bool $downloadCrawlUrls If set (and submitcrawlUrls is false) will fill $downloadUrls with entries)
445
     * @param array $duplicateTrack Array which is passed by reference and contains the an id per url to secure we will not crawl duplicates
446
     * @param array $downloadUrls Array which will be filled with URLS for download if flag is set.
447
     * @param array $incomingProcInstructions Array of processing instructions
448
     * @return string List of URLs (meant for display in backend module)
449
     */
450 4
    public function urlListFromUrlArray(
451
        array $vv,
452
        array $pageRow,
453
        $scheduledTime,
454
        $reqMinute,
455
        $submitCrawlUrls,
456
        $downloadCrawlUrls,
457
        array &$duplicateTrack,
458
        array &$downloadUrls,
459
        array $incomingProcInstructions
460
    ) {
461 4
        if (! is_array($vv['URLs'])) {
462
            return 'ERROR - no URL generated';
463
        }
464 4
        $urlLog = [];
465 4
        $pageId = (int) $pageRow['uid'];
466 4
        $configurationHash = $this->getConfigurationHash($vv);
467 4
        $skipInnerCheck = $this->queueRepository->noUnprocessedQueueEntriesForPageWithConfigurationHashExist($pageId, $configurationHash);
468
469 4
        $urlService = new UrlService();
470
471 4
        foreach ($vv['URLs'] as $urlQuery) {
472 4
            if (! $this->drawURLs_PIfilter($vv['subCfg']['procInstrFilter'], $incomingProcInstructions)) {
473
                continue;
474
            }
475 4
            $url = (string) $urlService->getUrlFromPageAndQueryParameters(
476 4
                $pageId,
477
                $urlQuery,
478 4
                $vv['subCfg']['baseUrl'] ?? null,
479 4
                $vv['subCfg']['force_ssl'] ?? 0
480
            );
481
482
            // Create key by which to determine unique-ness:
483 4
            $uKey = $url . '|' . $vv['subCfg']['userGroups'] . '|' . $vv['subCfg']['procInstrFilter'];
484
485 4
            if (isset($duplicateTrack[$uKey])) {
486
                //if the url key is registered just display it and do not resubmit is
487
                $urlLog[] = '<em><span class="text-muted">' . htmlspecialchars($url) . '</span></em>';
488
            } else {
489
                // Scheduled time:
490 4
                $schTime = $scheduledTime + round(count($duplicateTrack) * (60 / $reqMinute));
491 4
                $schTime = intval($schTime / 60) * 60;
492 4
                $formattedDate = BackendUtility::datetime($schTime);
493 4
                $this->urlList[] = '[' . $formattedDate . '] ' . $url;
494 4
                $urlList = '[' . $formattedDate . '] ' . htmlspecialchars($url);
495
496
                // Submit for crawling!
497 4
                if ($submitCrawlUrls) {
498 4
                    $added = $this->addUrl(
499 4
                        $pageId,
500
                        $url,
501 4
                        $vv['subCfg'],
502
                        $scheduledTime,
503
                        $configurationHash,
504
                        $skipInnerCheck
505
                    );
506 4
                    if ($added === false) {
507 4
                        $urlList .= ' (URL already existed)';
508
                    }
509
                } elseif ($downloadCrawlUrls) {
510
                    $downloadUrls[$url] = $url;
511
                }
512 4
                $urlLog[] = $urlList;
513
            }
514 4
            $duplicateTrack[$uKey] = true;
515
        }
516
517 4
        return implode('<br>', $urlLog);
518
    }
519
520
    /**
521
     * Returns true if input processing instruction is among registered ones.
522
     *
523
     * @param string $piString PI to test
524
     * @param array $incomingProcInstructions Processing instructions
525
     * @return boolean
526
     */
527 5
    public function drawURLs_PIfilter($piString, array $incomingProcInstructions)
528
    {
529 5
        if (empty($incomingProcInstructions)) {
530 1
            return true;
531
        }
532
533 4
        foreach ($incomingProcInstructions as $pi) {
534 4
            if (GeneralUtility::inList($piString, $pi)) {
535 2
                return true;
536
            }
537
        }
538 2
        return false;
539
    }
540
541 5
    public function getPageTSconfigForId($id): array
542
    {
543 5
        if (! $this->MP) {
544 5
            $pageTSconfig = BackendUtility::getPagesTSconfig($id);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Backend\Utilit...ity::getPagesTSconfig() has been deprecated. ( Ignorable by Annotation )

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

544
            $pageTSconfig = /** @scrutinizer ignore-deprecated */ BackendUtility::getPagesTSconfig($id);
Loading history...
545
        } else {
546
            // TODO: Please check, this makes no sense to split a boolean value.
547
            [, $mountPointId] = explode('-', $this->MP);
0 ignored issues
show
Bug introduced by
$this->MP of type true is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

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

547
            [, $mountPointId] = explode('-', /** @scrutinizer ignore-type */ $this->MP);
Loading history...
548
            $pageTSconfig = BackendUtility::getPagesTSconfig($mountPointId);
0 ignored issues
show
Bug introduced by
$mountPointId of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Backend\Utilit...ity::getPagesTSconfig(). ( Ignorable by Annotation )

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

548
            $pageTSconfig = BackendUtility::getPagesTSconfig(/** @scrutinizer ignore-type */ $mountPointId);
Loading history...
Deprecated Code introduced by
The function TYPO3\CMS\Backend\Utilit...ity::getPagesTSconfig() has been deprecated. ( Ignorable by Annotation )

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

548
            $pageTSconfig = /** @scrutinizer ignore-deprecated */ BackendUtility::getPagesTSconfig($mountPointId);
Loading history...
549
        }
550
551
        // Call a hook to alter configuration
552 5
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'])) {
553
            $params = [
554
                'pageId' => $id,
555
                'pageTSConfig' => &$pageTSconfig,
556
            ];
557
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'] as $userFunc) {
558
                GeneralUtility::callUserFunction($userFunc, $params, $this);
559
            }
560
        }
561 5
        return $pageTSconfig;
562
    }
563
564
    /**
565
     * This methods returns an array of configurations.
566
     * Adds no urls!
567
     */
568 4
    public function getUrlsForPageId(int $pageId): array
569
    {
570
        // Get page TSconfig for page ID
571 4
        $pageTSconfig = $this->getPageTSconfigForId($pageId);
572
573 4
        $res = [];
574
575
        // Fetch Crawler Configuration from pageTSconfig
576 4
        $crawlerCfg = $pageTSconfig['tx_crawler.']['crawlerCfg.']['paramSets.'] ?? [];
577 4
        foreach ($crawlerCfg as $key => $values) {
578 3
            if (! is_array($values)) {
579 3
                continue;
580
            }
581 3
            $key = str_replace('.', '', $key);
582
            // Sub configuration for a single configuration string:
583 3
            $subCfg = (array) $crawlerCfg[$key . '.'];
584 3
            $subCfg['key'] = $key;
585
586 3
            if (strcmp($subCfg['procInstrFilter'] ?? '', '')) {
587 3
                $subCfg['procInstrFilter'] = implode(',', GeneralUtility::trimExplode(',', $subCfg['procInstrFilter']));
588
            }
589 3
            $pidOnlyList = implode(',', GeneralUtility::trimExplode(',', $subCfg['pidsOnly'], true));
590
591
            // process configuration if it is not page-specific or if the specific page is the current page:
592
            // TODO: Check if $pidOnlyList can be kept as Array instead of imploded
593 3
            if (! strcmp((string) $subCfg['pidsOnly'], '') || GeneralUtility::inList($pidOnlyList, strval($pageId))) {
594
595
                // Explode, process etc.:
596 3
                $res[$key] = [];
597 3
                $res[$key]['subCfg'] = $subCfg;
598 3
                $res[$key]['paramParsed'] = GeneralUtility::explodeUrl2Array($crawlerCfg[$key]);
599 3
                $res[$key]['paramExpanded'] = $this->expandParameters($res[$key]['paramParsed'], $pageId);
600 3
                $res[$key]['origin'] = 'pagets';
601
602
                // recognize MP value
603 3
                if (! $this->MP) {
604 3
                    $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $pageId]);
605
                } else {
606
                    $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $pageId . '&MP=' . $this->MP]);
0 ignored issues
show
Bug introduced by
Are you sure $this->MP of type true can be used in concatenation? ( Ignorable by Annotation )

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

606
                    $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $pageId . '&MP=' . /** @scrutinizer ignore-type */ $this->MP]);
Loading history...
607
                }
608
            }
609
        }
610
611
        // Get configuration from tx_crawler_configuration records up the rootline
612 4
        $crawlerConfigurations = $this->configurationRepository->getCrawlerConfigurationRecordsFromRootLine($pageId);
613 4
        foreach ($crawlerConfigurations as $configurationRecord) {
614
615
            // check access to the configuration record
616 1
            if (empty($configurationRecord['begroups']) || $this->getBackendUser()->isAdmin() || $this->hasGroupAccess($this->getBackendUser()->user['usergroup_cached_list'], $configurationRecord['begroups'])) {
617 1
                $pidOnlyList = implode(',', GeneralUtility::trimExplode(',', $configurationRecord['pidsonly'], true));
618
619
                // process configuration if it is not page-specific or if the specific page is the current page:
620
                // TODO: Check if $pidOnlyList can be kept as Array instead of imploded
621 1
                if (! strcmp($configurationRecord['pidsonly'], '') || GeneralUtility::inList($pidOnlyList, strval($pageId))) {
622 1
                    $key = $configurationRecord['name'];
623
624
                    // don't overwrite previously defined paramSets
625 1
                    if (! isset($res[$key])) {
626
627
                        /* @var $TSparserObject TypoScriptParser */
628 1
                        $TSparserObject = GeneralUtility::makeInstance(TypoScriptParser::class);
629 1
                        $TSparserObject->parse($configurationRecord['processing_instruction_parameters_ts']);
630
631
                        $subCfg = [
632 1
                            'procInstrFilter' => $configurationRecord['processing_instruction_filter'],
633 1
                            'procInstrParams.' => $TSparserObject->setup,
634 1
                            'baseUrl' => $configurationRecord['base_url'],
635 1
                            'force_ssl' => (int) $configurationRecord['force_ssl'],
636 1
                            'userGroups' => $configurationRecord['fegroups'],
637 1
                            'exclude' => $configurationRecord['exclude'],
638 1
                            'key' => $key,
639
                        ];
640
641 1
                        if (! in_array($pageId, $this->expandExcludeString($subCfg['exclude']), true)) {
642 1
                            $res[$key] = [];
643 1
                            $res[$key]['subCfg'] = $subCfg;
644 1
                            $res[$key]['paramParsed'] = GeneralUtility::explodeUrl2Array($configurationRecord['configuration']);
645 1
                            $res[$key]['paramExpanded'] = $this->expandParameters($res[$key]['paramParsed'], $pageId);
646 1
                            $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $pageId]);
647 1
                            $res[$key]['origin'] = 'tx_crawler_configuration_' . $configurationRecord['uid'];
648
                        }
649
                    }
650
                }
651
            }
652
        }
653
654 4
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['processUrls'] ?? [] as $func) {
655
            $params = [
656
                'res' => &$res,
657
            ];
658
            GeneralUtility::callUserFunction($func, $params, $this);
659
        }
660 4
        return $res;
661
    }
662
663
    /**
664
     * Find all configurations of subpages of a page
665
     * TODO: Write Functional Tests
666
     */
667 1
    public function getConfigurationsForBranch(int $rootid, int $depth): array
668
    {
669 1
        $configurationsForBranch = [];
670 1
        $pageTSconfig = $this->getPageTSconfigForId($rootid);
671 1
        $sets = $pageTSconfig['tx_crawler.']['crawlerCfg.']['paramSets.'] ?? [];
672 1
        foreach ($sets as $key => $value) {
673
            if (! is_array($value)) {
674
                continue;
675
            }
676
            $configurationsForBranch[] = substr($key, -1) === '.' ? substr($key, 0, -1) : $key;
677
        }
678 1
        $pids = [];
679 1
        $rootLine = BackendUtility::BEgetRootLine($rootid);
680 1
        foreach ($rootLine as $node) {
681 1
            $pids[] = $node['uid'];
682
        }
683
        /* @var PageTreeView $tree */
684 1
        $tree = GeneralUtility::makeInstance(PageTreeView::class);
685 1
        $perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
686 1
        $tree->init(empty($perms_clause) ? '' : ('AND ' . $perms_clause));
687 1
        $tree->getTree($rootid, $depth, '');
688 1
        foreach ($tree->tree as $node) {
689
            $pids[] = $node['row']['uid'];
690
        }
691
692 1
        $queryBuilder = $this->getQueryBuilder('tx_crawler_configuration');
693
        $statement = $queryBuilder
694 1
            ->select('name')
695 1
            ->from('tx_crawler_configuration')
696 1
            ->where(
697 1
                $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($pids, Connection::PARAM_INT_ARRAY))
698
            )
699 1
            ->execute();
700
701 1
        while ($row = $statement->fetch()) {
702 1
            $configurationsForBranch[] = $row['name'];
703
        }
704 1
        return $configurationsForBranch;
705
    }
706
707
    /**
708
     * Check if a user has access to an item
709
     * (e.g. get the group list of the current logged in user from $GLOBALS['TSFE']->gr_list)
710
     *
711
     * @param string $groupList Comma-separated list of (fe_)group UIDs from a user
712
     * @param string $accessList Comma-separated list of (fe_)group UIDs of the item to access
713
     * @return bool TRUE if at least one of the users group UIDs is in the access list or the access list is empty
714
     * @see \TYPO3\CMS\Frontend\Page\PageRepository::getMultipleGroupsWhereClause()
715
     */
716 3
    public function hasGroupAccess($groupList, $accessList)
717
    {
718 3
        if (empty($accessList)) {
719 1
            return true;
720
        }
721 2
        foreach (GeneralUtility::intExplode(',', $groupList) as $groupUid) {
722 2
            if (GeneralUtility::inList($accessList, $groupUid)) {
723 1
                return true;
724
            }
725
        }
726 1
        return false;
727
    }
728
729
    /**
730
     * Will expand the parameters configuration to individual values. This follows a certain syntax of the value of each parameter.
731
     * Syntax of values:
732
     * - Basically: If the value is wrapped in [...] it will be expanded according to the following syntax, otherwise the value is taken literally
733
     * - Configuration is splitted by "|" and the parts are processed individually and finally added together
734
     * - For each configuration part:
735
     *         - "[int]-[int]" = Integer range, will be expanded to all values in between, values included, starting from low to high (max. 1000). Example "1-34" or "-40--30"
736
     *         - "_TABLE:[TCA table name];[_PID:[optional page id, default is current page]];[_ENABLELANG:1]" = Look up of table records from PID, filtering out deleted records. Example "_TABLE:tt_content; _PID:123"
737
     *        _ENABLELANG:1 picks only original records without their language overlays
738
     *         - Default: Literal value
739
     *
740
     * @param array $paramArray Array with key (GET var name) and values (value of GET var which is configuration for expansion)
741
     * @param integer $pid Current page ID
742
     * @return array
743
     *
744
     * TODO: Write Functional Tests
745
     */
746 11
    public function expandParameters($paramArray, $pid)
747
    {
748
        // Traverse parameter names:
749 11
        foreach ($paramArray as $p => $v) {
750 11
            $v = trim($v);
751
752
            // If value is encapsulated in square brackets it means there are some ranges of values to find, otherwise the value is literal
753 11
            if (strpos($v, '[') === 0 && substr($v, -1) === ']') {
754
                // So, find the value inside brackets and reset the paramArray value as an array.
755 11
                $v = substr($v, 1, -1);
756 11
                $paramArray[$p] = [];
757
758
                // Explode parts and traverse them:
759 11
                $parts = explode('|', $v);
760 11
                foreach ($parts as $pV) {
761
762
                    // Look for integer range: (fx. 1-34 or -40--30 // reads minus 40 to minus 30)
763 11
                    if (preg_match('/^(-?[0-9]+)\s*-\s*(-?[0-9]+)$/', trim($pV), $reg)) {
764 1
                        $reg = $this->swapIfFirstIsLargerThanSecond($reg);
765
766
                        // Traverse range, add values:
767
                        // Limit to size of range!
768 1
                        $runAwayBrake = 1000;
769 1
                        for ($a = $reg[1]; $a <= $reg[2]; $a++) {
770 1
                            $paramArray[$p][] = $a;
771 1
                            $runAwayBrake--;
772 1
                            if ($runAwayBrake <= 0) {
773
                                break;
774
                            }
775
                        }
776 10
                    } elseif (strpos(trim($pV), '_TABLE:') === 0) {
777
778
                        // Parse parameters:
779 6
                        $subparts = GeneralUtility::trimExplode(';', $pV);
780 6
                        $subpartParams = [];
781 6
                        foreach ($subparts as $spV) {
782 6
                            [$pKey, $pVal] = GeneralUtility::trimExplode(':', $spV);
783 6
                            $subpartParams[$pKey] = $pVal;
784
                        }
785
786
                        // Table exists:
787 6
                        if (isset($GLOBALS['TCA'][$subpartParams['_TABLE']])) {
788 6
                            $lookUpPid = isset($subpartParams['_PID']) ? intval($subpartParams['_PID']) : intval($pid);
789 6
                            $recursiveDepth = isset($subpartParams['_RECURSIVE']) ? intval($subpartParams['_RECURSIVE']) : 0;
790 6
                            $pidField = isset($subpartParams['_PIDFIELD']) ? trim($subpartParams['_PIDFIELD']) : 'pid';
791 6
                            $where = $subpartParams['_WHERE'] ?? '';
792 6
                            $addTable = $subpartParams['_ADDTABLE'] ?? '';
793
794 6
                            $fieldName = $subpartParams['_FIELD'] ? $subpartParams['_FIELD'] : 'uid';
795 6
                            if ($fieldName === 'uid' || $GLOBALS['TCA'][$subpartParams['_TABLE']]['columns'][$fieldName]) {
796 6
                                $queryBuilder = $this->getQueryBuilder($subpartParams['_TABLE']);
797
798 6
                                if ($recursiveDepth > 0) {
799
                                    /** @var QueryGenerator $queryGenerator */
800 2
                                    $queryGenerator = GeneralUtility::makeInstance(QueryGenerator::class);
801 2
                                    $pidList = $queryGenerator->getTreeList($lookUpPid, $recursiveDepth, 0, 1);
802 2
                                    $pidArray = GeneralUtility::intExplode(',', $pidList);
803
                                } else {
804 4
                                    $pidArray = [(string) $lookUpPid];
805
                                }
806
807 6
                                $queryBuilder->getRestrictions()
808 6
                                    ->removeAll()
809 6
                                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
810
811
                                $queryBuilder
812 6
                                    ->select($fieldName)
813 6
                                    ->from($subpartParams['_TABLE'])
814 6
                                    ->where(
815 6
                                        $queryBuilder->expr()->in($pidField, $queryBuilder->createNamedParameter($pidArray, Connection::PARAM_INT_ARRAY)),
816
                                        $where
817
                                    );
818
819 6
                                if (! empty($addTable)) {
820
                                    // TODO: Check if this works as intended!
821
                                    $queryBuilder->add('from', $addTable);
822
                                }
823 6
                                $transOrigPointerField = $GLOBALS['TCA'][$subpartParams['_TABLE']]['ctrl']['transOrigPointerField'];
824
825 6
                                if ($subpartParams['_ENABLELANG'] && $transOrigPointerField) {
826
                                    $queryBuilder->andWhere(
827
                                        $queryBuilder->expr()->lte(
828
                                            $transOrigPointerField,
829
                                            0
830
                                        )
831
                                    );
832
                                }
833
834 6
                                $statement = $queryBuilder->execute();
835
836 6
                                $rows = [];
837 6
                                while ($row = $statement->fetch()) {
838 6
                                    $rows[$row[$fieldName]] = $row;
839
                                }
840
841 6
                                if (is_array($rows)) {
842 6
                                    $paramArray[$p] = array_merge($paramArray[$p], array_keys($rows));
843
                                }
844
                            }
845
                        }
846
                    } else {
847
                        // Just add value:
848 4
                        $paramArray[$p][] = $pV;
849
                    }
850
                    // Hook for processing own expandParameters place holder
851 11
                    if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['crawler/class.tx_crawler_lib.php']['expandParameters'])) {
852
                        $_params = [
853
                            'pObj' => &$this,
854
                            'paramArray' => &$paramArray,
855
                            'currentKey' => $p,
856
                            'currentValue' => $pV,
857
                            'pid' => $pid,
858
                        ];
859
                        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['crawler/class.tx_crawler_lib.php']['expandParameters'] as $_funcRef) {
860
                            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
861
                        }
862
                    }
863
                }
864
865
                // Make unique set of values and sort array by key:
866 11
                $paramArray[$p] = array_unique($paramArray[$p]);
867 11
                ksort($paramArray);
868
            } else {
869
                // Set the literal value as only value in array:
870 4
                $paramArray[$p] = [$v];
871
            }
872
        }
873
874 11
        return $paramArray;
875
    }
876
877
    /**
878
     * Compiling URLs from parameter array (output of expandParameters())
879
     * The number of URLs will be the multiplication of the number of parameter values for each key
880
     *
881
     * @param array $paramArray Output of expandParameters(): Array with keys (GET var names) and for each an array of values
882
     * @param array $urls URLs accumulated in this array (for recursion)
883
     * @return array
884
     */
885 7
    public function compileUrls($paramArray, array $urls)
886
    {
887 7
        if (empty($paramArray)) {
888 7
            return $urls;
889
        }
890
        // shift first off stack:
891 6
        reset($paramArray);
892 6
        $varName = key($paramArray);
893 6
        $valueSet = array_shift($paramArray);
894
895
        // Traverse value set:
896 6
        $newUrls = [];
897 6
        foreach ($urls as $url) {
898 5
            foreach ($valueSet as $val) {
899 5
                $newUrls[] = $url . (strcmp((string) $val, '') ? '&' . rawurlencode($varName) . '=' . rawurlencode((string) $val) : '');
900
901 5
                if (count($newUrls) > $this->maximumUrlsToCompile) {
902
                    break;
903
                }
904
            }
905
        }
906 6
        return $this->compileUrls($paramArray, $newUrls);
907
    }
908
909
    /************************************
910
     *
911
     * Crawler log
912
     *
913
     ************************************/
914
915
    /**
916
     * Return array of records from crawler queue for input page ID
917
     *
918
     * @param integer $id Page ID for which to look up log entries.
919
     * @param boolean $doFlush If TRUE, then entries selected at DELETED(!) instead of selected!
920
     * @param boolean $doFullFlush
921
     * @param integer $itemsPerPage Limit the amount of entries per page default is 10
922
     * @return array
923
     */
924 4
    public function getLogEntriesForPageId($id, QueueFilter $queueFilter, $doFlush = false, $doFullFlush = false, $itemsPerPage = 10)
925
    {
926 4
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
927
        $queryBuilder
928 4
            ->select('*')
929 4
            ->from($this->tableName)
930 4
            ->where(
931 4
                $queryBuilder->expr()->eq('page_id', $queryBuilder->createNamedParameter($id, PDO::PARAM_INT))
932
            )
933 4
            ->orderBy('scheduled', 'DESC');
934
935 4
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
936 4
            ->getConnectionForTable($this->tableName)
937 4
            ->getExpressionBuilder();
938 4
        $query = $expressionBuilder->andX();
0 ignored issues
show
Unused Code introduced by
The assignment to $query is dead and can be removed.
Loading history...
939
        // PHPStorm adds the highlight that the $addWhere is immediately overwritten,
940
        // but the $query = $expressionBuilder->andX() ensures that the $addWhere is written correctly with AND
941
        // between the statements, it's not a mistake in the code.
942 4
        switch ($queueFilter) {
943 4
            case 'pending':
944
                $queryBuilder->andWhere($queryBuilder->expr()->eq('exec_time', 0));
945
                break;
946 4
            case 'finished':
947
                $queryBuilder->andWhere($queryBuilder->expr()->gt('exec_time', 0));
948
                break;
949
        }
950
951 4
        if ($doFlush) {
952 2
            if ($doFullFlush) {
953 1
                $this->queueRepository->flushQueue($queueFilter);
954
            } else {
955 1
                $this->queueRepository->flushQueue($queueFilter);
956
            }
957
        }
958 4
        if ($itemsPerPage > 0) {
959
            $queryBuilder
960 4
                ->setMaxResults((int) $itemsPerPage);
961
        }
962
963 4
        return $queryBuilder->execute()->fetchAll();
964
    }
965
966
    /**
967
     * Return array of records from crawler queue for input set ID
968
     *
969
     * @param int $set_id Set ID for which to look up log entries.
970
     * @param string $filter Filter: "all" => all entries, "pending" => all that is not yet run, "finished" => all complete ones
971
     * @param bool $doFlush If TRUE, then entries selected at DELETED(!) instead of selected!
972
     * @param int $itemsPerPage Limit the amount of entries per page default is 10
973
     * @return array
974
     *
975
     * @deprecated
976
     */
977 6
    public function getLogEntriesForSetId(int $set_id, string $filter = '', bool $doFlush = false, bool $doFullFlush = false, int $itemsPerPage = 10)
978
    {
979 6
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
980
        $queryBuilder
981 6
            ->select('*')
982 6
            ->from($this->tableName)
983 6
            ->where(
984 6
                $queryBuilder->expr()->eq('set_id', $queryBuilder->createNamedParameter($set_id, PDO::PARAM_INT))
985
            )
986 6
            ->orderBy('scheduled', 'DESC');
987
988 6
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
989 6
            ->getConnectionForTable($this->tableName)
990 6
            ->getExpressionBuilder();
991 6
        $query = $expressionBuilder->andX();
992
        // PHPStorm adds the highlight that the $addWhere is immediately overwritten,
993
        // but the $query = $expressionBuilder->andX() ensures that the $addWhere is written correctly with AND
994
        // between the statements, it's not a mistake in the code.
995 6
        $addWhere = '';
996 6
        switch ($filter) {
997 6
            case 'pending':
998 1
                $queryBuilder->andWhere($queryBuilder->expr()->eq('exec_time', 0));
999 1
                $addWhere = $query->add($expressionBuilder->eq('exec_time', 0));
0 ignored issues
show
Unused Code introduced by
The assignment to $addWhere is dead and can be removed.
Loading history...
1000 1
                break;
1001 5
            case 'finished':
1002 1
                $queryBuilder->andWhere($queryBuilder->expr()->gt('exec_time', 0));
1003 1
                $addWhere = $query->add($expressionBuilder->gt('exec_time', 0));
1004 1
                break;
1005
        }
1006 6
        if ($doFlush) {
1007 4
            $addWhere = $query->add($expressionBuilder->eq('set_id', (int) $set_id));
1008 4
            $this->flushQueue($doFullFlush ? '' : $addWhere);
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...ontroller::flushQueue() has been deprecated. ( Ignorable by Annotation )

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

1008
            /** @scrutinizer ignore-deprecated */ $this->flushQueue($doFullFlush ? '' : $addWhere);
Loading history...
1009 4
            return [];
1010
        }
1011 2
        if ($itemsPerPage > 0) {
1012
            $queryBuilder
1013 2
                ->setMaxResults((int) $itemsPerPage);
1014
        }
1015
1016 2
        return $queryBuilder->execute()->fetchAll();
1017
    }
1018
1019
    /**
1020
     * Adding call back entries to log (called from hooks typically, see indexed search class "class.crawler.php"
1021
     *
1022
     * @param integer $setId Set ID
1023
     * @param array $params Parameters to pass to call back function
1024
     * @param string $callBack Call back object reference, eg. 'EXT:indexed_search/class.crawler.php:&tx_indexedsearch_crawler'
1025
     * @param integer $page_id Page ID to attach it to
1026
     * @param integer $schedule Time at which to activate
1027
     */
1028
    public function addQueueEntry_callBack($setId, $params, $callBack, $page_id = 0, $schedule = 0): void
1029
    {
1030
        if (! is_array($params)) {
0 ignored issues
show
introduced by
The condition is_array($params) is always true.
Loading history...
1031
            $params = [];
1032
        }
1033
        $params['_CALLBACKOBJ'] = $callBack;
1034
1035
        GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_queue')
1036
            ->insert(
1037
                'tx_crawler_queue',
1038
                [
1039
                    'page_id' => (int) $page_id,
1040
                    'parameters' => json_encode($params),
1041
                    'scheduled' => (int) $schedule ?: $this->getCurrentTime(),
1042
                    'exec_time' => 0,
1043
                    'set_id' => (int) $setId,
1044
                    'result_data' => '',
1045
                ]
1046
            );
1047
    }
1048
1049
    /************************************
1050
     *
1051
     * URL setting
1052
     *
1053
     ************************************/
1054
1055
    /**
1056
     * Setting a URL for crawling:
1057
     *
1058
     * @param integer $id Page ID
1059
     * @param string $url Complete URL
1060
     * @param array $subCfg Sub configuration array (from TS config)
1061
     * @param integer $tstamp Scheduled-time
1062
     * @param string $configurationHash (optional) configuration hash
1063
     * @param bool $skipInnerDuplicationCheck (optional) skip inner duplication check
1064
     * @return bool
1065
     */
1066 8
    public function addUrl(
1067
        $id,
1068
        $url,
1069
        array $subCfg,
1070
        $tstamp,
1071
        $configurationHash = '',
1072
        $skipInnerDuplicationCheck = false
1073
    ) {
1074 8
        $urlAdded = false;
1075 8
        $rows = [];
1076
1077
        // Creating parameters:
1078
        $parameters = [
1079 8
            'url' => $url,
1080
        ];
1081
1082
        // fe user group simulation:
1083 8
        $uGs = implode(',', array_unique(GeneralUtility::intExplode(',', $subCfg['userGroups'], true)));
1084 8
        if ($uGs) {
1085 1
            $parameters['feUserGroupList'] = $uGs;
1086
        }
1087
1088
        // Setting processing instructions
1089 8
        $parameters['procInstructions'] = GeneralUtility::trimExplode(',', $subCfg['procInstrFilter']);
1090 8
        if (is_array($subCfg['procInstrParams.'])) {
1091 5
            $parameters['procInstrParams'] = $subCfg['procInstrParams.'];
1092
        }
1093
1094
        // Compile value array:
1095 8
        $parameters_serialized = json_encode($parameters);
1096
        $fieldArray = [
1097 8
            'page_id' => (int) $id,
1098 8
            'parameters' => $parameters_serialized,
1099 8
            'parameters_hash' => GeneralUtility::shortMD5($parameters_serialized),
1100 8
            'configuration_hash' => $configurationHash,
1101 8
            'scheduled' => $tstamp,
1102 8
            'exec_time' => 0,
1103 8
            'set_id' => (int) $this->setID,
1104 8
            'result_data' => '',
1105 8
            'configuration' => $subCfg['key'],
1106
        ];
1107
1108 8
        if ($this->registerQueueEntriesInternallyOnly) {
1109
            //the entries will only be registered and not stored to the database
1110 1
            $this->queueEntries[] = $fieldArray;
1111
        } else {
1112 7
            if (! $skipInnerDuplicationCheck) {
1113
                // check if there is already an equal entry
1114 6
                $rows = $this->queueRepository->getDuplicateQueueItemsIfExists(
1115 6
                    (bool) $this->extensionSettings['enableTimeslot'],
1116
                    $tstamp,
1117 6
                    $this->getCurrentTime(),
1118 6
                    $fieldArray['page_id'],
1119 6
                    $fieldArray['parameters_hash']
1120
                );
1121
            }
1122
1123 7
            if (empty($rows)) {
1124 6
                $connectionForCrawlerQueue = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_queue');
1125 6
                $connectionForCrawlerQueue->insert(
1126 6
                    'tx_crawler_queue',
1127
                    $fieldArray
1128
                );
1129 6
                $uid = $connectionForCrawlerQueue->lastInsertId('tx_crawler_queue', 'qid');
1130 6
                $rows[] = $uid;
1131 6
                $urlAdded = true;
1132
1133 6
                $signalPayload = ['uid' => $uid, 'fieldArray' => $fieldArray];
1134 6
                SignalSlotUtility::emitSignal(
1135 6
                    self::class,
1136 6
                    SignalSlotUtility::SIGNAL_URL_ADDED_TO_QUEUE,
1137
                    $signalPayload
1138
                );
1139
            } else {
1140 3
                $signalPayload = ['rows' => $rows, 'fieldArray' => $fieldArray];
1141 3
                SignalSlotUtility::emitSignal(
1142 3
                    self::class,
1143 3
                    SignalSlotUtility::SIGNAL_DUPLICATE_URL_IN_QUEUE,
1144
                    $signalPayload
1145
                );
1146
            }
1147
        }
1148
1149 8
        return $urlAdded;
1150
    }
1151
1152
    /**
1153
     * Returns the current system time
1154
     *
1155
     * @return int
1156
     */
1157 2
    public function getCurrentTime()
1158
    {
1159 2
        return time();
1160
    }
1161
1162
    /************************************
1163
     *
1164
     * URL reading
1165
     *
1166
     ************************************/
1167
1168
    /**
1169
     * Read URL for single queue entry
1170
     *
1171
     * @param integer $queueId
1172
     * @param boolean $force If set, will process even if exec_time has been set!
1173
     * @return integer
1174
     */
1175
    public function readUrl($queueId, $force = false)
1176
    {
1177
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
1178
        $ret = 0;
1179
        $this->logger->debug('crawler-readurl start ' . microtime(true));
1180
        // Get entry:
1181
        $queryBuilder
1182
            ->select('*')
1183
            ->from('tx_crawler_queue')
1184
            ->where(
1185
                $queryBuilder->expr()->eq('qid', $queryBuilder->createNamedParameter($queueId, PDO::PARAM_INT))
1186
            );
1187
        if (! $force) {
1188
            $queryBuilder
1189
                ->andWhere('exec_time = 0')
1190
                ->andWhere('process_scheduled > 0');
1191
        }
1192
        $queueRec = $queryBuilder->execute()->fetch();
1193
1194
        if (! is_array($queueRec)) {
1195
            return;
1196
        }
1197
1198
        SignalSlotUtility::emitSignal(
1199
            self::class,
1200
            SignalSlotUtility::SIGNAL_QUEUEITEM_PREPROCESS,
1201
            [$queueId, &$queueRec]
1202
        );
1203
1204
        // Set exec_time to lock record:
1205
        $field_array = ['exec_time' => $this->getCurrentTime()];
1206
1207
        if (isset($this->processID)) {
1208
            //if mulitprocessing is used we need to store the id of the process which has handled this entry
1209
            $field_array['process_id_completed'] = $this->processID;
1210
        }
1211
1212
        GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_queue')
1213
            ->update(
1214
                'tx_crawler_queue',
1215
                $field_array,
1216
                ['qid' => (int) $queueId]
1217
            );
1218
1219
        $result = $this->queueExecutor->executeQueueItem($queueRec, $this);
1220
        if ($result['content'] === null) {
1221
            $resultData = 'An errors happened';
1222
        } else {
1223
            /** @var JsonCompatibilityConverter $jsonCompatibilityConverter */
1224
            $jsonCompatibilityConverter = GeneralUtility::makeInstance(JsonCompatibilityConverter::class);
1225
            $resultData = $jsonCompatibilityConverter->convert($result['content']);
1226
        }
1227
1228
        //atm there's no need to point to specific pollable extensions
1229
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pollSuccess'])) {
1230
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pollSuccess'] as $pollable) {
1231
                // only check the success value if the instruction is runnig
1232
                // it is important to name the pollSuccess key same as the procInstructions key
1233
                if (is_array($resultData['parameters']['procInstructions'])
1234
                    && in_array(
1235
                        $pollable,
1236
                        $resultData['parameters']['procInstructions'], true
1237
                    )
1238
                ) {
1239
                    if (! empty($resultData['success'][$pollable]) && $resultData['success'][$pollable]) {
1240
                        $ret |= self::CLI_STATUS_POLLABLE_PROCESSED;
1241
                    }
1242
                }
1243
            }
1244
        }
1245
1246
        // Set result in log which also denotes the end of the processing of this entry.
1247
        $field_array = ['result_data' => json_encode($result)];
1248
1249
        SignalSlotUtility::emitSignal(
1250
            self::class,
1251
            SignalSlotUtility::SIGNAL_QUEUEITEM_POSTPROCESS,
1252
            [$queueId, &$field_array]
1253
        );
1254
1255
        GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_queue')
1256
            ->update(
1257
                'tx_crawler_queue',
1258
                $field_array,
1259
                ['qid' => (int) $queueId]
1260
            );
1261
1262
        $this->logger->debug('crawler-readurl stop ' . microtime(true));
1263
        return $ret;
1264
    }
1265
1266
    /**
1267
     * Read URL for not-yet-inserted log-entry
1268
     *
1269
     * @param array $field_array Queue field array,
1270
     *
1271
     * @return array|bool|mixed|string
1272
     */
1273
    public function readUrlFromArray($field_array)
1274
    {
1275
        // Set exec_time to lock record:
1276
        $field_array['exec_time'] = $this->getCurrentTime();
1277
        $connectionForCrawlerQueue = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->tableName);
1278
        $connectionForCrawlerQueue->insert(
1279
            $this->tableName,
1280
            $field_array
1281
        );
1282
        $queueId = $connectionForCrawlerQueue->lastInsertId($this->tableName, 'qid');
1283
        $result = $this->queueExecutor->executeQueueItem($field_array, $this);
1284
1285
        // Set result in log which also denotes the end of the processing of this entry.
1286
        $field_array = ['result_data' => json_encode($result)];
1287
1288
        SignalSlotUtility::emitSignal(
1289
            self::class,
1290
            SignalSlotUtility::SIGNAL_QUEUEITEM_POSTPROCESS,
1291
            [$queueId, &$field_array]
1292
        );
1293
1294
        $connectionForCrawlerQueue->update(
1295
            $this->tableName,
1296
            $field_array,
1297
            ['qid' => $queueId]
1298
        );
1299
1300
        return $result;
1301
    }
1302
1303
    /*****************************
1304
     *
1305
     * Compiling URLs to crawl - tools
1306
     *
1307
     *****************************/
1308
1309
    /**
1310
     * @param integer $id Root page id to start from.
1311
     * @param integer $depth Depth of tree, 0=only id-page, 1= on sublevel, 99 = infinite
1312
     * @param integer $scheduledTime Unix Time when the URL is timed to be visited when put in queue
1313
     * @param integer $reqMinute Number of requests per minute (creates the interleave between requests)
1314
     * @param boolean $submitCrawlUrls If set, submits the URLs to queue in database (real crawling)
1315
     * @param boolean $downloadCrawlUrls If set (and submitcrawlUrls is false) will fill $downloadUrls with entries)
1316
     * @param array $incomingProcInstructions Array of processing instructions
1317
     * @param array $configurationSelection Array of configuration keys
1318
     * @return string
1319
     */
1320
    public function getPageTreeAndUrls(
1321
        $id,
1322
        $depth,
1323
        $scheduledTime,
1324
        $reqMinute,
1325
        $submitCrawlUrls,
1326
        $downloadCrawlUrls,
1327
        array $incomingProcInstructions,
1328
        array $configurationSelection
1329
    ) {
1330
        $this->scheduledTime = $scheduledTime;
1331
        $this->reqMinute = $reqMinute;
1332
        $this->submitCrawlUrls = $submitCrawlUrls;
1333
        $this->downloadCrawlUrls = $downloadCrawlUrls;
1334
        $this->incomingProcInstructions = $incomingProcInstructions;
1335
        $this->incomingConfigurationSelection = $configurationSelection;
1336
1337
        $this->duplicateTrack = [];
1338
        $this->downloadUrls = [];
1339
1340
        // Drawing tree:
1341
        /* @var PageTreeView $tree */
1342
        $tree = GeneralUtility::makeInstance(PageTreeView::class);
1343
        $perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
1344
        $tree->init('AND ' . $perms_clause);
1345
1346
        $pageInfo = BackendUtility::readPageAccess($id, $perms_clause);
1347
        if (is_array($pageInfo)) {
0 ignored issues
show
introduced by
The condition is_array($pageInfo) is always true.
Loading history...
1348
            // Set root row:
1349
            $tree->tree[] = [
1350
                'row' => $pageInfo,
1351
                'HTML' => $this->iconFactory->getIconForRecord('pages', $pageInfo, Icon::SIZE_SMALL),
1352
            ];
1353
        }
1354
1355
        // Get branch beneath:
1356
        if ($depth) {
1357
            $tree->getTree($id, $depth, '');
1358
        }
1359
1360
        // Traverse page tree:
1361
        $code = '';
1362
1363
        foreach ($tree->tree as $data) {
1364
            $this->MP = false;
1365
1366
            // recognize mount points
1367
            if ($data['row']['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
1368
                $mountpage = $this->pageRepository->getPage($data['row']['uid']);
1369
1370
                // fetch mounted pages
1371
                $this->MP = $mountpage[0]['mount_pid'] . '-' . $data['row']['uid'];
0 ignored issues
show
Documentation Bug introduced by
The property $MP was declared of type boolean, but $mountpage[0]['mount_pid...' . $data['row']['uid'] is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1372
1373
                $mountTree = GeneralUtility::makeInstance(PageTreeView::class);
1374
                $mountTree->init('AND ' . $perms_clause);
1375
                $mountTree->getTree($mountpage[0]['mount_pid'], $depth);
1376
1377
                foreach ($mountTree->tree as $mountData) {
1378
                    $code .= $this->drawURLs_addRowsForPage(
1379
                        $mountData['row'],
1380
                        $mountData['HTML'] . BackendUtility::getRecordTitle('pages', $mountData['row'], true)
1381
                    );
1382
                }
1383
1384
                // replace page when mount_pid_ol is enabled
1385
                if ($mountpage[0]['mount_pid_ol']) {
1386
                    $data['row']['uid'] = $mountpage[0]['mount_pid'];
1387
                } else {
1388
                    // if the mount_pid_ol is not set the MP must not be used for the mountpoint page
1389
                    $this->MP = false;
1390
                }
1391
            }
1392
1393
            $code .= $this->drawURLs_addRowsForPage(
1394
                $data['row'],
1395
                $data['HTML'] . BackendUtility::getRecordTitle('pages', $data['row'], true)
1396
            );
1397
        }
1398
1399
        return $code;
1400
    }
1401
1402
    /**
1403
     * Expands exclude string
1404
     *
1405
     * @param string $excludeString Exclude string
1406
     * @return array
1407
     */
1408 2
    public function expandExcludeString($excludeString)
1409
    {
1410
        // internal static caches;
1411 2
        static $expandedExcludeStringCache;
1412 2
        static $treeCache;
1413
1414 2
        if (empty($expandedExcludeStringCache[$excludeString])) {
1415 2
            $pidList = [];
1416
1417 2
            if (! empty($excludeString)) {
1418
                /** @var PageTreeView $tree */
1419 1
                $tree = GeneralUtility::makeInstance(PageTreeView::class);
1420 1
                $tree->init('AND ' . $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
1421
1422 1
                $excludeParts = GeneralUtility::trimExplode(',', $excludeString);
1423
1424 1
                foreach ($excludeParts as $excludePart) {
1425 1
                    [$pid, $depth] = GeneralUtility::trimExplode('+', $excludePart);
1426
1427
                    // default is "page only" = "depth=0"
1428 1
                    if (empty($depth)) {
1429 1
                        $depth = (stristr($excludePart, '+')) ? 99 : 0;
1430
                    }
1431
1432 1
                    $pidList[] = (int) $pid;
1433
1434 1
                    if ($depth > 0) {
1435
                        if (empty($treeCache[$pid][$depth])) {
1436
                            $tree->reset();
1437
                            $tree->getTree($pid, $depth);
0 ignored issues
show
Bug introduced by
$pid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Tree\V...ractTreeView::getTree(). ( Ignorable by Annotation )

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

1437
                            $tree->getTree(/** @scrutinizer ignore-type */ $pid, $depth);
Loading history...
1438
                            $treeCache[$pid][$depth] = $tree->tree;
1439
                        }
1440
1441
                        foreach ($treeCache[$pid][$depth] as $data) {
1442
                            $pidList[] = (int) $data['row']['uid'];
1443
                        }
1444
                    }
1445
                }
1446
            }
1447
1448 2
            $expandedExcludeStringCache[$excludeString] = array_unique($pidList);
1449
        }
1450
1451 2
        return $expandedExcludeStringCache[$excludeString];
1452
    }
1453
1454
    /**
1455
     * Create the rows for display of the page tree
1456
     * For each page a number of rows are shown displaying GET variable configuration
1457
     */
1458
    public function drawURLs_addRowsForPage(array $pageRow, string $pageTitle): string
1459
    {
1460
        $skipMessage = '';
1461
1462
        // Get list of configurations
1463
        $configurations = $this->getUrlsForPageRow($pageRow, $skipMessage);
1464
1465
        if (! empty($this->incomingConfigurationSelection)) {
1466
            // remove configuration that does not match the current selection
1467
            foreach ($configurations as $confKey => $confArray) {
1468
                if (! in_array($confKey, $this->incomingConfigurationSelection, true)) {
1469
                    unset($configurations[$confKey]);
1470
                }
1471
            }
1472
        }
1473
1474
        // Traverse parameter combinations:
1475
        $c = 0;
1476
        $content = '';
1477
        if (! empty($configurations)) {
1478
            foreach ($configurations as $confKey => $confArray) {
1479
1480
                // Title column:
1481
                if (! $c) {
1482
                    $titleClm = '<td rowspan="' . count($configurations) . '">' . $pageTitle . '</td>';
1483
                } else {
1484
                    $titleClm = '';
1485
                }
1486
1487
                if (! in_array($pageRow['uid'], $this->expandExcludeString($confArray['subCfg']['exclude']), true)) {
1488
1489
                    // URL list:
1490
                    $urlList = $this->urlListFromUrlArray(
1491
                        $confArray,
1492
                        $pageRow,
1493
                        $this->scheduledTime,
1494
                        $this->reqMinute,
1495
                        $this->submitCrawlUrls,
1496
                        $this->downloadCrawlUrls,
1497
                        $this->duplicateTrack,
1498
                        $this->downloadUrls,
1499
                        // if empty the urls won't be filtered by processing instructions
1500
                        $this->incomingProcInstructions
1501
                    );
1502
1503
                    // Expanded parameters:
1504
                    $paramExpanded = '';
1505
                    $calcAccu = [];
1506
                    $calcRes = 1;
1507
                    foreach ($confArray['paramExpanded'] as $gVar => $gVal) {
1508
                        $paramExpanded .= '
1509
                            <tr>
1510
                                <td>' . htmlspecialchars('&' . $gVar . '=') . '<br/>' .
1511
                            '(' . count($gVal) . ')' .
1512
                            '</td>
1513
                                <td nowrap="nowrap">' . nl2br(htmlspecialchars(implode(chr(10), $gVal))) . '</td>
1514
                            </tr>
1515
                        ';
1516
                        $calcRes *= count($gVal);
1517
                        $calcAccu[] = count($gVal);
1518
                    }
1519
                    $paramExpanded = '<table>' . $paramExpanded . '</table>';
1520
                    $paramExpanded .= 'Comb: ' . implode('*', $calcAccu) . '=' . $calcRes;
1521
1522
                    // Options
1523
                    $optionValues = '';
1524
                    if ($confArray['subCfg']['userGroups']) {
1525
                        $optionValues .= 'User Groups: ' . $confArray['subCfg']['userGroups'] . '<br/>';
1526
                    }
1527
                    if ($confArray['subCfg']['procInstrFilter']) {
1528
                        $optionValues .= 'ProcInstr: ' . $confArray['subCfg']['procInstrFilter'] . '<br/>';
1529
                    }
1530
1531
                    // Compile row:
1532
                    $content .= '
1533
                        <tr>
1534
                            ' . $titleClm . '
1535
                            <td>' . htmlspecialchars($confKey) . '</td>
1536
                            <td>' . nl2br(htmlspecialchars(rawurldecode(trim(str_replace('&', chr(10) . '&', GeneralUtility::implodeArrayForUrl('', $confArray['paramParsed'])))))) . '</td>
1537
                            <td>' . $paramExpanded . '</td>
1538
                            <td nowrap="nowrap">' . $urlList . '</td>
1539
                            <td nowrap="nowrap">' . $optionValues . '</td>
1540
                            <td nowrap="nowrap">' . DebugUtility::viewArray($confArray['subCfg']['procInstrParams.']) . '</td>
1541
                        </tr>';
1542
                } else {
1543
                    $content .= '<tr>
1544
                            ' . $titleClm . '
1545
                            <td>' . htmlspecialchars($confKey) . '</td>
1546
                            <td colspan="5"><em>No entries</em> (Page is excluded in this configuration)</td>
1547
                        </tr>';
1548
                }
1549
1550
                $c++;
1551
            }
1552
        } else {
1553
            $message = ! empty($skipMessage) ? ' (' . $skipMessage . ')' : '';
1554
1555
            // Compile row:
1556
            $content .= '
1557
                <tr>
1558
                    <td>' . $pageTitle . '</td>
1559
                    <td colspan="6"><em>No entries</em>' . $message . '</td>
1560
                </tr>';
1561
        }
1562
1563
        return $content;
1564
    }
1565
1566
    /*****************************
1567
     *
1568
     * CLI functions
1569
     *
1570
     *****************************/
1571
1572
    /**
1573
     * Running the functionality of the CLI (crawling URLs from queue)
1574
     */
1575
    public function CLI_run(int $countInARun, int $sleepTime, int $sleepAfterFinish): int
1576
    {
1577
        $result = 0;
1578
        $counter = 0;
1579
1580
        // First, run hooks:
1581
        $this->CLI_runHooks();
1582
1583
        // Clean up the queue
1584
        $this->queueRepository->cleanupQueue();
1585
1586
        // Select entries:
1587
        $rows = $this->queueRepository->fetchRecordsToBeCrawled($countInARun);
1588
1589
        if (! empty($rows)) {
1590
            $quidList = [];
1591
1592
            foreach ($rows as $r) {
1593
                $quidList[] = $r['qid'];
1594
            }
1595
1596
            $processId = $this->CLI_buildProcessId();
1597
1598
            //save the number of assigned queue entries to determine how many have been processed later
1599
            $numberOfAffectedRows = $this->queueRepository->updateProcessIdAndSchedulerForQueueIds($quidList, $processId);
1600
            $this->processRepository->updateProcessAssignItemsCount($numberOfAffectedRows, $processId);
1601
1602
            if ($numberOfAffectedRows !== count($quidList)) {
1603
                $this->CLI_debug('Nothing processed due to multi-process collision (' . $this->CLI_buildProcessId() . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1603
                /** @scrutinizer ignore-deprecated */ $this->CLI_debug('Nothing processed due to multi-process collision (' . $this->CLI_buildProcessId() . ')');
Loading history...
1604
                return ($result | self::CLI_STATUS_ABORTED);
1605
            }
1606
1607
            foreach ($rows as $r) {
1608
                $result |= $this->readUrl($r['qid']);
1609
1610
                $counter++;
1611
                // Just to relax the system
1612
                usleep((int) $sleepTime);
1613
1614
                // if during the start and the current read url the cli has been disable we need to return from the function
1615
                // mark the process NOT as ended.
1616
                if ($this->crawler->isDisabled()) {
1617
                    return ($result | self::CLI_STATUS_ABORTED);
1618
                }
1619
1620
                if (! $this->processRepository->isProcessActive($this->CLI_buildProcessId())) {
1621
                    $this->CLI_debug('conflict / timeout (' . $this->CLI_buildProcessId() . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1621
                    /** @scrutinizer ignore-deprecated */ $this->CLI_debug('conflict / timeout (' . $this->CLI_buildProcessId() . ')');
Loading history...
1622
                    $result |= self::CLI_STATUS_ABORTED;
1623
                    //possible timeout
1624
                    break;
1625
                }
1626
            }
1627
1628
            sleep((int) $sleepAfterFinish);
1629
1630
            $msg = 'Rows: ' . $counter;
1631
            $this->CLI_debug($msg . ' (' . $this->CLI_buildProcessId() . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1631
            /** @scrutinizer ignore-deprecated */ $this->CLI_debug($msg . ' (' . $this->CLI_buildProcessId() . ')');
Loading history...
1632
        } else {
1633
            $this->CLI_debug('Nothing within queue which needs to be processed (' . $this->CLI_buildProcessId() . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1633
            /** @scrutinizer ignore-deprecated */ $this->CLI_debug('Nothing within queue which needs to be processed (' . $this->CLI_buildProcessId() . ')');
Loading history...
1634
        }
1635
1636
        if ($counter > 0) {
1637
            $result |= self::CLI_STATUS_PROCESSED;
1638
        }
1639
1640
        return $result;
1641
    }
1642
1643
    /**
1644
     * Activate hooks
1645
     */
1646
    public function CLI_runHooks(): void
1647
    {
1648
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['cli_hooks'] ?? [] as $objRef) {
1649
            $hookObj = GeneralUtility::makeInstance($objRef);
1650
            if (is_object($hookObj)) {
1651
                $hookObj->crawler_init($this);
1652
            }
1653
        }
1654
    }
1655
1656
    /**
1657
     * Try to acquire a new process with the given id
1658
     * also performs some auto-cleanup for orphan processes
1659
     * @param string $id identification string for the process
1660
     * @return boolean
1661
     * @todo preemption might not be the most elegant way to clean up
1662
     */
1663
    public function CLI_checkAndAcquireNewProcess($id)
1664
    {
1665
        $ret = true;
1666
1667
        $systemProcessId = getmypid();
1668
        if (! $systemProcessId) {
1669
            return false;
1670
        }
1671
1672
        $processCount = 0;
1673
        $orphanProcesses = [];
1674
1675
        $activeProcesses = $this->processRepository->findAllActive();
1676
        $currentTime = $this->getCurrentTime();
1677
1678
        /** @var Process $process */
1679
        foreach ($activeProcesses as $process) {
1680
            if ($process->getTtl() < $currentTime) {
1681
                $orphanProcesses[] = $process->getProcessId();
1682
            } else {
1683
                $processCount++;
1684
            }
1685
        }
1686
1687
        // if there are less than allowed active processes then add a new one
1688
        if ($processCount < (int) $this->extensionSettings['processLimit']) {
1689
            $this->CLI_debug('add process ' . $this->CLI_buildProcessId() . ' (' . ($processCount + 1) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1689
            /** @scrutinizer ignore-deprecated */ $this->CLI_debug('add process ' . $this->CLI_buildProcessId() . ' (' . ($processCount + 1) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
Loading history...
1690
1691
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_process')->insert(
1692
                'tx_crawler_process',
1693
                [
1694
                    'process_id' => $id,
1695
                    'active' => 1,
1696
                    'ttl' => $currentTime + (int) $this->extensionSettings['processMaxRunTime'],
1697
                    'system_process_id' => $systemProcessId,
1698
                ]
1699
            );
1700
        } else {
1701
            $this->CLI_debug('Processlimit reached (' . ($processCount) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...Controller::CLI_debug() has been deprecated. ( Ignorable by Annotation )

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

1701
            /** @scrutinizer ignore-deprecated */ $this->CLI_debug('Processlimit reached (' . ($processCount) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
Loading history...
1702
            $ret = false;
1703
        }
1704
1705
        $this->processRepository->deleteProcessesMarkedAsDeleted();
1706
        $this->CLI_releaseProcesses($orphanProcesses);
1707
1708
        return $ret;
1709
    }
1710
1711
    /**
1712
     * Release a process and the required resources
1713
     *
1714
     * @param mixed $releaseIds string with a single process-id or array with multiple process-ids
1715
     * @return boolean
1716
     */
1717
    public function CLI_releaseProcesses($releaseIds)
1718
    {
1719
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
1720
1721
        if (! is_array($releaseIds)) {
1722
            $releaseIds = [$releaseIds];
1723
        }
1724
1725
        if (empty($releaseIds)) {
1726
            //nothing to release
1727
            return false;
1728
        }
1729
1730
        // some kind of 2nd chance algo - this way you need at least 2 processes to have a real cleanup
1731
        // this ensures that a single process can't mess up the entire process table
1732
1733
        // mark all processes as deleted which have no "waiting" queue-entires and which are not active
1734
1735
        $queryBuilder
1736
            ->update($this->tableName, 'q')
1737
            ->where(
1738
                'q.process_id IN(SELECT p.process_id FROM tx_crawler_process as p WHERE p.active = 0)'
1739
            )
1740
            ->set('q.process_scheduled', 0)
1741
            ->set('q.process_id', '')
1742
            ->execute();
1743
1744
        // FIXME: Not entirely sure that this is equivalent to the previous version
1745
        $queryBuilder->resetQueryPart('set');
1746
1747
        $queryBuilder
1748
            ->update('tx_crawler_process')
1749
            ->where(
1750
                $queryBuilder->expr()->eq('active', 0),
1751
                'process_id IN(SELECT q.process_id FROM tx_crawler_queue as q WHERE q.exec_time = 0)'
1752
            )
1753
            ->set('system_process_id', 0)
1754
            ->execute();
1755
1756
        $this->processRepository->markRequestedProcessesAsNotActive($releaseIds);
1757
        $this->queueRepository->unsetProcessScheduledAndProcessIdForQueueEntries($releaseIds);
1758
1759
        return true;
1760
    }
1761
1762
    /**
1763
     * Create a unique Id for the current process
1764
     *
1765
     * @return string the ID
1766
     */
1767 1
    public function CLI_buildProcessId()
1768
    {
1769 1
        if (! $this->processID) {
1770
            $this->processID = GeneralUtility::shortMD5(microtime(true));
1771
        }
1772 1
        return $this->processID;
1773
    }
1774
1775
    /**
1776
     * Prints a message to the stdout (only if debug-mode is enabled)
1777
     *
1778
     * @param string $msg the message
1779
     * @deprecated
1780
     * @codeCoverageIgnore
1781
     */
1782
    public function CLI_debug($msg): void
1783
    {
1784
        if ((int) $this->extensionSettings['processDebug']) {
1785
            echo $msg . "\n";
1786
            flush();
1787
        }
1788
    }
1789
1790
    /**
1791
     * Cleans up entries that stayed for too long in the queue. These are:
1792
     * - processed entries that are over 1.5 days in age
1793
     * - scheduled entries that are over 7 days old
1794
     *
1795
     * @deprecated
1796
     */
1797 1
    public function cleanUpOldQueueEntries(): void
1798
    {
1799
        // 24*60*60 Seconds in 24 hours
1800 1
        $processedAgeInSeconds = $this->extensionSettings['cleanUpProcessedAge'] * 86400;
1801 1
        $scheduledAgeInSeconds = $this->extensionSettings['cleanUpScheduledAge'] * 86400;
1802
1803 1
        $now = time();
1804 1
        $condition = '(exec_time<>0 AND exec_time<' . ($now - $processedAgeInSeconds) . ') OR scheduled<=' . ($now - $scheduledAgeInSeconds);
1805 1
        $this->flushQueue($condition);
0 ignored issues
show
Deprecated Code introduced by
The function AOE\Crawler\Controller\C...ontroller::flushQueue() has been deprecated. ( Ignorable by Annotation )

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

1805
        /** @scrutinizer ignore-deprecated */ $this->flushQueue($condition);
Loading history...
1806 1
    }
1807
1808
    /**
1809
     * Removes queue entries
1810
     *
1811
     * @param string $where SQL related filter for the entries which should be removed
1812
     *
1813
     * @deprecated
1814
     */
1815 5
    protected function flushQueue($where = ''): void
1816
    {
1817 5
        $realWhere = strlen((string) $where) > 0 ? $where : '1=1';
1818
1819 5
        $queryBuilder = $this->getQueryBuilder($this->tableName);
1820
1821
        $groups = $queryBuilder
1822 5
            ->selectLiteral('DISTINCT set_id')
1823 5
            ->from($this->tableName)
1824 5
            ->where($realWhere)
1825 5
            ->execute()
1826 5
            ->fetchAll();
1827 5
        if (is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
1828 5
            foreach ($groups as $group) {
1829
                $subSet = $queryBuilder
1830 4
                    ->select('qid', 'set_id')
1831 4
                    ->from($this->tableName)
1832 4
                    ->where(
1833 4
                        $realWhere,
1834 4
                        $queryBuilder->expr()->eq('set_id', $group['set_id'])
1835
                    )
1836 4
                    ->execute()
1837 4
                    ->fetchAll();
1838
1839 4
                $payLoad = ['subSet' => $subSet];
1840 4
                SignalSlotUtility::emitSignal(
1841 4
                    self::class,
1842 4
                    SignalSlotUtility::SIGNAL_QUEUE_ENTRY_FLUSH,
1843
                    $payLoad
1844
                );
1845
            }
1846
        }
1847
1848
        $queryBuilder
1849 5
            ->delete($this->tableName)
1850 5
            ->where($realWhere)
1851 5
            ->execute();
1852 5
    }
1853
1854
    /**
1855
     * This method determines duplicates for a queue entry with the same parameters and this timestamp.
1856
     * If the timestamp is in the past, it will check if there is any unprocessed queue entry in the past.
1857
     * If the timestamp is in the future it will check, if the queued entry has exactly the same timestamp
1858
     *
1859
     * @param int $tstamp
1860
     * @param array $fieldArray
1861
     *
1862
     * @return array
1863
     * @deprecated
1864
     */
1865 5
    protected function getDuplicateRowsIfExist($tstamp, $fieldArray)
1866
    {
1867 5
        $rows = [];
1868
1869 5
        $currentTime = $this->getCurrentTime();
1870
1871 5
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
1872
        $queryBuilder
1873 5
            ->select('qid')
1874 5
            ->from('tx_crawler_queue');
1875
        //if this entry is scheduled with "now"
1876 5
        if ($tstamp <= $currentTime) {
1877 2
            if ($this->extensionSettings['enableTimeslot']) {
1878 1
                $timeBegin = $currentTime - 100;
1879 1
                $timeEnd = $currentTime + 100;
1880
                $queryBuilder
1881 1
                    ->where(
1882 1
                        'scheduled BETWEEN ' . $timeBegin . ' AND ' . $timeEnd . ''
1883
                    )
1884 1
                    ->orWhere(
1885 1
                        $queryBuilder->expr()->lte('scheduled', $currentTime)
1886
                    );
1887
            } else {
1888
                $queryBuilder
1889 1
                    ->where(
1890 2
                        $queryBuilder->expr()->lte('scheduled', $currentTime)
1891
                    );
1892
            }
1893 3
        } elseif ($tstamp > $currentTime) {
1894
            //entry with a timestamp in the future need to have the same schedule time
1895
            $queryBuilder
1896 3
                ->where(
1897 3
                    $queryBuilder->expr()->eq('scheduled', $tstamp)
1898
                );
1899
        }
1900
1901
        $queryBuilder
1902 5
            ->andWhere('NOT exec_time')
1903 5
            ->andWhere('NOT process_id')
1904 5
            ->andWhere($queryBuilder->expr()->eq('page_id', $queryBuilder->createNamedParameter($fieldArray['page_id'], PDO::PARAM_INT)))
1905 5
            ->andWhere($queryBuilder->expr()->eq('parameters_hash', $queryBuilder->createNamedParameter($fieldArray['parameters_hash'], PDO::PARAM_STR)));
1906
1907 5
        $statement = $queryBuilder->execute();
1908
1909 5
        while ($row = $statement->fetch()) {
1910 5
            $rows[] = $row['qid'];
1911
        }
1912
1913 5
        return $rows;
1914
    }
1915
1916
    /**
1917
     * Returns a md5 hash generated from a serialized configuration array.
1918
     *
1919
     * @return string
1920
     */
1921 10
    protected function getConfigurationHash(array $configuration)
1922
    {
1923 10
        unset($configuration['paramExpanded']);
1924 10
        unset($configuration['URLs']);
1925 10
        return md5(serialize($configuration));
1926
    }
1927
1928
    /**
1929
     * Build a URL from a Page and the Query String. If the page has a Site configuration, it can be built by using
1930
     * the Site instance.
1931
     *
1932
     * @param int $httpsOrHttp see tx_crawler_configuration.force_ssl
1933
     * @throws SiteNotFoundException
1934
     * @throws InvalidRouteArgumentsException
1935
     *
1936
     * @deprecated Using CrawlerController::getUrlFromPageAndQueryParameters() is deprecated since 9.1.1 and will be removed in v11.x, please use UrlService->getUrlFromPageAndQueryParameters() instead.
1937
     * @codeCoverageIgnore
1938
     */
1939
    protected function getUrlFromPageAndQueryParameters(int $pageId, string $queryString, ?string $alternativeBaseUrl, int $httpsOrHttp): UriInterface
1940
    {
1941
        $urlService = new UrlService();
1942
        return $urlService->getUrlFromPageAndQueryParameters($pageId, $queryString, $alternativeBaseUrl, $httpsOrHttp);
1943
    }
1944
1945 1
    protected function swapIfFirstIsLargerThanSecond(array $reg): array
1946
    {
1947
        // Swap if first is larger than last:
1948 1
        if ($reg[1] > $reg[2]) {
1949
            $temp = $reg[2];
1950
            $reg[2] = $reg[1];
1951
            $reg[1] = $temp;
1952
        }
1953
1954 1
        return $reg;
1955
    }
1956
1957
    /**
1958
     * @return BackendUserAuthentication
1959
     */
1960 2
    private function getBackendUser()
1961
    {
1962
        // Make sure the _cli_ user is loaded
1963 2
        Bootstrap::initializeBackendAuthentication();
1964 2
        if ($this->backendUser === null) {
1965 2
            $this->backendUser = $GLOBALS['BE_USER'];
1966
        }
1967 2
        return $this->backendUser;
1968
    }
1969
1970
    /**
1971
     * Get querybuilder for given table
1972
     *
1973
     * @return QueryBuilder
1974
     */
1975 12
    private function getQueryBuilder(string $table)
1976
    {
1977 12
        return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1978
    }
1979
}
1980