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