5f9388a7b6d5679ba84baf48eb6029f2fc159649
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / class.tx_scheduler_croncmd_normalize.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2010-2011 Christian Kuhn <lolli@schwarzbu.ch>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25 /**
26 * Validate and normalize a cron command.
27 *
28 * Special fields like three letter weekdays, ranges and steps are substituted
29 * to a comma separated list of integers. Example:
30 * '2-4 10-40/10 * mar * fri' will be nolmalized to '2,4 10,20,30,40 * * 3 1,2'
31 *
32 * @author Christian Kuhn <lolli@schwarzbu.ch>
33 *
34 * @package TYPO3
35 * @subpackage scheduler
36 */
37 class tx_scheduler_CronCmd_Normalize {
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 public static 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 protected static function convertKeywordsToCronCommand($cronCommand) {
66 switch ($cronCommand) {
67 case '@yearly':
68 case '@annually':
69 $cronCommand = '0 0 1 1 *';
70 break;
71 case '@monthly':
72 $cronCommand = '0 0 1 * *';
73 break;
74 case '@weekly':
75 $cronCommand = '0 0 * * 0';
76 break;
77 case '@daily':
78 case '@midnight':
79 $cronCommand = '0 0 * * *';
80 break;
81 case '@hourly':
82 $cronCommand = '0 * * * *';
83 break;
84 }
85
86 return $cronCommand;
87 }
88
89 /**
90 * Normalize cron command field to list of integers or *
91 *
92 * @param string $cronCommand cron command
93 * @return string Normalized cron command
94 */
95 protected static function normalizeFields($cronCommand) {
96 $fieldArray = self::splitFields($cronCommand);
97
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
104 $normalizedCronCommand = implode(' ', $fieldArray);
105 return $normalizedCronCommand;
106 }
107
108 /**
109 * Split a given cron command like '23 * * * *' to an array with five fields.
110 *
111 * @throws InvalidArgumentException If splitted array does not contain five entries
112 * @param string $cronCommand cron command
113 * @return array
114 * 0 => minute field
115 * 1 => hour field
116 * 2 => day of month field
117 * 3 => month field
118 * 4 => day of week field
119 */
120 protected static function splitFields($cronCommand) {
121 $fields = explode(' ', $cronCommand);
122
123 if (count($fields) !== 5) {
124 throw new InvalidArgumentException(
125 'Unable to split given cron command to five fields.',
126 1291227373
127 );
128 }
129
130 return $fields;
131 }
132
133 /**
134 * Normalize month field.
135 *
136 * @param string $expression Month field expression
137 * @param boolean $isMonthField TRUE if month field is handled, FALSE for weekday field
138 * @return string Normalized expression
139 */
140 protected static function normalizeMonthAndWeekdayField($expression, $isMonthField = TRUE) {
141 if ((string)$expression === '*') {
142 $fieldValues = '*';
143 } else {
144 // Fragment espression by , / and - and substitute three letter code of month and weekday to numbers
145 $listOfCommaValues = explode(',', $expression);
146 $fieldArray = array();
147 foreach ($listOfCommaValues as $listElement) {
148 if (strpos($listElement, '/') !== FALSE) {
149 list($left, $right) = explode('/', $listElement);
150 if (strpos($left, '-') !== FALSE) {
151 list($leftBound, $rightBound) = explode('-', $left);
152 $leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
153 $rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
154 $left = $leftBound . '-' . $rightBound;
155 } else {
156 if ((string)$left !== '*') {
157 $left = self::normalizeMonthAndWeekday($left, $isMonthField);
158 }
159 }
160 $fieldArray[] = $left . '/' . $right;
161 } elseif (strpos($listElement, '-') !== FALSE) {
162 list($left, $right) = explode('-', $listElement);
163 $left = self::normalizeMonthAndWeekday($left, $isMonthField);
164 $right = self::normalizeMonthAndWeekday($right, $isMonthField);
165 $fieldArray[] = $left . '-' . $right;
166 } else {
167 $fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
168 }
169 }
170 $fieldValues = implode(',', $fieldArray);
171 }
172
173 return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
174 }
175
176 /**
177 * Normalize integer field.
178 *
179 * @throws InvalidArgumentException If field is invalid or out of bounds
180 * @param string $expression Expression
181 * @param integer $lowerBound Lower limit of result list
182 * @param integer $upperBound Upper limit of result list
183 * @return string Normalized expression
184 */
185 protected static function normalizeIntegerField($expression, $lowerBound = 0, $upperBound = 59) {
186 if ((string)$expression === '*') {
187 $fieldValues = '*';
188 } else {
189 $listOfCommaValues = explode(',', $expression);
190 $fieldArray = array();
191 foreach ($listOfCommaValues as $listElement) {
192 if (strpos($listElement, '/') !== FALSE) {
193 list($left, $right) = explode('/', $listElement);
194 if ((string)$left === '*') {
195 $leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
196 } else {
197 $leftList = self::convertRangeToListOfValues($left);
198 }
199 $fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
200 } elseif (strpos($listElement, '-') !== FALSE) {
201 $fieldArray[] = self::convertRangeToListOfValues($listElement);
202 } elseif (strcmp(intval($listElement), $listElement) === 0) {
203 $fieldArray[] = $listElement;
204 } else {
205 throw new InvalidArgumentException(
206 'Unable to normalize integer field.',
207 1291429389
208 );
209 }
210 }
211 $fieldValues = implode(',', $fieldArray);
212 }
213
214 if (strlen($fieldValues) === 0) {
215 throw new InvalidArgumentException(
216 'Unable to convert integer field to list of values: Result list empty.',
217 1291422012
218 );
219 }
220
221 if ((string)$fieldValues !== '*') {
222 $fieldList = explode(',', $fieldValues);
223
224 sort($fieldList);
225 $fieldList = array_unique($fieldList);
226
227 if (current($fieldList) < $lowerBound) {
228 throw new InvalidArgumentException(
229 'Lowest element in list is smaller than allowed.',
230 1291470084
231 );
232 }
233
234 if (end($fieldList) > $upperBound) {
235 throw new InvalidArgumentException(
236 'An element in the list is higher than allowed.',
237 1291470170
238 );
239 }
240
241 $fieldValues = implode(',', $fieldList);
242 }
243
244 return (string)$fieldValues;
245 }
246
247
248 /**
249 * Convert a range of integers to a list: 4-6 results in a string '4,5,6'
250 *
251 * @throws InvalidArgumentException If range can not be converted to list
252 * @param string $range Integer-integer
253 * @return array
254 */
255 protected static function convertRangeToListOfValues($range) {
256 if (strlen($range) === 0) {
257 throw new InvalidArgumentException(
258 'Unable to convert range to list of values with empty string.',
259 1291234985
260 );
261 }
262
263 $rangeArray = explode('-', $range);
264
265 // Sanitize fields and cast to integer
266 foreach ($rangeArray as $fieldNumber => $fieldValue) {
267 if (strcmp(intval($fieldValue), $fieldValue) !== 0) {
268 throw new InvalidArgumentException(
269 'Unable to convert value to integer.',
270 1291237668
271 );
272 }
273 $rangeArray[$fieldNumber] = (int)$fieldValue;
274 }
275
276 $resultList = '';
277 if (count($rangeArray) === 1) {
278 $resultList = $rangeArray[0];
279 } elseif (count($rangeArray) === 2) {
280 $left = $rangeArray[0];
281 $right = $rangeArray[1];
282
283 if ($left > $right) {
284 throw new InvalidArgumentException(
285 'Unable to convert range to list: Left integer must not be greather than right integer.',
286 1291237145
287 );
288 }
289
290 $resultListArray = array();
291 for ($i = $left; $i <= $right; $i++) {
292 $resultListArray[] = $i;
293 }
294
295 $resultList = implode(',', $resultListArray);
296 } else {
297 throw new InvalidArgumentException(
298 'Unable to convert range to list of values.',
299 1291234985
300 );
301 }
302
303 return (string)$resultList;
304 }
305
306 /**
307 * Reduce a given list of values by step value.
308 * Following a range with ``/<number>'' specifies skips of the number's value through the range.
309 * 1-5/2 -> 1,3,5
310 * 2-10/3 -> 2,5,8
311 *
312 * @throws Exception if step value is invalid or if resulting list is empty
313 * @param string $stepExpression Step value expression
314 * @return string Comma separated list of valid values
315 */
316 protected static function reduceListOfValuesByStepValue($stepExpression) {
317 if (strlen($stepExpression) === 0) {
318 throw new InvalidArgumentException(
319 'Unable to convert step values.',
320 1291234985
321 );
322 }
323
324 $stepValuesAndStepArray = explode('/', $stepExpression);
325
326 if (count($stepValuesAndStepArray) < 1 || count($stepValuesAndStepArray) > 2) {
327 throw new InvalidArgumentException(
328 'Unable to convert step values: Multiple slashes found.',
329 1291242168
330 );
331 }
332
333 $left = $stepValuesAndStepArray[0];
334 $right = $stepValuesAndStepArray[1];
335
336 if (strlen($stepValuesAndStepArray[0]) === 0) {
337 throw new InvalidArgumentException(
338 'Unable to convert step values: Left part of / is empty.',
339 1291414955
340 );
341 }
342
343 if (strlen($stepValuesAndStepArray[1]) === 0) {
344 throw new InvalidArgumentException(
345 'Unable to convert step values: Right part of / is empty.',
346 1291414956
347 );
348 }
349
350 if (strcmp(intval($right), $right) !== 0) {
351 throw new InvalidArgumentException(
352 'Unable to convert step values: Right part must be a single integer.',
353 1291414957
354 );
355 }
356
357 $right = (int)$right;
358 $leftArray = explode(',', $left);
359
360 $validValues = array();
361 $currentStep = $right;
362 foreach ($leftArray as $leftValue) {
363 if (strcmp(intval($leftValue), $leftValue) !== 0) {
364 throw new InvalidArgumentException(
365 'Unable to convert step values: Left part must be a single integer or comma separated list of integers.',
366 1291414958
367 );
368 }
369
370 if ($currentStep === 0) {
371 $currentStep = $right;
372 }
373
374 if ($currentStep === $right) {
375 $validValues[] = (int)$leftValue;
376 }
377
378 $currentStep --;
379 }
380
381 if (count($validValues) === 0) {
382 throw new InvalidArgumentException(
383 'Unable to convert step values: Result value list is empty.',
384 1291414958
385 );
386 }
387
388 return implode(',', $validValues);
389 }
390
391 /**
392 * Dispatcher method for normalizeMonth and normalizeWeekday
393 *
394 * @param string $expression Month or weekday to be normalized
395 * @param boolean $isMonth TRUE if a month is handled, FALSE for weekday
396 * @return string normalized month or weekday
397 */
398 protected static function normalizeMonthAndWeekday($expression, $isMonth = TRUE) {
399 $expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
400
401 return (string)$expression;
402 }
403
404 /**
405 * Accept a string representation or integer number of a month like
406 * 'jan', 'February', 01, ... and convert to normalized integer value 1 .. 12
407 *
408 * @throws InvalidArgumentException If month string can not be converted to integer
409 * @param string $month Month representation
410 * @return integer month integer representation between 1 and 12
411 */
412 protected static function normalizeMonth($month) {
413 $timestamp = strtotime('2010-' . $month . '-01');
414
415 // timestamp must be >= 2010-01-01 and <= 2010-12-01
416 if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
417 throw new InvalidArgumentException(
418 'Unable to convert given month name.',
419 1291083486
420 );
421 }
422
423 return (int)date('n', $timestamp);
424 }
425
426 /**
427 * Accept a string representation or integer number of a weekday like
428 * 'mon', 'Friday', 3, ... and convert to normalized integer value 1 .. 7
429 *
430 * @throws InvalidArgumentException If weekday string can not be converted
431 * @param string $weekday Weekday representation
432 * @return integer weekday integer representation between 1 and 7
433 */
434 protected static function normalizeWeekday($weekday) {
435 $normalizedWeekday = FALSE;
436
437 // 0 (sunday) -> 7
438 if ((string)$weekday === '0') {
439 $weekday = 7;
440 }
441
442 if ($weekday >= 1 && $weekday <= 7) {
443 $normalizedWeekday = (int)$weekday;
444 }
445
446 if (!$normalizedWeekday) {
447 // Convert string representation like 'sun' to integer
448 $timestamp = strtotime('next ' . $weekday, mktime(0, 0, 0, 1, 1, 2010));
449 if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
450 throw new InvalidArgumentException(
451 'Unable to convert given weekday name.',
452 1291163589
453 );
454 }
455 $normalizedWeekday = (int)date('N', $timestamp);
456 }
457
458 return $normalizedWeekday;
459 }
460 }
461 ?>