d5ffd9c1875f55e42e74eea6043ced6d1bb86d23
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / CronCommand / NormalizeCommand.php
1 <?php
2 namespace TYPO3\CMS\Scheduler\CronCommand;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Christian Kuhn <lolli@schwarzbu.ch>
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 use TYPO3\CMS\Core\Utility\MathUtility;
27
28 /**
29 * Validate and normalize a cron command.
30 *
31 * Special fields like three letter weekdays, ranges and steps are substituted
32 * to a comma separated list of integers. Example:
33 * '2-4 10-40/10 * mar * fri' will be normalized to '2,4 10,20,30,40 * * 3 1,2'
34 *
35 * @author Christian Kuhn <lolli@schwarzbu.ch>
36 */
37 class NormalizeCommand {
38
39 /**
40 * Main API method: Get the cron command and normalize it.
41 *
42 * If no exception is thrown, the resulting cron command is validated
43 * and consists of five whitespace separated fields, which are either
44 * the letter '*' or a sorted, unique comma separated list of integers.
45 *
46 * @api
47 * @throws \InvalidArgumentException cron command is invalid or out of bounds
48 * @param string $cronCommand The cron command to normalize
49 * @return string Normalized cron command
50 */
51 static public function normalize($cronCommand) {
52 $cronCommand = trim($cronCommand);
53 $cronCommand = self::convertKeywordsToCronCommand($cronCommand);
54 $cronCommand = self::normalizeFields($cronCommand);
55 return $cronCommand;
56 }
57
58 /**
59 * Accept special cron command keywords and convert to standard cron syntax.
60 * Allowed keywords: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly
61 *
62 * @param string $cronCommand Cron command
63 * @return string Normalized cron command if keyword was found, else unchanged cron command
64 */
65 static protected function convertKeywordsToCronCommand($cronCommand) {
66 switch ($cronCommand) {
67 case '@yearly':
68
69 case '@annually':
70 $cronCommand = '0 0 1 1 *';
71 break;
72 case '@monthly':
73 $cronCommand = '0 0 1 * *';
74 break;
75 case '@weekly':
76 $cronCommand = '0 0 * * 0';
77 break;
78 case '@daily':
79
80 case '@midnight':
81 $cronCommand = '0 0 * * *';
82 break;
83 case '@hourly':
84 $cronCommand = '0 * * * *';
85 break;
86 }
87 return $cronCommand;
88 }
89
90 /**
91 * Normalize cron command field to list of integers or *
92 *
93 * @param string $cronCommand cron command
94 * @return string Normalized cron command
95 */
96 static protected function normalizeFields($cronCommand) {
97 $fieldArray = self::splitFields($cronCommand);
98 $fieldArray[0] = self::normalizeIntegerField($fieldArray[0], 0, 59);
99 $fieldArray[1] = self::normalizeIntegerField($fieldArray[1], 0, 23);
100 $fieldArray[2] = self::normalizeIntegerField($fieldArray[2], 1, 31);
101 $fieldArray[3] = self::normalizeMonthAndWeekdayField($fieldArray[3], TRUE);
102 $fieldArray[4] = self::normalizeMonthAndWeekdayField($fieldArray[4], FALSE);
103 $normalizedCronCommand = implode(' ', $fieldArray);
104 return $normalizedCronCommand;
105 }
106
107 /**
108 * Split a given cron command like '23 * * * *' to an array with five fields.
109 *
110 * @throws \InvalidArgumentException If splitted array does not contain five entries
111 * @param string $cronCommand cron command
112 * @return array
113 */
114 static protected function splitFields($cronCommand) {
115 $fields = explode(' ', $cronCommand);
116 if (count($fields) !== 5) {
117 throw new \InvalidArgumentException('Unable to split given cron command to five fields.', 1291227373);
118 }
119 return $fields;
120 }
121
122 /**
123 * Normalize month field.
124 *
125 * @param string $expression Month field expression
126 * @param boolean $isMonthField TRUE if month field is handled, FALSE for weekday field
127 * @return string Normalized expression
128 */
129 static protected function normalizeMonthAndWeekdayField($expression, $isMonthField = TRUE) {
130 if ((string) $expression === '*') {
131 $fieldValues = '*';
132 } else {
133 // Fragment expression by , / and - and substitute three letter code of month and weekday to numbers
134 $listOfCommaValues = explode(',', $expression);
135 $fieldArray = array();
136 foreach ($listOfCommaValues as $listElement) {
137 if (strpos($listElement, '/') !== FALSE) {
138 list($left, $right) = explode('/', $listElement);
139 if (strpos($left, '-') !== FALSE) {
140 list($leftBound, $rightBound) = explode('-', $left);
141 $leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
142 $rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
143 $left = $leftBound . '-' . $rightBound;
144 } else {
145 if ((string) $left !== '*') {
146 $left = self::normalizeMonthAndWeekday($left, $isMonthField);
147 }
148 }
149 $fieldArray[] = $left . '/' . $right;
150 } elseif (strpos($listElement, '-') !== FALSE) {
151 list($left, $right) = explode('-', $listElement);
152 $left = self::normalizeMonthAndWeekday($left, $isMonthField);
153 $right = self::normalizeMonthAndWeekday($right, $isMonthField);
154 $fieldArray[] = $left . '-' . $right;
155 } else {
156 $fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
157 }
158 }
159 $fieldValues = implode(',', $fieldArray);
160 }
161 return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
162 }
163
164 /**
165 * Normalize integer field.
166 *
167 * @throws \InvalidArgumentException If field is invalid or out of bounds
168 * @param string $expression Expression
169 * @param integer $lowerBound Lower limit of result list
170 * @param integer $upperBound Upper limit of result list
171 * @return string Normalized expression
172 */
173 static protected function normalizeIntegerField($expression, $lowerBound = 0, $upperBound = 59) {
174 if ((string) $expression === '*') {
175 $fieldValues = '*';
176 } else {
177 $listOfCommaValues = explode(',', $expression);
178 $fieldArray = array();
179 foreach ($listOfCommaValues as $listElement) {
180 if (strpos($listElement, '/') !== FALSE) {
181 list($left, $right) = explode('/', $listElement);
182 if ((string) $left === '*') {
183 $leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
184 } else {
185 $leftList = self::convertRangeToListOfValues($left);
186 }
187 $fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
188 } elseif (strpos($listElement, '-') !== FALSE) {
189 $fieldArray[] = self::convertRangeToListOfValues($listElement);
190 } elseif (MathUtility::canBeInterpretedAsInteger($listElement)) {
191 $fieldArray[] = $listElement;
192 } else {
193 throw new \InvalidArgumentException('Unable to normalize integer field.', 1291429389);
194 }
195 }
196 $fieldValues = implode(',', $fieldArray);
197 }
198 if (strlen($fieldValues) === 0) {
199 throw new \InvalidArgumentException('Unable to convert integer field to list of values: Result list empty.', 1291422012);
200 }
201 if ((string) $fieldValues !== '*') {
202 $fieldList = explode(',', $fieldValues);
203 sort($fieldList);
204 $fieldList = array_unique($fieldList);
205 if (current($fieldList) < $lowerBound) {
206 throw new \InvalidArgumentException('Lowest element in list is smaller than allowed.', 1291470084);
207 }
208 if (end($fieldList) > $upperBound) {
209 throw new \InvalidArgumentException('An element in the list is higher than allowed.', 1291470170);
210 }
211 $fieldValues = implode(',', $fieldList);
212 }
213 return (string) $fieldValues;
214 }
215
216 /**
217 * Convert a range of integers to a list: 4-6 results in a string '4,5,6'
218 *
219 * @throws \InvalidArgumentException If range can not be converted to list
220 * @param string $range Integer-integer
221 * @return array
222 */
223 static protected function convertRangeToListOfValues($range) {
224 if (strlen($range) === 0) {
225 throw new \InvalidArgumentException('Unable to convert range to list of values with empty string.', 1291234985);
226 }
227 $rangeArray = explode('-', $range);
228 // Sanitize fields and cast to integer
229 foreach ($rangeArray as $fieldNumber => $fieldValue) {
230 if (!MathUtility::canBeInterpretedAsInteger($fieldValue)) {
231 throw new \InvalidArgumentException('Unable to convert value to integer.', 1291237668);
232 }
233 $rangeArray[$fieldNumber] = (int)$fieldValue;
234 }
235 $resultList = '';
236 if (count($rangeArray) === 1) {
237 $resultList = $rangeArray[0];
238 } elseif (count($rangeArray) === 2) {
239 $left = $rangeArray[0];
240 $right = $rangeArray[1];
241 if ($left > $right) {
242 throw new \InvalidArgumentException('Unable to convert range to list: Left integer must not be greater than right integer.', 1291237145);
243 }
244 $resultListArray = array();
245 for ($i = $left; $i <= $right; $i++) {
246 $resultListArray[] = $i;
247 }
248 $resultList = implode(',', $resultListArray);
249 } else {
250 throw new \InvalidArgumentException('Unable to convert range to list of values.', 1291234986);
251 }
252 return (string) $resultList;
253 }
254
255 /**
256 * Reduce a given list of values by step value.
257 * Following a range with ``/<number>'' specifies skips of the number's value through the range.
258 * 1-5/2 -> 1,3,5
259 * 2-10/3 -> 2,5,8
260 *
261 * @throws \InvalidArgumentException if step value is invalid or if resulting list is empty
262 * @param string $stepExpression Step value expression
263 * @return string Comma separated list of valid values
264 */
265 static protected function reduceListOfValuesByStepValue($stepExpression) {
266 if (strlen($stepExpression) === 0) {
267 throw new \InvalidArgumentException('Unable to convert step values.', 1291234987);
268 }
269 $stepValuesAndStepArray = explode('/', $stepExpression);
270 if (count($stepValuesAndStepArray) < 1 || count($stepValuesAndStepArray) > 2) {
271 throw new \InvalidArgumentException('Unable to convert step values: Multiple slashes found.', 1291242168);
272 }
273 $left = $stepValuesAndStepArray[0];
274 $right = $stepValuesAndStepArray[1];
275 if (strlen($stepValuesAndStepArray[0]) === 0) {
276 throw new \InvalidArgumentException('Unable to convert step values: Left part of / is empty.', 1291414955);
277 }
278 if (strlen($stepValuesAndStepArray[1]) === 0) {
279 throw new \InvalidArgumentException('Unable to convert step values: Right part of / is empty.', 1291414956);
280 }
281 if (!MathUtility::canBeInterpretedAsInteger($right)) {
282 throw new \InvalidArgumentException('Unable to convert step values: Right part must be a single integer.', 1291414957);
283 }
284 $right = (int)$right;
285 $leftArray = explode(',', $left);
286 $validValues = array();
287 $currentStep = $right;
288 foreach ($leftArray as $leftValue) {
289 if (!MathUtility::canBeInterpretedAsInteger($leftValue)) {
290 throw new \InvalidArgumentException('Unable to convert step values: Left part must be a single integer or comma separated list of integers.', 1291414958);
291 }
292 if ($currentStep === 0) {
293 $currentStep = $right;
294 }
295 if ($currentStep === $right) {
296 $validValues[] = (int)$leftValue;
297 }
298 $currentStep--;
299 }
300 if (count($validValues) === 0) {
301 throw new \InvalidArgumentException('Unable to convert step values: Result value list is empty.', 1291414959);
302 }
303 return implode(',', $validValues);
304 }
305
306 /**
307 * Dispatcher method for normalizeMonth and normalizeWeekday
308 *
309 * @param string $expression Month or weekday to be normalized
310 * @param boolean $isMonth TRUE if a month is handled, FALSE for weekday
311 * @return string normalized month or weekday
312 */
313 static protected function normalizeMonthAndWeekday($expression, $isMonth = TRUE) {
314 $expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
315 return (string) $expression;
316 }
317
318 /**
319 * Accept a string representation or integer number of a month like
320 * 'jan', 'February', 01, ... and convert to normalized integer value 1 .. 12
321 *
322 * @throws \InvalidArgumentException If month string can not be converted to integer
323 * @param string $month Month representation
324 * @return integer month integer representation between 1 and 12
325 */
326 static protected function normalizeMonth($month) {
327 $timestamp = strtotime('2010-' . $month . '-01');
328 // timestamp must be >= 2010-01-01 and <= 2010-12-01
329 if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
330 throw new \InvalidArgumentException('Unable to convert given month name.', 1291083486);
331 }
332 return (int)date('n', $timestamp);
333 }
334
335 /**
336 * Accept a string representation or integer number of a weekday like
337 * 'mon', 'Friday', 3, ... and convert to normalized integer value 1 .. 7
338 *
339 * @throws \InvalidArgumentException If weekday string can not be converted
340 * @param string $weekday Weekday representation
341 * @return integer weekday integer representation between 1 and 7
342 */
343 static protected function normalizeWeekday($weekday) {
344 $normalizedWeekday = FALSE;
345 // 0 (sunday) -> 7
346 if ((string) $weekday === '0') {
347 $weekday = 7;
348 }
349 if ($weekday >= 1 && $weekday <= 7) {
350 $normalizedWeekday = (int)$weekday;
351 }
352 if (!$normalizedWeekday) {
353 // Convert string representation like 'sun' to integer
354 $timestamp = strtotime('next ' . $weekday, mktime(0, 0, 0, 1, 1, 2010));
355 if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
356 throw new \InvalidArgumentException('Unable to convert given weekday name.', 1291163589);
357 }
358 $normalizedWeekday = (int)date('N', $timestamp);
359 }
360 return $normalizedWeekday;
361 }
362
363 }