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