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