Completed
Push — tests/getLogentries-filter ( 625e2c )
by Tomas Norre
05:48
created

CrawlerController::getUnprocessedItemsCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace AOE\Crawler\Controller;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2017 AOE GmbH <[email protected]>
8
 *
9
 *  All rights reserved
10
 *
11
 *  This script is part of the TYPO3 project. The TYPO3 project is
12
 *  free software; you can redistribute it and/or modify
13
 *  it under the terms of the GNU General Public License as published by
14
 *  the Free Software Foundation; either version 3 of the License, or
15
 *  (at your option) any later version.
16
 *
17
 *  The GNU General Public License can be found at
18
 *  http://www.gnu.org/copyleft/gpl.html.
19
 *
20
 *  This script is distributed in the hope that it will be useful,
21
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 *  GNU General Public License for more details.
24
 *
25
 *  This copyright notice MUST APPEAR in all copies of the script!
26
 ***************************************************************/
27
28
use AOE\Crawler\Command\CrawlerCommandLineController;
29
use AOE\Crawler\Command\FlushCommandLineController;
30
use AOE\Crawler\Command\QueueCommandLineController;
31
use AOE\Crawler\Domain\Model\Reason;
32
use AOE\Crawler\Event\EventDispatcher;
33
use AOE\Crawler\Utility\IconUtility;
34
use AOE\Crawler\Utility\SignalSlotUtility;
35
use TYPO3\CMS\Backend\Utility\BackendUtility;
36
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
37
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
38
use TYPO3\CMS\Core\Database\DatabaseConnection;
39
use TYPO3\CMS\Core\Log\LogLevel;
40
use TYPO3\CMS\Core\TimeTracker\NullTimeTracker;
41
use TYPO3\CMS\Core\Utility\DebugUtility;
42
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
43
use TYPO3\CMS\Core\Utility\GeneralUtility;
44
use TYPO3\CMS\Core\Utility\MathUtility;
45
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
46
use TYPO3\CMS\Frontend\Page\PageGenerator;
47
use TYPO3\CMS\Frontend\Page\PageRepository;
48
use TYPO3\CMS\Frontend\Utility\EidUtility;
49
50
/**
51
 * Class CrawlerController
52
 *
53
 * @package AOE\Crawler\Controller
54
 */
55
class CrawlerController
56
{
57
    const CLI_STATUS_NOTHING_PROCCESSED = 0;
58
    const CLI_STATUS_REMAIN = 1; //queue not empty
59
    const CLI_STATUS_PROCESSED = 2; //(some) queue items where processed
60
    const CLI_STATUS_ABORTED = 4; //instance didn't finish
61
    const CLI_STATUS_POLLABLE_PROCESSED = 8;
62
63
    /**
64
     * @var integer
65
     */
66
    public $setID = 0;
67
68
    /**
69
     * @var string
70
     */
71
    public $processID = '';
72
73
    /**
74
     * One hour is max stalled time for the CLI
75
     * If the process had the status "start" for 3600 seconds, it will be regarded stalled and a new process is started
76
     *
77
     * @var integer
78
     */
79
    public $max_CLI_exec_time = 3600;
80
81
    /**
82
     * @var array
83
     */
84
    public $duplicateTrack = [];
85
86
    /**
87
     * @var array
88
     */
89
    public $downloadUrls = [];
90
91
    /**
92
     * @var array
93
     */
94
    public $incomingProcInstructions = [];
95
96
    /**
97
     * @var array
98
     */
99
    public $incomingConfigurationSelection = [];
100
101
    /**
102
     * @var bool
103
     */
104
    public $registerQueueEntriesInternallyOnly = false;
105
106
    /**
107
     * @var array
108
     */
109
    public $queueEntries = [];
110
111
    /**
112
     * @var array
113
     */
114
    public $urlList = [];
115
116
    /**
117
     * @var boolean
118
     */
119
    public $debugMode = false;
120
121
    /**
122
     * @var array
123
     */
124
    public $extensionSettings = [];
125
126
    /**
127
     * Mount Point
128
     *
129
     * @var boolean
130
     */
131
    public $MP = false;
132
133
    /**
134
     * @var string
135
     */
136
    protected $processFilename;
137
138
    /**
139
     * Holds the internal access mode can be 'gui','cli' or 'cli_im'
140
     *
141
     * @var string
142
     */
143
    protected $accessMode;
144
145
    /**
146
     * @var DatabaseConnection
147
     */
148
    private $db;
149
150
    /**
151
     * @var BackendUserAuthentication
152
     */
153
    private $backendUser;
154
155
    /**
156
     * @var integer
157
     */
158
    private $scheduledTime = 0;
159
160
    /**
161
     * @var integer
162
     */
163
    private $reqMinute = 0;
164
165
    /**
166
     * @var bool
167
     */
168
    private $submitCrawlUrls = false;
169
170
    /**
171
     * @var bool
172
     */
173
    private $downloadCrawlUrls = false;
174
175
    /**
176
     * Method to set the accessMode can be gui, cli or cli_im
177
     *
178
     * @return string
179
     */
180 1
    public function getAccessMode()
181
    {
182 1
        return $this->accessMode;
183
    }
184
185
    /**
186
     * @param string $accessMode
187
     */
188 1
    public function setAccessMode($accessMode)
189
    {
190 1
        $this->accessMode = $accessMode;
191 1
    }
192
193
    /**
194
     * Set disabled status to prevent processes from being processed
195
     *
196
     * @param  bool $disabled (optional, defaults to true)
197
     * @return void
198
     */
199 3
    public function setDisabled($disabled = true)
200
    {
201 3
        if ($disabled) {
202 2
            GeneralUtility::writeFile($this->processFilename, '');
203
        } else {
204 1
            if (is_file($this->processFilename)) {
205 1
                unlink($this->processFilename);
206
            }
207
        }
208 3
    }
209
210
    /**
211
     * Get disable status
212
     *
213
     * @return bool true if disabled
214
     */
215 3
    public function getDisabled()
216
    {
217 3
        if (is_file($this->processFilename)) {
218 2
            return true;
219
        } else {
220 1
            return false;
221
        }
222
    }
223
224
    /**
225
     * @param string $filenameWithPath
226
     *
227
     * @return void
228
     */
229 4
    public function setProcessFilename($filenameWithPath)
230
    {
231 4
        $this->processFilename = $filenameWithPath;
232 4
    }
233
234
    /**
235
     * @return string
236
     */
237 1
    public function getProcessFilename()
238
    {
239 1
        return $this->processFilename;
240
    }
241
242
    /************************************
243
     *
244
     * Getting URLs based on Page TSconfig
245
     *
246
     ************************************/
247
248 55
    public function __construct()
249
    {
250 55
        $this->db = $GLOBALS['TYPO3_DB'];
251 55
        $this->backendUser = $GLOBALS['BE_USER'];
252 55
        $this->processFilename = PATH_site . 'typo3temp/tx_crawler.proc';
253
254 55
        $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['crawler']);
255 55
        $settings = is_array($settings) ? $settings : [];
256
257
        // read ext_em_conf_template settings and set
258 55
        $this->setExtensionSettings($settings);
259
260
        // set defaults:
261 55
        if (MathUtility::convertToPositiveInteger($this->extensionSettings['countInARun']) == 0) {
262 23
            $this->extensionSettings['countInARun'] = 100;
263
        }
264
265 55
        $this->extensionSettings['processLimit'] = MathUtility::forceIntegerInRange($this->extensionSettings['processLimit'], 1, 99, 1);
266 55
    }
267
268
    /**
269
     * Sets the extensions settings (unserialized pendant of $TYPO3_CONF_VARS['EXT']['extConf']['crawler']).
270
     *
271
     * @param array $extensionSettings
272
     * @return void
273
     */
274 63
    public function setExtensionSettings(array $extensionSettings)
275
    {
276 63
        $this->extensionSettings = $extensionSettings;
277 63
    }
278
279
    /**
280
     * Check if the given page should be crawled
281
     *
282
     * @param array $pageRow
283
     * @return false|string false if the page should be crawled (not excluded), true / skipMessage if it should be skipped
284
     */
285 10
    public function checkIfPageShouldBeSkipped(array $pageRow)
286
    {
287 10
        $skipPage = false;
288 10
        $skipMessage = 'Skipped'; // message will be overwritten later
289
290
        // if page is hidden
291 10
        if (!$this->extensionSettings['crawlHiddenPages']) {
292 10
            if ($pageRow['hidden']) {
293 1
                $skipPage = true;
294 1
                $skipMessage = 'Because page is hidden';
295
            }
296
        }
297
298 10
        if (!$skipPage) {
299 9
            if (GeneralUtility::inList('3,4', $pageRow['doktype']) || $pageRow['doktype'] >= 199) {
300 3
                $skipPage = true;
301 3
                $skipMessage = 'Because doktype is not allowed';
302
            }
303
        }
304
305 10
        if (!$skipPage) {
306 6
            if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['excludeDoktype'])) {
307 2
                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['excludeDoktype'] as $key => $doktypeList) {
308 1
                    if (GeneralUtility::inList($doktypeList, $pageRow['doktype'])) {
309 1
                        $skipPage = true;
310 1
                        $skipMessage = 'Doktype was excluded by "' . $key . '"';
311 1
                        break;
312
                    }
313
                }
314
            }
315
        }
316
317 10
        if (!$skipPage) {
318
            // veto hook
319 5
            if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pageVeto'])) {
320
                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pageVeto'] as $key => $func) {
321
                    $params = [
322
                        'pageRow' => $pageRow
323
                    ];
324
                    // expects "false" if page is ok and "true" or a skipMessage if this page should _not_ be crawled
325
                    $veto = GeneralUtility::callUserFunction($func, $params, $this);
326
                    if ($veto !== false) {
327
                        $skipPage = true;
328
                        if (is_string($veto)) {
329
                            $skipMessage = $veto;
330
                        } else {
331
                            $skipMessage = 'Veto from hook "' . htmlspecialchars($key) . '"';
332
                        }
333
                        // no need to execute other hooks if a previous one return a veto
334
                        break;
335
                    }
336
                }
337
            }
338
        }
339
340 10
        return $skipPage ? $skipMessage : false;
341
    }
342
343
    /**
344
     * Wrapper method for getUrlsForPageId()
345
     * It returns an array of configurations and no urls!
346
     *
347
     * @param array $pageRow Page record with at least dok-type and uid columns.
348
     * @param string $skipMessage
349
     * @return array
350
     * @see getUrlsForPageId()
351
     */
352 6
    public function getUrlsForPageRow(array $pageRow, &$skipMessage = '')
353
    {
354 6
        $message = $this->checkIfPageShouldBeSkipped($pageRow);
355
356 6
        if ($message === false) {
357 5
            $forceSsl = ($pageRow['url_scheme'] === 2) ? true : false;
358 5
            $res = $this->getUrlsForPageId($pageRow['uid'], $forceSsl);
359 5
            $skipMessage = '';
360
        } else {
361 1
            $skipMessage = $message;
362 1
            $res = [];
363
        }
364
365 6
        return $res;
366
    }
367
368
    /**
369
     * This method is used to count if there are ANY unprocessed queue entries
370
     * of a given page_id and the configuration which matches a given hash.
371
     * If there if none, we can skip an inner detail check
372
     *
373
     * @param  int $uid
374
     * @param  string $configurationHash
375
     * @return boolean
376
     */
377 7
    protected function noUnprocessedQueueEntriesForPageWithConfigurationHashExist($uid, $configurationHash)
378
    {
379 7
        $configurationHash = $this->db->fullQuoteStr($configurationHash, 'tx_crawler_queue');
380 7
        $res = $this->db->exec_SELECTquery('count(*) as anz', 'tx_crawler_queue', "page_id=" . intval($uid) . " AND configuration_hash=" . $configurationHash . " AND exec_time=0");
381 7
        $row = $this->db->sql_fetch_assoc($res);
382
383 7
        return ($row['anz'] == 0);
384
    }
385
386
    /**
387
     * Creates a list of URLs from input array (and submits them to queue if asked for)
388
     * See Web > Info module script + "indexed_search"'s crawler hook-client using this!
389
     *
390
     * @param    array        Information about URLs from pageRow to crawl.
391
     * @param    array        Page row
392
     * @param    integer        Unix time to schedule indexing to, typically time()
393
     * @param    integer        Number of requests per minute (creates the interleave between requests)
394
     * @param    boolean        If set, submits the URLs to queue
395
     * @param    boolean        If set (and submitcrawlUrls is false) will fill $downloadUrls with entries)
396
     * @param    array        Array which is passed by reference and contains the an id per url to secure we will not crawl duplicates
397
     * @param    array        Array which will be filled with URLS for download if flag is set.
398
     * @param    array        Array of processing instructions
399
     * @return    string        List of URLs (meant for display in backend module)
400
     *
401
     */
402 4
    public function urlListFromUrlArray(
403
    array $vv,
404
    array $pageRow,
405
    $scheduledTime,
406
    $reqMinute,
407
    $submitCrawlUrls,
408
    $downloadCrawlUrls,
409
    array &$duplicateTrack,
410
    array &$downloadUrls,
411
    array $incomingProcInstructions
412
    ) {
413 4
        $urlList = '';
414
        // realurl support (thanks to Ingo Renner)
415 4
        if (ExtensionManagementUtility::isLoaded('realurl') && $vv['subCfg']['realurl']) {
416
417
            /** @var tx_realurl $urlObj */
418
            $urlObj = GeneralUtility::makeInstance('tx_realurl');
419
420
            if (!empty($vv['subCfg']['baseUrl'])) {
421
                $urlParts = parse_url($vv['subCfg']['baseUrl']);
422
                $host = strtolower($urlParts['host']);
423
                $urlObj->host = $host;
424
425
                // First pass, finding configuration OR pointer string:
426
                $urlObj->extConf = isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl'][$urlObj->host]) ? $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl'][$urlObj->host] : $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl']['_DEFAULT'];
427
428
                // If it turned out to be a string pointer, then look up the real config:
429
                if (is_string($urlObj->extConf)) {
430
                    $urlObj->extConf = is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl'][$urlObj->extConf]) ? $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl'][$urlObj->extConf] : $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['realurl']['_DEFAULT'];
431
                }
432
            }
433
434
            if (!$GLOBALS['TSFE']->sys_page) {
435
                $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance('TYPO3\CMS\Frontend\Page\PageRepository');
436
            }
437
            if (!$GLOBALS['TSFE']->csConvObj) {
438
                $GLOBALS['TSFE']->csConvObj = GeneralUtility::makeInstance('TYPO3\CMS\Core\Charset\CharsetConverter');
439
            }
440
            if (!$GLOBALS['TSFE']->tmpl->rootLine[0]['uid']) {
441
                $GLOBALS['TSFE']->tmpl->rootLine[0]['uid'] = $urlObj->extConf['pagePath']['rootpage_id'];
442
            }
443
        }
444
445 4
        if (is_array($vv['URLs'])) {
446 4
            $configurationHash = $this->getConfigurationHash($vv);
447 4
            $skipInnerCheck = $this->noUnprocessedQueueEntriesForPageWithConfigurationHashExist($pageRow['uid'], $configurationHash);
448
449 4
            foreach ($vv['URLs'] as $urlQuery) {
450 4
                if ($this->drawURLs_PIfilter($vv['subCfg']['procInstrFilter'], $incomingProcInstructions)) {
451
452
                    // Calculate cHash:
453 4
                    if ($vv['subCfg']['cHash']) {
454
                        /* @var $cacheHash \TYPO3\CMS\Frontend\Page\CacheHashCalculator */
455
                        $cacheHash = GeneralUtility::makeInstance('TYPO3\CMS\Frontend\Page\CacheHashCalculator');
456
                        $urlQuery .= '&cHash=' . $cacheHash->generateForParameters($urlQuery);
457
                    }
458
459
                    // Create key by which to determine unique-ness:
460 4
                    $uKey = $urlQuery . '|' . $vv['subCfg']['userGroups'] . '|' . $vv['subCfg']['baseUrl'] . '|' . $vv['subCfg']['procInstrFilter'];
461
462
                    // realurl support (thanks to Ingo Renner)
463 4
                    $urlQuery = 'index.php' . $urlQuery;
464 4
                    if (ExtensionManagementUtility::isLoaded('realurl') && $vv['subCfg']['realurl']) {
465
                        $params = [
466
                            'LD' => [
467
                                'totalURL' => $urlQuery
468
                            ],
469
                            'TCEmainHook' => true
470
                        ];
471
                        $urlObj->encodeSpURL($params);
0 ignored issues
show
Bug introduced by
The variable $urlObj does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
472
                        $urlQuery = $params['LD']['totalURL'];
473
                    }
474
475
                    // Scheduled time:
476 4
                    $schTime = $scheduledTime + round(count($duplicateTrack) * (60 / $reqMinute));
477 4
                    $schTime = floor($schTime / 60) * 60;
478
479 4
                    if (isset($duplicateTrack[$uKey])) {
480
481
                        //if the url key is registered just display it and do not resubmit is
482
                        $urlList = '<em><span class="typo3-dimmed">' . htmlspecialchars($urlQuery) . '</span></em><br/>';
483
                    } else {
484 4
                        $urlList = '[' . date('d.m.y H:i', $schTime) . '] ' . htmlspecialchars($urlQuery);
485 4
                        $this->urlList[] = '[' . date('d.m.y H:i', $schTime) . '] ' . $urlQuery;
486
487 4
                        $theUrl = ($vv['subCfg']['baseUrl'] ? $vv['subCfg']['baseUrl'] : GeneralUtility::getIndpEnv('TYPO3_SITE_URL')) . $urlQuery;
488
489
                        // Submit for crawling!
490 4
                        if ($submitCrawlUrls) {
491 4
                            $added = $this->addUrl(
492 4
                            $pageRow['uid'],
493 4
                            $theUrl,
494 4
                            $vv['subCfg'],
495 4
                            $scheduledTime,
496 4
                            $configurationHash,
497 4
                            $skipInnerCheck
498
                            );
499 4
                            if ($added === false) {
500 4
                                $urlList .= ' (Url already existed)';
501
                            }
502
                        } elseif ($downloadCrawlUrls) {
503
                            $downloadUrls[$theUrl] = $theUrl;
504
                        }
505
506 4
                        $urlList .= '<br />';
507
                    }
508 4
                    $duplicateTrack[$uKey] = true;
509
                }
510
            }
511
        } else {
512
            $urlList = 'ERROR - no URL generated';
513
        }
514
515 4
        return $urlList;
516
    }
517
518
    /**
519
     * Returns true if input processing instruction is among registered ones.
520
     *
521
     * @param string $piString PI to test
522
     * @param array $incomingProcInstructions Processing instructions
523
     * @return boolean
524
     */
525 5
    public function drawURLs_PIfilter($piString, array $incomingProcInstructions)
526
    {
527 5
        if (empty($incomingProcInstructions)) {
528 1
            return true;
529
        }
530
531 4
        foreach ($incomingProcInstructions as $pi) {
532 4
            if (GeneralUtility::inList($piString, $pi)) {
533 4
                return true;
534
            }
535
        }
536 2
    }
537
538 4
    public function getPageTSconfigForId($id)
539
    {
540 4
        if (!$this->MP) {
541 4
            $pageTSconfig = BackendUtility::getPagesTSconfig($id);
542
        } else {
543
            list(, $mountPointId) = explode('-', $this->MP);
544
            $pageTSconfig = BackendUtility::getPagesTSconfig($mountPointId);
545
        }
546
547
        // Call a hook to alter configuration
548 4
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'])) {
549
            $params = [
550
                'pageId' => $id,
551
                'pageTSConfig' => &$pageTSconfig
552
            ];
553
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['getPageTSconfigForId'] as $userFunc) {
554
                GeneralUtility::callUserFunction($userFunc, $params, $this);
555
            }
556
        }
557
558 4
        return $pageTSconfig;
559
    }
560
561
    /**
562
     * This methods returns an array of configurations.
563
     * And no urls!
564
     *
565
     * @param integer $id Page ID
566
     * @param bool $forceSsl Use https
567
     * @return array
568
     */
569 4
    protected function getUrlsForPageId($id, $forceSsl = false)
570
    {
571
572
        /**
573
         * Get configuration from tsConfig
574
         */
575
576
        // Get page TSconfig for page ID:
577 4
        $pageTSconfig = $this->getPageTSconfigForId($id);
578
579 4
        $res = [];
580
581 4
        if (is_array($pageTSconfig) && is_array($pageTSconfig['tx_crawler.']['crawlerCfg.'])) {
582 3
            $crawlerCfg = $pageTSconfig['tx_crawler.']['crawlerCfg.'];
583
584 3
            if (is_array($crawlerCfg['paramSets.'])) {
585 3
                foreach ($crawlerCfg['paramSets.'] as $key => $values) {
586 3
                    if (is_array($values)) {
587 3
                        $key = str_replace('.', '', $key);
588
                        // Sub configuration for a single configuration string:
589 3
                        $subCfg = (array)$crawlerCfg['paramSets.'][$key . '.'];
590 3
                        $subCfg['key'] = $key;
591
592 3
                        if (strcmp($subCfg['procInstrFilter'], '')) {
593 3
                            $subCfg['procInstrFilter'] = implode(',', GeneralUtility::trimExplode(',', $subCfg['procInstrFilter']));
594
                        }
595 3
                        $pidOnlyList = implode(',', GeneralUtility::trimExplode(',', $subCfg['pidsOnly'], true));
596
597
                        // process configuration if it is not page-specific or if the specific page is the current page:
598 3
                        if (!strcmp($subCfg['pidsOnly'], '') || GeneralUtility::inList($pidOnlyList, $id)) {
599
600
                                // add trailing slash if not present
601 3
                            if (!empty($subCfg['baseUrl']) && substr($subCfg['baseUrl'], -1) != '/') {
602
                                $subCfg['baseUrl'] .= '/';
603
                            }
604
605
                            // Explode, process etc.:
606 3
                            $res[$key] = [];
607 3
                            $res[$key]['subCfg'] = $subCfg;
608 3
                            $res[$key]['paramParsed'] = $this->parseParams($values);
0 ignored issues
show
Documentation introduced by
$values is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
609 3
                            $res[$key]['paramExpanded'] = $this->expandParameters($res[$key]['paramParsed'], $id);
610 3
                            $res[$key]['origin'] = 'pagets';
611
612
                            // recognize MP value
613 3
                            if (!$this->MP) {
614 3
                                $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $id]);
615
                            } else {
616 3
                                $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $id . '&MP=' . $this->MP]);
617
                            }
618
                        }
619
                    }
620
                }
621
            }
622
        }
623
624
        /**
625
         * Get configuration from tx_crawler_configuration records
626
         */
627
628
        // get records along the rootline
629 4
        $rootLine = BackendUtility::BEgetRootLine($id);
630
631 4
        foreach ($rootLine as $page) {
632 4
            $configurationRecordsForCurrentPage = BackendUtility::getRecordsByField(
633 4
                'tx_crawler_configuration',
634 4
                'pid',
635 4
                intval($page['uid']),
636 4
                BackendUtility::BEenableFields('tx_crawler_configuration') . BackendUtility::deleteClause('tx_crawler_configuration')
637
            );
638
639 4
            if (is_array($configurationRecordsForCurrentPage)) {
640 1
                foreach ($configurationRecordsForCurrentPage as $configurationRecord) {
641
642
                        // check access to the configuration record
643 1
                    if (empty($configurationRecord['begroups']) || $GLOBALS['BE_USER']->isAdmin() || $this->hasGroupAccess($GLOBALS['BE_USER']->user['usergroup_cached_list'], $configurationRecord['begroups'])) {
644 1
                        $pidOnlyList = implode(',', GeneralUtility::trimExplode(',', $configurationRecord['pidsonly'], true));
645
646
                        // process configuration if it is not page-specific or if the specific page is the current page:
647 1
                        if (!strcmp($configurationRecord['pidsonly'], '') || GeneralUtility::inList($pidOnlyList, $id)) {
648 1
                            $key = $configurationRecord['name'];
649
650
                            // don't overwrite previously defined paramSets
651 1
                            if (!isset($res[$key])) {
652
653
                                    /* @var $TSparserObject \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser */
654 1
                                $TSparserObject = GeneralUtility::makeInstance('TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser');
655 1
                                $TSparserObject->parse($configurationRecord['processing_instruction_parameters_ts']);
656
657
                                $subCfg = [
658 1
                                    'procInstrFilter' => $configurationRecord['processing_instruction_filter'],
659 1
                                    'procInstrParams.' => $TSparserObject->setup,
660 1
                                    'baseUrl' => $this->getBaseUrlForConfigurationRecord(
661 1
                                        $configurationRecord['base_url'],
662 1
                                        $configurationRecord['sys_domain_base_url'],
663 1
                                        $forceSsl
664
                                    ),
665 1
                                    'realurl' => $configurationRecord['realurl'],
666 1
                                    'cHash' => $configurationRecord['chash'],
667 1
                                    'userGroups' => $configurationRecord['fegroups'],
668 1
                                    'exclude' => $configurationRecord['exclude'],
669 1
                                    'rootTemplatePid' => (int) $configurationRecord['root_template_pid'],
670 1
                                    'key' => $key,
671
                                ];
672
673
                                // add trailing slash if not present
674 1
                                if (!empty($subCfg['baseUrl']) && substr($subCfg['baseUrl'], -1) != '/') {
675
                                    $subCfg['baseUrl'] .= '/';
676
                                }
677 1
                                if (!in_array($id, $this->expandExcludeString($subCfg['exclude']))) {
678 1
                                    $res[$key] = [];
679 1
                                    $res[$key]['subCfg'] = $subCfg;
680 1
                                    $res[$key]['paramParsed'] = $this->parseParams($configurationRecord['configuration']);
681 1
                                    $res[$key]['paramExpanded'] = $this->expandParameters($res[$key]['paramParsed'], $id);
682 1
                                    $res[$key]['URLs'] = $this->compileUrls($res[$key]['paramExpanded'], ['?id=' . $id]);
683 4
                                    $res[$key]['origin'] = 'tx_crawler_configuration_' . $configurationRecord['uid'];
684
                                }
685
                            }
686
                        }
687
                    }
688
                }
689
            }
690
        }
691
692 4
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['processUrls'])) {
693
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['processUrls'] as $func) {
694
                $params = [
695
                    'res' => &$res,
696
                ];
697
                GeneralUtility::callUserFunction($func, $params, $this);
698
            }
699
        }
700
701 4
        return $res;
702
    }
703
704
    /**
705
     * Checks if a domain record exist and returns the base-url based on the record. If not the given baseUrl string is used.
706
     *
707
     * @param string $baseUrl
708
     * @param integer $sysDomainUid
709
     * @param bool $ssl
710
     * @return string
711
     */
712 4
    protected function getBaseUrlForConfigurationRecord($baseUrl, $sysDomainUid, $ssl = false)
713
    {
714 4
        $sysDomainUid = intval($sysDomainUid);
715 4
        $urlScheme = ($ssl === false) ? 'http' : 'https';
716
717 4
        if ($sysDomainUid > 0) {
718 2
            $res = $this->db->exec_SELECTquery(
719 2
                '*',
720 2
                'sys_domain',
721 2
                'uid = ' . $sysDomainUid .
722 2
                BackendUtility::BEenableFields('sys_domain') .
723 2
                BackendUtility::deleteClause('sys_domain')
724
            );
725 2
            $row = $this->db->sql_fetch_assoc($res);
726 2
            if ($row['domainName'] != '') {
727 1
                return $urlScheme . '://' . $row['domainName'];
728
            }
729
        }
730 3
        return $baseUrl;
731
    }
732
733
    public function getConfigurationsForBranch($rootid, $depth)
734
    {
735
        $configurationsForBranch = [];
736
737
        $pageTSconfig = $this->getPageTSconfigForId($rootid);
738
        if (is_array($pageTSconfig) && is_array($pageTSconfig['tx_crawler.']['crawlerCfg.']) && is_array($pageTSconfig['tx_crawler.']['crawlerCfg.']['paramSets.'])) {
739
            $sets = $pageTSconfig['tx_crawler.']['crawlerCfg.']['paramSets.'];
740
            if (is_array($sets)) {
741
                foreach ($sets as $key => $value) {
742
                    if (!is_array($value)) {
743
                        continue;
744
                    }
745
                    $configurationsForBranch[] = substr($key, -1) == '.' ? substr($key, 0, -1) : $key;
746
                }
747
            }
748
        }
749
        $pids = [];
750
        $rootLine = BackendUtility::BEgetRootLine($rootid);
751
        foreach ($rootLine as $node) {
752
            $pids[] = $node['uid'];
753
        }
754
        /* @var PageTreeView $tree */
755
        $tree = GeneralUtility::makeInstance(PageTreeView::class);
756
        $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
757
        $tree->init('AND ' . $perms_clause);
758
        $tree->getTree($rootid, $depth, '');
759
        foreach ($tree->tree as $node) {
760
            $pids[] = $node['row']['uid'];
761
        }
762
763
        $res = $this->db->exec_SELECTquery(
764
            '*',
765
            'tx_crawler_configuration',
766
            'pid IN (' . implode(',', $pids) . ') ' .
767
            BackendUtility::BEenableFields('tx_crawler_configuration') .
768
            BackendUtility::deleteClause('tx_crawler_configuration') . ' ' .
769
            BackendUtility::versioningPlaceholderClause('tx_crawler_configuration') . ' '
770
        );
771
772
        while ($row = $this->db->sql_fetch_assoc($res)) {
773
            $configurationsForBranch[] = $row['name'];
774
        }
775
        $this->db->sql_free_result($res);
776
        return $configurationsForBranch;
777
    }
778
779
    /**
780
     * Check if a user has access to an item
781
     * (e.g. get the group list of the current logged in user from $GLOBALS['TSFE']->gr_list)
782
     *
783
     * @see \TYPO3\CMS\Frontend\Page\PageRepository::getMultipleGroupsWhereClause()
784
     * @param  string $groupList    Comma-separated list of (fe_)group UIDs from a user
785
     * @param  string $accessList   Comma-separated list of (fe_)group UIDs of the item to access
786
     * @return bool                 TRUE if at least one of the users group UIDs is in the access list or the access list is empty
787
     */
788 3
    public function hasGroupAccess($groupList, $accessList)
789
    {
790 3
        if (empty($accessList)) {
791 1
            return true;
792
        }
793 2
        foreach (GeneralUtility::intExplode(',', $groupList) as $groupUid) {
794 2
            if (GeneralUtility::inList($accessList, $groupUid)) {
795 2
                return true;
796
            }
797
        }
798 1
        return false;
799
    }
800
801
    /**
802
     * Parse GET vars of input Query into array with key=>value pairs
803
     *
804
     * @param string $inputQuery Input query string
805
     * @return array
806
     */
807 7
    public function parseParams($inputQuery)
808
    {
809
        // Extract all GET parameters into an ARRAY:
810 7
        $paramKeyValues = [];
811 7
        $GETparams = explode('&', $inputQuery);
812
813 7
        foreach ($GETparams as $paramAndValue) {
814 4
            list($p, $v) = explode('=', $paramAndValue, 2);
815 4
            if (strlen($p)) {
816 4
                $paramKeyValues[rawurldecode($p)] = rawurldecode($v);
817
            }
818
        }
819
820 7
        return $paramKeyValues;
821
    }
822
823
    /**
824
     * Will expand the parameters configuration to individual values. This follows a certain syntax of the value of each parameter.
825
     * Syntax of values:
826
     * - Basically: If the value is wrapped in [...] it will be expanded according to the following syntax, otherwise the value is taken literally
827
     * - Configuration is splitted by "|" and the parts are processed individually and finally added together
828
     * - For each configuration part:
829
     *         - "[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"
830
     *         - "_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"
831
     *        _ENABLELANG:1 picks only original records without their language overlays
832
     *         - Default: Literal value
833
     *
834
     * @param array $paramArray Array with key (GET var name) and values (value of GET var which is configuration for expansion)
835
     * @param integer $pid Current page ID
836
     * @return array
837
     */
838 4
    public function expandParameters($paramArray, $pid)
839
    {
840 4
        global $TCA;
841
842
        // Traverse parameter names:
843 4
        foreach ($paramArray as $p => $v) {
844 1
            $v = trim($v);
845
846
            // If value is encapsulated in square brackets it means there are some ranges of values to find, otherwise the value is literal
847 1
            if (substr($v, 0, 1) === '[' && substr($v, -1) === ']') {
848
                // So, find the value inside brackets and reset the paramArray value as an array.
849 1
                $v = substr($v, 1, -1);
850 1
                $paramArray[$p] = [];
851
852
                // Explode parts and traverse them:
853 1
                $parts = explode('|', $v);
854 1
                foreach ($parts as $pV) {
855
856
                        // Look for integer range: (fx. 1-34 or -40--30 // reads minus 40 to minus 30)
857 1
                    if (preg_match('/^(-?[0-9]+)\s*-\s*(-?[0-9]+)$/', trim($pV), $reg)) {
858
859
                        // Swap if first is larger than last:
860
                        if ($reg[1] > $reg[2]) {
861
                            $temp = $reg[2];
862
                            $reg[2] = $reg[1];
863
                            $reg[1] = $temp;
864
                        }
865
866
                        // Traverse range, add values:
867
                        $runAwayBrake = 1000; // Limit to size of range!
868
                        for ($a = $reg[1]; $a <= $reg[2];$a++) {
869
                            $paramArray[$p][] = $a;
870
                            $runAwayBrake--;
871
                            if ($runAwayBrake <= 0) {
872
                                break;
873
                            }
874
                        }
875 1
                    } elseif (substr(trim($pV), 0, 7) == '_TABLE:') {
876
877
                        // Parse parameters:
878
                        $subparts = GeneralUtility::trimExplode(';', $pV);
879
                        $subpartParams = [];
880
                        foreach ($subparts as $spV) {
881
                            list($pKey, $pVal) = GeneralUtility::trimExplode(':', $spV);
882
                            $subpartParams[$pKey] = $pVal;
883
                        }
884
885
                        // Table exists:
886
                        if (isset($TCA[$subpartParams['_TABLE']])) {
887
                            $lookUpPid = isset($subpartParams['_PID']) ? intval($subpartParams['_PID']) : $pid;
888
                            $pidField = isset($subpartParams['_PIDFIELD']) ? trim($subpartParams['_PIDFIELD']) : 'pid';
889
                            $where = isset($subpartParams['_WHERE']) ? $subpartParams['_WHERE'] : '';
890
                            $addTable = isset($subpartParams['_ADDTABLE']) ? $subpartParams['_ADDTABLE'] : '';
891
892
                            $fieldName = $subpartParams['_FIELD'] ? $subpartParams['_FIELD'] : 'uid';
893
                            if ($fieldName === 'uid' || $TCA[$subpartParams['_TABLE']]['columns'][$fieldName]) {
894
                                $andWhereLanguage = '';
895
                                $transOrigPointerField = $TCA[$subpartParams['_TABLE']]['ctrl']['transOrigPointerField'];
896
897
                                if ($subpartParams['_ENABLELANG'] && $transOrigPointerField) {
898
                                    $andWhereLanguage = ' AND ' . $this->db->quoteStr($transOrigPointerField, $subpartParams['_TABLE']) . ' <= 0 ';
899
                                }
900
901
                                $where = $this->db->quoteStr($pidField, $subpartParams['_TABLE']) . '=' . intval($lookUpPid) . ' ' .
902
                                    $andWhereLanguage . $where;
903
904
                                $rows = $this->db->exec_SELECTgetRows(
905
                                    $fieldName,
906
                                    $subpartParams['_TABLE'] . $addTable,
907
                                    $where . BackendUtility::deleteClause($subpartParams['_TABLE']),
908
                                    '',
909
                                    '',
910
                                    '',
911
                                    $fieldName
912
                                );
913
914
                                if (is_array($rows)) {
915
                                    $paramArray[$p] = array_merge($paramArray[$p], array_keys($rows));
916
                                }
917
                            }
918
                        }
919
                    } else { // Just add value:
920 1
                        $paramArray[$p][] = $pV;
921
                    }
922
                    // Hook for processing own expandParameters place holder
923 1
                    if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['crawler/class.tx_crawler_lib.php']['expandParameters'])) {
924
                        $_params = [
925
                            'pObj' => &$this,
926
                            'paramArray' => &$paramArray,
927
                            'currentKey' => $p,
928
                            'currentValue' => $pV,
929
                            'pid' => $pid
930
                        ];
931
                        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['crawler/class.tx_crawler_lib.php']['expandParameters'] as $key => $_funcRef) {
932 1
                            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
933
                        }
934
                    }
935
                }
936
937
                // Make unique set of values and sort array by key:
938 1
                $paramArray[$p] = array_unique($paramArray[$p]);
939 1
                ksort($paramArray);
940
            } else {
941
                // Set the literal value as only value in array:
942 1
                $paramArray[$p] = [$v];
943
            }
944
        }
945
946 4
        return $paramArray;
947
    }
948
949
    /**
950
     * Compiling URLs from parameter array (output of expandParameters())
951
     * The number of URLs will be the multiplication of the number of parameter values for each key
952
     *
953
     * @param array $paramArray Output of expandParameters(): Array with keys (GET var names) and for each an array of values
954
     * @param array $urls URLs accumulated in this array (for recursion)
955
     * @return array
956
     */
957 7
    public function compileUrls($paramArray, $urls = [])
958
    {
959 7
        if (count($paramArray) && is_array($urls)) {
960
            // shift first off stack:
961 3
            reset($paramArray);
962 3
            $varName = key($paramArray);
963 3
            $valueSet = array_shift($paramArray);
964
965
            // Traverse value set:
966 3
            $newUrls = [];
967 3
            foreach ($urls as $url) {
968 2
                foreach ($valueSet as $val) {
969 2
                    $newUrls[] = $url . (strcmp($val, '') ? '&' . rawurlencode($varName) . '=' . rawurlencode($val) : '');
970
971 2
                    if (count($newUrls) > MathUtility::forceIntegerInRange($this->extensionSettings['maxCompileUrls'], 1, 1000000000, 10000)) {
972 2
                        break;
973
                    }
974
                }
975
            }
976 3
            $urls = $newUrls;
977 3
            $urls = $this->compileUrls($paramArray, $urls);
978
        }
979
980 7
        return $urls;
981
    }
982
983
    /************************************
984
     *
985
     * Crawler log
986
     *
987
     ************************************/
988
989
    /**
990
     * Return array of records from crawler queue for input page ID
991
     *
992
     * @param integer $id Page ID for which to look up log entries.
993
     * @param string$filter Filter: "all" => all entries, "pending" => all that is not yet run, "finished" => all complete ones
994
     * @param boolean $doFlush If TRUE, then entries selected at DELETED(!) instead of selected!
995
     * @param boolean $doFullFlush
996
     * @param integer $itemsPerPage Limit the amount of entries per page default is 10
997
     * @return array
998
     */
999 4
    public function getLogEntriesForPageId($id, $filter = '', $doFlush = false, $doFullFlush = false, $itemsPerPage = 10)
1000
    {
1001
        switch ($filter) {
1002 4
            case 'pending':
1003
                $addWhere = ' AND exec_time=0';
1004
                break;
1005 4
            case 'finished':
1006
                $addWhere = ' AND exec_time>0';
1007
                break;
1008
            default:
1009 4
                $addWhere = '';
1010 4
                break;
1011
        }
1012
1013
        // FIXME: Write unit test that ensures that the right records are deleted.
1014 4
        if ($doFlush) {
1015 2
            $this->flushQueue(($doFullFlush ? '1=1' : ('page_id=' . intval($id))) . $addWhere);
1016 2
            return [];
1017
        } else {
1018 2
            return $this->db->exec_SELECTgetRows(
1019 2
                '*',
1020 2
                'tx_crawler_queue',
1021 2
                'page_id=' . intval($id) . $addWhere,
1022 2
                '',
1023 2
                'scheduled DESC',
1024 2
                (intval($itemsPerPage) > 0 ? intval($itemsPerPage) : '')
1025
            );
1026
        }
1027
    }
1028
1029
    /**
1030
     * Return array of records from crawler queue for input set ID
1031
     *
1032
     * @param integer $set_id Set ID for which to look up log entries.
1033
     * @param string $filter Filter: "all" => all entries, "pending" => all that is not yet run, "finished" => all complete ones
1034
     * @param boolean $doFlush If TRUE, then entries selected at DELETED(!) instead of selected!
1035
     * @param integer $itemsPerPage Limit the amount of entires per page default is 10
1036
     * @return array
1037
     */
1038 6
    public function getLogEntriesForSetId($set_id, $filter = '', $doFlush = false, $doFullFlush = false, $itemsPerPage = 10)
1039
    {
1040
        // FIXME: Write Unit tests for Filters
1041
        switch ($filter) {
1042 6
            case 'pending':
1043 1
                $addWhere = ' AND exec_time=0';
1044 1
                break;
1045 5
            case 'finished':
1046 1
                $addWhere = ' AND exec_time>0';
1047 1
                break;
1048
            default:
1049 4
                $addWhere = '';
1050 4
                break;
1051
        }
1052
        // FIXME: Write unit test that ensures that the right records are deleted.
1053 6
        if ($doFlush) {
1054 4
            $this->flushQueue($doFullFlush ? '' : ('set_id=' . intval($set_id) . $addWhere));
1055 4
            return [];
1056
        } else {
1057 2
            return $this->db->exec_SELECTgetRows(
1058 2
                '*',
1059 2
                'tx_crawler_queue',
1060 2
                'set_id=' . intval($set_id) . $addWhere,
1061 2
                '',
1062 2
                'scheduled DESC',
1063 2
                (intval($itemsPerPage) > 0 ? intval($itemsPerPage) : '')
1064
            );
1065
        }
1066
    }
1067
1068
    /**
1069
     * Removes queue entries
1070
     *
1071
     * @param string $where SQL related filter for the entries which should be removed
1072
     * @return void
1073
     */
1074 10
    protected function flushQueue($where = '')
1075
    {
1076 10
        $realWhere = strlen($where) > 0 ? $where : '1=1';
1077
1078 10
        if (EventDispatcher::getInstance()->hasObserver('queueEntryFlush')) {
1079
            $groups = $this->db->exec_SELECTgetRows('DISTINCT set_id', 'tx_crawler_queue', $realWhere);
1080
            if (is_array($groups)) {
1081
                foreach ($groups as $group) {
1082
                    EventDispatcher::getInstance()->post('queueEntryFlush', $group['set_id'], $this->db->exec_SELECTgetRows('uid, set_id', 'tx_crawler_queue', $realWhere . ' AND set_id="' . $group['set_id'] . '"'));
1083
                }
1084
            }
1085
        }
1086
1087 10
        $this->db->exec_DELETEquery('tx_crawler_queue', $realWhere);
1088 10
    }
1089
1090
    /**
1091
     * Adding call back entries to log (called from hooks typically, see indexed search class "class.crawler.php"
1092
     *
1093
     * @param integer $setId Set ID
1094
     * @param array $params Parameters to pass to call back function
1095
     * @param string $callBack Call back object reference, eg. 'EXT:indexed_search/class.crawler.php:&tx_indexedsearch_crawler'
1096
     * @param integer $page_id Page ID to attach it to
1097
     * @param integer $schedule Time at which to activate
1098
     * @return void
1099
     */
1100
    public function addQueueEntry_callBack($setId, $params, $callBack, $page_id = 0, $schedule = 0)
1101
    {
1102
        if (!is_array($params)) {
1103
            $params = [];
1104
        }
1105
        $params['_CALLBACKOBJ'] = $callBack;
1106
1107
        // Compile value array:
1108
        $fieldArray = [
1109
            'page_id' => intval($page_id),
1110
            'parameters' => serialize($params),
1111
            'scheduled' => intval($schedule) ? intval($schedule) : $this->getCurrentTime(),
1112
            'exec_time' => 0,
1113
            'set_id' => intval($setId),
1114
            'result_data' => '',
1115
        ];
1116
1117
        $this->db->exec_INSERTquery('tx_crawler_queue', $fieldArray);
1118
    }
1119
1120
    /************************************
1121
     *
1122
     * URL setting
1123
     *
1124
     ************************************/
1125
1126
    /**
1127
     * Setting a URL for crawling:
1128
     *
1129
     * @param integer $id Page ID
1130
     * @param string $url Complete URL
1131
     * @param array $subCfg Sub configuration array (from TS config)
1132
     * @param integer $tstamp Scheduled-time
1133
     * @param string $configurationHash (optional) configuration hash
1134
     * @param bool $skipInnerDuplicationCheck (optional) skip inner duplication check
1135
     * @return bool
1136
     */
1137 4
    public function addUrl(
1138
        $id,
1139
        $url,
1140
        array $subCfg,
1141
        $tstamp,
1142
        $configurationHash = '',
1143
        $skipInnerDuplicationCheck = false
1144
    ) {
1145 4
        $urlAdded = false;
1146 4
        $rows = [];
1147
1148
        // Creating parameters:
1149
        $parameters = [
1150 4
            'url' => $url
1151
        ];
1152
1153
        // fe user group simulation:
1154 4
        $uGs = implode(',', array_unique(GeneralUtility::intExplode(',', $subCfg['userGroups'], true)));
1155 4
        if ($uGs) {
1156
            $parameters['feUserGroupList'] = $uGs;
1157
        }
1158
1159
        // Setting processing instructions
1160 4
        $parameters['procInstructions'] = GeneralUtility::trimExplode(',', $subCfg['procInstrFilter']);
1161 4
        if (is_array($subCfg['procInstrParams.'])) {
1162 4
            $parameters['procInstrParams'] = $subCfg['procInstrParams.'];
1163
        }
1164
1165
        // Possible TypoScript Template Parents
1166 4
        $parameters['rootTemplatePid'] = $subCfg['rootTemplatePid'];
1167
1168
        // Compile value array:
1169 4
        $parameters_serialized = serialize($parameters);
1170
        $fieldArray = [
1171 4
            'page_id' => intval($id),
1172 4
            'parameters' => $parameters_serialized,
1173 4
            'parameters_hash' => GeneralUtility::shortMD5($parameters_serialized),
1174 4
            'configuration_hash' => $configurationHash,
1175 4
            'scheduled' => $tstamp,
1176 4
            'exec_time' => 0,
1177 4
            'set_id' => intval($this->setID),
1178 4
            'result_data' => '',
1179 4
            'configuration' => $subCfg['key'],
1180
        ];
1181
1182 4
        if ($this->registerQueueEntriesInternallyOnly) {
1183
            //the entries will only be registered and not stored to the database
1184
            $this->queueEntries[] = $fieldArray;
1185
        } else {
1186 4
            if (!$skipInnerDuplicationCheck) {
1187
                // check if there is already an equal entry
1188 4
                $rows = $this->getDuplicateRowsIfExist($tstamp, $fieldArray);
1189
            }
1190
1191 4
            if (count($rows) == 0) {
1192 4
                $this->db->exec_INSERTquery('tx_crawler_queue', $fieldArray);
1193 4
                $uid = $this->db->sql_insert_id();
1194 4
                $rows[] = $uid;
1195 4
                $urlAdded = true;
1196 4
                EventDispatcher::getInstance()->post('urlAddedToQueue', $this->setID, ['uid' => $uid, 'fieldArray' => $fieldArray]);
1197
            } else {
1198 2
                EventDispatcher::getInstance()->post('duplicateUrlInQueue', $this->setID, ['rows' => $rows, 'fieldArray' => $fieldArray]);
1199
            }
1200
        }
1201
1202 4
        return $urlAdded;
1203
    }
1204
1205
    /**
1206
     * This method determines duplicates for a queue entry with the same parameters and this timestamp.
1207
     * If the timestamp is in the past, it will check if there is any unprocessed queue entry in the past.
1208
     * If the timestamp is in the future it will check, if the queued entry has exactly the same timestamp
1209
     *
1210
     * @param int $tstamp
1211
     * @param array $fieldArray
1212
     *
1213
     * @return array
1214
     */
1215 4
    protected function getDuplicateRowsIfExist($tstamp, $fieldArray)
1216
    {
1217 4
        $rows = [];
1218
1219 4
        $currentTime = $this->getCurrentTime();
1220
1221
        //if this entry is scheduled with "now"
1222 4
        if ($tstamp <= $currentTime) {
1223 1
            if ($this->extensionSettings['enableTimeslot']) {
1224 1
                $timeBegin = $currentTime - 100;
1225 1
                $timeEnd = $currentTime + 100;
1226 1
                $where = ' ((scheduled BETWEEN ' . $timeBegin . ' AND ' . $timeEnd . ' ) OR scheduled <= ' . $currentTime . ') ';
1227
            } else {
1228 1
                $where = 'scheduled <= ' . $currentTime;
1229
            }
1230 3
        } elseif ($tstamp > $currentTime) {
1231
            //entry with a timestamp in the future need to have the same schedule time
1232 3
            $where = 'scheduled = ' . $tstamp ;
1233
        }
1234
1235 4
        if (!empty($where)) {
1236 4
            $result = $this->db->exec_SELECTgetRows(
1237 4
                'qid',
1238 4
                'tx_crawler_queue',
1239
                $where .
1240 4
                ' AND NOT exec_time' .
1241 4
                ' AND NOT process_id ' .
1242 4
                ' AND page_id=' . intval($fieldArray['page_id']) .
1243 4
                ' AND parameters_hash = ' . $this->db->fullQuoteStr($fieldArray['parameters_hash'], 'tx_crawler_queue')
1244
            );
1245
1246 4
            if (is_array($result)) {
1247 4
                foreach ($result as $value) {
1248 2
                    $rows[] = $value['qid'];
1249
                }
1250
            }
1251
        }
1252
1253 4
        return $rows;
1254
    }
1255
1256
    /**
1257
     * Returns the current system time
1258
     *
1259
     * @return int
1260
     */
1261 1
    public function getCurrentTime()
1262
    {
1263 1
        return time();
1264
    }
1265
1266
    /************************************
1267
     *
1268
     * URL reading
1269
     *
1270
     ************************************/
1271
1272
    /**
1273
     * Read URL for single queue entry
1274
     *
1275
     * @param integer $queueId
1276
     * @param boolean $force If set, will process even if exec_time has been set!
1277
     * @return integer
1278
     */
1279
    public function readUrl($queueId, $force = false)
1280
    {
1281
        $ret = 0;
1282
        if ($this->debugMode) {
1283
            GeneralUtility::devlog('crawler-readurl start ' . microtime(true), __FUNCTION__);
1284
        }
1285
        // Get entry:
1286
        list($queueRec) = $this->db->exec_SELECTgetRows(
1287
            '*',
1288
            'tx_crawler_queue',
1289
            'qid=' . intval($queueId) . ($force ? '' : ' AND exec_time=0 AND process_scheduled > 0')
1290
        );
1291
1292
        if (!is_array($queueRec)) {
1293
            return;
1294
        }
1295
1296
        $parameters = unserialize($queueRec['parameters']);
1297
        if ($parameters['rootTemplatePid']) {
1298
            $this->initTSFE((int)$parameters['rootTemplatePid']);
1299
        } else {
1300
            GeneralUtility::sysLog(
1301
                'Page with (' . $queueRec['page_id'] . ') could not be crawled, please check your crawler configuration. Perhaps no Root Template Pid is set',
1302
                'crawler',
1303
                GeneralUtility::SYSLOG_SEVERITY_WARNING
1304
            );
1305
        }
1306
1307
        SignalSlotUtility::emitSignal(
1308
            __CLASS__,
1309
            SignalSlotUtility::SIGNNAL_QUEUEITEM_PREPROCESS,
1310
            [$queueId, &$queueRec]
1311
        );
1312
1313
        // Set exec_time to lock record:
1314
        $field_array = ['exec_time' => $this->getCurrentTime()];
1315
1316
        if (isset($this->processID)) {
1317
            //if mulitprocessing is used we need to store the id of the process which has handled this entry
1318
            $field_array['process_id_completed'] = $this->processID;
1319
        }
1320
        $this->db->exec_UPDATEquery('tx_crawler_queue', 'qid=' . intval($queueId), $field_array);
1321
1322
        $result = $this->readUrl_exec($queueRec);
1323
        $resultData = unserialize($result['content']);
1324
1325
        //atm there's no need to point to specific pollable extensions
1326
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pollSuccess'])) {
1327
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['crawler']['pollSuccess'] as $pollable) {
1328
                // only check the success value if the instruction is runnig
1329
                // it is important to name the pollSuccess key same as the procInstructions key
1330
                if (is_array($resultData['parameters']['procInstructions']) && in_array(
1331
                    $pollable,
1332
                        $resultData['parameters']['procInstructions']
1333
                )
1334
                ) {
1335
                    if (!empty($resultData['success'][$pollable]) && $resultData['success'][$pollable]) {
1336
                        $ret |= self::CLI_STATUS_POLLABLE_PROCESSED;
1337
                    }
1338
                }
1339
            }
1340
        }
1341
1342
        // Set result in log which also denotes the end of the processing of this entry.
1343
        $field_array = ['result_data' => serialize($result)];
1344
1345
        SignalSlotUtility::emitSignal(
1346
            __CLASS__,
1347
            SignalSlotUtility::SIGNNAL_QUEUEITEM_POSTPROCESS,
1348
            [$queueId, &$field_array]
1349
        );
1350
1351
        $this->db->exec_UPDATEquery('tx_crawler_queue', 'qid=' . intval($queueId), $field_array);
1352
1353
        if ($this->debugMode) {
1354
            GeneralUtility::devlog('crawler-readurl stop ' . microtime(true), __FUNCTION__);
1355
        }
1356
1357
        return $ret;
1358
    }
1359
1360
    /**
1361
     * Read URL for not-yet-inserted log-entry
1362
     *
1363
     * @param array $field_array Queue field array,
1364
     *
1365
     * @return string
1366
     */
1367
    public function readUrlFromArray($field_array)
1368
    {
1369
1370
            // Set exec_time to lock record:
1371
        $field_array['exec_time'] = $this->getCurrentTime();
1372
        $this->db->exec_INSERTquery('tx_crawler_queue', $field_array);
1373
        $queueId = $field_array['qid'] = $this->db->sql_insert_id();
1374
1375
        $result = $this->readUrl_exec($field_array);
1376
1377
        // Set result in log which also denotes the end of the processing of this entry.
1378
        $field_array = ['result_data' => serialize($result)];
1379
1380
        SignalSlotUtility::emitSignal(
1381
            __CLASS__,
1382
            SignalSlotUtility::SIGNNAL_QUEUEITEM_POSTPROCESS,
1383
            [$queueId, &$field_array]
1384
        );
1385
1386
        $this->db->exec_UPDATEquery('tx_crawler_queue', 'qid=' . intval($queueId), $field_array);
1387
1388
        return $result;
1389
    }
1390
1391
    /**
1392
     * Read URL for a queue record
1393
     *
1394
     * @param array $queueRec Queue record
1395
     * @return string
1396
     */
1397
    public function readUrl_exec($queueRec)
1398
    {
1399
        // Decode parameters:
1400
        $parameters = unserialize($queueRec['parameters']);
1401
        $result = 'ERROR';
1402
        if (is_array($parameters)) {
1403
            if ($parameters['_CALLBACKOBJ']) { // Calling object:
1404
                $objRef = $parameters['_CALLBACKOBJ'];
1405
                $callBackObj = &GeneralUtility::getUserObj($objRef);
1406
                if (is_object($callBackObj)) {
1407
                    unset($parameters['_CALLBACKOBJ']);
1408
                    $result = ['content' => serialize($callBackObj->crawler_execute($parameters, $this))];
1409
                } else {
1410
                    $result = ['content' => 'No object: ' . $objRef];
1411
                }
1412
            } else { // Regular FE request:
1413
1414
                // Prepare:
1415
                $crawlerId = $queueRec['qid'] . ':' . md5($queueRec['qid'] . '|' . $queueRec['set_id'] . '|' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']);
1416
1417
                // Get result:
1418
                $result = $this->requestUrl($parameters['url'], $crawlerId);
1419
1420
                EventDispatcher::getInstance()->post('urlCrawled', $queueRec['set_id'], ['url' => $parameters['url'], 'result' => $result]);
1421
            }
1422
        }
1423
1424
        return $result;
1425
    }
1426
1427
    /**
1428
     * Gets the content of a URL.
1429
     *
1430
     * @param string $originalUrl URL to read
1431
     * @param string $crawlerId Crawler ID string (qid + hash to verify)
1432
     * @param integer $timeout Timeout time
1433
     * @param integer $recursion Recursion limiter for 302 redirects
1434
     * @return array
1435
     */
1436 2
    public function requestUrl($originalUrl, $crawlerId, $timeout = 2, $recursion = 10)
1437
    {
1438 2
        if (!$recursion) {
1439
            return false;
1440
        }
1441
1442
        // Parse URL, checking for scheme:
1443 2
        $url = parse_url($originalUrl);
1444
1445 2
        if ($url === false) {
1446
            if (TYPO3_DLOG) {
1447
                GeneralUtility::devLog(sprintf('Could not parse_url() for string "%s"', $url), 'crawler', 4, ['crawlerId' => $crawlerId]);
1448
            }
1449
            return false;
1450
        }
1451
1452 2
        if (!in_array($url['scheme'], ['','http','https'])) {
1453
            if (TYPO3_DLOG) {
1454
                GeneralUtility::devLog(sprintf('Scheme does not match for url "%s"', $url), 'crawler', 4, ['crawlerId' => $crawlerId]);
1455
            }
1456
            return false;
1457
        }
1458
1459
        // direct request
1460 2
        if ($this->extensionSettings['makeDirectRequests']) {
1461 2
            $result = $this->sendDirectRequest($originalUrl, $crawlerId);
1462 2
            return $result;
1463
        }
1464
1465
        $reqHeaders = $this->buildRequestHeaderArray($url, $crawlerId);
1466
1467
        // thanks to Pierrick Caillon for adding proxy support
1468
        $rurl = $url;
1469
1470
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['curlUse'] && $GLOBALS['TYPO3_CONF_VARS']['SYS']['curlProxyServer']) {
1471
            $rurl = parse_url($GLOBALS['TYPO3_CONF_VARS']['SYS']['curlProxyServer']);
1472
            $url['path'] = $url['scheme'] . '://' . $url['host'] . ($url['port'] > 0 ? ':' . $url['port'] : '') . $url['path'];
1473
            $reqHeaders = $this->buildRequestHeaderArray($url, $crawlerId);
1474
        }
1475
1476
        $host = $rurl['host'];
1477
1478
        if ($url['scheme'] == 'https') {
1479
            $host = 'ssl://' . $host;
1480
            $port = ($rurl['port'] > 0) ? $rurl['port'] : 443;
1481
        } else {
1482
            $port = ($rurl['port'] > 0) ? $rurl['port'] : 80;
1483
        }
1484
1485
        $startTime = microtime(true);
1486
        $fp = fsockopen($host, $port, $errno, $errstr, $timeout);
1487
1488
        if (!$fp) {
1489
            if (TYPO3_DLOG) {
1490
                GeneralUtility::devLog(sprintf('Error while opening "%s"', $url), 'crawler', 4, ['crawlerId' => $crawlerId]);
1491
            }
1492
            return false;
1493
        } else {
1494
            // Request message:
1495
            $msg = implode("\r\n", $reqHeaders) . "\r\n\r\n";
1496
            fputs($fp, $msg);
1497
1498
            // Read response:
1499
            $d = $this->getHttpResponseFromStream($fp);
1500
            fclose($fp);
1501
1502
            $time = microtime(true) - $startTime;
1503
            $this->log($originalUrl . ' ' . $time);
1504
1505
            // Implode content and headers:
1506
            $result = [
1507
                'request' => $msg,
1508
                'headers' => implode('', $d['headers']),
1509
                'content' => implode('', (array)$d['content'])
1510
            ];
1511
1512
            if (($this->extensionSettings['follow30x']) && ($newUrl = $this->getRequestUrlFrom302Header($d['headers'], $url['user'], $url['pass']))) {
1513
                $result = array_merge(['parentRequest' => $result], $this->requestUrl($newUrl, $crawlerId, $recursion--));
0 ignored issues
show
Bug introduced by
It seems like $newUrl defined by $this->getRequestUrlFrom...['user'], $url['pass']) on line 1512 can also be of type boolean; however, AOE\Crawler\Controller\C...ontroller::requestUrl() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1514
                $newRequestUrl = $this->requestUrl($newUrl, $crawlerId, $timeout, --$recursion);
0 ignored issues
show
Bug introduced by
It seems like $newUrl defined by $this->getRequestUrlFrom...['user'], $url['pass']) on line 1512 can also be of type boolean; however, AOE\Crawler\Controller\C...ontroller::requestUrl() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1515
1516
                if (is_array($newRequestUrl)) {
1517
                    $result = array_merge(['parentRequest' => $result], $newRequestUrl);
1518
                } else {
1519
                    if (TYPO3_DLOG) {
1520
                        GeneralUtility::devLog(sprintf('Error while opening "%s"', $url), 'crawler', 4, ['crawlerId' => $crawlerId]);
1521
                    }
1522
                    return false;
1523
                }
1524
            }
1525
1526
            return $result;
1527
        }
1528
    }
1529
1530
    /**
1531
     * Gets the base path of the website frontend.
1532
     * (e.g. if you call http://mydomain.com/cms/index.php in
1533
     * the browser the base path is "/cms/")
1534
     *
1535
     * @return string Base path of the website frontend
1536
     */
1537
    protected function getFrontendBasePath()
1538
    {
1539
        $frontendBasePath = '/';
1540
1541
        // Get the path from the extension settings:
1542
        if (isset($this->extensionSettings['frontendBasePath']) && $this->extensionSettings['frontendBasePath']) {
1543
            $frontendBasePath = $this->extensionSettings['frontendBasePath'];
1544
            // If empty, try to use config.absRefPrefix:
1545
        } elseif (isset($GLOBALS['TSFE']->absRefPrefix) && !empty($GLOBALS['TSFE']->absRefPrefix)) {
1546
            $frontendBasePath = $GLOBALS['TSFE']->absRefPrefix;
1547
            // If not in CLI mode the base path can be determined from $_SERVER environment:
1548
        } elseif (!defined('TYPO3_REQUESTTYPE_CLI') || !TYPO3_REQUESTTYPE_CLI) {
1549
            $frontendBasePath = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
1550
        }
1551
1552
        // Base path must be '/<pathSegements>/':
1553
        if ($frontendBasePath != '/') {
1554
            $frontendBasePath = '/' . ltrim($frontendBasePath, '/');
1555
            $frontendBasePath = rtrim($frontendBasePath, '/') . '/';
1556
        }
1557
1558
        return $frontendBasePath;
1559
    }
1560
1561
    /**
1562
     * Executes a shell command and returns the outputted result.
1563
     *
1564
     * @param string $command Shell command to be executed
1565
     * @return string Outputted result of the command execution
1566
     */
1567
    protected function executeShellCommand($command)
1568
    {
1569
        $result = shell_exec($command);
1570
        return $result;
1571
    }
1572
1573
    /**
1574
     * Reads HTTP response from the given stream.
1575
     *
1576
     * @param  resource $streamPointer  Pointer to connection stream.
1577
     * @return array                    Associative array with the following items:
1578
     *                                  headers <array> Response headers sent by server.
1579
     *                                  content <array> Content, with each line as an array item.
1580
     */
1581 1
    protected function getHttpResponseFromStream($streamPointer)
1582
    {
1583 1
        $response = ['headers' => [], 'content' => []];
1584
1585 1
        if (is_resource($streamPointer)) {
1586
            // read headers
1587 1
            while ($line = fgets($streamPointer, '2048')) {
1588 1
                $line = trim($line);
1589 1
                if ($line !== '') {
1590 1
                    $response['headers'][] = $line;
1591
                } else {
1592 1
                    break;
1593
                }
1594
            }
1595
1596
            // read content
1597 1
            while ($line = fgets($streamPointer, '2048')) {
1598 1
                $response['content'][] = $line;
1599
            }
1600
        }
1601
1602 1
        return $response;
1603
    }
1604
1605
    /**
1606
     * @param message
1607
     */
1608 2
    protected function log($message)
1609
    {
1610 2
        if (!empty($this->extensionSettings['logFileName'])) {
1611
            $fileResult = @file_put_contents($this->extensionSettings['logFileName'], date('Ymd His') . ' ' . $message . PHP_EOL, FILE_APPEND);
1612
            if (!$fileResult) {
1613
                GeneralUtility::devLog('File "' . $this->extensionSettings['logFileName'] . '" could not be written, please check file permissions.', 'crawler', LogLevel::INFO);
1614
            }
1615
        }
1616 2
    }
1617
1618
    /**
1619
     * Builds HTTP request headers.
1620
     *
1621
     * @param array $url
1622
     * @param string $crawlerId
1623
     *
1624
     * @return array
1625
     */
1626 6
    protected function buildRequestHeaderArray(array $url, $crawlerId)
1627
    {
1628 6
        $reqHeaders = [];
1629 6
        $reqHeaders[] = 'GET ' . $url['path'] . ($url['query'] ? '?' . $url['query'] : '') . ' HTTP/1.0';
1630 6
        $reqHeaders[] = 'Host: ' . $url['host'];
1631 6
        if (stristr($url['query'], 'ADMCMD_previewWS')) {
1632 2
            $reqHeaders[] = 'Cookie: $Version="1"; be_typo_user="1"; $Path=/';
1633
        }
1634 6
        $reqHeaders[] = 'Connection: close';
1635 6
        if ($url['user'] != '') {
1636 2
            $reqHeaders[] = 'Authorization: Basic ' . base64_encode($url['user'] . ':' . $url['pass']);
1637
        }
1638 6
        $reqHeaders[] = 'X-T3crawler: ' . $crawlerId;
1639 6
        $reqHeaders[] = 'User-Agent: TYPO3 crawler';
1640 6
        return $reqHeaders;
1641
    }
1642
1643
    /**
1644
     * Check if the submitted HTTP-Header contains a redirect location and built new crawler-url
1645
     *
1646
     * @param array $headers HTTP Header
1647
     * @param string $user HTTP Auth. User
1648
     * @param string $pass HTTP Auth. Password
1649
     * @return bool|string
1650
     */
1651 12
    protected function getRequestUrlFrom302Header($headers, $user = '', $pass = '')
1652
    {
1653 12
        $header = [];
1654 12
        if (!is_array($headers)) {
1655 1
            return false;
1656
        }
1657 11
        if (!(stristr($headers[0], '301 Moved') || stristr($headers[0], '302 Found') || stristr($headers[0], '302 Moved'))) {
1658 2
            return false;
1659
        }
1660
1661 9
        foreach ($headers as $hl) {
1662 9
            $tmp = explode(": ", $hl);
1663 9
            $header[trim($tmp[0])] = trim($tmp[1]);
1664 9
            if (trim($tmp[0]) == 'Location') {
1665 9
                break;
1666
            }
1667
        }
1668 9
        if (!array_key_exists('Location', $header)) {
1669 3
            return false;
1670
        }
1671
1672 6
        if ($user != '') {
1673 3
            if (!($tmp = parse_url($header['Location']))) {
1674 1
                return false;
1675
            }
1676 2
            $newUrl = $tmp['scheme'] . '://' . $user . ':' . $pass . '@' . $tmp['host'] . $tmp['path'];
1677 2
            if ($tmp['query'] != '') {
1678 2
                $newUrl .= '?' . $tmp['query'];
1679
            }
1680
        } else {
1681 3
            $newUrl = $header['Location'];
1682
        }
1683 5
        return $newUrl;
1684
    }
1685
1686
    /**************************
1687
     *
1688
     * tslib_fe hooks:
1689
     *
1690
     **************************/
1691
1692
    /**
1693
     * Initialization hook (called after database connection)
1694
     * Takes the "HTTP_X_T3CRAWLER" header and looks up queue record and verifies if the session comes from the system (by comparing hashes)
1695
     *
1696
     * @param array $params Parameters from frontend
1697
     * @param object $ref TSFE object (reference under PHP5)
1698
     * @return void
1699
     *
1700
     * FIXME: Look like this is not used, in commit 9910d3f40cce15f4e9b7bcd0488bf21f31d53ebc it's added as public,
1701
     * FIXME: I think this can be removed. (TNM)
1702
     */
1703
    public function fe_init(&$params, $ref)
0 ignored issues
show
Unused Code introduced by
The parameter $ref is not used and could be removed.

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

Loading history...
1704
    {
1705
        // Authenticate crawler request:
1706
        if (isset($_SERVER['HTTP_X_T3CRAWLER'])) {
1707
            list($queueId, $hash) = explode(':', $_SERVER['HTTP_X_T3CRAWLER']);
1708
            list($queueRec) = $this->db->exec_SELECTgetSingleRow('*', 'tx_crawler_queue', 'qid=' . intval($queueId));
1709
1710
            // If a crawler record was found and hash was matching, set it up:
1711
            if (is_array($queueRec) && $hash === md5($queueRec['qid'] . '|' . $queueRec['set_id'] . '|' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
1712
                $params['pObj']->applicationData['tx_crawler']['running'] = true;
1713
                $params['pObj']->applicationData['tx_crawler']['parameters'] = unserialize($queueRec['parameters']);
1714
                $params['pObj']->applicationData['tx_crawler']['log'] = [];
1715
            } else {
1716
                die('No crawler entry found!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method fe_init() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1717
            }
1718
        }
1719
    }
1720
1721
    /*****************************
1722
     *
1723
     * Compiling URLs to crawl - tools
1724
     *
1725
     *****************************/
1726
1727
    /**
1728
     * @param integer $id Root page id to start from.
1729
     * @param integer $depth Depth of tree, 0=only id-page, 1= on sublevel, 99 = infinite
1730
     * @param integer $scheduledTime Unix Time when the URL is timed to be visited when put in queue
1731
     * @param integer $reqMinute Number of requests per minute (creates the interleave between requests)
1732
     * @param boolean $submitCrawlUrls If set, submits the URLs to queue in database (real crawling)
1733
     * @param boolean $downloadCrawlUrls If set (and submitcrawlUrls is false) will fill $downloadUrls with entries)
1734
     * @param array $incomingProcInstructions Array of processing instructions
1735
     * @param array $configurationSelection Array of configuration keys
1736
     * @return string
1737
     */
1738
    public function getPageTreeAndUrls(
1739
        $id,
1740
        $depth,
1741
        $scheduledTime,
1742
        $reqMinute,
1743
        $submitCrawlUrls,
1744
        $downloadCrawlUrls,
1745
        array $incomingProcInstructions,
1746
        array $configurationSelection
1747
    ) {
1748
        global $BACK_PATH;
1749
        global $LANG;
1750
        if (!is_object($LANG)) {
1751
            $LANG = GeneralUtility::makeInstance('language');
1752
            $LANG->init(0);
1753
        }
1754
        $this->scheduledTime = $scheduledTime;
1755
        $this->reqMinute = $reqMinute;
1756
        $this->submitCrawlUrls = $submitCrawlUrls;
1757
        $this->downloadCrawlUrls = $downloadCrawlUrls;
1758
        $this->incomingProcInstructions = $incomingProcInstructions;
1759
        $this->incomingConfigurationSelection = $configurationSelection;
1760
1761
        $this->duplicateTrack = [];
1762
        $this->downloadUrls = [];
1763
1764
        // Drawing tree:
1765
        /* @var PageTreeView $tree */
1766
        $tree = GeneralUtility::makeInstance(PageTreeView::class);
1767
        $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
1768
        $tree->init('AND ' . $perms_clause);
1769
1770
        $pageInfo = BackendUtility::readPageAccess($id, $perms_clause);
1771
        if (is_array($pageInfo)) {
1772
            // Set root row:
1773
            $tree->tree[] = [
1774
                'row' => $pageInfo,
1775
                'HTML' => IconUtility::getIconForRecord('pages', $pageInfo)
1776
            ];
1777
        }
1778
1779
        // Get branch beneath:
1780
        if ($depth) {
1781
            $tree->getTree($id, $depth, '');
1782
        }
1783
1784
        // Traverse page tree:
1785
        $code = '';
1786
1787
        foreach ($tree->tree as $data) {
1788
            $this->MP = false;
1789
1790
            // recognize mount points
1791
            if ($data['row']['doktype'] == 7) {
1792
                $mountpage = $this->db->exec_SELECTgetRows('*', 'pages', 'uid = ' . $data['row']['uid']);
1793
1794
                // fetch mounted pages
1795
                $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...
1796
1797
                $mountTree = GeneralUtility::makeInstance(PageTreeView::class);
1798
                $mountTree->init('AND ' . $perms_clause);
1799
                $mountTree->getTree($mountpage[0]['mount_pid'], $depth, '');
1800
1801
                foreach ($mountTree->tree as $mountData) {
1802
                    $code .= $this->drawURLs_addRowsForPage(
1803
                        $mountData['row'],
1804
                        $mountData['HTML'] . BackendUtility::getRecordTitle('pages', $mountData['row'], true)
1805
                    );
1806
                }
1807
1808
                // replace page when mount_pid_ol is enabled
1809
                if ($mountpage[0]['mount_pid_ol']) {
1810
                    $data['row']['uid'] = $mountpage[0]['mount_pid'];
1811
                } else {
1812
                    // if the mount_pid_ol is not set the MP must not be used for the mountpoint page
1813
                    $this->MP = false;
1814
                }
1815
            }
1816
1817
            $code .= $this->drawURLs_addRowsForPage(
1818
                $data['row'],
1819
                $data['HTML'] . BackendUtility::getRecordTitle('pages', $data['row'], true)
1820
            );
1821
        }
1822
1823
        return $code;
1824
    }
1825
1826
    /**
1827
     * Expands exclude string
1828
     *
1829
     * @param string $excludeString Exclude string
1830
     * @return array
1831
     */
1832 1
    public function expandExcludeString($excludeString)
1833
    {
1834
        // internal static caches;
1835 1
        static $expandedExcludeStringCache;
1836 1
        static $treeCache;
1837
1838 1
        if (empty($expandedExcludeStringCache[$excludeString])) {
1839 1
            $pidList = [];
1840
1841 1
            if (!empty($excludeString)) {
1842
                /** @var PageTreeView $tree */
1843
                $tree = GeneralUtility::makeInstance(PageTreeView::class);
1844
                $tree->init('AND ' . $this->backendUser->getPagePermsClause(1));
1845
1846
                $excludeParts = GeneralUtility::trimExplode(',', $excludeString);
1847
1848
                foreach ($excludeParts as $excludePart) {
1849
                    list($pid, $depth) = GeneralUtility::trimExplode('+', $excludePart);
1850
1851
                    // default is "page only" = "depth=0"
1852
                    if (empty($depth)) {
1853
                        $depth = (stristr($excludePart, '+')) ? 99 : 0;
1854
                    }
1855
1856
                    $pidList[] = $pid;
1857
1858
                    if ($depth > 0) {
1859
                        if (empty($treeCache[$pid][$depth])) {
1860
                            $tree->reset();
1861
                            $tree->getTree($pid, $depth);
1862
                            $treeCache[$pid][$depth] = $tree->tree;
1863
                        }
1864
1865
                        foreach ($treeCache[$pid][$depth] as $data) {
1866
                            $pidList[] = $data['row']['uid'];
1867
                        }
1868
                    }
1869
                }
1870
            }
1871
1872 1
            $expandedExcludeStringCache[$excludeString] = array_unique($pidList);
1873
        }
1874
1875 1
        return $expandedExcludeStringCache[$excludeString];
1876
    }
1877
1878
    /**
1879
     * Create the rows for display of the page tree
1880
     * For each page a number of rows are shown displaying GET variable configuration
1881
     *
1882
     * @param    array        Page row
1883
     * @param    string        Page icon and title for row
1884
     * @return    string        HTML <tr> content (one or more)
1885
     */
1886
    public function drawURLs_addRowsForPage(array $pageRow, $pageTitleAndIcon)
1887
    {
1888
        $skipMessage = '';
1889
1890
        // Get list of configurations
1891
        $configurations = $this->getUrlsForPageRow($pageRow, $skipMessage);
1892
1893
        if (count($this->incomingConfigurationSelection) > 0) {
1894
            // remove configuration that does not match the current selection
1895
            foreach ($configurations as $confKey => $confArray) {
1896
                if (!in_array($confKey, $this->incomingConfigurationSelection)) {
1897
                    unset($configurations[$confKey]);
1898
                }
1899
            }
1900
        }
1901
1902
        // Traverse parameter combinations:
1903
        $c = 0;
1904
        $content = '';
1905
        if (count($configurations)) {
1906
            foreach ($configurations as $confKey => $confArray) {
1907
1908
                    // Title column:
1909
                if (!$c) {
1910
                    $titleClm = '<td rowspan="' . count($configurations) . '">' . $pageTitleAndIcon . '</td>';
1911
                } else {
1912
                    $titleClm = '';
1913
                }
1914
1915
                if (!in_array($pageRow['uid'], $this->expandExcludeString($confArray['subCfg']['exclude']))) {
1916
1917
                        // URL list:
1918
                    $urlList = $this->urlListFromUrlArray(
1919
                        $confArray,
1920
                        $pageRow,
1921
                        $this->scheduledTime,
1922
                        $this->reqMinute,
1923
                        $this->submitCrawlUrls,
1924
                        $this->downloadCrawlUrls,
1925
                        $this->duplicateTrack,
1926
                        $this->downloadUrls,
1927
                        $this->incomingProcInstructions // if empty the urls won't be filtered by processing instructions
1928
                    );
1929
1930
                    // Expanded parameters:
1931
                    $paramExpanded = '';
1932
                    $calcAccu = [];
1933
                    $calcRes = 1;
1934
                    foreach ($confArray['paramExpanded'] as $gVar => $gVal) {
1935
                        $paramExpanded .= '
1936
                            <tr>
1937
                                <td class="bgColor4-20">' . htmlspecialchars('&' . $gVar . '=') . '<br/>' .
1938
                                                '(' . count($gVal) . ')' .
1939
                                                '</td>
1940
                                <td class="bgColor4" nowrap="nowrap">' . nl2br(htmlspecialchars(implode(chr(10), $gVal))) . '</td>
1941
                            </tr>
1942
                        ';
1943
                        $calcRes *= count($gVal);
1944
                        $calcAccu[] = count($gVal);
1945
                    }
1946
                    $paramExpanded = '<table class="lrPadding c-list param-expanded">' . $paramExpanded . '</table>';
1947
                    $paramExpanded .= 'Comb: ' . implode('*', $calcAccu) . '=' . $calcRes;
1948
1949
                    // Options
1950
                    $optionValues = '';
1951
                    if ($confArray['subCfg']['userGroups']) {
1952
                        $optionValues .= 'User Groups: ' . $confArray['subCfg']['userGroups'] . '<br/>';
1953
                    }
1954
                    if ($confArray['subCfg']['baseUrl']) {
1955
                        $optionValues .= 'Base Url: ' . $confArray['subCfg']['baseUrl'] . '<br/>';
1956
                    }
1957
                    if ($confArray['subCfg']['procInstrFilter']) {
1958
                        $optionValues .= 'ProcInstr: ' . $confArray['subCfg']['procInstrFilter'] . '<br/>';
1959
                    }
1960
1961
                    // Compile row:
1962
                    $content .= '
1963
                        <tr class="bgColor' . ($c % 2 ? '-20' : '-10') . '">
1964
                            ' . $titleClm . '
1965
                            <td>' . htmlspecialchars($confKey) . '</td>
1966
                            <td>' . nl2br(htmlspecialchars(rawurldecode(trim(str_replace('&', chr(10) . '&', GeneralUtility::implodeArrayForUrl('', $confArray['paramParsed'])))))) . '</td>
1967
                            <td>' . $paramExpanded . '</td>
1968
                            <td nowrap="nowrap">' . $urlList . '</td>
1969
                            <td nowrap="nowrap">' . $optionValues . '</td>
1970
                            <td nowrap="nowrap">' . DebugUtility::viewArray($confArray['subCfg']['procInstrParams.']) . '</td>
1971
                        </tr>';
1972
                } else {
1973
                    $content .= '<tr class="bgColor' . ($c % 2 ? '-20' : '-10') . '">
1974
                            ' . $titleClm . '
1975
                            <td>' . htmlspecialchars($confKey) . '</td>
1976
                            <td colspan="5"><em>No entries</em> (Page is excluded in this configuration)</td>
1977
                        </tr>';
1978
                }
1979
1980
                $c++;
1981
            }
1982
        } else {
1983
            $message = !empty($skipMessage) ? ' (' . $skipMessage . ')' : '';
1984
1985
            // Compile row:
1986
            $content .= '
1987
                <tr class="bgColor-20" style="border-bottom: 1px solid black;">
1988
                    <td>' . $pageTitleAndIcon . '</td>
1989
                    <td colspan="6"><em>No entries</em>' . $message . '</td>
1990
                </tr>';
1991
        }
1992
1993
        return $content;
1994
    }
1995
1996
    /**
1997
     * @return int
1998
     */
1999 1
    public function getUnprocessedItemsCount()
2000
    {
2001 1
        $res = $this->db->exec_SELECTquery(
2002 1
            'count(*) as num',
2003 1
            'tx_crawler_queue',
2004 1
            'exec_time=0 AND process_scheduled=0 AND scheduled<=' . $this->getCurrentTime()
2005
        );
2006
2007 1
        $count = $this->db->sql_fetch_assoc($res);
2008 1
        return $count['num'];
2009
    }
2010
2011
    /*****************************
2012
     *
2013
     * CLI functions
2014
     *
2015
     *****************************/
2016
2017
    /**
2018
     * Main function for running from Command Line PHP script (cron job)
2019
     * See ext/crawler/cli/crawler_cli.phpsh for details
2020
     *
2021
     * @return int number of remaining items or false if error
2022
     */
2023
    public function CLI_main()
2024
    {
2025
        $this->setAccessMode('cli');
2026
        $result = self::CLI_STATUS_NOTHING_PROCCESSED;
2027
        $cliObj = GeneralUtility::makeInstance(CrawlerCommandLineController::class);
2028
2029
        if (isset($cliObj->cli_args['-h']) || isset($cliObj->cli_args['--help'])) {
2030
            $cliObj->cli_validateArgs();
2031
            $cliObj->cli_help();
2032
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method CLI_main() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2033
        }
2034
2035
        if (!$this->getDisabled() && $this->CLI_checkAndAcquireNewProcess($this->CLI_buildProcessId())) {
2036
            $countInARun = $cliObj->cli_argValue('--countInARun') ? intval($cliObj->cli_argValue('--countInARun')) : $this->extensionSettings['countInARun'];
2037
            // Seconds
2038
            $sleepAfterFinish = $cliObj->cli_argValue('--sleepAfterFinish') ? intval($cliObj->cli_argValue('--sleepAfterFinish')) : $this->extensionSettings['sleepAfterFinish'];
2039
            // Milliseconds
2040
            $sleepTime = $cliObj->cli_argValue('--sleepTime') ? intval($cliObj->cli_argValue('--sleepTime')) : $this->extensionSettings['sleepTime'];
2041
2042
            try {
2043
                // Run process:
2044
                $result = $this->CLI_run($countInARun, $sleepTime, $sleepAfterFinish);
2045
            } catch (\Exception $e) {
2046
                $this->CLI_debug(get_class($e) . ': ' . $e->getMessage());
2047
                $result = self::CLI_STATUS_ABORTED;
2048
            }
2049
2050
            // Cleanup
2051
            $this->db->exec_DELETEquery('tx_crawler_process', 'assigned_items_count = 0');
2052
2053
            //TODO can't we do that in a clean way?
2054
            $releaseStatus = $this->CLI_releaseProcesses($this->CLI_buildProcessId());
0 ignored issues
show
Unused Code introduced by
$releaseStatus is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
2055
2056
            $this->CLI_debug("Unprocessed Items remaining:" . $this->getUnprocessedItemsCount() . " (" . $this->CLI_buildProcessId() . ")");
2057
            $result |= ($this->getUnprocessedItemsCount() > 0 ? self::CLI_STATUS_REMAIN : self::CLI_STATUS_NOTHING_PROCCESSED);
2058
        } else {
2059
            $result |= self::CLI_STATUS_ABORTED;
2060
        }
2061
2062
        return $result;
2063
    }
2064
2065
    /**
2066
     * Function executed by crawler_im.php cli script.
2067
     *
2068
     * @return void
2069
     */
2070
    public function CLI_main_im()
2071
    {
2072
        $this->setAccessMode('cli_im');
2073
2074
        $cliObj = GeneralUtility::makeInstance(QueueCommandLineController::class);
2075
2076
        // Force user to admin state and set workspace to "Live":
2077
        $this->backendUser->user['admin'] = 1;
2078
        $this->backendUser->setWorkspace(0);
2079
2080
        // Print help
2081
        if (!isset($cliObj->cli_args['_DEFAULT'][1])) {
2082
            $cliObj->cli_validateArgs();
2083
            $cliObj->cli_help();
2084
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method CLI_main_im() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2085
        }
2086
2087
        $cliObj->cli_validateArgs();
2088
2089
        if ($cliObj->cli_argValue('-o') === 'exec') {
2090
            $this->registerQueueEntriesInternallyOnly = true;
2091
        }
2092
2093
        if (isset($cliObj->cli_args['_DEFAULT'][2])) {
2094
            // Crawler is called over TYPO3 BE
2095
            $pageId = MathUtility::forceIntegerInRange($cliObj->cli_args['_DEFAULT'][2], 0);
2096
        } else {
2097
            // Crawler is called over cli
2098
            $pageId = MathUtility::forceIntegerInRange($cliObj->cli_args['_DEFAULT'][1], 0);
2099
        }
2100
2101
        $configurationKeys = $this->getConfigurationKeys($cliObj);
2102
2103
        if (!is_array($configurationKeys)) {
2104
            $configurations = $this->getUrlsForPageId($pageId);
2105
            if (is_array($configurations)) {
2106
                $configurationKeys = array_keys($configurations);
2107
            } else {
2108
                $configurationKeys = [];
2109
            }
2110
        }
2111
2112
        if ($cliObj->cli_argValue('-o') === 'queue' || $cliObj->cli_argValue('-o') === 'exec') {
2113
            $reason = new Reason();
2114
            $reason->setReason(Reason::REASON_GUI_SUBMIT);
2115
            $reason->setDetailText('The cli script of the crawler added to the queue');
2116
            EventDispatcher::getInstance()->post(
2117
                'invokeQueueChange',
2118
                $this->setID,
2119
                ['reason' => $reason]
2120
            );
2121
        }
2122
2123
        if ($this->extensionSettings['cleanUpOldQueueEntries']) {
2124
            $this->cleanUpOldQueueEntries();
2125
        }
2126
2127
        $this->setID = (int) GeneralUtility::md5int(microtime());
2128
        $this->getPageTreeAndUrls(
2129
            $pageId,
2130
            MathUtility::forceIntegerInRange($cliObj->cli_argValue('-d'), 0, 99),
2131
            $this->getCurrentTime(),
2132
            MathUtility::forceIntegerInRange($cliObj->cli_isArg('-n') ? $cliObj->cli_argValue('-n') : 30, 1, 1000),
2133
            $cliObj->cli_argValue('-o') === 'queue' || $cliObj->cli_argValue('-o') === 'exec',
2134
            $cliObj->cli_argValue('-o') === 'url',
2135
            GeneralUtility::trimExplode(',', $cliObj->cli_argValue('-proc'), true),
2136
            $configurationKeys
2137
        );
2138
2139
        if ($cliObj->cli_argValue('-o') === 'url') {
2140
            $cliObj->cli_echo(implode(chr(10), $this->downloadUrls) . chr(10), true);
2141
        } elseif ($cliObj->cli_argValue('-o') === 'exec') {
2142
            $cliObj->cli_echo("Executing " . count($this->urlList) . " requests right away:\n\n");
2143
            $cliObj->cli_echo(implode(chr(10), $this->urlList) . chr(10));
2144
            $cliObj->cli_echo("\nProcessing:\n");
2145
2146
            foreach ($this->queueEntries as $queueRec) {
2147
                $p = unserialize($queueRec['parameters']);
2148
                $cliObj->cli_echo($p['url'] . ' (' . implode(',', $p['procInstructions']) . ') => ');
2149
2150
                $result = $this->readUrlFromArray($queueRec);
2151
2152
                $requestResult = unserialize($result['content']);
2153
                if (is_array($requestResult)) {
2154
                    $resLog = is_array($requestResult['log']) ? chr(10) . chr(9) . chr(9) . implode(chr(10) . chr(9) . chr(9), $requestResult['log']) : '';
2155
                    $cliObj->cli_echo('OK: ' . $resLog . chr(10));
2156
                } else {
2157
                    $cliObj->cli_echo('Error checking Crawler Result: ' . substr(preg_replace('/\s+/', ' ', strip_tags($result['content'])), 0, 30000) . '...' . chr(10));
2158
                }
2159
            }
2160
        } elseif ($cliObj->cli_argValue('-o') === 'queue') {
2161
            $cliObj->cli_echo("Putting " . count($this->urlList) . " entries in queue:\n\n");
2162
            $cliObj->cli_echo(implode(chr(10), $this->urlList) . chr(10));
2163
        } else {
2164
            $cliObj->cli_echo(count($this->urlList) . " entries found for processing. (Use -o to decide action):\n\n", true);
2165
            $cliObj->cli_echo(implode(chr(10), $this->urlList) . chr(10), true);
2166
        }
2167
    }
2168
2169
    /**
2170
     * Function executed by crawler_im.php cli script.
2171
     *
2172
     * @return bool
2173
     */
2174
    public function CLI_main_flush()
2175
    {
2176
        $this->setAccessMode('cli_flush');
2177
        $cliObj = GeneralUtility::makeInstance(FlushCommandLineController::class);
2178
2179
        // Force user to admin state and set workspace to "Live":
2180
        $this->backendUser->user['admin'] = 1;
2181
        $this->backendUser->setWorkspace(0);
2182
2183
        // Print help
2184
        if (!isset($cliObj->cli_args['_DEFAULT'][1])) {
2185
            $cliObj->cli_validateArgs();
2186
            $cliObj->cli_help();
2187
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method CLI_main_flush() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2188
        }
2189
2190
        $cliObj->cli_validateArgs();
2191
        $pageId = MathUtility::forceIntegerInRange($cliObj->cli_args['_DEFAULT'][1], 0);
2192
        $fullFlush = ($pageId == 0);
2193
2194
        $mode = $cliObj->cli_argValue('-o');
2195
2196
        switch ($mode) {
2197
            case 'all':
2198
                $result = $this->getLogEntriesForPageId($pageId, '', true, $fullFlush);
2199
                break;
2200
            case 'finished':
2201
            case 'pending':
2202
                $result = $this->getLogEntriesForPageId($pageId, $mode, true, $fullFlush);
2203
                break;
2204
            default:
2205
                $cliObj->cli_validateArgs();
2206
                $cliObj->cli_help();
2207
                $result = false;
2208
        }
2209
2210
        return $result !== false;
2211
    }
2212
2213
    /**
2214
     * Obtains configuration keys from the CLI arguments
2215
     *
2216
     * @param  QueueCommandLineController $cliObj    Command line object
2217
     * @return mixed                        Array of keys or null if no keys found
2218
     */
2219
    protected function getConfigurationKeys(QueueCommandLineController &$cliObj)
2220
    {
2221
        $parameter = trim($cliObj->cli_argValue('-conf'));
2222
        return ($parameter != '' ? GeneralUtility::trimExplode(',', $parameter) : []);
2223
    }
2224
2225
    /**
2226
     * Running the functionality of the CLI (crawling URLs from queue)
2227
     *
2228
     * @param int $countInARun
2229
     * @param int $sleepTime
2230
     * @param int $sleepAfterFinish
2231
     * @return string
2232
     */
2233
    public function CLI_run($countInARun, $sleepTime, $sleepAfterFinish)
2234
    {
2235
        $result = 0;
2236
        $counter = 0;
2237
2238
        // First, run hooks:
2239
        $this->CLI_runHooks();
2240
2241
        // Clean up the queue
2242
        if (intval($this->extensionSettings['purgeQueueDays']) > 0) {
2243
            $purgeDate = $this->getCurrentTime() - 24 * 60 * 60 * intval($this->extensionSettings['purgeQueueDays']);
2244
            $del = $this->db->exec_DELETEquery(
2245
                'tx_crawler_queue',
2246
                'exec_time!=0 AND exec_time<' . $purgeDate
2247
            );
2248
            if (false == $del) {
2249
                GeneralUtility::devLog('Records could not be deleted.', 'crawler', LogLevel::INFO);
2250
            }
2251
        }
2252
2253
        // Select entries:
2254
        //TODO Shouldn't this reside within the transaction?
2255
        $rows = $this->db->exec_SELECTgetRows(
2256
            'qid,scheduled',
2257
            'tx_crawler_queue',
2258
            'exec_time=0
2259
                AND process_scheduled= 0
2260
                AND scheduled<=' . $this->getCurrentTime(),
2261
            '',
2262
            'scheduled, qid',
2263
        intval($countInARun)
2264
        );
2265
2266
        if (count($rows) > 0) {
2267
            $quidList = [];
2268
2269
            foreach ($rows as $r) {
0 ignored issues
show
Bug introduced by
The expression $rows of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2270
                $quidList[] = $r['qid'];
2271
            }
2272
2273
            $processId = $this->CLI_buildProcessId();
2274
2275
            //reserve queue entries for process
2276
            $this->db->sql_query('BEGIN');
2277
            //TODO make sure we're not taking assigned queue-entires
2278
            $this->db->exec_UPDATEquery(
2279
                'tx_crawler_queue',
2280
                'qid IN (' . implode(',', $quidList) . ')',
2281
                [
2282
                    'process_scheduled' => intval($this->getCurrentTime()),
2283
                    'process_id' => $processId
2284
                ]
2285
            );
2286
2287
            //save the number of assigned queue entrys to determine who many have been processed later
2288
            $numberOfAffectedRows = $this->db->sql_affected_rows();
2289
            $this->db->exec_UPDATEquery(
2290
                'tx_crawler_process',
2291
                "process_id = '" . $processId . "'",
2292
                [
2293
                    'assigned_items_count' => intval($numberOfAffectedRows)
2294
                ]
2295
            );
2296
2297
            if ($numberOfAffectedRows == count($quidList)) {
2298
                $this->db->sql_query('COMMIT');
2299
            } else {
2300
                $this->db->sql_query('ROLLBACK');
2301
                $this->CLI_debug("Nothing processed due to multi-process collision (" . $this->CLI_buildProcessId() . ")");
2302
                return ($result | self::CLI_STATUS_ABORTED);
2303
            }
2304
2305
            foreach ($rows as $r) {
0 ignored issues
show
Bug introduced by
The expression $rows of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2306
                $result |= $this->readUrl($r['qid']);
2307
2308
                $counter++;
2309
                usleep(intval($sleepTime)); // Just to relax the system
2310
2311
                // if during the start and the current read url the cli has been disable we need to return from the function
2312
                // mark the process NOT as ended.
2313
                if ($this->getDisabled()) {
2314
                    return ($result | self::CLI_STATUS_ABORTED);
2315
                }
2316
2317
                if (!$this->CLI_checkIfProcessIsActive($this->CLI_buildProcessId())) {
2318
                    $this->CLI_debug("conflict / timeout (" . $this->CLI_buildProcessId() . ")");
2319
2320
                    //TODO might need an additional returncode
2321
                    $result |= self::CLI_STATUS_ABORTED;
2322
                    break; //possible timeout
2323
                }
2324
            }
2325
2326
            sleep(intval($sleepAfterFinish));
2327
2328
            $msg = 'Rows: ' . $counter;
2329
            $this->CLI_debug($msg . " (" . $this->CLI_buildProcessId() . ")");
2330
        } else {
2331
            $this->CLI_debug("Nothing within queue which needs to be processed (" . $this->CLI_buildProcessId() . ")");
2332
        }
2333
2334
        if ($counter > 0) {
2335
            $result |= self::CLI_STATUS_PROCESSED;
2336
        }
2337
2338
        return $result;
2339
    }
2340
2341
    /**
2342
     * Activate hooks
2343
     *
2344
     * @return void
2345
     */
2346
    public function CLI_runHooks()
2347
    {
2348
        global $TYPO3_CONF_VARS;
2349
        if (is_array($TYPO3_CONF_VARS['EXTCONF']['crawler']['cli_hooks'])) {
2350
            foreach ($TYPO3_CONF_VARS['EXTCONF']['crawler']['cli_hooks'] as $objRef) {
2351
                $hookObj = &GeneralUtility::getUserObj($objRef);
2352
                if (is_object($hookObj)) {
2353
                    $hookObj->crawler_init($this);
2354
                }
2355
            }
2356
        }
2357
    }
2358
2359
    /**
2360
     * Try to acquire a new process with the given id
2361
     * also performs some auto-cleanup for orphan processes
2362
     * @todo preemption might not be the most elegant way to clean up
2363
     *
2364
     * @param string $id identification string for the process
2365
     * @return boolean
2366
     */
2367
    public function CLI_checkAndAcquireNewProcess($id)
2368
    {
2369
        $ret = true;
2370
2371
        $systemProcessId = getmypid();
2372
        if ($systemProcessId < 1) {
2373
            return false;
2374
        }
2375
2376
        $processCount = 0;
2377
        $orphanProcesses = [];
2378
2379
        $this->db->sql_query('BEGIN');
2380
2381
        $res = $this->db->exec_SELECTquery(
2382
            'process_id,ttl',
2383
            'tx_crawler_process',
2384
            'active=1 AND deleted=0'
2385
            );
2386
2387
        $currentTime = $this->getCurrentTime();
2388
2389
        while ($row = $this->db->sql_fetch_assoc($res)) {
2390
            if ($row['ttl'] < $currentTime) {
2391
                $orphanProcesses[] = $row['process_id'];
2392
            } else {
2393
                $processCount++;
2394
            }
2395
        }
2396
2397
        // if there are less than allowed active processes then add a new one
2398
        if ($processCount < intval($this->extensionSettings['processLimit'])) {
2399
            $this->CLI_debug("add process " . $this->CLI_buildProcessId() . " (" . ($processCount + 1) . "/" . intval($this->extensionSettings['processLimit']) . ")");
2400
2401
            // create new process record
2402
            $this->db->exec_INSERTquery(
2403
                'tx_crawler_process',
2404
                [
2405
                    'process_id' => $id,
2406
                    'active' => '1',
2407
                    'ttl' => ($currentTime + intval($this->extensionSettings['processMaxRunTime'])),
2408
                    'system_process_id' => $systemProcessId
2409
                ]
2410
                );
2411
        } else {
2412
            $this->CLI_debug("Processlimit reached (" . ($processCount) . "/" . intval($this->extensionSettings['processLimit']) . ")");
2413
            $ret = false;
2414
        }
2415
2416
        $this->CLI_releaseProcesses($orphanProcesses, true); // maybe this should be somehow included into the current lock
2417
        $this->CLI_deleteProcessesMarkedDeleted();
2418
2419
        $this->db->sql_query('COMMIT');
2420
2421
        return $ret;
2422
    }
2423
2424
    /**
2425
     * Release a process and the required resources
2426
     *
2427
     * @param  mixed    $releaseIds   string with a single process-id or array with multiple process-ids
2428
     * @param  boolean  $withinLock   show whether the DB-actions are included within an existing lock
2429
     * @return boolean
2430
     */
2431
    public function CLI_releaseProcesses($releaseIds, $withinLock = false)
2432
    {
2433
        if (!is_array($releaseIds)) {
2434
            $releaseIds = [$releaseIds];
2435
        }
2436
2437
        if (!count($releaseIds) > 0) {
2438
            return false;   //nothing to release
2439
        }
2440
2441
        if (!$withinLock) {
2442
            $this->db->sql_query('BEGIN');
2443
        }
2444
2445
        // some kind of 2nd chance algo - this way you need at least 2 processes to have a real cleanup
2446
        // this ensures that a single process can't mess up the entire process table
2447
2448
        // mark all processes as deleted which have no "waiting" queue-entires and which are not active
2449
        $this->db->exec_UPDATEquery(
2450
            'tx_crawler_queue',
2451
            'process_id IN (SELECT process_id FROM tx_crawler_process WHERE active=0 AND deleted=0)',
2452
            [
2453
                'process_scheduled' => 0,
2454
                'process_id' => ''
2455
            ]
2456
        );
2457
        $this->db->exec_UPDATEquery(
2458
            'tx_crawler_process',
2459
            'active=0 AND deleted=0
2460
            AND NOT EXISTS (
2461
                SELECT * FROM tx_crawler_queue
2462
                WHERE tx_crawler_queue.process_id = tx_crawler_process.process_id
2463
                AND tx_crawler_queue.exec_time = 0
2464
            )',
2465
            [
2466
                'deleted' => '1',
2467
                'system_process_id' => 0
2468
            ]
2469
        );
2470
        // mark all requested processes as non-active
2471
        $this->db->exec_UPDATEquery(
2472
            'tx_crawler_process',
2473
            'process_id IN (\'' . implode('\',\'', $releaseIds) . '\') AND deleted=0',
2474
            [
2475
                'active' => '0'
2476
            ]
2477
        );
2478
        $this->db->exec_UPDATEquery(
2479
            'tx_crawler_queue',
2480
            'exec_time=0 AND process_id IN ("' . implode('","', $releaseIds) . '")',
2481
            [
2482
                'process_scheduled' => 0,
2483
                'process_id' => ''
2484
            ]
2485
        );
2486
2487
        if (!$withinLock) {
2488
            $this->db->sql_query('COMMIT');
2489
        }
2490
2491
        return true;
2492
    }
2493
2494
    /**
2495
     * Delete processes marked as deleted
2496
     *
2497
     * @return void
2498
     */
2499 1
    public function CLI_deleteProcessesMarkedDeleted()
2500
    {
2501 1
        $this->db->exec_DELETEquery('tx_crawler_process', 'deleted = 1');
2502 1
    }
2503
2504
    /**
2505
     * Check if there are still resources left for the process with the given id
2506
     * Used to determine timeouts and to ensure a proper cleanup if there's a timeout
2507
     *
2508
     * @param  string  identification string for the process
2509
     * @return boolean determines if the process is still active / has resources
2510
     *
2511
     * FIXME: Please remove Transaction, not needed as only a select query.
2512
     */
2513
    public function CLI_checkIfProcessIsActive($pid)
2514
    {
2515
        $ret = false;
2516
        $this->db->sql_query('BEGIN');
2517
        $res = $this->db->exec_SELECTquery(
2518
            'process_id,active,ttl',
2519
            'tx_crawler_process',
2520
            'process_id = \'' . $pid . '\'  AND deleted=0',
2521
            '',
2522
            'ttl',
2523
            '0,1'
2524
        );
2525
        if ($row = $this->db->sql_fetch_assoc($res)) {
2526
            $ret = intVal($row['active']) == 1;
2527
        }
2528
        $this->db->sql_query('COMMIT');
2529
2530
        return $ret;
2531
    }
2532
2533
    /**
2534
     * Create a unique Id for the current process
2535
     *
2536
     * @return string  the ID
2537
     */
2538 2
    public function CLI_buildProcessId()
2539
    {
2540 2
        if (!$this->processID) {
2541 1
            $this->processID = GeneralUtility::shortMD5($this->microtime(true));
2542
        }
2543 2
        return $this->processID;
2544
    }
2545
2546
    /**
2547
     * @param bool $get_as_float
2548
     *
2549
     * @return mixed
2550
     */
2551
    protected function microtime($get_as_float = false)
2552
    {
2553
        return microtime($get_as_float);
2554
    }
2555
2556
    /**
2557
     * Prints a message to the stdout (only if debug-mode is enabled)
2558
     *
2559
     * @param  string $msg  the message
2560
     */
2561
    public function CLI_debug($msg)
2562
    {
2563
        if (intval($this->extensionSettings['processDebug'])) {
2564
            echo $msg . "\n";
2565
            flush();
2566
        }
2567
    }
2568
2569
    /**
2570
     * Get URL content by making direct request to TYPO3.
2571
     *
2572
     * @param  string $url          Page URL
2573
     * @param  int    $crawlerId    Crawler-ID
2574
     * @return array
2575
     */
2576 2
    protected function sendDirectRequest($url, $crawlerId)
2577
    {
2578 2
        $parsedUrl = parse_url($url);
2579 2
        if (!is_array($parsedUrl)) {
2580
            return [];
2581
        }
2582
2583 2
        $requestHeaders = $this->buildRequestHeaderArray($parsedUrl, $crawlerId);
2584
2585 2
        $cmd = escapeshellcmd($this->extensionSettings['phpPath']);
2586 2
        $cmd .= ' ';
2587 2
        $cmd .= escapeshellarg(ExtensionManagementUtility::extPath('crawler') . 'cli/bootstrap.php');
2588 2
        $cmd .= ' ';
2589 2
        $cmd .= escapeshellarg($this->getFrontendBasePath());
2590 2
        $cmd .= ' ';
2591 2
        $cmd .= escapeshellarg($url);
2592 2
        $cmd .= ' ';
2593 2
        $cmd .= escapeshellarg(base64_encode(serialize($requestHeaders)));
2594
2595 2
        $startTime = microtime(true);
2596 2
        $content = $this->executeShellCommand($cmd);
2597 2
        $this->log($url . ' ' . (microtime(true) - $startTime));
2598
2599
        $result = [
2600 2
            'request' => implode("\r\n", $requestHeaders) . "\r\n\r\n",
2601 2
            'headers' => '',
2602 2
            'content' => $content
2603
        ];
2604
2605 2
        return $result;
2606
    }
2607
2608
    /**
2609
     * Cleans up entries that stayed for too long in the queue. These are:
2610
     * - processed entries that are over 1.5 days in age
2611
     * - scheduled entries that are over 7 days old
2612
     *
2613
     * @return void
2614
     */
2615
    protected function cleanUpOldQueueEntries()
2616
    {
2617
        $processedAgeInSeconds = $this->extensionSettings['cleanUpProcessedAge'] * 86400; // 24*60*60 Seconds in 24 hours
2618
        $scheduledAgeInSeconds = $this->extensionSettings['cleanUpScheduledAge'] * 86400;
2619
2620
        $now = time();
2621
        $condition = '(exec_time<>0 AND exec_time<' . ($now - $processedAgeInSeconds) . ') OR scheduled<=' . ($now - $scheduledAgeInSeconds);
2622
        $this->flushQueue($condition);
2623
    }
2624
2625
    /**
2626
     * Initializes a TypoScript Frontend necessary for using TypoScript and TypoLink functions
2627
     *
2628
     * @param int $id
2629
     * @param int $typeNum
2630
     *
2631
     * @return void
2632
     */
2633
    protected function initTSFE($id = 1, $typeNum = 0)
2634
    {
2635
        EidUtility::initTCA();
2636
        if (!is_object($GLOBALS['TT'])) {
2637
            $GLOBALS['TT'] = new NullTimeTracker();
2638
            $GLOBALS['TT']->start();
2639
        }
2640
2641
        $GLOBALS['TSFE'] = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $GLOBALS['TYPO3_CONF_VARS'], $id, $typeNum);
2642
        $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class);
2643
        $GLOBALS['TSFE']->sys_page->init(true);
2644
        $GLOBALS['TSFE']->connectToDB();
2645
        $GLOBALS['TSFE']->initFEuser();
2646
        $GLOBALS['TSFE']->determineId();
2647
        $GLOBALS['TSFE']->initTemplate();
2648
        $GLOBALS['TSFE']->rootLine = $GLOBALS['TSFE']->sys_page->getRootLine($id, '');
2649
        $GLOBALS['TSFE']->getConfigArray();
2650
        PageGenerator::pagegenInit();
2651
    }
2652
2653
    /**
2654
     * Returns a md5 hash generated from a serialized configuration array.
2655
     *
2656
     * @param array $configuration
2657
     *
2658
     * @return string
2659
     */
2660 9
    protected function getConfigurationHash(array $configuration) {
2661 9
        unset($configuration['paramExpanded']);
2662 9
        unset($configuration['URLs']);
2663 9
        return md5(serialize($configuration));
2664
    }
2665
}
2666