[TASK] Update php-cs-fixer to 2.5.0
[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 isset($searchFields) ? $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 if (is_array($typesTmp)) {
415 if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
416 && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
417 ) {
418 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $type => $value) {
419 if (in_array($type, $typesTmp)) {
420 $linkTypes[$type] = 1;
421 }
422 }
423 }
424 }
425 return $linkTypes;
426 }
427
428 /**
429 * Build and send warning email when new broken links were found
430 *
431 * @param string $pageSections Content of page section
432 * @param array $modTsConfig TSconfig array
433 * @return bool TRUE if mail was sent, FALSE if or not
434 * @throws \Exception if required modTsConfig settings are missing
435 */
436 protected function reportEmail($pageSections, array $modTsConfig)
437 {
438 $lang = $this->getLanguageService();
439 $content = $this->templateService->substituteSubpart($this->templateMail, '###PAGE_SECTION###', $pageSections);
440 /** @var array $markerArray */
441 $markerArray = [];
442 /** @var array $validEmailList */
443 $validEmailList = [];
444 /** @var bool $sendEmail */
445 $sendEmail = true;
446 $markerArray['totalBrokenLink'] = $this->totalBrokenLink;
447 $markerArray['totalBrokenLink_old'] = $this->oldTotalBrokenLink;
448
449 // Hook
450 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['reportEmailMarkers'])) {
451 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['reportEmailMarkers'] as $userFunc) {
452 $params = [
453 'pObj' => &$this,
454 'markerArray' => $markerArray
455 ];
456 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
457 if (is_array($newMarkers)) {
458 $markerArray = $newMarkers + $markerArray;
459 }
460 unset($params);
461 }
462 }
463 $content = $this->templateService->substituteMarkerArray($content, $markerArray, '###|###', true, true);
464 /** @var $mail MailMessage */
465 $mail = GeneralUtility::makeInstance(MailMessage::class);
466 if (empty($modTsConfig['mail.']['fromemail'])) {
467 $modTsConfig['mail.']['fromemail'] = MailUtility::getSystemFromAddress();
468 }
469 if (empty($modTsConfig['mail.']['fromname'])) {
470 $modTsConfig['mail.']['fromname'] = MailUtility::getSystemFromName();
471 }
472 if (GeneralUtility::validEmail($modTsConfig['mail.']['fromemail'])) {
473 $mail->setFrom([$modTsConfig['mail.']['fromemail'] => $modTsConfig['mail.']['fromname']]);
474 } else {
475 throw new \Exception(
476 $lang->sL($this->languageFile . ':tasks.error.invalidFromEmail'),
477 '1295476760'
478 );
479 }
480 if (GeneralUtility::validEmail($modTsConfig['mail.']['replytoemail'])) {
481 $mail->setReplyTo([$modTsConfig['mail.']['replytoemail'] => $modTsConfig['mail.']['replytoname']]);
482 }
483 if (!empty($modTsConfig['mail.']['subject'])) {
484 $mail->setSubject($modTsConfig['mail.']['subject']);
485 } else {
486 throw new \Exception(
487 $lang->sL($this->languageFile . ':tasks.error.noSubject'),
488 '1295476808'
489 );
490 }
491 if (!empty($this->email)) {
492 // Check if old input field value is still there and save the value a
493 if (strpos($this->email, ',') !== false) {
494 $emailList = GeneralUtility::trimExplode(',', $this->email, true);
495 $this->email = implode(LF, $emailList);
496 $this->save();
497 } else {
498 $emailList = GeneralUtility::trimExplode(LF, $this->email, true);
499 }
500
501 foreach ($emailList as $emailAdd) {
502 if (!GeneralUtility::validEmail($emailAdd)) {
503 throw new \Exception(
504 $lang->sL($this->languageFile . ':tasks.error.invalidToEmail'),
505 '1295476821'
506 );
507 }
508 $validEmailList[] = $emailAdd;
509 }
510 }
511 if (is_array($validEmailList) && !empty($validEmailList)) {
512 $mail->setTo($validEmailList);
513 } else {
514 $sendEmail = false;
515 }
516 if ($sendEmail) {
517 $mail->setBody($content, 'text/html');
518 $mail->send();
519 }
520 return $sendEmail;
521 }
522
523 /**
524 * Build the mail content
525 *
526 * @param int $curPage Id of the current page
527 * @param string $pageList List of pages id
528 * @param array $markerArray Array of markers
529 * @param array $oldBrokenLink Marker array with the number of link found
530 * @return string Content of the mail
531 */
532 protected function buildMail($curPage, $pageList, array $markerArray, array $oldBrokenLink)
533 {
534 $pageSectionHtml = $this->templateService->getSubpart($this->templateMail, '###PAGE_SECTION###');
535 // Hook
536 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['buildMailMarkers'])) {
537 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['buildMailMarkers'] as $userFunc) {
538 $params = [
539 'curPage' => $curPage,
540 'pageList' => $pageList,
541 'markerArray' => $markerArray,
542 'oldBrokenLink' => $oldBrokenLink,
543 'pObj' => &$this
544 ];
545 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
546 if (is_array($newMarkers)) {
547 $markerArray = $newMarkers + $markerArray;
548 }
549 unset($params);
550 }
551 }
552 if (is_array($markerArray)) {
553 foreach ($markerArray as $markerKey => $markerValue) {
554 if (empty($oldBrokenLink[$markerKey])) {
555 $oldBrokenLink[$markerKey] = 0;
556 }
557 if ($markerValue != $oldBrokenLink[$markerKey]) {
558 $this->isDifferentToLastRun = true;
559 }
560 $markerArray[$markerKey . '_old'] = $oldBrokenLink[$markerKey];
561 }
562 }
563 $markerArray['title'] = BackendUtility::getRecordTitle(
564 'pages',
565 BackendUtility::getRecord('pages', $curPage)
566 );
567 $content = '';
568 if ($markerArray['brokenlinkCount'] > 0) {
569 $content = $this->templateService->substituteMarkerArray(
570 $pageSectionHtml,
571 $markerArray,
572 '###|###',
573 true,
574 true
575 );
576 }
577 return $content;
578 }
579
580 /**
581 * Returns the most important properties of the link validator task as a
582 * comma separated string that will be displayed in the scheduler module.
583 *
584 * @return string
585 */
586 public function getAdditionalInformation()
587 {
588 $additionalInformation = [];
589
590 $page = (int)$this->getPage();
591 $pageLabel = $page;
592 if ($page !== 0) {
593 $pageData = BackendUtility::getRecord('pages', $page);
594 if (!empty($pageData)) {
595 $pageTitle = BackendUtility::getRecordTitle('pages', $pageData);
596 $pageLabel = $pageTitle . ' (' . $page . ')';
597 }
598 }
599 $lang = $this->getLanguageService();
600 $depth = (int)$this->getDepth();
601 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.page') . ': ' . $pageLabel;
602 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.depth') . ': '
603 . $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.depth_' . ($depth === 999 ? 'infi' : $depth));
604 $additionalInformation[] = $lang->sL($this->languageFile . ':tasks.validate.email') . ': '
605 . $this->getEmail();
606
607 return implode(', ', $additionalInformation);
608 }
609
610 /**
611 * Simulate cli call with setting the required options to the $_SERVER['argv']
612 */
613 protected function setCliArguments()
614 {
615 $_SERVER['argv'] = [
616 $_SERVER['argv'][0],
617 'tx_link_scheduler_link',
618 '0',
619 '-ss',
620 '--sleepTime',
621 $this->sleepTime,
622 '--sleepAfterFinish',
623 $this->sleepAfterFinish,
624 '--countInARun',
625 $this->countInARun
626 ];
627 }
628
629 /**
630 * @return LanguageService
631 */
632 protected function getLanguageService()
633 {
634 return $GLOBALS['LANG'];
635 }
636 }