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 }