[FEATURE] Add scheduler task to remove deleted records 13/9013/22
authorPhilipp Bergsmann <p.bergsmann@opendo.at>
Mon, 13 Feb 2012 18:19:47 +0000 (19:19 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Mon, 2 Mar 2015 16:40:05 +0000 (17:40 +0100)
Scheduler task to remove deleted records from
content table(s) which are older than x days.

If a deleted record also contains an upload field,
then the file is also deleted.

Releases: master
Resolves: #32651
Change-Id: I58577c05a1a3b228579c05578cc8fdf2e3b393fa
Reviewed-on: http://review.typo3.org/9013
Reviewed-by: Markus Klein <klein.t3@reelworx.at>
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/core/Documentation/Changelog/master/Feature-32651-AddSchedulerTaskToRemoveDeletedRecords.rst [new file with mode: 0644]
typo3/sysext/recycler/Classes/Task/CleanerFieldProvider.php [new file with mode: 0644]
typo3/sysext/recycler/Classes/Task/CleanerTask.php [new file with mode: 0644]
typo3/sysext/recycler/Resources/Private/Language/locallang_tasks.xlf [new file with mode: 0644]
typo3/sysext/recycler/Tests/Unit/Task/CleanerFieldProviderTest.php [new file with mode: 0644]
typo3/sysext/recycler/Tests/Unit/Task/CleanerTaskTest.php [new file with mode: 0644]
typo3/sysext/recycler/ext_localconf.php
typo3/sysext/scheduler/Classes/Scheduler.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-32651-AddSchedulerTaskToRemoveDeletedRecords.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-32651-AddSchedulerTaskToRemoveDeletedRecords.rst
new file mode 100644 (file)
index 0000000..acbea45
--- /dev/null
@@ -0,0 +1,9 @@
+==============================================================
+Feature: #32651 - Add scheduler task to remove deleted records
+==============================================================
+
+Description
+===========
+
+A new scheduler task for removing deleted records has been added. The maximum age and
+the affected tables are configurable in the task's settings.
diff --git a/typo3/sysext/recycler/Classes/Task/CleanerFieldProvider.php b/typo3/sysext/recycler/Classes/Task/CleanerFieldProvider.php
new file mode 100644 (file)
index 0000000..eeef26c
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+namespace TYPO3\CMS\Recycler\Task;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+use TYPO3\CMS\Scheduler\Task\AbstractTask;
+
+/**
+ * A task that should be run regularly that deletes
+ * datasets flagged as "deleted" from the DB.
+ *
+ * @author Philipp Bergsmann <p.bergsmann@opendo.at>
+ */
+class CleanerFieldProvider implements \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface {
+
+       /**
+        * Gets additional fields to render in the form to add/edit a task
+        *
+        * @param array $taskInfo Values of the fields from the add/edit task form
+        * @param \TYPO3\CMS\Recycler\Task\CleanerTask $task The task object being edited. NULL when adding a task!
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
+        */
+       public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule) {
+               if ($schedulerModule->CMD === 'edit') {
+                       $taskInfo['RecyclerCleanerTCA'] = $task->getTcaTables();
+                       $taskInfo['RecyclerCleanerPeriod'] = $task->getPeriod();
+               }
+
+               $additionalFields['period'] = array(
+                       'code' => '<input type="text" class="form-control" name="tx_scheduler[RecyclerCleanerPeriod]" value="' . $taskInfo['RecyclerCleanerPeriod'] . '">',
+                       'label' => 'LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskPeriod',
+                       'cshKey' => '',
+                       'cshLabel' => 'task_recyclerCleaner_selectedPeriod'
+               );
+
+               $additionalFields['tca'] = array(
+                       'code' => $this->getTcaSelectHtml($taskInfo['RecyclerCleanerTCA']),
+                       'label' => 'LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskTCA',
+                       'cshKey' => '',
+                       'cshLabel' => 'task_recyclerCleaner_selectedTables'
+               );
+
+               return $additionalFields;
+       }
+
+       /**
+        * Gets the select-box from the TCA-fields
+        *
+        * @param array $selectedTables
+        * @return string
+        */
+       protected function getTcaSelectHtml($selectedTables = array()) {
+               if (!is_array($selectedTables)) {
+                       $selectedTables = array();
+               }
+               $tcaSelectHtml = '<select name="tx_scheduler[RecyclerCleanerTCA][]" multiple="multiple" class="form-control" size="10">';
+
+               $options = array();
+               foreach ($GLOBALS['TCA'] as $table => $tableConf) {
+                       if (!$tableConf['ctrl']['adminOnly'] && !empty($tableConf['ctrl']['delete'])) {
+                               $selected = in_array($table, $selectedTables, TRUE) ? ' selected="selected"' : '';
+                               $tableTitle = $this->getLanguageService()->sL($tableConf['ctrl']['title']);
+                               $options[$tableTitle] = '<option' . $selected . ' value="' . $table . '">' . htmlspecialchars($tableTitle . ' (' . $table . ')') . '</option>';
+                       }
+               }
+               ksort($options);
+
+               $tcaSelectHtml .= implode('', $options);
+               $tcaSelectHtml .= '</select>';
+
+               return $tcaSelectHtml;
+       }
+
+       /**
+        * Validates the additional fields' values
+        *
+        * @param array $submittedData An array containing the data submitted by the add/edit task form
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
+        */
+       public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule) {
+               $validPeriod = $this->validateAdditionalFieldPeriod($submittedData['RecyclerCleanerPeriod'], $schedulerModule);
+               $validTca = $this->validateAdditionalFieldTca($submittedData['RecyclerCleanerTCA'], $schedulerModule);
+
+               return $validPeriod && $validTca;
+       }
+
+       /**
+        * Validates the selected Tables
+        *
+        * @param array $tca The given TCA-tables as array
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return bool TRUE if validation was ok, FALSE otherwise
+        */
+       protected function validateAdditionalFieldTca($tca, SchedulerModuleController $schedulerModule) {
+               return $this->checkTcaIsNotEmpty($tca, $schedulerModule) && $this->checkTcaIsValid($tca, $schedulerModule);
+       }
+
+       /**
+        * Checks if the array is empty
+        *
+        * @param array $tca The given TCA-tables as array
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return bool TRUE if validation was ok, FALSE otherwise
+        */
+       protected function checkTcaIsNotEmpty($tca, SchedulerModuleController $schedulerModule) {
+               if (is_array($tca) && !empty($tca)) {
+                       $validTca = TRUE;
+               } else {
+                       $schedulerModule->addMessage(
+                               $this->getLanguageService()->sL('LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskErrorTCAempty', TRUE),
+                               FlashMessage::ERROR
+                       );
+                       $validTca = FALSE;
+               }
+
+               return $validTca;
+       }
+
+       /**
+        * Checks if the given tables are in the TCA
+        *
+        * @param array $tca The given TCA-tables as array
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return bool TRUE if validation was ok, FALSE otherwise
+        */
+       protected function checkTcaIsValid(array $tca, SchedulerModuleController $schedulerModule) {
+               $checkTca = FALSE;
+               foreach ($tca as $tcaTable) {
+                       if (!isset($GLOBALS['TCA'][$tcaTable])) {
+                               $checkTca = FALSE;
+                               $schedulerModule->addMessage(
+                                       sprintf($this->getLanguageService()->sL('LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskErrorTCANotSet', TRUE), $tcaTable),
+                                       FlashMessage::ERROR
+                               );
+                               break;
+                       } else {
+                               $checkTca = TRUE;
+                       }
+               }
+
+               return $checkTca;
+       }
+
+       /**
+        * Validates the input of period
+        *
+        * @param int $period The given period as integer
+        * @param SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+        * @return bool TRUE if validation was ok, FALSE otherwise
+        */
+       protected function validateAdditionalFieldPeriod($period, SchedulerModuleController $schedulerModule) {
+               if (!empty($period) && filter_var($period, FILTER_VALIDATE_INT) !== FALSE) {
+                       $validPeriod = TRUE;
+               } else {
+                       $schedulerModule->addMessage(
+                               $this->getLanguageService()->sL('LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskErrorPeriod', TRUE),
+                               FlashMessage::ERROR
+                       );
+                       $validPeriod = FALSE;
+               }
+
+               return $validPeriod;
+       }
+
+       /**
+        * Takes care of saving the additional fields' values in the task's object
+        *
+        * @param array $submittedData An array containing the data submitted by the add/edit task form
+        * @param AbstractTask $task Reference to the scheduler backend module
+        * @return void
+        * @throws \InvalidArgumentException
+        */
+       public function saveAdditionalFields(array $submittedData, AbstractTask $task) {
+               if (!$task instanceof CleanerTask) {
+                       throw new \InvalidArgumentException(
+                               'Expected a task of type \TYPO3\CMS\Recycler\Task\CleanerTask, but got ' . get_class($task),
+                               1329219449
+                       );
+               }
+
+               $task->setTcaTables($submittedData['RecyclerCleanerTCA']);
+               $task->setPeriod($submittedData['RecyclerCleanerPeriod']);
+       }
+
+       /**
+        * @return \TYPO3\CMS\Lang\LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
diff --git a/typo3/sysext/recycler/Classes/Task/CleanerTask.php b/typo3/sysext/recycler/Classes/Task/CleanerTask.php
new file mode 100644 (file)
index 0000000..a43621c
--- /dev/null
@@ -0,0 +1,232 @@
+<?php
+namespace TYPO3\CMS\Recycler\Task;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * A task that should be run regularly that deletes deleted
+ * datasets from the DB.
+ *
+ * @author Philipp Bergsmann <p.bergsmann@opendo.at>
+ */
+class CleanerTask extends \TYPO3\CMS\Scheduler\Task\AbstractTask {
+
+       /**
+        * @var int The time period, after which the rows are deleted
+        */
+       protected $period = 0;
+
+       /**
+        * @var array The tables to clean
+        */
+       protected $tcaTables = array();
+
+       /**
+        * @var \TYPO3\CMS\Core\Database\DatabaseConnection
+        */
+       protected $databaseConnection = NULL;
+
+       /**
+        * The main method of the task. Iterates through
+        * the tables and calls the cleaning function
+        *
+        * @return bool Returns TRUE on successful execution, FALSE on error
+        */
+       public function execute() {
+               $success = TRUE;
+               $tables = $this->getTcaTables();
+               foreach ($tables as $table) {
+                       if (!$this->cleanTable($table)) {
+                               $success = FALSE;
+                       }
+               }
+
+               return $success;
+       }
+
+       /**
+        * Executes the delete-query for the given table
+        *
+        * @param string $tableName
+        * @return bool
+        */
+       protected function cleanTable($tableName) {
+               $queryParts = array();
+               if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
+                       $queryParts[] = $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . ' = 1';
+                       if ($GLOBALS['TCA'][$tableName]['ctrl']['tstamp']) {
+                               $dateBefore = $this->getPeriodAsTimestamp();
+                               $queryParts[] = $GLOBALS['TCA'][$tableName]['ctrl']['tstamp'] . ' < ' . $dateBefore;
+                       }
+                       $where = implode(' AND ', $queryParts);
+
+                       $this->checkFileResourceFieldsBeforeDeletion($tableName, $where);
+
+                       $this->getDatabaseConnection()->exec_DELETEquery($tableName, $where);
+               }
+
+               return $this->getDatabaseConnection()->sql_error() === '';
+       }
+
+       /**
+        * Returns the information shown in the task-list
+        *
+        * @return string Information-text fot the scheduler task-list
+        */
+       public function getAdditionalInformation() {
+               $message = '';
+
+               $message .= sprintf(
+                       $this->getLanguageService()->sL('LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskDescriptionTables'),
+                       implode(', ', $this->getTcaTables())
+               );
+
+               $message .= '; ';
+
+               $message .= sprintf(
+                       $this->getLanguageService()->sL('LLL:EXT:recycler/locallang_tasks.xlf:cleanerTaskDescriptionDays'),
+                       $this->getPeriod()
+               );
+
+               return $message;
+       }
+
+       /**
+        * Sets the period after which a row is deleted
+        *
+        * @param int $period
+        */
+       public function setPeriod($period) {
+               $this->period = (int)$period;
+       }
+
+       /**
+        * Returns the period after which a row is deleted
+        *
+        * @return int
+        */
+       public function getPeriod() {
+               return $this->period;
+       }
+
+       /**
+        * @return int
+        */
+       public function getPeriodAsTimestamp() {
+               return strtotime('-' . $this->getPeriod() . ' days');
+       }
+
+       /**
+        * Sets the TCA-tables which are cleaned
+        *
+        * @param array $tcaTables
+        */
+       public function setTcaTables($tcaTables = array()) {
+               $this->tcaTables = $tcaTables;
+       }
+
+       /**
+        * Returns the TCA-tables which are cleaned
+        *
+        * @return array
+        */
+       public function getTcaTables() {
+               return $this->tcaTables;
+       }
+
+       /**
+        * @param \TYPO3\CMS\Core\Database\DatabaseConnection
+        */
+       public function setDatabaseConnection($databaseConnection) {
+               $this->databaseConnection = $databaseConnection;
+       }
+
+       /**
+        * Checks if the table has fields for uploaded files and removes those files.
+        *
+        * @param string $table
+        * @param string $where
+        * @return void
+        */
+       protected function checkFileResourceFieldsBeforeDeletion($table, $where) {
+               $fieldList = $this->getFileResourceFields($table);
+               if (!empty($fieldList)) {
+                       $this->deleteFilesForTable($table, $where, $fieldList);
+               }
+       }
+
+       /**
+        * Removes all files from the given field list in the table.
+        *
+        * @param string $table
+        * @param string $where
+        * @param array $fieldList
+        * @return void
+        */
+       protected function deleteFilesForTable($table, $where, array $fieldList) {
+               $rows = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                       implode(',', $fieldList),
+                       $table,
+                       $where
+               );
+               foreach ($rows as $row) {
+                       foreach ($fieldList as $fieldName) {
+                               $uploadDir = PATH_site . $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['uploadfolder'] . '/';
+                               $fileList = GeneralUtility::trimExplode(',', $row[$fieldName]);
+                               foreach ($fileList as $fileName) {
+                                       @unlink($uploadDir . $fileName);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Checks the $TCA for fields that can list file resources.
+        *
+        * @param string $table
+        * @return array
+        */
+       protected function getFileResourceFields($table) {
+               $result = array();
+               if (isset($GLOBALS['TCA'][$table]['columns'])) {
+                       foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldConfiguration) {
+                               if ($fieldConfiguration['config']['type'] === 'group'
+                                       && $fieldConfiguration['config']['internal_type'] === 'file'
+                               ) {
+                                       $result[] = $fieldName;
+                                       break;
+                               }
+                       }
+               }
+               return $result;
+       }
+
+       /**
+        * @return \TYPO3\CMS\Core\Database\DatabaseConnection
+        */
+       protected function getDatabaseConnection() {
+               if ($this->databaseConnection === NULL) {
+                       $this->databaseConnection = $GLOBALS['TYPO3_DB'];
+               }
+               return $this->databaseConnection;
+       }
+
+       /**
+        * @return \TYPO3\CMS\Lang\LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+}
diff --git a/typo3/sysext/recycler/Resources/Private/Language/locallang_tasks.xlf b/typo3/sysext/recycler/Resources/Private/Language/locallang_tasks.xlf
new file mode 100644 (file)
index 0000000..5271f5c
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
+       <file t3:id="1424776442" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:35Z" product-name="recycler">
+               <header />
+               <body>
+                       <trans-unit id="cleanerTaskTitle" xml:space="preserve">
+                               <source>Remove deleted records</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskDescription" xml:space="preserve">
+                               <source>Deletes rows from the database which are flagged "deleted"</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskPeriod" xml:space="preserve">
+                               <source>Delete entries older than (in days)</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskTCA" xml:space="preserve">
+                               <source>Tables</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskDescriptionTables" xml:space="preserve">
+                               <source>Tables: %s</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskDescriptionDays" xml:space="preserve">
+                               <source>Older than %d days(s)</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskErrorPeriod" xml:space="preserver">
+                               <source>The period has to be an integer and greater than zero.</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskErrorTCAempty" xml:space="preserver">
+                               <source>You have to select at least one table.</source>
+                       </trans-unit>
+                       <trans-unit id="cleanerTaskErrorTCANotSet" xml:space="preserver">
+                               <source>The table "%s" is not in the TCA.</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
diff --git a/typo3/sysext/recycler/Tests/Unit/Task/CleanerFieldProviderTest.php b/typo3/sysext/recycler/Tests/Unit/Task/CleanerFieldProviderTest.php
new file mode 100644 (file)
index 0000000..74284ac
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+namespace TYPO3\CMS\Recycler\Tests\Unit\Task;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Recycler\Task\CleanerFieldProvider;
+use TYPO3\CMS\Recycler\Task\CleanerTask;
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+
+/**
+ * Testcase
+ */
+class CleanerFieldProviderTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var CleanerFieldProvider
+        */
+       protected $subject = NULL;
+
+       /**
+        * Sets up an instance of \TYPO3\CMS\Recycler\Task\CleanerFieldProvider
+        */
+       public function setUp() {
+               $languageServiceMock = $this->getMock(LanguageService::class, array('sL'), array(), '', FALSE);
+               $languageServiceMock->expects($this->any())->method('sL')->will($this->returnValue('titleTest'));
+               $this->subject = $this->getMock(CleanerFieldProvider::class, array('getLanguageService'));
+               $this->subject->expects($this->any())->method('getLanguageService')->willReturn($languageServiceMock);
+       }
+
+       /**
+        * @param array $mockedMethods
+        * @return \PHPUnit_Framework_MockObject_MockObject|SchedulerModuleController
+        */
+       protected function getScheduleModuleControllerMock($mockedMethods = array()) {
+               $languageServiceMock = $this->getMock(LanguageService::class, array('sL'), array(), '', FALSE);
+               $languageServiceMock->expects($this->any())->method('sL')->will($this->returnValue('titleTest'));
+
+               $mockedMethods = array_merge(array('getLanguageService'), $mockedMethods);
+               $scheduleModuleMock = $this->getMock(SchedulerModuleController::class, $mockedMethods, array(), '', FALSE);
+               $scheduleModuleMock->expects($this->any())->method('getLanguageService')->willReturn($languageServiceMock);
+
+               return $scheduleModuleMock;
+       }
+
+       /**
+        * @return array
+        */
+       public function validateAdditionalFieldsLogsPeriodErrorDataProvider() {
+               return array(
+                       array('abc'),
+                       array($this->getMockBuilder(CleanerTask::class)->disableOriginalConstructor()->getMock()),
+                       array(NULL),
+                       array(''),
+                       array(0),
+                       array('1234abc')
+               );
+       }
+
+       /**
+        * @param mixed $period
+        * @test
+        * @dataProvider validateAdditionalFieldsLogsPeriodErrorDataProvider
+        */
+       public function validateAdditionalFieldsLogsPeriodError($period) {
+               $submittedData = array(
+                       'RecyclerCleanerPeriod' => $period,
+                       'RecyclerCleanerTCA' => array('pages')
+               );
+
+               $scheduleModuleControllerMock = $this->getScheduleModuleControllerMock(array('addMessage'));
+               $scheduleModuleControllerMock->expects($this->atLeastOnce())
+                       ->method('addMessage')
+                       ->with($this->equalTo('titleTest'), FlashMessage::ERROR);
+
+               $this->subject->validateAdditionalFields($submittedData, $scheduleModuleControllerMock);
+       }
+
+       /**
+        * @return array
+        */
+       public function validateAdditionalFieldsDataProvider() {
+               return array(
+                       array('abc'),
+                       array($this->getMockBuilder(CleanerTask::class)->disableOriginalConstructor()->getMock()),
+                       array(NULL),
+                       array(123)
+               );
+       }
+
+       /**
+        * @param mixed $table
+        * @test
+        * @dataProvider validateAdditionalFieldsDataProvider
+        */
+       public function validateAdditionalFieldsLogsTableError($table) {
+               $submittedData = array(
+                       'RecyclerCleanerPeriod' => 14,
+                       'RecyclerCleanerTCA' => $table
+               );
+
+               $this->subject->validateAdditionalFields($submittedData, $this->getScheduleModuleControllerMock());
+       }
+
+       /**
+        * @test
+        */
+       public function validateAdditionalFieldsIsTrueIfValid() {
+               $submittedData = array(
+                       'RecyclerCleanerPeriod' => 14,
+                       'RecyclerCleanerTCA' => array('pages')
+               );
+
+               $scheduleModuleControllerMock = $this->getScheduleModuleControllerMock();
+               $GLOBALS['TCA']['pages'] = array('foo' => 'bar');
+               $this->assertTrue($this->subject->validateAdditionalFields($submittedData, $scheduleModuleControllerMock));
+       }
+
+       /**
+        * @test
+        */
+       public function saveAdditionalFieldsSavesFields() {
+               $submittedData = array(
+                       'RecyclerCleanerPeriod' => 14,
+                       'RecyclerCleanerTCA' => array('pages')
+               );
+
+               $taskMock = $this->getMock(CleanerTask::class);
+
+               $taskMock->expects($this->once())
+                       ->method('setTcaTables')
+                       ->with($this->equalTo(array('pages')));
+
+               $taskMock->expects($this->once())
+                       ->method('setPeriod')
+                       ->with($this->equalTo(14));
+
+               $this->subject->saveAdditionalFields($submittedData, $taskMock);
+       }
+}
diff --git a/typo3/sysext/recycler/Tests/Unit/Task/CleanerTaskTest.php b/typo3/sysext/recycler/Tests/Unit/Task/CleanerTaskTest.php
new file mode 100644 (file)
index 0000000..f2f4096
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+namespace TYPO3\CMS\Recycler\Tests\Unit\Task;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Recycler\Task\CleanerTask;
+
+/**
+ * Testcase
+ */
+class CleanerTaskTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|CleanerTask
+        */
+       protected $subject = NULL;
+
+       /**
+        * sets up an instance of \TYPO3\CMS\Recycler\Task\CleanerTask
+        */
+       public function setUp() {
+               $this->subject = $this->getMock(CleanerTask::class, array('dummy'), array(), '', FALSE);
+       }
+
+       /**
+        * @test
+        */
+       public function getPeriodCanBeSet() {
+               $period = 14;
+               $this->subject->setPeriod($period);
+
+               $this->assertEquals($period, $this->subject->getPeriod());
+       }
+
+       /**
+        * @test
+        */
+       public function getTcaTablesCanBeSet() {
+               $tables = array('pages', 'tt_content');
+               $this->subject->setTcaTables($tables);
+
+               $this->assertEquals($tables, $this->subject->getTcaTables());
+       }
+
+       /**
+        * @test
+        */
+       public function taskBuildsCorrectQuery() {
+               $GLOBALS['TCA']['pages']['ctrl']['delete'] = 'deleted';
+               $GLOBALS['TCA']['pages']['ctrl']['tstamp'] = 'tstamp';
+
+               /** @var \PHPUnit_Framework_MockObject_MockObject|CleanerTask $subject */
+               $subject = $this->getMock(CleanerTask::class, array('getPeriodAsTimestamp'), array(), '', FALSE);
+
+               $tables = array('pages');
+               $subject->setTcaTables($tables);
+
+               $period = 14;
+               $subject->setPeriod($period);
+               $periodAsTimestamp = strtotime('-' . $period . ' days');
+               $subject->expects($this->once())->method('getPeriodAsTimestamp')->willReturn($periodAsTimestamp);
+
+               $dbMock = $this->getMock(DatabaseConnection::class);
+               $dbMock->expects($this->once())
+                       ->method('exec_DELETEquery')
+                       ->with($this->equalTo('pages'), $this->equalTo('deleted = 1 AND tstamp < ' . $periodAsTimestamp));
+
+               $dbMock->expects($this->once())
+                       ->method('sql_error')
+                       ->will($this->returnValue(''));
+
+               $subject->setDatabaseConnection($dbMock);
+
+               $this->assertTrue($subject->execute());
+       }
+
+       /**
+        * @test
+        */
+       public function taskFailsOnError() {
+               $GLOBALS['TCA']['pages']['ctrl']['delete'] = 'deleted';
+               $GLOBALS['TCA']['pages']['ctrl']['tstamp'] = 'tstamp';
+
+               $tables = array('pages');
+               $this->subject->setTcaTables($tables);
+
+               $period = 14;
+               $this->subject->setPeriod($period);
+
+               $dbMock = $this->getMock(DatabaseConnection::class);
+               $dbMock->expects($this->once())
+                       ->method('sql_error')
+                       ->willReturn(1049);
+
+               $this->subject->setDatabaseConnection($dbMock);
+
+               $this->assertFalse($this->subject->execute());
+       }
+}
index 7ec4801..3fcbcc7 100644 (file)
@@ -4,3 +4,11 @@ defined('TYPO3_MODE') or die();
 if (TYPO3_MODE === 'BE') {
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerAjaxHandler('RecyclerAjaxController::dispatch', \TYPO3\CMS\Recycler\Controller\RecyclerAjaxController::class . '->dispatch');
 }
+$GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX']['RecyclerAjaxController::init'] = \TYPO3\CMS\Recycler\Task\CleanerTask::class . '->init';
+
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Recycler\Task\CleanerTask::class] = array(
+       'extension' => $_EXTKEY,
+       'title' => 'LLL:EXT:' . $_EXTKEY . '/locallang_tasks.xlf:cleanerTaskTitle',
+       'description' => 'LLL:EXT:' . $_EXTKEY . '/locallang_tasks.xlf:cleanerTaskDescription',
+       'additionalFields' => \TYPO3\CMS\Recycler\Task\CleanerFieldProvider::class
+);
\ No newline at end of file
index 26dd67b..34b8b42 100644 (file)
@@ -14,7 +14,10 @@ namespace TYPO3\CMS\Scheduler;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Registry;
 use TYPO3\CMS\Core\Utility\CommandUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
  * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
@@ -51,10 +54,10 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
        /**
         * Adds a task to the pool
         *
-        * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The object representing the task to add
+        * @param Task\AbstractTask $task The object representing the task to add
         * @return bool TRUE if the task was successfully added, FALSE otherwise
         */
-       public function addTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
+       public function addTask(Task\AbstractTask $task) {
                $taskUid = $task->getTaskUid();
                if (empty($taskUid)) {
                        $fields = array(
@@ -64,9 +67,9 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                'task_group' => $task->getTaskGroup(),
                                'serialized_task_object' => 'RESERVED'
                        );
-                       $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
+                       $result = $this->getDatabaseConnection()->exec_INSERTquery('tx_scheduler_task', $fields);
                        if ($result) {
-                               $task->setTaskUid($GLOBALS['TYPO3_DB']->sql_insert_id());
+                               $task->setTaskUid($this->getDatabaseConnection()->sql_insert_id());
                                $task->save();
                                $result = TRUE;
                        } else {
@@ -86,12 +89,13 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
         */
        protected function cleanExecutionArrays() {
                $tstamp = $GLOBALS['EXEC_TIME'];
+               $db = $this->getDatabaseConnection();
                // Select all tasks with executions
                // NOTE: this cleanup is done for disabled tasks too,
                // to avoid leaving old executions lying around
-               $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid, serialized_executions, serialized_task_object', 'tx_scheduler_task', 'serialized_executions <> \'\'');
+               $res = $db->exec_SELECTquery('uid, serialized_executions, serialized_task_object', 'tx_scheduler_task', 'serialized_executions <> \'\'');
                $maxDuration = $this->extConf['maxLifetime'] * 60;
-               while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
+               while ($row = $db->sql_fetch_assoc($res)) {
                        $executions = array();
                        if ($serialized_executions = unserialize($row['serialized_executions'])) {
                                foreach ($serialized_executions as $task) {
@@ -110,20 +114,22 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                } else {
                                        $value = serialize($executions);
                                }
-                               $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . (int)$row['uid'], array('serialized_executions' => $value));
+                               $db->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . (int)$row['uid'], array('serialized_executions' => $value));
                        }
                }
-               $GLOBALS['TYPO3_DB']->sql_free_result($res);
+               $db->sql_free_result($res);
        }
 
        /**
         * This method executes the given task and properly marks and records that execution
         * It is expected to return FALSE if the task was barred from running or if it was not saved properly
         *
-        * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The task to execute
+        * @param Task\AbstractTask $task The task to execute
         * @return bool Whether the task was saved successfully to the database or not
+        * @throws FailedExecutionException
+        * @throws \Exception
         */
-       public function executeTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
+       public function executeTask(Task\AbstractTask $task) {
                // Trigger the saving of the task, as this will calculate its next execution time
                // This should be calculated all the time, even if the execution is skipped
                // (in case it is skipped, this pushes back execution to the next possible date)
@@ -149,7 +155,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                // Execute task
                                $successfullyExecuted = $task->execute();
                                if (!$successfullyExecuted) {
-                                       throw new \TYPO3\CMS\Scheduler\FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
+                                       throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
                                }
                        } catch (\Exception $e) {
                                // Store exception, so that it can be saved to database
@@ -180,23 +186,24 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                if ($type !== 'manual' && $type !== 'cli-by-id') {
                        $type = 'cron';
                }
-               /** @var $registry \TYPO3\CMS\Core\Registry */
-               $registry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
+               /** @var Registry $registry */
+               $registry = GeneralUtility::makeInstance(Registry::class);
                $runInformation = array('start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type);
                $registry->set('tx_scheduler', 'lastRun', $runInformation);
        }
 
        /**
         * Removes a task completely from the system.
+        *
         * @todo find a way to actually kill the existing jobs
         *
-        * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The object representing the task to delete
+        * @param Task\AbstractTask $task The object representing the task to delete
         * @return bool TRUE if task was successfully deleted, FALSE otherwise
         */
-       public function removeTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
+       public function removeTask(Task\AbstractTask $task) {
                $taskUid = $task->getTaskUid();
                if (!empty($taskUid)) {
-                       $result = $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
+                       $result = $this->getDatabaseConnection()->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
                } else {
                        $result = FALSE;
                }
@@ -209,10 +216,10 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
        /**
         * Updates a task in the pool
         *
-        * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Scheduler task object
+        * @param Task\AbstractTask $task Scheduler task object
         * @return bool False if submitted task was not of proper class
         */
-       public function saveTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
+       public function saveTask(Task\AbstractTask $task) {
                $taskUid = $task->getTaskUid();
                if (!empty($taskUid)) {
                        try {
@@ -230,7 +237,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                'task_group' => $task->getTaskGroup(),
                                'serialized_task_object' => serialize($task)
                        );
-                       $result = $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $taskUid, $fields);
+                       $result = $this->getDatabaseConnection()->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $taskUid, $fields);
                } else {
                        $result = FALSE;
                }
@@ -246,7 +253,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
         * If there are no due tasks the method throws an exception.
         *
         * @param int $uid Primary key of a task
-        * @return \TYPO3\CMS\Scheduler\Task\AbstractTask The fetched task object
+        * @return Task\AbstractTask The fetched task object
         * @throws \OutOfBoundsException
         * @throws \UnexpectedValueException
         */
@@ -269,16 +276,17 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                        );
                }
 
-               $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
+               $db = $this->getDatabaseConnection();
+               $res = $db->exec_SELECT_queryArray($queryArray);
                if ($res === FALSE) {
                        throw new \UnexpectedValueException('Query could not be executed. Possible defect in tables tx_scheduler_task or tx_scheduler_task_group', 1422044826);
                }
                // If there are no available tasks, thrown an exception
-               if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
+               if ($db->sql_num_rows($res) == 0) {
                        throw new \OutOfBoundsException('No task', 1247827244);
                } else {
-                       $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
-                       /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
+                       $row = $db->sql_fetch_assoc($res);
+                       /** @var $task Task\AbstractTask */
                        $task = unserialize($row['serialized_task_object']);
                        if ($this->isValidTaskObject($task)) {
                                // The task is valid, return it
@@ -286,11 +294,11 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                        } else {
                                // Forcibly set the disable flag to 1 in the database,
                                // so that the task does not come up again and again for execution
-                               $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $row['uid'], array('disable' => 1));
+                               $db->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $row['uid'], array('disable' => 1));
                                // Throw an exception to raise the problem
                                throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
                        }
-                       $GLOBALS['TYPO3_DB']->sql_free_result($res);
+                       $db->sql_free_result($res);
                }
                return $task;
        }
@@ -305,13 +313,14 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
         * @throws \OutOfBoundsException
         */
        public function fetchTaskRecord($uid) {
-               $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'tx_scheduler_task', 'uid = ' . (int)$uid);
+               $db = $this->getDatabaseConnection();
+               $res = $db->exec_SELECTquery('*', 'tx_scheduler_task', 'uid = ' . (int)$uid);
                // If the task is not found, throw an exception
-               if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
+               if ($db->sql_num_rows($res) == 0) {
                        throw new \OutOfBoundsException('No task', 1247827245);
                } else {
-                       $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
-                       $GLOBALS['TYPO3_DB']->sql_free_result($res);
+                       $row = $db->sql_fetch_assoc($res);
+                       $db->sql_free_result($res);
                }
                return $row;
        }
@@ -336,10 +345,11 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                        }
                        $whereClause .= 'disable = 0';
                }
-               $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
+               $db = $this->getDatabaseConnection();
+               $res = $db->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
                if ($res) {
-                       while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
-                               /** @var $task Task */
+                       while ($row = $db->sql_fetch_assoc($res)) {
+                               /** @var Task\AbstractTask $task */
                                $task = unserialize($row['serialized_task_object']);
                                // Add the task to the list only if it is valid
                                if ($this->isValidTaskObject($task)) {
@@ -347,7 +357,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                        $tasks[] = $task;
                                }
                        }
-                       $GLOBALS['TYPO3_DB']->sql_free_result($res);
+                       $db->sql_free_result($res);
                }
                return $tasks;
        }
@@ -365,7 +375,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
         * @return bool TRUE if object is a task, FALSE otherwise
         */
        public function isValidTaskObject($task) {
-               return $task instanceof \TYPO3\CMS\Scheduler\Task\AbstractTask;
+               return $task instanceof Task\AbstractTask;
        }
 
        /**
@@ -395,11 +405,11 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                if ((int)$this->extConf['useAtdaemon'] !== 1) {
                        return FALSE;
                }
-               /** @var $registry \TYPO3\CMS\Core\Registry */
-               $registry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
+               /** @var $registry Registry */
+               $registry = GeneralUtility::makeInstance(Registry::class);
                // Get at job id from registry and remove at job
                $atJobId = $registry->get('tx_scheduler', 'atJobId');
-               if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($atJobId)) {
+               if (MathUtility::canBeInterpretedAsInteger($atJobId)) {
                        shell_exec('atrm ' . (int)$atJobId . ' 2>&1');
                }
                // Can not use fetchTask() here because if tasks have just executed
@@ -408,7 +418,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                $nextExecution = FALSE;
                foreach ($tasks as $task) {
                        try {
-                               /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
+                               /** @var $task Task\AbstractTask */
                                $tempNextExecution = $task->getNextDueExecution();
                                if ($nextExecution === FALSE || $tempNextExecution < $nextExecution) {
                                        $nextExecution = $tempNextExecution;
@@ -431,12 +441,12 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                        $output = shell_exec($cmd);
                        $outputParts = '';
                        foreach (explode(LF, $output) as $outputLine) {
-                               if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
+                               if (GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
                                        $outputParts = explode(' ', $outputLine, 3);
                                        break;
                                }
                        }
-                       if ($outputParts[0] === 'job' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
+                       if ($outputParts[0] === 'job' && MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
                                $atJobId = (int)$outputParts[1];
                                $registry->set('tx_scheduler', 'atJobId', $atJobId);
                        }
@@ -444,4 +454,10 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                return TRUE;
        }
 
+       /**
+        * @return \TYPO3\CMS\Core\Database\DatabaseConnection
+        */
+       protected function getDatabaseConnection() {
+               return $GLOBALS['TYPO3_DB'];
+       }
 }