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