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