[TASK] Use null coalescing operator where possible
[Packages/TYPO3.CMS.git] / typo3 / sysext / linkvalidator / Classes / Task / ValidatorTask.php
1 <?php
2 namespace TYPO3\CMS\Linkvalidator\Task;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Localization\LanguageService;
19 use TYPO3\CMS\Core\Mail\MailMessage;
20 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
21 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
22 use TYPO3\CMS\Core\Utility\ArrayUtility;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\MailUtility;
25 use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
26 use TYPO3\CMS\Scheduler\Task\AbstractTask;
27
28 /**
29 * This class provides Scheduler plugin implementation
30 */
31 class ValidatorTask extends AbstractTask
32 {
33 /**
34 * @var int
35 */
36 protected $sleepTime;
37
38 /**
39 * @var int
40 */
41 protected $sleepAfterFinish;
42
43 /**
44 * @var int
45 */
46 protected $countInARun;
47
48 /**
49 * Total number of broken links
50 *
51 * @var int
52 */
53 protected $totalBrokenLink = 0;
54
55 /**
56 * Total number of broken links from the last run
57 *
58 * @var int
59 */
60 protected $oldTotalBrokenLink = 0;
61
62 /**
63 * Mail template fetched from the given template file
64 *
65 * @var string
66 */
67 protected $templateMail;
68
69 /**
70 * specific TSconfig for this task.
71 *
72 * @var array
73 */
74 protected $configuration = [];
75
76 /**
77 * Shows if number of result was different from the result of the last check
78 *
79 * @var bool
80 */
81 protected $isDifferentToLastRun;
82
83 /**
84 * Template to be used for the email
85 *
86 * @var string
87 */
88 protected $emailTemplateFile;
89
90 /**
91 * Level of pages the task should check
92 *
93 * @var int
94 */
95 protected $depth;
96
97 /**
98 * UID of the start page for this task
99 *
100 * @var int
101 */
102 protected $page;
103
104 /**
105 * Email address to which an email report is sent
106 *
107 * @var string
108 */
109 protected $email;
110
111 /**
112 * Only send an email, if new broken links were found
113 *
114 * @var bool
115 */
116 protected $emailOnBrokenLinkOnly;
117
118 /**
119 * @var MarkerBasedTemplateService
120 */
121 protected $templateService;
122
123 /**
124 * Default language file of the extension linkvalidator
125 *
126 * @var string
127 */
128 protected $languageFile = 'LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf';
129
130 /**
131 * Get the value of the protected property email
132 *
133 * @return string Email address to which an email report is sent
134 */
135 public function getEmail()
136 {
137 return $this->email;
138 }
139
140 /**
141 * Set the value of the private property email.
142 *
143 * @param string $email Email address to which an email report is sent
144 */
145 public function setEmail($email)
146 {
147 $this->email = $email;
148 }
149
150 /**
151 * Get the value of the protected property emailOnBrokenLinkOnly
152 *
153 * @return bool Whether to send an email, if new broken links were found
154 */
155 public function getEmailOnBrokenLinkOnly()
156 {
157 return $this->emailOnBrokenLinkOnly;
158 }
159
160 /**
161 * Set the value of the private property emailOnBrokenLinkOnly
162 *
163 * @param bool $emailOnBrokenLinkOnly Only send an email, if new broken links were found
164 */
165 public function setEmailOnBrokenLinkOnly($emailOnBrokenLinkOnly)
166 {
167 $this->emailOnBrokenLinkOnly = $emailOnBrokenLinkOnly;
168 }
169
170 /**
171 * Get the value of the protected property page
172 *
173 * @return int UID of the start page for this task
174 */
175 public function getPage()
176 {
177 return $this->page;
178 }
179
180 /**
181 * Set the value of the private property page
182 *
183 * @param int $page UID of the start page for this task.
184 */
185 public function setPage($page)
186 {
187 $this->page = $page;
188 }
189
190 /**
191 * Get the value of the protected property depth
192 *
193 * @return int Level of pages the task should check
194 */
195 public function getDepth()
196 {
197 return $this->depth;
198 }
199
200 /**
201 * Set the value of the private property depth
202 *
203 * @param int $depth Level of pages the task should check
204 */
205 public function setDepth($depth)
206 {
207 $this->depth = $depth;
208 }
209
210 /**
211 * Get the value of the protected property emailTemplateFile
212 *
213 * @return string Template to be used for the email
214 */
215 public function getEmailTemplateFile()
216 {
217 return $this->emailTemplateFile;
218 }
219
220 /**
221 * Set the value of the private property emailTemplateFile
222 *
223 * @param string $emailTemplateFile Template to be used for the email
224 */
225 public function setEmailTemplateFile($emailTemplateFile)
226 {
227 $this->emailTemplateFile = $emailTemplateFile;
228 }
229
230 /**
231 * Get the value of the protected property configuration
232 *
233 * @return array specific TSconfig for this task
234 */
235 public function getConfiguration()
236 {
237 return $this->configuration;
238 }
239
240 /**
241 * Set the value of the private property configuration
242 *
243 * @param array $configuration specific TSconfig for this task
244 */
245 public function setConfiguration($configuration)
246 {
247 $this->configuration = $configuration;
248 }
249
250 /**
251 * Function execute from the Scheduler
252 *
253 * @return bool TRUE on successful execution, FALSE on error
254 * @throws \InvalidArgumentException if the email template file can not be read
255 */
256 public function execute()
257 {
258 $this->setCliArguments();
259 $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
260 $successfullyExecuted = true;
261 if (!file_exists(($file = GeneralUtility::getFileAbsFileName($this->emailTemplateFile)))
262 && !empty($this->email)
263 ) {
264 if ($this->emailTemplateFile === 'EXT:linkvalidator/res/mailtemplate.html') {
265 // Update the default email template file path
266 $this->emailTemplateFile = 'EXT:linkvalidator/Resources/Private/Templates/mailtemplate.html';
267 $this->save();
268 } else {
269 $lang = $this->getLanguageService();
270 throw new \InvalidArgumentException(
271 $lang->sL($this->languageFile . ':tasks.error.invalidEmailTemplateFile'),
272 '1295476972'
273 );
274 }
275 }
276 $htmlFile = file_get_contents($file);
277 $this->templateMail = $this->templateService->getSubpart($htmlFile, '###REPORT_TEMPLATE###');
278 // The array to put the content into
279 $pageSections = '';
280 $this->isDifferentToLastRun = false;
281 $pageList = GeneralUtility::trimExplode(',', $this->page, true);
282 $modTs = $this->loadModTsConfig($this->page);
283 if (is_array($pageList)) {
284 // reset broken link counts as they were stored in the serialized object
285 $this->oldTotalBrokenLink = 0;
286 $this->totalBrokenLink = 0;
287 foreach ($pageList as $page) {
288 $pageSections .= $this->checkPageLinks($page);
289 }
290 }
291 if ($this->totalBrokenLink != $this->oldTotalBrokenLink) {
292 $this->isDifferentToLastRun = true;
293 }
294 if ($this->totalBrokenLink > 0
295 && (!$this->emailOnBrokenLinkOnly || $this->isDifferentToLastRun)
296 && !empty($this->email)
297 ) {
298 $successfullyExecuted = $this->reportEmail($pageSections, $modTs);
299 }
300 return $successfullyExecuted;
301 }
302
303 /**
304 * Validate all links for a page based on the task configuration
305 *
306 * @param int $page Uid of the page to parse
307 * @return string $pageSections Content of page section
308 * @throws \InvalidArgumentException
309 */
310 protected function checkPageLinks($page)
311 {
312 $page = (int)$page;
313 $pageSections = '';
314 $pageIds = '';
315 $oldLinkCounts = [];
316 $modTs = $this->loadModTsConfig($page);
317 $searchFields = $this->getSearchField($modTs);
318 $linkTypes = $this->getLinkTypes($modTs);
319 /** @var $processor LinkAnalyzer */
320 $processor = GeneralUtility::makeInstance(LinkAnalyzer::class);
321 if ($page === 0) {
322 $rootLineHidden = false;
323 } else {
324 $pageRow = BackendUtility::getRecord('pages', $page, '*', '', false);
325 if ($pageRow === null) {
326 throw new \InvalidArgumentException(
327 sprintf($this->getLanguageService()->sL($this->languageFile . ':tasks.error.invalidPageUid'), $page),
328 1502800555
329 );
330 }
331 $rootLineHidden = $processor->getRootLineIsHidden($pageRow);
332 }
333 if (!$rootLineHidden || $modTs['checkhidden'] == 1) {
334 $pageIds = $processor->extGetTreeList($page, $this->depth, 0, '1=1', $modTs['checkhidden']);
335 if (isset($pageRow) && $pageRow['hidden'] == 0 || $modTs['checkhidden'] == 1) {
336 // \TYPO3\CMS\Linkvalidator\LinkAnalyzer->extGetTreeList() always adds trailing comma
337 $pageIds .= $page;
338 }
339 }
340 if (!empty($pageIds)) {
341 $processor->init($searchFields, $pageIds, $modTs);
342 if (!empty($this->email)) {
343 $oldLinkCounts = $processor->getLinkCounts($page);
344 $this->oldTotalBrokenLink += $oldLinkCounts['brokenlinkCount'];
345 }
346 $processor->getLinkStatistics($linkTypes, $modTs['checkhidden']);
347 if (!empty($this->email)) {
348 $linkCounts = $processor->getLinkCounts($page);
349 $this->totalBrokenLink += $linkCounts['brokenlinkCount'];
350 $pageSections = $this->buildMail($page, $pageIds, $linkCounts, $oldLinkCounts);
351 }
352 }
353 return $pageSections;
354 }
355
356 /**
357 * Get the linkvalidator modTSconfig for a page
358 *
359 * @param int $page Uid of the page
360 * @return array $modTsConfig mod.linkvalidator TSconfig array
361 * @throws \Exception
362 */
363 protected function loadModTsConfig($page)
364 {
365 $modTs = BackendUtility::getModTSconfig($page, 'mod.linkvalidator');
366 $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
367 $parseObj->parse($this->configuration);
368 if (!empty($parseObj->errors)) {
369 $languageService = $this->getLanguageService();
370 $parseErrorMessage = $languageService->sL($this->languageFile . ':tasks.error.invalidTSconfig')
371 . '<br />';
372 foreach ($parseObj->errors as $errorInfo) {
373 $parseErrorMessage .= $errorInfo[0] . '<br />';
374 }
375 throw new \Exception($parseErrorMessage, '1295476989');
376 }
377 $tsConfig = $parseObj->setup;
378 $modTs = $modTs['properties'];
379 $overrideTs = $tsConfig['mod.']['linkvalidator.'];
380 if (is_array($overrideTs)) {
381 ArrayUtility::mergeRecursiveWithOverrule($modTs, $overrideTs);
382 }
383 return $modTs;
384 }
385
386 /**
387 * Get the list of fields to parse in modTSconfig
388 *
389 * @param array $modTS mod.linkvalidator TSconfig array
390 * @return array $searchFields List of fields
391 */
392 protected function getSearchField(array $modTS)
393 {
394 // Get the searchFields from TypoScript
395 foreach ($modTS['searchFields.'] as $table => $fieldList) {
396 $fields = GeneralUtility::trimExplode(',', $fieldList);
397 foreach ($fields as $field) {
398 $searchFields[$table][] = $field;
399 }
400 }
401 return $searchFields ?? [];
402 }
403
404 /**
405 * Get the list of linkTypes to parse in modTSconfig
406 *
407 * @param array $modTS mod.linkvalidator TSconfig array
408 * @return array $linkTypes list of link types
409 */
410 protected function getLinkTypes(array $modTS)
411 {
412 $linkTypes = [];
413 $typesTmp = GeneralUtility::trimExplode(',', $modTS['linktypes'], true);
414 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $type => $value) {
415 if (in_array($type, $typesTmp)) {
416 $linkTypes[$type] = 1;
417 }
418 }
419 return $linkTypes;
420 }
421
422 /**
423 * Build and send warning email when new broken links were found
424 *
425 * @param string $pageSections Content of page section
426 * @param array $modTsConfig TSconfig array
427 * @return bool TRUE if mail was sent, FALSE if or not
428 * @throws \Exception if required modTsConfig settings are missing
429 */
430 protected function reportEmail($pageSections, array $modTsConfig)
431 {
432 $lang = $this->getLanguageService();
433 $content = $this->templateService->substituteSubpart($this->templateMail, '###PAGE_SECTION###', $pageSections);
434 /** @var array $markerArray */
435 $markerArray = [];
436 /** @var array $validEmailList */
437 $validEmailList = [];
438 /** @var bool $sendEmail */
439 $sendEmail = true;
440 $markerArray['totalBrokenLink'] = $this->totalBrokenLink;
441 $markerArray['totalBrokenLink_old'] = $this->oldTotalBrokenLink;
442
443 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['reportEmailMarkers'] ?? [] as $userFunc) {
444 $params = [
445 'pObj' => &$this,
446 'markerArray' => $markerArray
447 ];
448 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
449 if (is_array($newMarkers)) {
450 $markerArray = $newMarkers + $markerArray;
451 }
452 unset($params);
453 }
454 $content = $this->templateService->substituteMarkerArray($content, $markerArray, '###|###', true, true);
455 /** @var $mail MailMessage */
456 $mail = GeneralUtility::makeInstance(MailMessage::class);
457 if (empty($modTsConfig['mail.']['fromemail'])) {
458 $modTsConfig['mail.']['fromemail'] = MailUtility::getSystemFromAddress();
459 }
460 if (empty($modTsConfig['mail.']['fromname'])) {
461 $modTsConfig['mail.']['fromname'] = MailUtility::getSystemFromName();
462 }
463 if (GeneralUtility::validEmail($modTsConfig['mail.']['fromemail'])) {
464 $mail->setFrom([$modTsConfig['mail.']['fromemail'] => $modTsConfig['mail.']['fromname']]);
465 } else {
466 throw new \Exception(
467 $lang->sL($this->languageFile . ':tasks.error.invalidFromEmail'),
468 '1295476760'
469 );
470 }
471 if (GeneralUtility::validEmail($modTsConfig['mail.']['replytoemail'])) {
472 $mail->setReplyTo([$modTsConfig['mail.']['replytoemail'] => $modTsConfig['mail.']['replytoname']]);
473 }
474 if (!empty($modTsConfig['mail.']['subject'])) {
475 $mail->setSubject($modTsConfig['mail.']['subject']);
476 } else {
477 throw new \Exception(
478 $lang->sL($this->languageFile . ':tasks.error.noSubject'),
479 '1295476808'
480 );
481 }
482 if (!empty($this->email)) {
483 // Check if old input field value is still there and save the value a
484 if (strpos($this->email, ',') !== false) {
485 $emailList = GeneralUtility::trimExplode(',', $this->email, true);
486 $this->email = implode(LF, $emailList);
487 $this->save();
488 } else {
489 $emailList = GeneralUtility::trimExplode(LF, $this->email, true);
490 }
491
492 foreach ($emailList as $emailAdd) {
493 if (!GeneralUtility::validEmail($emailAdd)) {
494 throw new \Exception(
495 $lang->sL($this->languageFile . ':tasks.error.invalidToEmail'),
496 '1295476821'
497 );
498 }
499 $validEmailList[] = $emailAdd;
500 }
501 }
502 if (is_array($validEmailList) && !empty($validEmailList)) {
503 $mail->setTo($validEmailList);
504 } else {
505 $sendEmail = false;
506 }
507 if ($sendEmail) {
508 $mail->setBody($content, 'text/html');
509 $mail->send();
510 }
511 return $sendEmail;
512 }
513
514 /**
515 * Build the mail content
516 *
517 * @param int $curPage Id of the current page
518 * @param string $pageList List of pages id
519 * @param array $markerArray Array of markers
520 * @param array $oldBrokenLink Marker array with the number of link found
521 * @return string Content of the mail
522 */
523 protected function buildMail($curPage, $pageList, array $markerArray, array $oldBrokenLink)
524 {
525 $pageSectionHtml = $this->templateService->getSubpart($this->templateMail, '###PAGE_SECTION###');
526 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['buildMailMarkers'] ?? [] as $userFunc) {
527 $params = [
528 'curPage' => $curPage,
529 'pageList' => $pageList,
530 'markerArray' => $markerArray,
531 'oldBrokenLink' => $oldBrokenLink,
532 'pObj' => &$this
533 ];
534 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
535 if (is_array($newMarkers)) {
536 $markerArray = $newMarkers + $markerArray;
537 }
538 unset($params);
539 }
540 foreach ($markerArray as $markerKey => $markerValue) {
541 if (empty($oldBrokenLink[$markerKey])) {
542 $oldBrokenLink[$markerKey] = 0;
543 }
544 if ($markerValue != $oldBrokenLink[$markerKey]) {
545 $this->isDifferentToLastRun = true;
546 }
547 $markerArray[$markerKey . '_old'] = $oldBrokenLink[$markerKey];
548 }
549 $markerArray['title'] = BackendUtility::getRecordTitle(
550 'pages',
551 BackendUtility::getRecord('pages', $curPage)
552 );
553 $content = '';
554 if ($markerArray['brokenlinkCount'] > 0) {
555 $content = $this->templateService->substituteMarkerArray(
556 $pageSectionHtml,
557 $markerArray,
558 '###|###',
559 true,
560 true
561 );
562 }
563 return $content;
564 }
565
566 /**
567 * Returns the most important properties of the link validator task as a
568 * comma separated string that will be displayed in the scheduler module.
569 *
570 * @return string
571 */
572 public function getAdditionalInformation()
573 {
574 $additionalInformation = [];
575
576 $page = (int)$this->getPage();
577 $pageLabel = $page;
578 if ($page !== 0) {
579 $pageData = BackendUtility::getRecord('pages', $page);
580 if (!empty($pageData)) {
581 $pageTitle = BackendUtility::getRecordTitle('pages', $pageData);
582 $pageLabel = $pageTitle . ' (' . $page . ')';
583 }
584 }
585 $lang = $this->getLanguageService();
586 $depth = (int)$this->getDepth();
587 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.page') . ': ' . $pageLabel;
588 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.depth') . ': '
589 . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.depth_' . ($depth === 999 ? 'infi' : $depth));
590 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.email') . ': '
591 . $this->getEmail();
592
593 return implode(', ', $additionalInformation);
594 }
595
596 /**
597 * Simulate cli call with setting the required options to the $_SERVER['argv']
598 */
599 protected function setCliArguments()
600 {
601 $_SERVER['argv'] = [
602 $_SERVER['argv'][0],
603 'tx_link_scheduler_link',
604 '0',
605 '-ss',
606 '--sleepTime',
607 $this->sleepTime,
608 '--sleepAfterFinish',
609 $this->sleepAfterFinish,
610 '--countInARun',
611 $this->countInARun
612 ];
613 }
614
615 /**
616 * @return LanguageService
617 */
618 protected function getLanguageService()
619 {
620 return $GLOBALS['LANG'];
621 }
622 }