568501803379dbc13a8a1d0fb49846a1cb856998
[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\Mail\MailMessage;
19 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
20 use TYPO3\CMS\Core\Utility\ArrayUtility;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Core\Html\HtmlParser;
23 use TYPO3\CMS\Core\Utility\MailUtility;
24 use TYPO3\CMS\Lang\LanguageService;
25 use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
26
27 /**
28 * This class provides Scheduler plugin implementation
29 *
30 * @author Michael Miousse <michael.miousse@infoglobe.ca>
31 */
32 class ValidatorTask extends \TYPO3\CMS\Scheduler\Task\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 = array();
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 * Get the value of the protected property email
121 *
122 * @return string Email address to which an email report is sent
123 */
124 public function getEmail() {
125 return $this->email;
126 }
127
128 /**
129 * Set the value of the private property email.
130 *
131 * @param string $email Email address to which an email report is sent
132 * @return void
133 */
134 public function setEmail($email) {
135 $this->email = $email;
136 }
137
138 /**
139 * Get the value of the protected property emailOnBrokenLinkOnly
140 *
141 * @return bool Whether to send an email, if new broken links were found
142 */
143 public function getEmailOnBrokenLinkOnly() {
144 return $this->emailOnBrokenLinkOnly;
145 }
146
147 /**
148 * Set the value of the private property emailOnBrokenLinkOnly
149 *
150 * @param bool $emailOnBrokenLinkOnly Only send an email, if new broken links were found
151 * @return void
152 */
153 public function setEmailOnBrokenLinkOnly($emailOnBrokenLinkOnly) {
154 $this->emailOnBrokenLinkOnly = $emailOnBrokenLinkOnly;
155 }
156
157 /**
158 * Get the value of the protected property page
159 *
160 * @return int UID of the start page for this task
161 */
162 public function getPage() {
163 return $this->page;
164 }
165
166 /**
167 * Set the value of the private property page
168 *
169 * @param int $page UID of the start page for this task.
170 * @return void
171 */
172 public function setPage($page) {
173 $this->page = $page;
174 }
175
176 /**
177 * Get the value of the protected property depth
178 *
179 * @return int Level of pages the task should check
180 */
181 public function getDepth() {
182 return $this->depth;
183 }
184
185 /**
186 * Set the value of the private property depth
187 *
188 * @param int $depth Level of pages the task should check
189 * @return void
190 */
191 public function setDepth($depth) {
192 $this->depth = $depth;
193 }
194
195 /**
196 * Get the value of the protected property emailTemplateFile
197 *
198 * @return string Template to be used for the email
199 */
200 public function getEmailTemplateFile() {
201 return $this->emailTemplateFile;
202 }
203
204 /**
205 * Set the value of the private property emailTemplateFile
206 *
207 * @param string $emailTemplateFile Template to be used for the email
208 * @return void
209 */
210 public function setEmailTemplateFile($emailTemplateFile) {
211 $this->emailTemplateFile = $emailTemplateFile;
212 }
213
214 /**
215 * Get the value of the protected property configuration
216 *
217 * @return array specific TSconfig for this task
218 */
219 public function getConfiguration() {
220 return $this->configuration;
221 }
222
223 /**
224 * Set the value of the private property configuration
225 *
226 * @param array $configuration specific TSconfig for this task
227 * @return void
228 */
229 public function setConfiguration($configuration) {
230 $this->configuration = $configuration;
231 }
232
233 /**
234 * Function execute from the Scheduler
235 *
236 * @return bool TRUE on successful execution, FALSE on error
237 * @throws \InvalidArgumentException if the email template file can not be read
238 */
239 public function execute() {
240 $this->setCliArguments();
241 $successfullyExecuted = TRUE;
242 if (
243 !file_exists(($file = GeneralUtility::getFileAbsFileName($this->emailTemplateFile)))
244 && !empty($this->email)
245 ) {
246 if ($this->emailTemplateFile === 'EXT:linkvalidator/res/mailtemplate.html') {
247 // Update the default email template file path
248 $this->emailTemplateFile = 'EXT:linkvalidator/Resources/Private/Templates/mailtemplate.html';
249 $this->save();
250 } else {
251 throw new \InvalidArgumentException(
252 $this->getLanguageService()->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.error.invalidEmailTemplateFile'),
253 '1295476972'
254 );
255 }
256 }
257 $htmlFile = GeneralUtility::getURL($file);
258 $this->templateMail = HtmlParser::getSubpart($htmlFile, '###REPORT_TEMPLATE###');
259 // The array to put the content into
260 $pageSections = '';
261 $this->isDifferentToLastRun = FALSE;
262 $pageList = GeneralUtility::trimExplode(',', $this->page, TRUE);
263 $modTs = $this->loadModTsConfig($this->page);
264 if (is_array($pageList)) {
265 foreach ($pageList as $page) {
266 $pageSections .= $this->checkPageLinks($page);
267 }
268 }
269 if ($this->totalBrokenLink != $this->oldTotalBrokenLink) {
270 $this->isDifferentToLastRun = TRUE;
271 }
272 if ($this->totalBrokenLink > 0 && (!$this->emailOnBrokenLinkOnly || $this->isDifferentToLastRun) && !empty($this->email)) {
273 $successfullyExecuted = $this->reportEmail($pageSections, $modTs);
274 }
275 return $successfullyExecuted;
276 }
277
278 /**
279 * Validate all links for a page based on the task configuration
280 *
281 * @param int $page Uid of the page to parse
282 * @return string $pageSections Content of page section
283 */
284 protected function checkPageLinks($page) {
285 $page = (int)$page;
286 $pageSections = '';
287 $pageIds = '';
288 $oldLinkCounts = array();
289 $modTs = $this->loadModTsConfig($page);
290 $searchFields = $this->getSearchField($modTs);
291 $linkTypes = $this->getLinkTypes($modTs);
292 /** @var $processor LinkAnalyzer */
293 $processor = GeneralUtility::makeInstance(LinkAnalyzer::class);
294 if ($page === 0) {
295 $rootLineHidden = FALSE;
296 } else {
297 $pageRow = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'pages', 'uid=' . $page);
298 $rootLineHidden = $processor->getRootLineIsHidden($pageRow);
299 }
300 if (!$rootLineHidden || $modTs['checkhidden'] == 1) {
301 $pageIds = $processor->extGetTreeList($page, $this->depth, 0, '1=1', $modTs['checkhidden']);
302 if (isset($pageRow) && $pageRow['hidden'] == 0 || $modTs['checkhidden'] == 1) {
303 // \TYPO3\CMS\Linkvalidator\LinkAnalyzer->extGetTreeList() always adds trailing comma
304 $pageIds .= $page;
305 }
306 }
307 if (!empty($pageIds)) {
308 $processor->init($searchFields, $pageIds, $modTs);
309 if (!empty($this->email)) {
310 $oldLinkCounts = $processor->getLinkCounts($page);
311 $this->oldTotalBrokenLink += $oldLinkCounts['brokenlinkCount'];
312 }
313 $processor->getLinkStatistics($linkTypes, $modTs['checkhidden']);
314 if (!empty($this->email)) {
315 $linkCounts = $processor->getLinkCounts($page);
316 $this->totalBrokenLink += $linkCounts['brokenlinkCount'];
317 $pageSections = $this->buildMail($page, $pageIds, $linkCounts, $oldLinkCounts);
318 }
319 }
320 return $pageSections;
321 }
322
323 /**
324 * Get the linkvalidator modTSconfig for a page
325 *
326 * @param int $page Uid of the page
327 * @return array $modTsConfig mod.linkvalidator TSconfig array
328 * @throws \Exception
329 */
330 protected function loadModTsConfig($page) {
331 $modTs = BackendUtility::getModTSconfig($page, 'mod.linkvalidator');
332 $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
333 $parseObj->parse($this->configuration);
334 if (count($parseObj->errors) > 0) {
335 $parseErrorMessage = $this->getLanguageService()->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.error.invalidTSconfig') . '<br />';
336 foreach ($parseObj->errors as $errorInfo) {
337 $parseErrorMessage .= $errorInfo[0] . '<br />';
338 }
339 throw new \Exception($parseErrorMessage, '1295476989');
340 }
341 $tsConfig = $parseObj->setup;
342 $modTs = $modTs['properties'];
343 $overrideTs = $tsConfig['mod.']['tx_linkvalidator.'];
344 if (is_array($overrideTs)) {
345 ArrayUtility::mergeRecursiveWithOverrule($modTs, $overrideTs);
346 }
347 return $modTs;
348 }
349
350 /**
351 * Get the list of fields to parse in modTSconfig
352 *
353 * @param array $modTS mod.linkvalidator TSconfig array
354 * @return array $searchFields List of fields
355 */
356 protected function getSearchField(array $modTS) {
357 // Get the searchFields from TypoScript
358 foreach ($modTS['searchFields.'] as $table => $fieldList) {
359 $fields = GeneralUtility::trimExplode(',', $fieldList);
360 foreach ($fields as $field) {
361 $searchFields[$table][] = $field;
362 }
363 }
364 return isset($searchFields) ? $searchFields : array();
365 }
366
367 /**
368 * Get the list of linkTypes to parse in modTSconfig
369 *
370 * @param array $modTS mod.linkvalidator TSconfig array
371 * @return array $linkTypes list of link types
372 */
373 protected function getLinkTypes(array $modTS) {
374 $linkTypes = array();
375 $typesTmp = GeneralUtility::trimExplode(',', $modTS['linktypes'], TRUE);
376 if (is_array($typesTmp)) {
377 if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])) {
378 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $type => $value) {
379 if (in_array($type, $typesTmp)) {
380 $linkTypes[$type] = 1;
381 }
382 }
383 }
384 }
385 return $linkTypes;
386 }
387
388 /**
389 * Build and send warning email when new broken links were found
390 *
391 * @param string $pageSections Content of page section
392 * @param array $modTsConfig TSconfig array
393 * @return bool TRUE if mail was sent, FALSE if or not
394 * @throws \Exception if required modTsConfig settings are missing
395 */
396 protected function reportEmail($pageSections, array $modTsConfig) {
397 $content = HtmlParser::substituteSubpart($this->templateMail, '###PAGE_SECTION###', $pageSections);
398 /** @var array $markerArray */
399 $markerArray = array();
400 /** @var array $validEmailList */
401 $validEmailList = array();
402 /** @var bool $sendEmail */
403 $sendEmail = TRUE;
404 $markerArray['totalBrokenLink'] = $this->totalBrokenLink;
405 $markerArray['totalBrokenLink_old'] = $this->oldTotalBrokenLink;
406 // Hook
407 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['reportEmailMarkers'])) {
408 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['reportEmailMarkers'] as $userFunc) {
409 $params = array(
410 'pObj' => &$this,
411 'markerArray' => $markerArray
412 );
413 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
414 if (is_array($newMarkers)) {
415 $markerArray = GeneralUtility::array_merge($markerArray, $newMarkers);
416 }
417 unset($params);
418 }
419 }
420 $content = HtmlParser::substituteMarkerArray($content, $markerArray, '###|###', TRUE, TRUE);
421 /** @var $mail MailMessage */
422 $mail = GeneralUtility::makeInstance(MailMessage::class);
423 if (empty($modTsConfig['mail.']['fromemail'])) {
424 $modTsConfig['mail.']['fromemail'] = MailUtility::getSystemFromAddress();
425 }
426 if (empty($modTsConfig['mail.']['fromname'])) {
427 $modTsConfig['mail.']['fromname'] = MailUtility::getSystemFromName();
428 }
429 if (GeneralUtility::validEmail($modTsConfig['mail.']['fromemail'])) {
430 $mail->setFrom(array($modTsConfig['mail.']['fromemail'] => $modTsConfig['mail.']['fromname']));
431 } else {
432 throw new \Exception($this->getLanguageService()->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.error.invalidFromEmail'), '1295476760');
433 }
434 if (GeneralUtility::validEmail($modTsConfig['mail.']['replytoemail'])) {
435 $mail->setReplyTo(array($modTsConfig['mail.']['replytoemail'] => $modTsConfig['mail.']['replytoname']));
436 }
437 if (!empty($modTsConfig['mail.']['subject'])) {
438 $mail->setSubject($modTsConfig['mail.']['subject']);
439 } else {
440 throw new \Exception($this->getLanguageService()->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.error.noSubject'), '1295476808');
441 }
442 if (!empty($this->email)) {
443 $emailList = GeneralUtility::trimExplode(',', $this->email);
444 foreach ($emailList as $emailAdd) {
445 if (!GeneralUtility::validEmail($emailAdd)) {
446 throw new \Exception($this->getLanguageService()->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.error.invalidToEmail'), '1295476821');
447 } else {
448 $validEmailList[] = $emailAdd;
449 }
450 }
451 }
452 if (is_array($validEmailList) && !empty($validEmailList)) {
453 $mail->setTo($validEmailList);
454 } else {
455 $sendEmail = FALSE;
456 }
457 if ($sendEmail) {
458 $mail->setBody($content, 'text/html');
459 $mail->send();
460 }
461 return $sendEmail;
462 }
463
464 /**
465 * Build the mail content
466 *
467 * @param int $curPage Id of the current page
468 * @param string $pageList List of pages id
469 * @param array $markerArray Array of markers
470 * @param array $oldBrokenLink Marker array with the number of link found
471 * @return string Content of the mail
472 */
473 protected function buildMail($curPage, $pageList, array $markerArray, array $oldBrokenLink) {
474 $pageSectionHtml = HtmlParser::getSubpart($this->templateMail, '###PAGE_SECTION###');
475 // Hook
476 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['buildMailMarkers'])) {
477 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['buildMailMarkers'] as $userFunc) {
478 $params = array(
479 'curPage' => $curPage,
480 'pageList' => $pageList,
481 'markerArray' => $markerArray,
482 'oldBrokenLink' => $oldBrokenLink,
483 'pObj' => &$this
484 );
485 $newMarkers = GeneralUtility::callUserFunction($userFunc, $params, $this);
486 if (is_array($newMarkers)) {
487 $markerArray = GeneralUtility::array_merge($markerArray, $newMarkers);
488 }
489 unset($params);
490 }
491 }
492 if (is_array($markerArray)) {
493 foreach ($markerArray as $markerKey => $markerValue) {
494 if (empty($oldBrokenLink[$markerKey])) {
495 $oldBrokenLink[$markerKey] = 0;
496 }
497 if ($markerValue != $oldBrokenLink[$markerKey]) {
498 $this->isDifferentToLastRun = TRUE;
499 }
500 $markerArray[$markerKey . '_old'] = $oldBrokenLink[$markerKey];
501 }
502 }
503 $markerArray['title'] = BackendUtility::getRecordTitle(
504 'pages',
505 BackendUtility::getRecord('pages', $curPage)
506 );
507 $content = '';
508 if ($markerArray['brokenlinkCount'] > 0) {
509 $content = HtmlParser::substituteMarkerArray($pageSectionHtml, $markerArray, '###|###', TRUE, TRUE);
510 }
511 return $content;
512 }
513
514 /**
515 * Returns the most important properties of the link validator task as a
516 * comma separated string that will be displayed in the scheduler module.
517 *
518 * @return string
519 */
520 public function getAdditionalInformation() {
521 $additionalInformation = array();
522
523 $page = (int)$this->getPage();
524 $pageLabel = $page;
525 if ($page !== 0) {
526 $pageData = BackendUtility::getRecord('pages', $page);
527 if (!empty($pageData)) {
528 $pageTitle = BackendUtility::getRecordTitle('pages', $pageData);
529 $pageLabel = $pageTitle . ' (' . $page . ')';
530 }
531 }
532 $lang = $this->getLanguageService();
533 $additionalInformation[] = $lang->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.validate.page') . ': ' . $pageLabel;
534
535 $depth = (int)$this->getDepth();
536 $additionalInformation[] = $lang->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.validate.depth') . ': ' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_' . ($depth === 999 ? 'infi' : $depth));
537
538 $additionalInformation[] = $lang->sL('LLL:EXT:linkvalidator/Resources/Private/Language/locallang.xlf:tasks.validate.email') . ': ' . $this->getEmail();
539
540 return implode(', ', $additionalInformation);
541 }
542
543 /**
544 * Simulate cli call with setting the required options to the $_SERVER['argv']
545 *
546 * @return void
547 */
548 protected function setCliArguments() {
549 $_SERVER['argv'] = array(
550 $_SERVER['argv'][0],
551 'tx_link_scheduler_link',
552 '0',
553 '-ss',
554 '--sleepTime',
555 $this->sleepTime,
556 '--sleepAfterFinish',
557 $this->sleepAfterFinish,
558 '--countInARun',
559 $this->countInARun
560 );
561 }
562
563 /**
564 * @return LanguageService
565 */
566 protected function getLanguageService() {
567 return $GLOBALS['LANG'];
568 }
569
570 /**
571 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
572 */
573 protected function getDatabaseConnection() {
574 return $GLOBALS['TYPO3_DB'];
575 }
576 }