TYPO3 API  SVNRelease
class.tx_scheduler_croncmd_normalize.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003  *  Copyright notice
00004  *
00005  *  (c) 2010-2011 Christian Kuhn <lolli@schwarzbu.ch>
00006  *  All rights reserved
00007  *
00008  *  This script is part of the TYPO3 project. The TYPO3 project is
00009  *  free software; you can redistribute it and/or modify
00010  *  it under the terms of the GNU General Public License as published by
00011  *  the Free Software Foundation; either version 2 of the License, or
00012  *  (at your option) any later version.
00013  *
00014  *  The GNU General Public License can be found at
00015  *  http://www.gnu.org/copyleft/gpl.html.
00016  *
00017  *  This script is distributed in the hope that it will be useful,
00018  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00019  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020  *  GNU General Public License for more details.
00021  *
00022  *  This copyright notice MUST APPEAR in all copies of the script!
00023  ***************************************************************/
00024 
00025 /**
00026  * Validate and normalize a cron command.
00027  *
00028  * Special fields like three letter weekdays, ranges and steps are substituted
00029  * to a comma separated list of integers. Example:
00030  *  '2-4 10-40/10 * mar * fri'  will be nolmalized to '2,4 10,20,30,40 * * 3 1,2'
00031  *
00032  * @author Christian Kuhn <lolli@schwarzbu.ch>
00033  *
00034  * @package TYPO3
00035  * @subpackage scheduler
00036  */
00037 final class tx_scheduler_CronCmd_Normalize {
00038 
00039     /**
00040      * Main API method: Get the cron command and normalize it.
00041      *
00042      * If no exception is thrown, the resulting cron command is validated
00043      * and consists of five whitespace separated fields, which are either
00044      * the letter '*' or a sorted, unique comma separated list of integers.
00045      *
00046      * @api
00047      * @throws InvalidArgumentException cron command is invalid or out of bounds
00048      * @param string $cronCommand The cron command to normalize
00049      * @return string Normalized cron command
00050      */
00051     public static function normalize($cronCommand) {
00052         $cronCommand = trim($cronCommand);
00053         $cronCommand = self::convertKeywordsToCronCommand($cronCommand);
00054         $cronCommand = self::normalizeFields($cronCommand);
00055         return $cronCommand;
00056     }
00057 
00058     /**
00059      * Accept special cron command keywords and convert to standard cron syntax.
00060      * Allowed keywords: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly
00061      *
00062      * @params string $cronCommand cron command
00063      * @return string Normalized cron command if keyword was found, else unchanged cron command
00064      */
00065     public static function convertKeywordsToCronCommand($cronCommand) {
00066         switch ($cronCommand) {
00067             case '@yearly':
00068             case '@annually':
00069                 $cronCommand = '0 0 1 1 *';
00070             break;
00071             case '@monthly':
00072                 $cronCommand = '0 0 1 * *';
00073             break;
00074             case '@weekly':
00075                 $cronCommand = '0 0 * * 0';
00076             break;
00077             case '@daily':
00078             case '@midnight':
00079                 $cronCommand = '0 0 * * *';
00080             break;
00081             case '@hourly':
00082                 $cronCommand = '0 * * * *';
00083             break;
00084         }
00085 
00086         return $cronCommand;
00087     }
00088 
00089     /**
00090      * Normalize cron command field to list of integers or *
00091      *
00092      * @param string $cronCommand cron command
00093      * @return string Normalized cron command
00094      */
00095     public static function normalizeFields($cronCommand) {
00096         $fieldArray = self::splitFields($cronCommand);
00097 
00098         $fieldArray[0] = self::normalizeIntegerField($fieldArray[0], 0, 59);
00099         $fieldArray[1] = self::normalizeIntegerField($fieldArray[1], 0, 23);
00100         $fieldArray[2] = self::normalizeIntegerField($fieldArray[2], 1, 31);
00101         $fieldArray[3] = self::normalizeMonthAndWeekdayField($fieldArray[3], TRUE);
00102         $fieldArray[4] = self::normalizeMonthAndWeekdayField($fieldArray[4], FALSE);
00103 
00104         $normalizedCronCommand = implode(' ', $fieldArray);
00105         return $normalizedCronCommand;
00106     }
00107 
00108     /**
00109      * Split a given cron command like '23 * * * *' to an array with five fields.
00110      *
00111      * @throws InvalidArgumentException If splitted array does not contain five entries
00112      * @param string $cronCommand cron command
00113      * @return array
00114      *      0 => minute field
00115      *      1 => hour field
00116      *      2 => day of month field
00117      *      3 => month field
00118      *      4 => day of week field
00119      */
00120     public static function splitFields($cronCommand) {
00121         $fields = explode(' ', $cronCommand);
00122 
00123         if (count($fields) !== 5) {
00124             throw new InvalidArgumentException(
00125                 'Unable to split given cron command to five fields.',
00126                 1291227373
00127             );
00128         }
00129 
00130         return $fields;
00131     }
00132 
00133     /**
00134      * Normalize month field.
00135      *
00136      * @param string $expression Month field expression
00137      * @param boolean $isMonthField True if month field is handled, false for weekday field
00138      * @return string Normalized expression
00139      */
00140     public static function normalizeMonthAndWeekdayField($expression, $isMonthField = TRUE) {
00141         if ((string)$expression === '*') {
00142             $fieldValues = '*';
00143         } else {
00144                 // Fragment espression by , / and - and substitute three letter code of month and weekday to numbers
00145             $listOfCommaValues = explode(',', $expression);
00146             $fieldArray = array();
00147             foreach ($listOfCommaValues as $listElement) {
00148                 if (strpos($listElement, '/') !== FALSE) {
00149                     list($left, $right) = explode('/', $listElement);
00150                     if (strpos($left, '-') !== FALSE) {
00151                         list($leftBound, $rightBound) = explode('-', $left);
00152                         $leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
00153                         $rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
00154                         $left = $leftBound . '-' . $rightBound;
00155                     } else {
00156                         if ((string)$left !== '*') {
00157                             $left = self::normalizeMonthAndWeekday($left, $isMonthField);
00158                         }
00159                     }
00160                     $fieldArray[] = $left . '/' . $right;
00161                 } elseif (strpos($listElement, '-') !== FALSE) {
00162                     list($left, $right) = explode('-', $listElement);
00163                     $left = self::normalizeMonthAndWeekday($left, $isMonthField);
00164                     $right = self::normalizeMonthAndWeekday($right, $isMonthField);
00165                     $fieldArray[] = $left . '-' . $right;
00166                 } else {
00167                     $fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
00168                 }
00169             }
00170             $fieldValues = implode(',', $fieldArray);
00171         }
00172 
00173         return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
00174     }
00175 
00176     /**
00177      * Normalize integer field.
00178      *
00179      * @throws InvalidArgumentException If field is invalid or out of bounds
00180      * @param string $expression Expression
00181      * @param integer $lowerBound Lower limit of result list
00182      * @param integer $upperBound Upper limit of result list
00183      * @return string Normalized expression
00184      */
00185     public static function normalizeIntegerField($expression, $lowerBound = 0, $upperBound = 59) {
00186         if ((string)$expression === '*') {
00187             $fieldValues = '*';
00188         } else {
00189             $listOfCommaValues = explode(',', $expression);
00190             $fieldArray = array();
00191             foreach ($listOfCommaValues as $listElement) {
00192                 if (strpos($listElement, '/') !== FALSE) {
00193                     list($left, $right) = explode('/', $listElement);
00194                     if ((string)$left === '*') {
00195                         $leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
00196                     } else {
00197                         $leftList = self::convertRangeToListOfValues($left);
00198                     }
00199                     $fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
00200                 } elseif (strpos($listElement, '-') !== FALSE) {
00201                     $fieldArray[] = self::convertRangeToListOfValues($listElement);
00202                 } elseif (strcmp(intval($listElement), $listElement) === 0) {
00203                     $fieldArray[] = $listElement;
00204                 } else {
00205                     throw new InvalidArgumentException(
00206                         'Unable to normalize integer field.',
00207                         1291429389
00208                     );
00209                 }
00210             }
00211             $fieldValues = implode(',', $fieldArray);
00212         }
00213 
00214         if (strlen($fieldValues) === 0) {
00215             throw new InvalidArgumentException(
00216                 'Unable to convert integer field to list of values: Result list empty.',
00217                 1291422012
00218             );
00219         }
00220 
00221         if ((string)$fieldValues !== '*') {
00222             $fieldList = explode(',', $fieldValues);
00223 
00224             sort($fieldList);
00225             $fieldList = array_unique($fieldList);
00226 
00227             if (current($fieldList) < $lowerBound) {
00228                 throw new InvalidArgumentException(
00229                     'Lowest element in list is smaller than allowed.',
00230                     1291470084
00231                 );
00232             }
00233 
00234             if (end($fieldList) > $upperBound) {
00235                 throw new InvalidArgumentException(
00236                     'An element in the list is higher than allowed.',
00237                     1291470170
00238                 );
00239             }
00240 
00241             $fieldValues = implode(',', $fieldList);
00242         }
00243 
00244         return (string)$fieldValues;
00245     }
00246 
00247 
00248     /**
00249      * Convert a range of integers to a list: 4-6 results in a string '4,5,6'
00250      *
00251      * @throws InvalidArgumentException If range can not be coverted to list
00252      * @params string $range integer-integer
00253      * @return array
00254      */
00255     public static function convertRangeToListOfValues($range) {
00256         if (strlen($range) === 0) {
00257             throw new InvalidArgumentException(
00258                 'Unable to convert range to list of values with empty string.',
00259                 1291234985
00260             );
00261         }
00262 
00263         $rangeArray = explode('-', $range);
00264 
00265             // Sanitize fields and cast to integer
00266         foreach ($rangeArray as $fieldNumber => $fieldValue) {
00267             if (strcmp(intval($fieldValue), $fieldValue) !== 0) {
00268                 throw new InvalidArgumentException(
00269                     'Unable to convert value to integer.',
00270                     1291237668
00271                 );
00272             }
00273             $rangeArray[$fieldNumber] = (int)$fieldValue;
00274         }
00275 
00276         $resultList = '';
00277         if (count($rangeArray) === 1) {
00278             $resultList = $rangeArray[0];
00279         } elseif (count($rangeArray) === 2) {
00280             $left = $rangeArray[0];
00281             $right = $rangeArray[1];
00282 
00283             if ($left > $right) {
00284                 throw new InvalidArgumentException(
00285                     'Unable to convert range to list: Left integer must not be greather than right integer.',
00286                     1291237145
00287                 );
00288             }
00289 
00290             $resultListArray = array();
00291             for ($i = $left; $i <= $right; $i++) {
00292                 $resultListArray[] = $i;
00293             }
00294 
00295             $resultList = implode(',', $resultListArray);
00296         } else {
00297             throw new InvalidArgumentException(
00298                 'Unable to convert range to list of values.',
00299                 1291234985
00300             );
00301         }
00302 
00303         return (string)$resultList;
00304     }
00305 
00306     /**
00307      * Reduce a given list of values by step value.
00308      * Following a range with ``/<number>'' specifies skips of the number's value through the range.
00309      *  1-5/2 -> 1,3,5
00310      *  2-10/3 -> 2,5,8
00311      *
00312      * @throws Exception if step value is invalid or if resulting list is empty
00313      * @param string #stepExpression stepvalue expression
00314      * @return string Comma separated list of valid values
00315      */
00316     public static function reduceListOfValuesByStepValue($stepExpression) {
00317         if (strlen($stepExpression) === 0) {
00318             throw new InvalidArgumentException(
00319                 'Unable to convert step values.',
00320                 1291234985
00321             );
00322         }
00323 
00324         $stepValuesAndStepArray = explode('/', $stepExpression);
00325 
00326         if (count($stepValuesAndStepArray) < 1 || count($stepValuesAndStepArray) > 2) {
00327             throw new InvalidArgumentException(
00328                 'Unable to convert step values: Multiple slashes found.',
00329                 1291242168
00330             );
00331         }
00332 
00333         $left = $stepValuesAndStepArray[0];
00334         $right = $stepValuesAndStepArray[1];
00335 
00336         if (strlen($stepValuesAndStepArray[0]) === 0) {
00337             throw new InvalidArgumentException(
00338                 'Unable to convert step values: Left part of / is empty.',
00339                 1291414955
00340             );
00341         }
00342 
00343         if (strlen($stepValuesAndStepArray[1]) === 0) {
00344             throw new InvalidArgumentException(
00345                 'Unable to convert step values: Right part of / is empty.',
00346                 1291414956
00347             );
00348         }
00349 
00350         if (strcmp(intval($right), $right) !== 0) {
00351             throw new InvalidArgumentException(
00352                 'Unable to convert step values: Right part must be a single integer.',
00353                 1291414957
00354             );
00355         }
00356 
00357         $right = (int)$right;
00358         $leftArray = explode(',', $left);
00359 
00360         $validValues = array();
00361         $currentStep = $right;
00362         foreach ($leftArray as $leftValue) {
00363             if (strcmp(intval($leftValue), $leftValue) !== 0) {
00364                 throw new InvalidArgumentException(
00365                     'Unable to convert step values: Left part must be a single integer or comma separated list of integers.',
00366                     1291414958
00367                 );
00368             }
00369 
00370             if ($currentStep === 0) {
00371                 $currentStep = $right;
00372             }
00373 
00374             if ($currentStep === $right) {
00375                 $validValues[] = (int)$leftValue;
00376             }
00377 
00378             $currentStep --;
00379         }
00380 
00381         if (count($validValues) === 0) {
00382             throw new InvalidArgumentException(
00383                 'Unable to convert step values: Result value list is empty.',
00384                 1291414958
00385             );
00386         }
00387 
00388         return implode(',', $validValues);
00389     }
00390 
00391     /**
00392      * Dispatcher method for normalizeMonth and normalizeWeekday
00393      *
00394      * @param string $expression Month or weekday to be normalized
00395      * @param boolean $isMonth TRUE if a month is handled, FALSE for weekday
00396      * @return string normalized month or weekday
00397      */
00398     public static function normalizeMonthAndWeekday($expression, $isMonth = TRUE) {
00399         $expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
00400 
00401         return (string)$expression;
00402     }
00403 
00404     /**
00405      * Accept a string representation or integer number of a month like
00406      * 'jan', 'February', 01, ... and convert to normalized integer value 1 .. 12
00407      *
00408      * @throws InvalidArgumentException If month string can not be converted to integer
00409      * @param string $month Month representation
00410      * @return integer month integer representation between 1 and 12
00411      */
00412     public static function normalizeMonth($month) {
00413         $timestamp = strtotime('2010-' . $month . '-01');
00414 
00415             // timestamp must be >= 2010-01-01 and <= 2010-12-01
00416         if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
00417             throw new InvalidArgumentException(
00418                 'Unable to convert given month name.',
00419                 1291083486
00420             );
00421         }
00422 
00423         return (int)date('n', $timestamp);
00424     }
00425 
00426     /**
00427      * Accept a string representation or integer number of a weekday like
00428      * 'mon', 'Friday', 3, ... and convert to normalized integer value 1 .. 7
00429      *
00430      * @throws InvalidArgumentException If weekday string can not be converted
00431      * @param string $weekday Weekday representation
00432      * @return integer weekday integer representation between 1 and 7
00433      */
00434     public static function normalizeWeekday($weekday) {
00435         $normalizedWeekday = FALSE;
00436 
00437             // 0 (sunday) -> 7
00438         if ((string)$weekday === '0') {
00439             $weekday = 7;
00440         }
00441 
00442         if ($weekday >= 1 && $weekday <= 7) {
00443             $normalizedWeekday = (int)$weekday;
00444         }
00445 
00446         if (!$normalizedWeekday) {
00447                 // Convert string representation like 'sun' to integer
00448             $timestamp = strtotime('next ' . $weekday, mktime(0, 0, 0, 1, 1, 2010));
00449             if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
00450                 throw new InvalidArgumentException(
00451                     'Unable to convert given weekday name.',
00452                     1291163589
00453                 );
00454             }
00455             $normalizedWeekday = (int)date('N', $timestamp);
00456         }
00457 
00458         return $normalizedWeekday;
00459     }
00460 }
00461 ?>