Passed
Push — UrlService ( 5b56f8 )
by Tomas Norre
08:13
created

CrawlerController::urlListFromUrlArray()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 8.1348

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 34
cts 39
cp 0.8718
crap 8.1348
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\Domain\Repository\ConfigurationRepository;
34
use AOE\Crawler\Domain\Repository\ProcessRepository;
35
use AOE\Crawler\Domain\Repository\QueueRepository;
36
use AOE\Crawler\QueueExecutor;
37
use AOE\Crawler\Service\UrlService;
38
use AOE\Crawler\Utility\SignalSlotUtility;
39
use Psr\Http\Message\UriInterface;
40
use Psr\Log\LoggerAwareInterface;
41
use Psr\Log\LoggerAwareTrait;
42
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
43
use TYPO3\CMS\Backend\Utility\BackendUtility;
44
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
45
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
46
use TYPO3\CMS\Core\Core\Bootstrap;
47
use TYPO3\CMS\Core\Core\Environment;
48
use TYPO3\CMS\Core\Database\Connection;
49
use TYPO3\CMS\Core\Database\ConnectionPool;
50
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
51
use TYPO3\CMS\Core\Imaging\Icon;
52
use TYPO3\CMS\Core\Imaging\IconFactory;
53
use TYPO3\CMS\Core\Site\Entity\Site;
54
use TYPO3\CMS\Core\Type\Bitmask\Permission;
55
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
56
use TYPO3\CMS\Core\Utility\DebugUtility;
57
use TYPO3\CMS\Core\Utility\GeneralUtility;
58
use TYPO3\CMS\Core\Utility\MathUtility;
59
use TYPO3\CMS\Extbase\Object\ObjectManager;
60
use TYPO3\CMS\Frontend\Page\PageRepository;
61
62
/**
63
 * Class CrawlerController
64
 *
65
 * @package AOE\Crawler\Controller
66
 */
67
class CrawlerController implements LoggerAwareInterface
68
{
69
    use LoggerAwareTrait;
70
    use PublicMethodDeprecationTrait;
71
72
    public const CLI_STATUS_NOTHING_PROCCESSED = 0;
73
74
    public const CLI_STATUS_REMAIN = 1; //queue not empty
75
76
    public const CLI_STATUS_PROCESSED = 2; //(some) queue items where processed
77
78
    public const CLI_STATUS_ABORTED = 4; //instance didn't finish
79
80
    public const CLI_STATUS_POLLABLE_PROCESSED = 8;
81
82
    /**
83
     * @var integer
84
     */
85
    public $setID = 0;
86
87
    /**
88
     * @var string
89
     */
90
    public $processID = '';
91
92
    /**
93
     * @var array
94
     */
95
    public $duplicateTrack = [];
96
97
    /**
98
     * @var array
99
     */
100
    public $downloadUrls = [];
101
102
    /**
103
     * @var array
104
     */
105
    public $incomingProcInstructions = [];
106
107
    /**
108
     * @var array
109
     */
110
    public $incomingConfigurationSelection = [];
111
112
    /**
113
     * @var bool
114
     */
115
    public $registerQueueEntriesInternallyOnly = false;
116
117
    /**
118
     * @var array
119
     */
120
    public $queueEntries = [];
121
122
    /**
123
     * @var array
124
     */
125
    public $urlList = [];
126
127
    /**
128
     * @var array
129
     */
130
    public $extensionSettings = [];
131
132
    /**
133
     * Mount Point
134
     *
135
     * @var bool
136
     * Todo: Check what this is used for and adjust the type hint or code, as bool doesn't match the current code.
137
     */
138
    public $MP = false;
139
140
    /**
141
     * @var string
142
     */
143
    protected $processFilename;
144
145
    /**
146
     * Holds the internal access mode can be 'gui','cli' or 'cli_im'
147
     *
148
     * @var string
149
     */
150
    protected $accessMode;
151
152
    /**
153
     * @var QueueRepository
154
     */
155
    protected $queueRepository;
156
157
    /**
158
     * @var ProcessRepository
159
     */
160
    protected $processRepository;
161
162
    /**
163
     * @var ConfigurationRepository
164
     */
165
    protected $configurationRepository;
166
167
    /**
168
     * @var string
169
     */
170
    protected $tableName = 'tx_crawler_queue';
171
172
    /**
173
     * @var QueueExecutor
174
     */
175
    protected $queueExecutor;
176
177
    /**
178
     * @var int
179
     */
180
    protected $maximumUrlsToCompile = 10000;
181
182
    /**
183
     * @var IconFactory
184
     */
185
    protected $iconFactory;
186
187
    /**
188
     * @var string[]
189
     */
190
    private $deprecatedPublicMethods = [
0 ignored issues
show
introduced by
The private property $deprecatedPublicMethods is not used, and could be removed.
Loading history...
191
        'getLogEntriesForSetId' => 'Using crawlerController::getLogEntriesForSetId() is deprecated since 9.0.1 and will be removed in v11.x',
192
        'flushQueue' => 'Using CrawlerController::flushQueue() is deprecated since 9.0.1 and will be removed in v11.x, please use QueueRepository->flushQueue() instead.',
193
        'cleanUpOldQueueEntries' => 'Using CrawlerController::cleanUpOldQueueEntries() is deprecated since 9.0.1 and will be removed in v11.x, please use QueueRepository->cleanUpOldQueueEntries() instead.',
194
    ];
195
196
    /**
197
     * @var BackendUserAuthentication|null
198
     */
199
    private $backendUser;
200
201
    /**
202
     * @var integer
203
     */
204
    private $scheduledTime = 0;
205
206
    /**
207
     * @var integer
208
     */
209
    private $reqMinute = 0;
210
211
    /**
212
     * @var bool
213
     */
214
    private $submitCrawlUrls = false;
215
216
    /**
217
     * @var bool
218
     */
219
    private $downloadCrawlUrls = false;
220
221
    /************************************
222
     *
223
     * Getting URLs based on Page TSconfig
224
     *
225
     ************************************/
226
227 43
    public function __construct()
228
    {
229 43
        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
230 43
        $this->queueRepository = $objectManager->get(QueueRepository::class);
231 43
        $this->processRepository = $objectManager->get(ProcessRepository::class);
232 43
        $this->configurationRepository = $objectManager->get(ConfigurationRepository::class);
233 43
        $this->queueExecutor = $objectManager->get(QueueExecutor::class);
234 43
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
235
236 43
        $this->processFilename = Environment::getVarPath() . '/lock/tx_crawler.proc';
237
238
        /** @var ExtensionConfigurationProvider $configurationProvider */
239 43
        $configurationProvider = GeneralUtility::makeInstance(ExtensionConfigurationProvider::class);
240 43
        $settings = $configurationProvider->getExtensionConfiguration();
241 43
        $this->extensionSettings = is_array($settings) ? $settings : [];
0 ignored issues
show
introduced by
The condition is_array($settings) is always true.
Loading history...
242
243
        // set defaults:
244 43
        if (MathUtility::convertToPositiveInteger($this->extensionSettings['countInARun']) === 0) {
245
            $this->extensionSettings['countInARun'] = 100;
246
        }
247
248 43
        $this->extensionSettings['processLimit'] = MathUtility::forceIntegerInRange($this->extensionSettings['processLimit'], 1, 99, 1);
249 43
        $this->maximumUrlsToCompile = MathUtility::forceIntegerInRange($this->extensionSettings['maxCompileUrls'], 1, 1000000000, 10000);
250 43
    }
251
252
    /**
253
     * Method to set the accessMode can be gui, cli or cli_im
254
     *
255
     * @return string
256
     */
257 1
    public function getAccessMode()
258
    {
259 1
        return $this->accessMode;
260
    }
261
262
    /**
263
     * @param string $accessMode
264
     */
265 1
    public function setAccessMode($accessMode): void
266
    {
267 1
        $this->accessMode = $accessMode;
268 1
    }
269
270
    /**
271
     * Set disabled status to prevent processes from being processed
272
     *
273
     * @param bool $disabled (optional, defaults to true)
274
     */
275 2
    public function setDisabled($disabled = true): void
276
    {
277 2
        if ($disabled) {
278 1
            GeneralUtility::writeFile($this->processFilename, '');
279
        } else {
280 1
            if (is_file($this->processFilename)) {
281 1
                unlink($this->processFilename);
282
            }
283
        }
284 2
    }
285
286
    /**
287
     * Get disable status
288
     *
289
     * @return bool true if disabled
290
     */
291 2
    public function getDisabled()
292
    {
293 2
        return is_file($this->processFilename);
294
    }
295
296
    /**
297
     * @param string $filenameWithPath
298
     */
299 3
    public function setProcessFilename($filenameWithPath): void
300
    {
301 3
        $this->processFilename = $filenameWithPath;
302 3
    }
303
304
    /**
305
     * @return string
306
     */
307 1
    public function getProcessFilename()
308
    {
309 1
        return $this->processFilename;
310
    }
311
312
    /**
313
     * Sets the extensions settings (unserialized pendant of $TYPO3_CONF_VARS['EXT']['extConf']['crawler']).
314
     */
315 14
    public function setExtensionSettings(array $extensionSettings): void
316
    {
317 14
        $this->extensionSettings = $extensionSettings;
318 14
    }
319
320
    /**
321
     * Check if the given page should be crawled
322
     *
323
     * @return false|string false if the page should be crawled (not excluded), true / skipMessage if it should be skipped
324
     */
325 12
    public function checkIfPageShouldBeSkipped(array $pageRow)
326
    {
327 12
        $skipPage = false;
328 12
        $skipMessage = 'Skipped'; // message will be overwritten later
329
330
        // if page is hidden
331 12
        if (! $this->extensionSettings['crawlHiddenPages']) {
332 12
            if ($pageRow['hidden']) {
333 1
                $skipPage = true;
334 1
                $skipMessage = 'Because page is hidden';
335
            }
336
        }
337
338 12
        if (! $skipPage) {
339 11
            if (GeneralUtility::inList('3,4,199,254,255', $pageRow['doktype'])) {
340 3
                $skipPage = true;
341 3
                $skipMessage = 'Because doktype is not allowed';
342
            }
343
        }
344
345 12
        if (! $skipPage) {
346 8
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['excludeDoktype'] ?? [] as $key => $doktypeList) {
347 1
                if (GeneralUtility::inList($doktypeList, $pageRow['doktype'])) {
348 1
                    $skipPage = true;
349 1
                    $skipMessage = 'Doktype was excluded by "' . $key . '"';
350 1
                    break;
351
                }
352
            }
353
        }
354
355 12
        if (! $skipPage) {
356
            // veto hook
357 7
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pageVeto'] ?? [] as $key => $func) {
358
                $params = [
359 2
                    'pageRow' => $pageRow,
360
                ];
361
                // expects "false" if page is ok and "true" or a skipMessage if this page should _not_ be crawled
362 2
                $veto = GeneralUtility::callUserFunction($func, $params, $this);
363 2
                if ($veto !== false) {
364 2
                    $skipPage = true;
365 2
                    if (is_string($veto)) {
366 1
                        $skipMessage = $veto;
367
                    } else {
368 1
                        $skipMessage = 'Veto from hook "' . htmlspecialchars($key) . '"';
369
                    }
370
                    // no need to execute other hooks if a previous one return a veto
371 2
                    break;
372
                }
373
            }
374
        }
375
376 12
        return $skipPage ? $skipMessage : false;
377
    }
378
379
    /**
380
     * Wrapper method for getUrlsForPageId()
381
     * It returns an array of configurations and no urls!
382
     *
383
     * @param array $pageRow Page record with at least dok-type and uid columns.
384
     * @param string $skipMessage
385
     * @return array
386
     * @see getUrlsForPageId()
387
     */
388 6
    public function getUrlsForPageRow(array $pageRow, &$skipMessage = '')
389
    {
390 6
        $message = $this->checkIfPageShouldBeSkipped($pageRow);
391 6
        if ($message === false) {
392 5
            $res = $this->getUrlsForPageId($pageRow['uid']);
393 5
            $skipMessage = '';
394
        } else {
395 1
            $skipMessage = $message;
396 1
            $res = [];
397
        }
398
399 6
        return $res;
400
    }
401
402
    /**
403
     * Creates a list of URLs from input array (and submits them to queue if asked for)
404
     * See Web > Info module script + "indexed_search"'s crawler hook-client using this!
405
     *
406
     * @param array $vv Information about URLs from pageRow to crawl.
407
     * @param array $pageRow Page row
408
     * @param int $scheduledTime Unix time to schedule indexing to, typically time()
409
     * @param int $reqMinute Number of requests per minute (creates the interleave between requests)
410
     * @param bool $submitCrawlUrls If set, submits the URLs to queue
411
     * @param bool $downloadCrawlUrls If set (and submitcrawlUrls is false) will fill $downloadUrls with entries)
412
     * @param array $duplicateTrack Array which is passed by reference and contains the an id per url to secure we will not crawl duplicates
413
     * @param array $downloadUrls Array which will be filled with URLS for download if flag is set.
414
     * @param array $incomingProcInstructions Array of processing instructions
415
     * @return string List of URLs (meant for display in backend module)
416
     */
417 4
    public function urlListFromUrlArray(
418
        array $vv,
419
        array $pageRow,
420
        $scheduledTime,
421
        $reqMinute,
422
        $submitCrawlUrls,
423
        $downloadCrawlUrls,
424
        array &$duplicateTrack,
425
        array &$downloadUrls,
426
        array $incomingProcInstructions
427
    ) {
428 4
        if (! is_array($vv['URLs'])) {
429
            return 'ERROR - no URL generated';
430
        }
431 4
        $urlLog = [];
432 4
        $pageId = (int) $pageRow['uid'];
433 4
        $configurationHash = $this->getConfigurationHash($vv);
434 4
        $skipInnerCheck = $this->queueRepository->noUnprocessedQueueEntriesForPageWithConfigurationHashExist($pageId, $configurationHash);
435
436 4
        $urlService = new UrlService();
437
438 4
        foreach ($vv['URLs'] as $urlQuery) {
439 4
            if (! $this->drawURLs_PIfilter($vv['subCfg']['procInstrFilter'], $incomingProcInstructions)) {
440
                continue;
441
            }
442 4
            $url = (string) $urlService->getUrlFromPageAndQueryParameters(
443 4
                $pageId,
444 4
                $urlQuery,
445 4
                $vv['subCfg']['baseUrl'] ?? null,
446 4
                $vv['subCfg']['force_ssl'] ?? 0
447
            );
448
449
            // Create key by which to determine unique-ness:
450 4
            $uKey = $url . '|' . $vv['subCfg']['userGroups'] . '|' . $vv['subCfg']['procInstrFilter'];
451
452 4
            if (isset($duplicateTrack[$uKey])) {
453
                //if the url key is registered just display it and do not resubmit is
454
                $urlLog[] = '<em><span class="text-muted">' . htmlspecialchars($url) . '</span></em>';
455
            } else {
456
                // Scheduled time:
457 4
                $schTime = $scheduledTime + round(count($duplicateTrack) * (60 / $reqMinute));
458 4
                $schTime = intval($schTime / 60) * 60;
459 4
                $formattedDate = BackendUtility::datetime($schTime);
460 4
                $this->urlList[] = '[' . $formattedDate . '] ' . $url;
461 4
                $urlList = '[' . $formattedDate . '] ' . htmlspecialchars($url);
462
463
                // Submit for crawling!
464 4
                if ($submitCrawlUrls) {
465 4
                    $added = $this->addUrl(
466 4
                        $pageId,
467 4
                        $url,
468 4
                        $vv['subCfg'],
469 4
                        $scheduledTime,
470 4
                        $configurationHash,
471 4
                        $skipInnerCheck
472
                    );
473 4
                    if ($added === false) {
474 4
                        $urlList .= ' (URL already existed)';
475
                    }
476
                } elseif ($downloadCrawlUrls) {
477
                    $downloadUrls[$url] = $url;
478
                }
479 4
                $urlLog[] = $urlList;
480
            }
481 4
            $duplicateTrack[$uKey] = true;
482
        }
483
484 4
        return implode('<br>', $urlLog);
485
    }
486
487
    /**
488
     * Returns true if input processing instruction is among registered ones.
489
     *
490
     * @param string $piString PI to test
491
     * @param array $incomingProcInstructions Processing instructions
492
     * @return boolean
493
     */
494 5
    public function drawURLs_PIfilter($piString, array $incomingProcInstructions)
495
    {
496 5
        if (empty($incomingProcInstructions)) {
497 1
            return true;
498
        }
499
500 4
        foreach ($incomingProcInstructions as $pi) {
501 4
            if (GeneralUtility::inList($piString, $pi)) {
502 2
                return true;
503
            }
504
        }
505 2
        return false;
506
    }
507
508 5
    public function getPageTSconfigForId($id): array
509
    {
510 5
        if (! $this->MP) {
511 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

511
            $pageTSconfig = /** @scrutinizer ignore-deprecated */ BackendUtility::getPagesTSconfig($id);
Loading history...
512
        } else {
513
            // TODO: Please check, this makes no sense to split a boolean value.
514
            [, $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

514
            [, $mountPointId] = explode('-', /** @scrutinizer ignore-type */ $this->MP);
Loading history...
515
            $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

515
            $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

515
            $pageTSconfig = /** @scrutinizer ignore-deprecated */ BackendUtility::getPagesTSconfig($mountPointId);
Loading history...
516
        }
517
518
        // Call a hook to alter configuration
519 5
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'])) {
520
            $params = [
521
                'pageId' => $id,
522
                'pageTSConfig' => &$pageTSconfig,
523
            ];
524
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'] as $userFunc) {
525
                GeneralUtility::callUserFunction($userFunc, $params, $this);
526
            }
527
        }
528 5
        return $pageTSconfig;
529
    }
530
531
    /**
532
     * This methods returns an array of configurations.
533
     * Adds no urls!
534
     */
535 4
    public function getUrlsForPageId(int $pageId): array
536
    {
537
        // Get page TSconfig for page ID
538 4
        $pageTSconfig = $this->getPageTSconfigForId($pageId);
539
540 4
        $res = [];
541
542
        // Fetch Crawler Configuration from pageTSconfig
543 4
        $crawlerCfg = $pageTSconfig['tx_crawler.']['crawlerCfg.']['paramSets.'] ?? [];
544 4
        foreach ($crawlerCfg as $key => $values) {
545 3
            if (! is_array($values)) {
546 3
                continue;
547
            }
548 3
            $key = str_replace('.', '', $key);
549
            // Sub configuration for a single configuration string:
550 3
            $subCfg = (array) $crawlerCfg[$key . '.'];
551 3
            $subCfg['key'] = $key;
552
553 3
            if (strcmp($subCfg['procInstrFilter'] ?? '', '')) {
554 3
                $subCfg['procInstrFilter'] = implode(',', GeneralUtility::trimExplode(',', $subCfg['procInstrFilter']));
555
            }
556 3
            $pidOnlyList = implode(',', GeneralUtility::trimExplode(',', $subCfg['pidsOnly'], true));
557
558
            // process configuration if it is not page-specific or if the specific page is the current page:
559
            // TODO: Check if $pidOnlyList can be kept as Array instead of imploded
560 3
            if (! strcmp((string) $subCfg['pidsOnly'], '') || GeneralUtility::inList($pidOnlyList, strval($pageId))) {
561
562
                // Explode, process etc.:
563 3
                $res[$key] = [];
564 3
                $res[$key]['subCfg'] = $subCfg;
565 3
                $res[$key]['paramParsed'] = GeneralUtility::explodeUrl2Array($crawlerCfg[$key]);
566 3
                $res[$key]['paramExpanded'] = $this->expandParameters($res[$key]['paramParsed'], $pageId);
567 3
                $res[$key]['origin'] = 'pagets';
568
569
                // recognize MP value
570 3
                if (! $this->MP) {
571 3
                    $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $pageId]);
572
                } else {
573
                    $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

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

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

1408
                            $tree->getTree(/** @scrutinizer ignore-type */ $pid, $depth);
Loading history...
1409
                            $treeCache[$pid][$depth] = $tree->tree;
1410
                        }
1411
1412
                        foreach ($treeCache[$pid][$depth] as $data) {
1413
                            $pidList[] = $data['row']['uid'];
1414
                        }
1415
                    }
1416
                }
1417
            }
1418
1419 1
            $expandedExcludeStringCache[$excludeString] = array_unique($pidList);
1420
        }
1421
1422 1
        return $expandedExcludeStringCache[$excludeString];
1423
    }
1424
1425
    /**
1426
     * Create the rows for display of the page tree
1427
     * For each page a number of rows are shown displaying GET variable configuration
1428
     */
1429
    public function drawURLs_addRowsForPage(array $pageRow, string $pageTitle): string
1430
    {
1431
        $skipMessage = '';
1432
1433
        // Get list of configurations
1434
        $configurations = $this->getUrlsForPageRow($pageRow, $skipMessage);
1435
1436
        if (! empty($this->incomingConfigurationSelection)) {
1437
            // remove configuration that does not match the current selection
1438
            foreach ($configurations as $confKey => $confArray) {
1439
                if (! in_array($confKey, $this->incomingConfigurationSelection, true)) {
1440
                    unset($configurations[$confKey]);
1441
                }
1442
            }
1443
        }
1444
1445
        // Traverse parameter combinations:
1446
        $c = 0;
1447
        $content = '';
1448
        if (! empty($configurations)) {
1449
            foreach ($configurations as $confKey => $confArray) {
1450
1451
                // Title column:
1452
                if (! $c) {
1453
                    $titleClm = '<td rowspan="' . count($configurations) . '">' . $pageTitle . '</td>';
1454
                } else {
1455
                    $titleClm = '';
1456
                }
1457
1458
                if (! in_array($pageRow['uid'], $this->expandExcludeString($confArray['subCfg']['exclude']), true)) {
1459
1460
                    // URL list:
1461
                    $urlList = $this->urlListFromUrlArray(
1462
                        $confArray,
1463
                        $pageRow,
1464
                        $this->scheduledTime,
1465
                        $this->reqMinute,
1466
                        $this->submitCrawlUrls,
1467
                        $this->downloadCrawlUrls,
1468
                        $this->duplicateTrack,
1469
                        $this->downloadUrls,
1470
                        $this->incomingProcInstructions // if empty the urls won't be filtered by processing instructions
1471
                    );
1472
1473
                    // Expanded parameters:
1474
                    $paramExpanded = '';
1475
                    $calcAccu = [];
1476
                    $calcRes = 1;
1477
                    foreach ($confArray['paramExpanded'] as $gVar => $gVal) {
1478
                        $paramExpanded .= '
1479
                            <tr>
1480
                                <td>' . htmlspecialchars('&' . $gVar . '=') . '<br/>' .
1481
                            '(' . count($gVal) . ')' .
1482
                            '</td>
1483
                                <td nowrap="nowrap">' . nl2br(htmlspecialchars(implode(chr(10), $gVal))) . '</td>
1484
                            </tr>
1485
                        ';
1486
                        $calcRes *= count($gVal);
1487
                        $calcAccu[] = count($gVal);
1488
                    }
1489
                    $paramExpanded = '<table>' . $paramExpanded . '</table>';
1490
                    $paramExpanded .= 'Comb: ' . implode('*', $calcAccu) . '=' . $calcRes;
1491
1492
                    // Options
1493
                    $optionValues = '';
1494
                    if ($confArray['subCfg']['userGroups']) {
1495
                        $optionValues .= 'User Groups: ' . $confArray['subCfg']['userGroups'] . '<br/>';
1496
                    }
1497
                    if ($confArray['subCfg']['procInstrFilter']) {
1498
                        $optionValues .= 'ProcInstr: ' . $confArray['subCfg']['procInstrFilter'] . '<br/>';
1499
                    }
1500
1501
                    // Compile row:
1502
                    $content .= '
1503
                        <tr>
1504
                            ' . $titleClm . '
1505
                            <td>' . htmlspecialchars($confKey) . '</td>
1506
                            <td>' . nl2br(htmlspecialchars(rawurldecode(trim(str_replace('&', chr(10) . '&', GeneralUtility::implodeArrayForUrl('', $confArray['paramParsed'])))))) . '</td>
1507
                            <td>' . $paramExpanded . '</td>
1508
                            <td nowrap="nowrap">' . $urlList . '</td>
1509
                            <td nowrap="nowrap">' . $optionValues . '</td>
1510
                            <td nowrap="nowrap">' . DebugUtility::viewArray($confArray['subCfg']['procInstrParams.']) . '</td>
1511
                        </tr>';
1512
                } else {
1513
                    $content .= '<tr>
1514
                            ' . $titleClm . '
1515
                            <td>' . htmlspecialchars($confKey) . '</td>
1516
                            <td colspan="5"><em>No entries</em> (Page is excluded in this configuration)</td>
1517
                        </tr>';
1518
                }
1519
1520
                $c++;
1521
            }
1522
        } else {
1523
            $message = ! empty($skipMessage) ? ' (' . $skipMessage . ')' : '';
1524
1525
            // Compile row:
1526
            $content .= '
1527
                <tr>
1528
                    <td>' . $pageTitle . '</td>
1529
                    <td colspan="6"><em>No entries</em>' . $message . '</td>
1530
                </tr>';
1531
        }
1532
1533
        return $content;
1534
    }
1535
1536
    /*****************************
1537
     *
1538
     * CLI functions
1539
     *
1540
     *****************************/
1541
1542
    /**
1543
     * Running the functionality of the CLI (crawling URLs from queue)
1544
     */
1545
    public function CLI_run(int $countInARun, int $sleepTime, int $sleepAfterFinish): int
1546
    {
1547
        $result = 0;
1548
        $counter = 0;
1549
1550
        // First, run hooks:
1551
        $this->CLI_runHooks();
1552
1553
        // Clean up the queue
1554
        $this->queueRepository->cleanupQueue();
1555
1556
        // Select entries:
1557
        $rows = $this->queueRepository->fetchRecordsToBeCrawled($countInARun);
1558
1559
        if (! empty($rows)) {
1560
            $quidList = [];
1561
1562
            foreach ($rows as $r) {
1563
                $quidList[] = $r['qid'];
1564
            }
1565
1566
            $processId = $this->CLI_buildProcessId();
1567
1568
            //save the number of assigned queue entries to determine how many have been processed later
1569
            $numberOfAffectedRows = $this->queueRepository->updateProcessIdAndSchedulerForQueueIds($quidList, $processId);
1570
            $this->processRepository->updateProcessAssignItemsCount($numberOfAffectedRows, $processId);
1571
1572
            if ($numberOfAffectedRows !== count($quidList)) {
1573
                $this->CLI_debug('Nothing processed due to multi-process collision (' . $this->CLI_buildProcessId() . ')');
1574
                return ($result | self::CLI_STATUS_ABORTED);
1575
            }
1576
1577
            foreach ($rows as $r) {
1578
                $result |= $this->readUrl($r['qid']);
1579
1580
                $counter++;
1581
                usleep((int) $sleepTime); // Just to relax the system
1582
1583
                // if during the start and the current read url the cli has been disable we need to return from the function
1584
                // mark the process NOT as ended.
1585
                if ($this->getDisabled()) {
1586
                    return ($result | self::CLI_STATUS_ABORTED);
1587
                }
1588
1589
                if (! $this->processRepository->isProcessActive($this->CLI_buildProcessId())) {
1590
                    $this->CLI_debug('conflict / timeout (' . $this->CLI_buildProcessId() . ')');
1591
                    $result |= self::CLI_STATUS_ABORTED;
1592
                    break; //possible timeout
1593
                }
1594
            }
1595
1596
            sleep((int) $sleepAfterFinish);
1597
1598
            $msg = 'Rows: ' . $counter;
1599
            $this->CLI_debug($msg . ' (' . $this->CLI_buildProcessId() . ')');
1600
        } else {
1601
            $this->CLI_debug('Nothing within queue which needs to be processed (' . $this->CLI_buildProcessId() . ')');
1602
        }
1603
1604
        if ($counter > 0) {
1605
            $result |= self::CLI_STATUS_PROCESSED;
1606
        }
1607
1608
        return $result;
1609
    }
1610
1611
    /**
1612
     * Activate hooks
1613
     */
1614
    public function CLI_runHooks(): void
1615
    {
1616
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['cli_hooks'] ?? [] as $objRef) {
1617
            $hookObj = GeneralUtility::makeInstance($objRef);
1618
            if (is_object($hookObj)) {
1619
                $hookObj->crawler_init($this);
1620
            }
1621
        }
1622
    }
1623
1624
    /**
1625
     * Try to acquire a new process with the given id
1626
     * also performs some auto-cleanup for orphan processes
1627
     * @param string $id identification string for the process
1628
     * @return boolean
1629
     * @todo preemption might not be the most elegant way to clean up
1630
     */
1631
    public function CLI_checkAndAcquireNewProcess($id)
1632
    {
1633
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
1634
        $ret = true;
1635
1636
        $systemProcessId = getmypid();
1637
        if ($systemProcessId < 1) {
1638
            return false;
1639
        }
1640
1641
        $processCount = 0;
1642
        $orphanProcesses = [];
1643
1644
        $statement = $queryBuilder
1645
            ->select('process_id', 'ttl')
1646
            ->from('tx_crawler_process')
1647
            ->where(
1648
                'active = 1 AND deleted = 0'
1649
            )
1650
            ->execute();
1651
1652
        $currentTime = $this->getCurrentTime();
1653
1654
        while ($row = $statement->fetch()) {
1655
            if ($row['ttl'] < $currentTime) {
1656
                $orphanProcesses[] = $row['process_id'];
1657
            } else {
1658
                $processCount++;
1659
            }
1660
        }
1661
1662
        // if there are less than allowed active processes then add a new one
1663
        if ($processCount < (int) $this->extensionSettings['processLimit']) {
1664
            $this->CLI_debug('add process ' . $this->CLI_buildProcessId() . ' (' . ($processCount + 1) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
1665
1666
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_crawler_process')->insert(
1667
                'tx_crawler_process',
1668
                [
1669
                    'process_id' => $id,
1670
                    'active' => 1,
1671
                    'ttl' => $currentTime + (int) $this->extensionSettings['processMaxRunTime'],
1672
                    'system_process_id' => $systemProcessId,
1673
                ]
1674
            );
1675
        } else {
1676
            $this->CLI_debug('Processlimit reached (' . ($processCount) . '/' . (int) $this->extensionSettings['processLimit'] . ')');
1677
            $ret = false;
1678
        }
1679
1680
        $this->processRepository->deleteProcessesMarkedAsDeleted();
1681
        $this->CLI_releaseProcesses($orphanProcesses);
1682
1683
        return $ret;
1684
    }
1685
1686
    /**
1687
     * Release a process and the required resources
1688
     *
1689
     * @param mixed $releaseIds string with a single process-id or array with multiple process-ids
1690
     * @return boolean
1691
     */
1692
    public function CLI_releaseProcesses($releaseIds)
1693
    {
1694
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName);
1695
1696
        if (! is_array($releaseIds)) {
1697
            $releaseIds = [$releaseIds];
1698
        }
1699
1700
        if (empty($releaseIds)) {
1701
            return false;   //nothing to release
1702
        }
1703
1704
        // some kind of 2nd chance algo - this way you need at least 2 processes to have a real cleanup
1705
        // this ensures that a single process can't mess up the entire process table
1706
1707
        // mark all processes as deleted which have no "waiting" queue-entires and which are not active
1708
1709
        $queryBuilder
1710
            ->update($this->tableName, 'q')
1711
            ->where(
1712
                'q.process_id IN(SELECT p.process_id FROM tx_crawler_process as p WHERE p.active = 0)'
1713
            )
1714
            ->set('q.process_scheduled', 0)
1715
            ->set('q.process_id', '')
1716
            ->execute();
1717
1718
        // FIXME: Not entirely sure that this is equivalent to the previous version
1719
        $queryBuilder->resetQueryPart('set');
1720
1721
        $queryBuilder
1722
            ->update('tx_crawler_process')
1723
            ->where(
1724
                $queryBuilder->expr()->eq('active', 0),
1725
                'process_id IN(SELECT q.process_id FROM tx_crawler_queue as q WHERE q.exec_time = 0)'
1726
            )
1727
            ->set('system_process_id', 0)
1728
            ->execute();
1729
1730
        $this->processRepository->markRequestedProcessesAsNotActive($releaseIds);
1731
        $this->queueRepository->unsetProcessScheduledAndProcessIdForQueueEntries($releaseIds);
1732
1733
        return true;
1734
    }
1735
1736
    /**
1737
     * Create a unique Id for the current process
1738
     *
1739
     * @return string  the ID
1740
     */
1741 1
    public function CLI_buildProcessId()
1742
    {
1743 1
        if (! $this->processID) {
1744
            $this->processID = GeneralUtility::shortMD5(microtime(true));
1745
        }
1746 1
        return $this->processID;
1747
    }
1748
1749
    /**
1750
     * Prints a message to the stdout (only if debug-mode is enabled)
1751
     *
1752
     * @param string $msg the message
1753
     */
1754
    public function CLI_debug($msg): void
1755
    {
1756
        if ((int) $this->extensionSettings['processDebug']) {
1757
            echo $msg . "\n";
1758
            flush();
1759
        }
1760
    }
1761
1762
    /**
1763
     * Cleans up entries that stayed for too long in the queue. These are:
1764
     * - processed entries that are over 1.5 days in age
1765
     * - scheduled entries that are over 7 days old
1766
     *
1767
     * @deprecated
1768
     */
1769 1
    public function cleanUpOldQueueEntries(): void
1770
    {
1771 1
        $processedAgeInSeconds = $this->extensionSettings['cleanUpProcessedAge'] * 86400; // 24*60*60 Seconds in 24 hours
1772 1
        $scheduledAgeInSeconds = $this->extensionSettings['cleanUpScheduledAge'] * 86400;
1773
1774 1
        $now = time();
1775 1
        $condition = '(exec_time<>0 AND exec_time<' . ($now - $processedAgeInSeconds) . ') OR scheduled<=' . ($now - $scheduledAgeInSeconds);
1776 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

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