[FOLLOWUP][BUGFIX] keep options array for TableGarbageCollectionTask
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / CronCommand / CronCommand.php
1 <?php
2 namespace TYPO3\CMS\Scheduler\CronCommand;
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 /**
18 * This class provides calculations for the cron command format.
19 *
20 * @author Markus Friedrich <markus.friedrich@dkd.de>
21 * @author Christian Kuhn <lolli@schwarzbu.ch>
22 */
23 class CronCommand {
24
25 /**
26 * Normalized sections of the cron command.
27 * Allowed are comma separated lists of integers and the character '*'
28 *
29 * field lower and upper bound
30 * ----- --------------
31 * minute 0-59
32 * hour 0-23
33 * day of month 1-31
34 * month 1-12
35 * day of week 1-7
36 *
37 * @var array $cronCommandSections
38 */
39 protected $cronCommandSections;
40
41 /**
42 * Timestamp of next execution date.
43 * This value starts with 'now + 1 minute' if not set externally
44 * by unit tests. After a call to calculateNextValue() it holds the timestamp of
45 * the next execution date which matches the cron command restrictions.
46 */
47 protected $timestamp;
48
49 /**
50 * Constructor
51 *
52 * @api
53 * @param string $cronCommand The cron command can hold any combination documented as valid
54 * @param bool|int $timestamp Optional start time, used in unit tests
55 * @return \TYPO3\CMS\Scheduler\CronCommand\CronCommand
56 */
57 public function __construct($cronCommand, $timestamp = FALSE) {
58 $cronCommand = NormalizeCommand::normalize($cronCommand);
59 // Explode cron command to sections
60 $this->cronCommandSections = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(' ', $cronCommand);
61 // Initialize the values with the starting time
62 // This takes care that the calculated time is always in the future
63 if ($timestamp === FALSE) {
64 $timestamp = strtotime('+1 minute');
65 } else {
66 $timestamp += 60;
67 }
68 $this->timestamp = $this->roundTimestamp($timestamp);
69 }
70
71 /**
72 * Calculates the date of the next execution.
73 *
74 * @api
75 * @return void
76 * @throws \RuntimeException
77 */
78 public function calculateNextValue() {
79 $newTimestamp = $this->getTimestamp();
80 // Calculate next minute and hour field
81 $loopCount = 0;
82 while (TRUE) {
83 $loopCount++;
84 // If there was no match within two days, cron command is invalid.
85 // The second day is needed to catch the summertime leap in some countries.
86 if ($loopCount > 2880) {
87 throw new \RuntimeException('Unable to determine next execution timestamp: Hour and minute combination is invalid.', 1291494126);
88 }
89 if ($this->minuteAndHourMatchesCronCommand($newTimestamp)) {
90 break;
91 }
92 $newTimestamp += 60;
93 }
94 $loopCount = 0;
95 while (TRUE) {
96 $loopCount++;
97 // A date must match within the next 4 years, this high number makes
98 // sure leap year cron command configuration are caught.
99 // If the loop runs longer than that, the cron command is invalid.
100 if ($loopCount > 1464) {
101 throw new \RuntimeException('Unable to determine next execution timestamp: Day of month, month and day of week combination is invalid.', 1291501280);
102 }
103 if ($this->dayMatchesCronCommand($newTimestamp)) {
104 break;
105 }
106 $newTimestamp += $this->numberOfSecondsInDay($newTimestamp);
107 }
108 $this->timestamp = $newTimestamp;
109 }
110
111 /**
112 * Get next timestamp
113 *
114 * @api
115 * @return int Unix timestamp
116 */
117 public function getTimestamp() {
118 return $this->timestamp;
119 }
120
121 /**
122 * Get cron command sections. Array of strings, each containing either
123 * a list of comma separated integers or *
124 *
125 * @return array command sections:
126 */
127 public function getCronCommandSections() {
128 return $this->cronCommandSections;
129 }
130
131 /**
132 * Determine if current timestamp matches minute and hour cron command restriction.
133 *
134 * @param int $timestamp to test
135 * @return bool TRUE if cron command conditions are met
136 */
137 protected function minuteAndHourMatchesCronCommand($timestamp) {
138 $minute = (int)date('i', $timestamp);
139 $hour = (int)date('G', $timestamp);
140 $commandMatch = FALSE;
141 if ($this->isInCommandList($this->cronCommandSections[0], $minute) && $this->isInCommandList($this->cronCommandSections[1], $hour)) {
142 $commandMatch = TRUE;
143 }
144 return $commandMatch;
145 }
146
147 /**
148 * Determine if current timestamp matches day of month, month and day of week
149 * cron command restriction
150 *
151 * @param int $timestamp to test
152 * @return bool TRUE if cron command conditions are met
153 */
154 protected function dayMatchesCronCommand($timestamp) {
155 $dayOfMonth = date('j', $timestamp);
156 $month = date('n', $timestamp);
157 $dayOfWeek = date('N', $timestamp);
158 $isInDayOfMonth = $this->isInCommandList($this->cronCommandSections[2], $dayOfMonth);
159 $isInMonth = $this->isInCommandList($this->cronCommandSections[3], $month);
160 $isInDayOfWeek = $this->isInCommandList($this->cronCommandSections[4], $dayOfWeek);
161 // Quote from vixiecron:
162 // Note: The day of a command's execution can be specified by two fields — day of month, and day of week.
163 // If both fields are restricted (i.e., aren't *), the command will be run when either field
164 // matches the current time. For example, `30 4 1,15 * 5' would cause
165 // a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
166 $isDayOfMonthRestricted = (string)$this->cronCommandSections[2] !== '*';
167 $isDayOfWeekRestricted = (string)$this->cronCommandSections[4] !== '*';
168 $commandMatch = FALSE;
169 if ($isInMonth) {
170 if ($isInDayOfMonth && $isDayOfMonthRestricted || $isInDayOfWeek && $isDayOfWeekRestricted || $isInDayOfMonth && !$isDayOfMonthRestricted && $isInDayOfWeek && !$isDayOfWeekRestricted) {
171 $commandMatch = TRUE;
172 }
173 }
174 return $commandMatch;
175 }
176
177 /**
178 * Determine if a given number validates a cron command section. The given cron
179 * command must be a 'normalized' list with only comma separated integers or '*'
180 *
181 * @param string $commandExpression: cron command
182 * @param int $numberToMatch: number to look up
183 * @return bool TRUE if number is in list
184 */
185 protected function isInCommandList($commandExpression, $numberToMatch) {
186 $inList = FALSE;
187 if ((string)$commandExpression === '*') {
188 $inList = TRUE;
189 } else {
190 $inList = \TYPO3\CMS\Core\Utility\GeneralUtility::inList($commandExpression, $numberToMatch);
191 }
192 return $inList;
193 }
194
195 /**
196 * Helper method to calculate number of seconds in a day.
197 *
198 * This is not always 86400 (60*60*24) and depends on the timezone:
199 * Some countries like Germany have a summertime / wintertime switch,
200 * on every last sunday in march clocks are forwarded by one hour (set from 2:00 to 3:00),
201 * and on last sunday of october they are set back one hour (from 3:00 to 2:00).
202 * This shortens and lengthens the length of a day by one hour.
203 *
204 * @param int $timestamp Unix timestamp
205 * @return int Number of seconds of day
206 */
207 protected function numberOfSecondsInDay($timestamp) {
208 $now = mktime(0, 0, 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
209 // Make sure to be in next day, even if day has 25 hours
210 $nextDay = $now + 60 * 60 * 25;
211 $nextDay = mktime(0, 0, 0, date('n', $nextDay), date('j', $nextDay), date('Y', $nextDay));
212 return $nextDay - $now;
213 }
214
215 /**
216 * Round a timestamp down to full minute.
217 *
218 * @param int $timestamp Unix timestamp
219 * @return int Rounded timestamp
220 */
221 protected function roundTimestamp($timestamp) {
222 return mktime(date('H', $timestamp), date('i', $timestamp), 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
223 }
224
225 }