File indexing completed on 2024-12-22 05:36:18
0001 <?php 0002 0003 /** 0004 * @author Ryan Faerman <ryan.faerman@gmail.com> 0005 * @author Krzysztof Suszyński <k.suszynski@mediovski.pl> 0006 * @version 0.2 0007 * @package php.manager.crontab 0008 * 0009 * Copyright (c) 2009 Ryan Faerman <ryan.faerman@gmail.com> 0010 * 0011 * Permission is hereby granted, free of charge, to any person obtaining a copy 0012 * of this software and associated documentation files (the "Software"), to deal 0013 * in the Software without restriction, including without limitation the rights 0014 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 0015 * copies of the Software, and to permit persons to whom the Software is 0016 * furnished to do so, subject to the following conditions: 0017 * 0018 * The above copyright notice and this permission notice shall be included in 0019 * all copies or substantial portions of the Software. 0020 * 0021 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 0022 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 0023 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 0024 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 0025 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 0026 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 0027 * THE SOFTWARE. 0028 * 0029 */ 0030 0031 //namespace Crontab\Manager; 0032 0033 /** 0034 * Crontab manager implementation 0035 * 0036 * @author Krzysztof Suszyński <k.suszynski@mediovski.pl> 0037 * @author Ryan Faerman <ryan.faerman@gmail.com> 0038 */ 0039 class Crontab_Manager_CrontabManager 0040 { 0041 0042 /** 0043 * Location of the crontab executable 0044 * 0045 * @var string 0046 */ 0047 public $crontab = '/usr/bin/crontab'; 0048 public $cronContent = ''; 0049 0050 /** 0051 * Name of user to install crontab 0052 * 0053 * @var string 0054 */ 0055 public $user = null; 0056 /** 0057 * @var boolean 0058 */ 0059 public $prependRootPath = true; 0060 /** 0061 * Location to save the crontab file. 0062 * 0063 * @var string 0064 */ 0065 private $_tmpfile; 0066 /** 0067 * @var CronEntry[] 0068 */ 0069 private $jobs = array(); 0070 /** 0071 * @var CronEntry[] 0072 */ 0073 private $replace = array(); 0074 /** 0075 * @var CronEntry[] 0076 */ 0077 private $files = array(); 0078 /** 0079 * @var array 0080 */ 0081 private $fileHashes = array(); 0082 /** 0083 * @var array 0084 */ 0085 private $filesToRemove = array(); 0086 /** 0087 * @var string[] 0088 */ 0089 private $_comments = array(); 0090 /** 0091 * @var string 0092 */ 0093 private $_beginBlock = 'BEGIN:%s'; 0094 /** 0095 * @var string 0096 */ 0097 private $_endBlock = 'END:%s'; 0098 /** 0099 * @var string 0100 */ 0101 private $_before = "Autogenerated by CrontabManager.\n# Do not edit. Orginal file: %s"; 0102 /** 0103 * @var string 0104 */ 0105 private $_after = 'End of autogenerated code.'; 0106 0107 /** 0108 * Constructor 0109 * 0110 * @return void 0111 */ 0112 public function __construct() 0113 { 0114 $this->_setTempFile(); 0115 } 0116 0117 /** 0118 * Sets tempfile name 0119 * 0120 * @return Crontab_Manager_CrontabManager 0121 */ 0122 protected function _setTempFile() 0123 { 0124 if ($this->_tmpfile && is_file($this->_tmpfile)) { 0125 unlink($this->_tmpfile); 0126 } 0127 $tmpDir = sys_get_temp_dir(); 0128 $this->_tmpfile = tempnam($tmpDir, 'cronman'); 0129 chmod($this->_tmpfile, 0666); 0130 0131 return $this; 0132 } 0133 0134 /** 0135 * Destrutor 0136 */ 0137 public function __destruct() 0138 { 0139 if ($this->_tmpfile && is_file($this->_tmpfile)) { 0140 unlink($this->_tmpfile); 0141 } 0142 } 0143 0144 /** 0145 * Replace job with another one 0146 * 0147 * @param Crontab_Manager_CronEntry $from 0148 * @param Crontab_Manager_CronEntry $to 0149 * 0150 * @return Crontab_Manager_CrontabManager 0151 */ 0152 public function replace(Crontab_Manager_CronEntry $from, Crontab_Manager_CronEntry $to) 0153 { 0154 $this->replace[] = array($from, $to); 0155 return $this; 0156 } 0157 0158 /** 0159 * Reads cron file and adds jobs to list 0160 * 0161 * @param string $filename 0162 * 0163 * @returns Crontab_Manager_CrontabManager 0164 * @throws \InvalidArgumentException 0165 */ 0166 public function enableOrUpdate($filename) 0167 { 0168 $path = realpath($filename); 0169 if (!$path || !is_readable($path)) { 0170 throw new \InvalidArgumentException( 0171 sprintf( 0172 '"%s" don\'t exists or isn\'t readable', $filename 0173 ) 0174 ); 0175 } 0176 $hash = $this->_shortHash($path); 0177 0178 if (isset($this->filesToRemove[$hash])) { 0179 unset($this->filesToRemove[$hash]); 0180 } 0181 $this->fileHashes[$path] = $hash; 0182 $jobs = $this->_parseFile($path, $hash); 0183 foreach ($jobs as $job) { 0184 $this->add($job, $path); 0185 } 0186 0187 return $this; 0188 } 0189 0190 /** 0191 * Calculates short hash of string 0192 * 0193 * @param string $input 0194 * @return string 0195 */ 0196 private function _shortHash($input) 0197 { 0198 $hash = base_convert( 0199 $this->_signedInt(crc32($input)), 10, 36 0200 ); 0201 return $hash; 0202 } 0203 0204 /** 0205 * Gets signed int from unsigned 64bit int 0206 * 0207 * @param integer $in 0208 * @return integer 0209 */ 0210 private static function _signedInt($in) 0211 { 0212 $int_max = 2147483647; // pow(2, 31) - 1 0213 if ($in > $int_max) { 0214 $out = $in - $int_max * 2 - 2; 0215 } else { 0216 $out = $in; 0217 } 0218 return $out; 0219 } 0220 0221 /** 0222 * Parse input cron file to cron entires 0223 * 0224 * @param string $path 0225 * @param string $hash 0226 * 0227 * @return Crontab_Manager_CronEntry[] 0228 * @throws \InvalidArgumentException 0229 */ 0230 private function _parseFile($path, $hash) 0231 { 0232 $jobs = array(); 0233 0234 $lines = file($path); 0235 foreach ($lines as $lineno => $line) { 0236 try { 0237 $job = $this->newJob($line, $hash); 0238 if ($this->prependRootPath) { 0239 $job->setRootForCommands(dirname($path)); 0240 } 0241 $job->addComments($this->_comments); 0242 $this->_comments = array(); 0243 $jobs[] = $job; 0244 } catch (\Exception $exc) { 0245 if (preg_match('/^\s*\#/', $line)) { 0246 $this->_comments[] = trim($line); 0247 } elseif (trim($line) == '') { 0248 $this->_comments = array(); 0249 continue; 0250 } else { 0251 $msg = sprintf('Line #%d of file: "%s" is invalid!', $lineno, $path); 0252 throw new \InvalidArgumentException($msg); 0253 } 0254 } 0255 } 0256 return $jobs; 0257 } 0258 0259 /** 0260 * Creates new job 0261 * 0262 * @param string $jobSpec 0263 * @param string $group 0264 * 0265 * @return CronEntry 0266 */ 0267 public function newJob($jobSpec = null, $group = null) 0268 { 0269 return new Crontab_Manager_CronEntry($jobSpec, $this, $group); 0270 } 0271 0272 /** 0273 * Adds job to managed list 0274 * 0275 * @param Crontab_Manager_CronEntry $job 0276 * @param string $file optional 0277 * 0278 * @return Crontab_Manager_CrontabManager 0279 */ 0280 public function add(Crontab_Manager_CronEntry $job, $file = null) 0281 { 0282 if (!$file) { 0283 $this->jobs[] = $job; 0284 } else { 0285 if (!isset($this->files[$file])) { 0286 $this->files[$file] = array(); 0287 $hash = $this->_shortHash($file); 0288 $this->fileHashes[$file] = $hash; 0289 } 0290 $this->files[$file][] = $job; 0291 } 0292 return $this; 0293 } 0294 0295 /** 0296 * Disable file from crontab 0297 * 0298 * @param string $filename 0299 * 0300 * @return Crontab_Manager_CrontabManager 0301 * @throws \InvalidArgumentException 0302 */ 0303 public function disable($filename) 0304 { 0305 $path = realpath($filename); 0306 if (!$path || !is_readable($path)) { 0307 throw new \InvalidArgumentException( 0308 sprintf( 0309 '"%s" don\'t exists or isn\'t readable', $filename 0310 ) 0311 ); 0312 } 0313 $hash = $this->_shortHash($path); 0314 if (isset($this->fileHashes[$path])) { 0315 unset($this->fileHashes[$path]); 0316 unset($this->files[$path]); 0317 } 0318 $this->filesToRemove[$hash] = $path; 0319 0320 return $this; 0321 } 0322 0323 /** 0324 * Save the jobs to disk, remove existing cron 0325 * 0326 * @param boolean $includeOldJobs optional 0327 * 0328 * @return boolean 0329 * @throws \UnexpectedValueException 0330 */ 0331 public function save($includeOldJobs = true) 0332 { 0333 $this->cronContent = ''; 0334 if ($includeOldJobs) { 0335 try { 0336 $this->cronContent = $this->listJobs(); 0337 } catch (\UnexpectedValueException $e) { 0338 0339 } 0340 } 0341 0342 $this->cronContent = $this->_prepareContents($this->cronContent); 0343 0344 $this->_replaceCronContents(); 0345 } 0346 0347 /** 0348 * List current cron jobs 0349 * 0350 * @return string 0351 * @throws \UnexpectedValueException 0352 */ 0353 public function listJobs() 0354 { 0355 $out = $this->_exec($this->_command() . ' -l', $retVal); 0356 if ($retVal != 0) { 0357 throw new \UnexpectedValueException('No cron file or no permissions to list', $retVal); 0358 } 0359 return $out; 0360 } 0361 0362 /** 0363 * Runs command in terminal 0364 * 0365 * @param string $command 0366 * @param integer $returnVal 0367 * 0368 * @return string 0369 */ 0370 private function _exec($command, & $returnVal) 0371 { 0372 ob_start(); 0373 system($command, $returnVal); 0374 $output = ob_get_clean(); 0375 return $output; 0376 } 0377 0378 /** 0379 * calcuates crontab command 0380 * 0381 * @return string 0382 */ 0383 protected function _command() 0384 { 0385 $cmd = ''; 0386 if ($this->user) { 0387 $cmd .= sprintf('sudo -u %s ', $this->user); 0388 } 0389 $cmd .= $this->crontab; 0390 return $cmd; 0391 } 0392 0393 /** 0394 * @param string $contents 0395 * 0396 * @return string 0397 */ 0398 private function _prepareContents($contents) 0399 { 0400 if (empty($contents)) { 0401 $contents = array(); 0402 } else { 0403 $contents = explode("\n", $contents); 0404 } 0405 0406 foreach ($this->filesToRemove as $hash => $path) { 0407 $contents = $this->_removeBlock($contents, $hash); 0408 } 0409 0410 foreach ($this->fileHashes as $file => $hash) { 0411 $contents = $this->_removeBlock($contents, $hash); 0412 $contents = $this->_addBlock($contents, $file, $hash); 0413 } 0414 if ($this->jobs) { 0415 $contents[] = ''; 0416 } 0417 foreach ($this->jobs as $job) { 0418 $contents[] = $job; 0419 } 0420 $out = $this->_doReplace($contents); 0421 $out = preg_replace('/[\n]{3,}/m', "\n\n", $out); 0422 return trim($out) . "\n"; 0423 } 0424 0425 /** 0426 * @param array $contents 0427 * @param string $hash 0428 * 0429 * @return array 0430 */ 0431 private function _removeBlock(array $contents, $hash) 0432 { 0433 $from = sprintf('# ' . $this->_beginBlock, $hash); 0434 $to = sprintf('# ' . $this->_endBlock, $hash); 0435 $cut = false; 0436 $toCut = array(); 0437 foreach ($contents as $no => $line) { 0438 if (substr($line, 0, strlen($from)) == $from) { 0439 $cut = true; 0440 } 0441 if ($cut) { 0442 $toCut[] = $no; 0443 } 0444 if (substr($line, 0, strlen($to)) == $to) { 0445 break; 0446 } 0447 } 0448 foreach ($toCut as $lineNo) { 0449 unset($contents[$lineNo]); 0450 } 0451 return $contents; 0452 } 0453 0454 /** 0455 * @param array $contents 0456 * @param string $file 0457 * @param string $hash 0458 * 0459 * @return array 0460 */ 0461 private function _addBlock(array $contents, $file, $hash) 0462 { 0463 $pre = sprintf('# ' . $this->_beginBlock, $hash); 0464 $pre .= sprintf(' ' . $this->_before, $file); 0465 $contents[] = $pre; 0466 $contents[] = ''; 0467 0468 foreach ($this->files as $jobs) { 0469 foreach ($jobs as $job) { 0470 $contents[] = $job; 0471 } 0472 } 0473 0474 $contents[] = ''; 0475 $after = sprintf('# ' . $this->_endBlock, $hash); 0476 $after .= ' ' . $this->_after; 0477 $contents[] = $after; 0478 0479 return $contents; 0480 } 0481 0482 /** 0483 * @param array $contents 0484 * 0485 * @return string 0486 */ 0487 private function _doReplace(array $contents) 0488 { 0489 $out = join("\n", $contents); 0490 foreach ($this->replace as $entry) { 0491 list($fromJob, $toTob) = $entry; 0492 $from = $fromJob->render(false); 0493 /* @var $fromJob Crontab_Manager_CronEntry */ 0494 $out = str_replace($fromJob, $toTob, $out); 0495 /* @var $toTob Crontab_Manager_CronEntry */ 0496 $out = str_replace($from, $toTob, $out); 0497 } 0498 return $out; 0499 } 0500 0501 /** 0502 * Replaces cron contents 0503 * 0504 * @throws \UnexpectedValueException 0505 * @return Crontab_Manager_CrontabManager 0506 */ 0507 protected function _replaceCronContents() 0508 { 0509 file_put_contents($this->_tmpfile, $this->cronContent, LOCK_EX); 0510 $out = $this->_exec($this->_command() . ' ' . 0511 $this->_tmpfile . ' 2>&1', $ret); 0512 $this->_setTempFile(); 0513 if ($ret != 0) { 0514 throw new \UnexpectedValueException( 0515 $out . "\n" . $this->cronContent, $ret 0516 ); 0517 } 0518 return $this; 0519 } 0520 0521 /** 0522 * Cleans an instance without saving to disk 0523 * 0524 * @return Crontab_Manager_CrontabManager 0525 */ 0526 public function cleanManager() 0527 { 0528 $this->fileHashes = array(); 0529 $this->jobs = array(); 0530 $this->files = array(); 0531 $this->replace = array(); 0532 $this->filesToRemove = array(); 0533 0534 return $this; 0535 } 0536 0537 /** 0538 * Delete one job from current jobs. 0539 * <p> 0540 * Exemple of use: 0541 * </p> 0542 * <pre> 0543 * $crontab = new Crontab_Manager_CrontabManager(); 0544 * $crontab->deleteJob("ms8xjs"); 0545 * $crontab->save(false); 0546 * </pre> 0547 * 0548 * @param string $job id or part of description of the job you wanna delete 0549 * @return int number of jobs deleted 0550 */ 0551 function deleteJob($job = null) 0552 { 0553 $jobsDeleted = 0; 0554 if (!is_null($job)) { 0555 $data = array(); 0556 $oldJobs = explode("\n", $this->listJobs()); // get the old jobs 0557 if (is_array($oldJobs)) { 0558 foreach ($oldJobs as $oldJob) { 0559 if ($oldJob != '') { 0560 if (!preg_match('/' . $job . '/', $oldJob)) { 0561 $newJob = new Crontab_Manager_CronEntry($oldJob, $this); 0562 $newJob->lineComment = ''; 0563 $data[] = $newJob; 0564 } else { 0565 $jobsDeleted++; 0566 } 0567 } 0568 } 0569 } 0570 $this->jobs = $data; 0571 } 0572 return $jobsDeleted; 0573 } 0574 0575 /** 0576 * Verify if a job exists or not. 0577 * <p> 0578 * Exemple of uses: 0579 * </p> 0580 * <pre> 0581 * $crontab = new Crontab_Manager_CrontabManager(); 0582 * $result = $crontab->jobExists("* * * * * /path/to/job"); 0583 * </pre> 0584 * 0585 * @param string $job id or part of description of the job you wanna verify if exists or not 0586 * @return boolean [true|false] true if exists. false if not exists 0587 */ 0588 function jobExists($job = null) 0589 { 0590 if (!is_null($job)) { 0591 $jobs = explode("\n", $this->listJobs()); // get the old jobs 0592 $needle = $job->__toString(); 0593 if (is_array($jobs)) { 0594 foreach ($jobs as $oneJob) { 0595 if ($oneJob != '') { 0596 if (false !== strpos($oneJob, $needle)) { 0597 return true; 0598 } 0599 // if (false !== preg_match('/' . $needle . '/', $oneJob)) { 0600 // return true; 0601 // } else { 0602 // $dummy = preg_last_error(); 0603 // } 0604 } 0605 } 0606 } 0607 } 0608 return false; 0609 } 0610 0611 }