Completed
Push — master ( 5da019...46d695 )
by
unknown
19:03
created

ValidatorTask::getPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Linkvalidator\Task;
19
20
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
21
use Symfony\Component\Mime\Address;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
24
use TYPO3\CMS\Core\Mail\FluidEmail;
25
use TYPO3\CMS\Core\Mail\Mailer;
26
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
27
use TYPO3\CMS\Core\Utility\ArrayUtility;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\MailUtility;
30
use TYPO3\CMS\Fluid\View\TemplatePaths;
31
use TYPO3\CMS\Linkvalidator\Event\ModifyValidatorTaskEmailEvent;
32
use TYPO3\CMS\Linkvalidator\Result\LinkAnalyzerResult;
33
use TYPO3\CMS\Scheduler\Task\AbstractTask;
34
35
/**
36
 * This class provides Scheduler plugin implementation
37
 * @internal This class is a specific Scheduler task implementation and is not part of the TYPO3's Core API.
38
 */
39
class ValidatorTask extends AbstractTask
40
{
41
    /**
42
     * @var int
43
     */
44
    protected $sleepTime;
45
46
    /**
47
     * @var int
48
     */
49
    protected $sleepAfterFinish;
50
51
    /**
52
     * @var int
53
     */
54
    protected $countInARun;
55
56
    /**
57
     * Specific TSconfig for this task.
58
     *
59
     * @var string
60
     */
61
    protected $configuration = '';
62
63
    /**
64
     * Template name to be used for the email
65
     *
66
     * @var string
67
     */
68
    protected $emailTemplateName = '';
69
70
    /**
71
     * Level of pages the task should check
72
     *
73
     * @var int
74
     */
75
    protected $depth = 0;
76
77
    /**
78
     * UID of the start page for this task
79
     *
80
     * @var int
81
     */
82
    protected $page = 0;
83
84
    /**
85
     * Languages to check for broken links
86
     *
87
     * @var string
88
     */
89
    protected $languages = '';
90
91
    /**
92
     * Email address to which an email report is sent
93
     *
94
     * @var string
95
     */
96
    protected $email = '';
97
98
    /**
99
     * Only send an email, if new broken links were found
100
     *
101
     * @var bool
102
     */
103
    protected $emailOnBrokenLinkOnly = true;
104
105
    /**
106
     * Default language file of the extension linkvalidator
107
     *
108
     * @var string
109
     */
110
    protected $languageFile = 'LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf';
111
112
    /**
113
     * Merged mod TSconfig
114
     *
115
     * @var array
116
     */
117
    protected $modTSconfig = [];
118
119
    /**
120
     * Defines if the task should be updated as some values have changed during task execution
121
     *
122
     * @var bool
123
     */
124
    protected $taskNeedsUpdate = false;
125
126
    /**
127
     * Get the value of the protected property email
128
     *
129
     * @return string Email address to which an email report is sent
130
     */
131
    public function getEmail(): string
132
    {
133
        return $this->email;
134
    }
135
136
    /**
137
     * Set the value of the private property email.
138
     *
139
     * @param string $email Email address to which an email report is sent
140
     * @return ValidatorTask
141
     */
142
    public function setEmail(string $email): self
143
    {
144
        $this->email = $email;
145
        return $this;
146
    }
147
148
    /**
149
     * Get the value of the protected property emailOnBrokenLinkOnly
150
     *
151
     * @todo type cast needed for backwards compatibility - should be removed in v12
152
     * @return bool Whether to send an email, if new broken links were found
153
     */
154
    public function getEmailOnBrokenLinkOnly(): bool
155
    {
156
        return (bool)$this->emailOnBrokenLinkOnly;
157
    }
158
159
    /**
160
     * Set the value of the private property emailOnBrokenLinkOnly
161
     *
162
     * @param bool $emailOnBrokenLinkOnly Only send an email, if new broken links were found
163
     * @return ValidatorTask
164
     */
165
    public function setEmailOnBrokenLinkOnly(bool $emailOnBrokenLinkOnly): self
166
    {
167
        $this->emailOnBrokenLinkOnly = $emailOnBrokenLinkOnly;
168
        return $this;
169
    }
170
171
    /**
172
     * Get the value of the protected property page
173
     *
174
     * @todo type cast needed for backwards compatibility - should be removed in v12
175
     * @return int UID of the start page for this task
176
     */
177
    public function getPage(): int
178
    {
179
        return (int)$this->page;
180
    }
181
182
    /**
183
     * Set the value of the private property page
184
     *
185
     * @param int $page UID of the start page for this task.
186
     * @return ValidatorTask
187
     */
188
    public function setPage(int $page): self
189
    {
190
        $this->page = $page;
191
        return $this;
192
    }
193
194
    /**
195
     * Get the value of the protected property languages
196
     *
197
     * @return string Languages to fetch broken links
198
     */
199
    public function getLanguages(): string
200
    {
201
        return $this->languages;
202
    }
203
204
    /**
205
     * Set the value of the private property languages
206
     *
207
     * @param string $languages Languages to fetch broken links
208
     * @return ValidatorTask
209
     */
210
    public function setLanguages(string $languages): self
211
    {
212
        $this->languages = $languages;
213
        return $this;
214
    }
215
216
    /**
217
     * Get the value of the protected property depth
218
     *
219
     * @todo type cast needed for backwards compatibility - should be removed in v12
220
     * @return int Level of pages the task should check
221
     */
222
    public function getDepth(): int
223
    {
224
        return (int)$this->depth;
225
    }
226
227
    /**
228
     * Set the value of the private property depth
229
     *
230
     * @param int $depth Level of pages the task should check
231
     * @return ValidatorTask
232
     */
233
    public function setDepth(int $depth): self
234
    {
235
        $this->depth = $depth;
236
        return $this;
237
    }
238
239
    /**
240
     * Get the value of the protected property emailTemplateName
241
     *
242
     * @return string Template name to be used for the email
243
     */
244
    public function getEmailTemplateName(): string
245
    {
246
        return $this->emailTemplateName;
247
    }
248
249
    /**
250
     * Set the value of the private property emailTemplateName
251
     *
252
     * @param string $emailTemplateName Template name to be used for the email
253
     * @return ValidatorTask
254
     */
255
    public function setEmailTemplateName(string $emailTemplateName): self
256
    {
257
        $this->emailTemplateName = $emailTemplateName;
258
        return $this;
259
    }
260
261
    /**
262
     * Get the value of the protected property configuration
263
     *
264
     * @return string specific TSconfig for this task
265
     */
266
    public function getConfiguration(): string
267
    {
268
        return $this->configuration;
269
    }
270
271
    /**
272
     * Set the value of the private property configuration
273
     *
274
     * @param string $configuration specific TSconfig for this task
275
     * @return ValidatorTask
276
     */
277
    public function setConfiguration(string $configuration): self
278
    {
279
        $this->configuration = $configuration;
280
        return $this;
281
    }
282
283
    /**
284
     * Function execute from the Scheduler
285
     *
286
     * @return bool TRUE on successful execution, FALSE on error
287
     */
288
    public function execute(): bool
289
    {
290
        // @todo type cast needed for backwards compatibility - should be removed in v12
291
        $this->page = (int)$this->page;
292
        $this->depth = (int)$this->depth;
293
        $this->emailOnBrokenLinkOnly = (bool)$this->emailOnBrokenLinkOnly;
294
295
        if ($this->page === 0) {
296
            return false;
297
        }
298
299
        $this
300
            ->setCliArguments()
301
            ->loadModTSconfig();
302
303
        $successfullyExecuted = true;
304
        $linkAnalyzerResult = $this->getLinkAnalyzerResult();
305
306
        if ($linkAnalyzerResult->getTotalBrokenLinksCount() > 0
307
            && (!$this->emailOnBrokenLinkOnly || $linkAnalyzerResult->isDifferentToLastResult())
308
        ) {
309
            $successfullyExecuted = $this->reportEmail($linkAnalyzerResult);
310
        }
311
312
        if ($this->taskNeedsUpdate) {
313
            $this->taskNeedsUpdate = false;
314
            $this->save();
315
        }
316
317
        return $successfullyExecuted;
318
    }
319
320
    /**
321
     * Validate all broken links for pages set in the task configuration
322
     * and return the analyzers result as object.
323
     *
324
     * @return LinkAnalyzerResult
325
     */
326
    protected function getLinkAnalyzerResult(): LinkAnalyzerResult
327
    {
328
        $pageRow = BackendUtility::getRecord('pages', $this->page, '*', '', false);
329
        if ($pageRow === null) {
330
            throw new \InvalidArgumentException(
331
                sprintf($this->getLanguageService()->sL($this->languageFile . ':tasks.error.invalidPageUid'), $this->page),
332
                1502800555
333
            );
334
        }
335
336
        return GeneralUtility::makeInstance(LinkAnalyzerResult::class)
337
            ->getResultForTask(
338
                $this->page,
339
                $this->depth,
340
                $pageRow,
341
                $this->modTSconfig,
342
                $this->getSearchField(),
343
                $this->getLinkTypes(),
344
                $this->languages
345
            );
346
    }
347
348
    /**
349
     * Load and merge linkvalidator TSconfig from task configuration with page TSconfig
350
     *
351
     * @return ValidatorTask
352
     */
353
    protected function loadModTSconfig(): self
354
    {
355
        $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
356
        $parseObj->parse($this->configuration);
357
        if (!empty($parseObj->errors)) {
358
            $parseErrorMessage = $this->getLanguageService()->sL($this->languageFile . ':tasks.error.invalidTSconfig') . '<br />';
359
            foreach ($parseObj->errors as $errorInfo) {
360
                $parseErrorMessage .= $errorInfo[0] . '<br />';
361
            }
362
            throw new \Exception($parseErrorMessage, 1295476989);
363
        }
364
        $modTs = BackendUtility::getPagesTSconfig($this->page)['mod.']['linkvalidator.'] ?? [];
365
        $overrideTs = $parseObj->setup['mod.']['linkvalidator.'] ?? [];
366
        if (is_array($overrideTs) && $overrideTs !== []) {
367
            ArrayUtility::mergeRecursiveWithOverrule($modTs, $overrideTs);
368
        }
369
        if (empty($modTs['mail.']['fromemail'])) {
370
            $modTs['mail.']['fromemail'] = MailUtility::getSystemFromAddress();
371
        }
372
        if (empty($modTs['mail.']['fromname'])) {
373
            $modTs['mail.']['fromname'] = MailUtility::getSystemFromName();
374
        }
375
376
        $this->modTSconfig = $modTs;
377
378
        return $this;
379
    }
380
381
    /**
382
     * Get the list of fields to consider for fetching broken links
383
     *
384
     * @return array $searchFields List of search fields
385
     */
386
    protected function getSearchField(): array
387
    {
388
        $searchFields = [];
389
        foreach ($this->modTSconfig['searchFields.'] as $table => $fieldList) {
390
            $fields = GeneralUtility::trimExplode(',', $fieldList);
391
            foreach ($fields as $field) {
392
                $searchFields[$table][] = $field;
393
            }
394
        }
395
        return $searchFields;
396
    }
397
398
    /**
399
     * Get the list of linkTypes to consider for fetching broken links
400
     *
401
     * @return array<int, string> $linkTypes list of link types
402
     */
403
    protected function getLinkTypes(): array
404
    {
405
        $linkTypes = [];
406
        $typesTmp = GeneralUtility::trimExplode(',', $this->modTSconfig['linktypes'], true);
407
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $type => $value) {
408
            if (in_array($type, $typesTmp, true)) {
409
                $linkTypes[$type] = 1;
410
            }
411
        }
412
        return $linkTypes;
413
    }
414
415
    /**
416
     * Build and send report email when broken links were found
417
     *
418
     * @param LinkAnalyzerResult $linkAnalyzerResult
419
     * @return bool TRUE if mail was sent, FALSE if or not
420
     */
421
    protected function reportEmail(LinkAnalyzerResult $linkAnalyzerResult): bool
422
    {
423
        $lang = $this->getLanguageService();
424
        $fluidEmail = $this->getFluidEmail();
425
426
        // Initialize and call the event
427
        $validatorTaskEmailEvent = new ModifyValidatorTaskEmailEvent($linkAnalyzerResult, $fluidEmail, $this->modTSconfig);
428
        GeneralUtility::makeInstance(EventDispatcher::class)->dispatch($validatorTaskEmailEvent);
429
        $fluidEmail->assign('linkAnalyzerResult', $linkAnalyzerResult);
430
431
        if (!empty($this->modTSconfig['mail.']['subject']) && $fluidEmail->getSubject() === null) {
432
            $fluidEmail->subject($this->modTSconfig['mail.']['subject']);
433
        }
434
435
        if ($fluidEmail->getFrom() === []) {
436
            if (GeneralUtility::validEmail($this->modTSconfig['mail.']['fromemail'] ?? '')) {
437
                $fluidEmail->from(
438
                    new Address($this->modTSconfig['mail.']['fromemail'], $this->modTSconfig['mail.']['fromname'] ?? '')
439
                );
440
            } else {
441
                throw new \Exception($lang->sL($this->languageFile . ':tasks.error.invalidFromEmail'), 1295476760);
442
            }
443
        }
444
445
        if ($this->email !== '') {
446
            $validEmailList = [];
447
448
            if (strpos($this->email, ',') !== false) {
449
                $emailList = GeneralUtility::trimExplode(',', $this->email, true);
450
                $this->email = implode(LF, $emailList);
451
                $this->taskNeedsUpdate = true;
452
            } else {
453
                $emailList = GeneralUtility::trimExplode(LF, $this->email, true);
454
            }
455
456
            foreach ($emailList as $email) {
457
                if (GeneralUtility::validEmail($email)) {
458
                    $validEmailList[] = $email;
459
                    continue;
460
                }
461
                throw new \Exception($lang->sL($this->languageFile . ':tasks.error.invalidToEmail'), 1295476821);
462
            }
463
464
            $fluidEmail->addTo(...$validEmailList);
465
        }
466
467
        if ($fluidEmail->getTo() === []) {
468
            throw new \Exception($lang->sL($this->languageFile . ':tasks.error.emptyToEmail'), 1599724418);
469
        }
470
471
        if ($fluidEmail->getReplyTo() === [] &&
472
            GeneralUtility::validEmail($this->modTSconfig['mail.']['replytoemail'] ?? '')
473
        ) {
474
            $fluidEmail->replyTo(
475
                new Address($this->modTSconfig['mail.']['replytoemail'], $this->modTSconfig['mail.']['replytoname'] ?? '')
476
            );
477
        }
478
479
        try {
480
            GeneralUtility::makeInstance(Mailer::class)->send($fluidEmail);
481
        } catch (TransportExceptionInterface $e) {
482
            return false;
483
        }
484
485
        return true;
486
    }
487
488
    /**
489
     * Returns the most important properties of the link validator task as a
490
     * comma separated string that will be displayed in the scheduler module.
491
     *
492
     * @return string
493
     */
494
    public function getAdditionalInformation(): string
495
    {
496
        $additionalInformation = [];
497
        $pageLabel = $this->page;
498
        if ($this->page) {
499
            $pageData = BackendUtility::getRecord('pages', $this->page);
500
            if (!empty($pageData)) {
501
                $pageTitle = BackendUtility::getRecordTitle('pages', $pageData);
502
                $pageLabel = $pageTitle . ' (' . $this->page . ')';
503
            }
504
        }
505
        $lang = $this->getLanguageService();
506
        $depth = $this->depth;
507
        $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.page') . ': ' . $pageLabel;
508
        $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.depth') . ': '
509
            . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_' . ($depth === 999 ? 'infi' : $depth));
510
        $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.email') . ': '
511
            . $this->getEmail();
512
513
        return implode(', ', $additionalInformation);
514
    }
515
516
    /**
517
     * Simulate cli call with setting the required options to the $_SERVER['argv']
518
     *
519
     * @return ValidatorTask
520
     */
521
    protected function setCliArguments(): self
522
    {
523
        $_SERVER['argv'] = [
524
            $_SERVER['argv'][0],
525
            'tx_link_scheduler_link',
526
            '0',
527
            '-ss',
528
            '--sleepTime',
529
            $this->sleepTime,
530
            '--sleepAfterFinish',
531
            $this->sleepAfterFinish,
532
            '--countInARun',
533
            $this->countInARun
534
        ];
535
536
        return $this;
537
    }
538
539
    /**
540
     * Get FluidEmail with template from the task configuration
541
     *
542
     * @return FluidEmail
543
     */
544
    protected function getFluidEmail(): FluidEmail
545
    {
546
        $templateConfiguration = array_replace_recursive(
547
            $GLOBALS['TYPO3_CONF_VARS']['MAIL'],
548
            ['templateRootPaths' => [20 => 'EXT:linkvalidator/Resources/Private/Templates/Email/']]
549
        );
550
551
        // must be sorted after adding the default path to ensure already registered custom paths are called first
552
        ksort($templateConfiguration['templateRootPaths']);
553
        $templatePaths = GeneralUtility::makeInstance(TemplatePaths::class, $templateConfiguration);
554
555
        if ($this->emailTemplateName === '' || !$this->templateFilesExist($templatePaths->getTemplateRootPaths())) {
556
            // Add default template name to task if empty or given template name does not exist
557
            $this->emailTemplateName = 'ValidatorTask';
558
            $this->taskNeedsUpdate = true;
559
            $this->logger->notice(
560
                $this->getLanguageService()->sL($this->languageFile . ':tasks.notice.useDefaultTemplate')
561
            );
562
        }
563
564
        $fluidEmail = GeneralUtility::makeInstance(FluidEmail::class, $templatePaths);
565
        $fluidEmail->setTemplate($this->emailTemplateName);
566
567
        return $fluidEmail;
568
    }
569
570
    /**
571
     * Check if both template files (html and txt) exist under at least one template path
572
     *
573
     * @param array $templatePaths
574
     * @return bool
575
     */
576
    protected function templateFilesExist(array $templatePaths): bool
577
    {
578
        foreach ($templatePaths as $templatePath) {
579
            if (file_exists($templatePath . $this->emailTemplateName . '.html')
580
                && file_exists($templatePath . $this->emailTemplateName . '.txt')
581
            ) {
582
                return true;
583
            }
584
        }
585
        return false;
586
    }
587
}
588