<?php 
/**
 * Copyright (c) 2010 David Soyez, http://code.google.com/p/yii-crontab/
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *  
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *  
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 * Crontab helps to add system cron jobs
 *
 * @author David Soyez <david.soyez@yiiframework.fr>
 * @link http://code.google.com/p/yii-crontab/
 * @copyright Copyright &copy; 2009-2010 yiiframework.fr
 * @license http://www.opensource.org/licenses/mit-license.php
 * @version 0.3
 * @package crontab
 * @since 0.1
 */
class Crontab extends CApplicationComponent{
	
	protected $jobs	= array();
	protected $directory	= NULL;
	protected $filename		= "crons";
	protected $crontabPath	= NULL;
	protected $handle		= NULL;
	
	/**
	 *	Constructor. Attempts to create directory for
	 *	holding cron jobs
	 *
	 *	@param	string	$dir		 Directory to hold cron job files (slash terminated)
	 *	@param	string	$filename	 Filename to write to
	 *	@param	string	$crontabPath Path to cron program
	 *	@access	public
	 */
	function Crontab($filename=NULL, $dir=NULL, $crontabPath=NULL){
		$result				=(!$dir) ? $this->setDirectory(Yii::getPathOfAlias('webroot').'/college_data/crontabs/') : $this->setDirectory($dir);

		if(!$result)
			exit('Directory error');
		$result				=(!$filename) ? $this->createCronFile("crons") : $this->createCronFile($filename);
		if(!$result)
			exit('File error');
		$this->crontabPath=($crontabPath) ? NULL : $crontabPath;
		
		$this->loadJobs();
	}
	
	

	/**
	 *	Add a job
	 *
	 *	If any parameters are left NULL then they default to *
	 *
	 *	A hyphen (-) between integers specifies a range of integers. For
	 *	example, 1-4 means the integers 1, 2, 3, and 4.
	 *
	 *	A list of values separated by commas (,) specifies a list. For
	 *	example, 3, 4, 6, 8 indicates those four specific integers.
	 *
	 *	The forward slash (/) can be used to specify step values. The value
	 *	of an integer can be skipped within a range by following the range
	 *	with /<integer>. For example, 0-59/2 can be used to define every other
	 *	minute in the minute field. Step values can also be used with an asterisk.
	 *	For instance, the value * /3 (no space) can be used in the month field to run the
	 *	task every third month...
	 *
	 *	@param	string	$command	Command
	 *	@param	mixed	$min		Minute(s)... 0 to 59
	 *	@param	mixed	$hour		Hour(s)... 0 to 23
	 *	@param	mixed	$day		Day(s)... 1 to 31
	 *	@param	mixed	$month		Month(s)... 1 to 12 or short name
	 *	@param	mixed	$dayofweek	Day(s) of week... 0 to 7 or short name. 0 and 7 = sunday
	 *  @return CCrontab return this
	 */
	function addJob($command, $min=NULL, $hour=NULL, $day=NULL, $month=NULL, $dayofweek=NULL)
	{
		$this->jobs[] = new Cronjob($command, $min, $hour, $day, $month, $dayofweek);
		
		return $this;
		
	}

	
	/**
	 *	Add an application job
	 */
	function addApplicationJob($entryScript, $commandName, $parameters = array(), $min=NULL, $hour=NULL, $day=NULL, $month=NULL, $dayofweek=NULL)
	{
		$this->jobs[] = new CronApplicationJob($entryScript, $commandName, $parameters, $min, $hour, $day, $month, $dayofweek);

		return $this;
	}
	
	/**
	 * Add job object
	 * @param mixed $job CronApplicationJob or Cronjob
	 * @return CCrontab
	 */
	public function add($job)
	{
		if($job instanceof CronApplicationJob OR $job instanceof Cronjob)
			$this->jobs[] = $job;
				
		return $this;
	}
	
	
	
	/**
	 *	Write cron command to file. Make sure you used createCronFile
	 *	before using this function of it will return false
	 *  @return CCrontab return this or false
	 */
	function saveCronFile(){
		$this->emptyCrontabFile();
		foreach ($this->jobs as $job)
		{
			if(!fwrite($this->handle, $job->getJobCommand()))
				return false;				
		}
		
		return $this;
	}
	
	
	/**
	 *	Save cron in system
	 *	@return boolean this if successful else false
	 */
	function saveToCrontab(){
		
		if(!$this->filename)
			exit('No name specified for cron file');
					
		if(exec($this->crontabPath."crontab ".$this->directory.$this->filename))
			return $this;
		else
			return false;
	}
	

	/**
	 * Get jobs
	 * @return array jobs
	 */
	public function getJobs()
	{
		return $this->jobs;
	}
	
	/**
	 * Remove a job with given offset
	 * @return CCrontab
	 */
	public function removeJob($offset = NULL)
	{
		if($offset !== NULL)
			unset($this->jobs[$offset]);
		
		return $this;
	}
	
	/**
	 * remove all jobs
	 * @return CCrontab
	 */
	public function eraseJobs()
	{
		$this->jobs = array();
		
		return $this;
	}	
	
	
	
	
	/*********************************/
	/********* Protected *************/
	/*********************************/
	
	/**
	 *	Set the directory path. Will check it if it exists then
	 *	try to open it. Also if it doesn't exist then it will try to
	 *	create it, makes it with mode 0700
	 *
	 *	@param	string	$directory	Directory, relative or full path
	 *	@access	public
	 *  @return CCrontab return this
	 */
	protected function setDirectory($directory){
		if(!$directory) return false;
		
		if(is_dir($directory)){
			if($dh=opendir($directory)){
				$this->directory=$directory;
				return $this;
			}else
				return false;
		}else{
			if(mkdir($directory, 0700)){
				$this->directory=$directory;
				return $this;
			}
		}
		return false;
	}
	
	
	/**
	 *	Create cron file
	 *
	 *	This will create a cron job file for you and set the filename
	 *	of this class to use it. Make sure you have already set the directory
	 *	path variable with the consructor. If the file exists and we can write
	 *	it then return true esle false. Also sets $handle with the resource handle
	 *	to the file
	 *
	 *	@param	string	$filename	Name of file you want to create
	 *	@access	public
	 *  @return CCrontab return this or false
	 */
	protected function createCronFile($filename=NULL){
		if(!$filename)
			return false;
		
		if(file_exists($this->directory.$filename)){
			if($this->openFile($handle,$filename, 'a+')){
				$this->handle=&$handle;
				$this->filename=$filename;
				return $this;
			}else
				return false;
		}
		
		if(!$this->openFile($handle,$filename, 'a+'))
			return false;
		else{
			$this->handle=&$handle;
			$this->filename=$filename;
			return $this;
		}
	}
			
	
	/**
	 * Load jobs from crontab file
	 */
	protected function loadJobs()
	{
		fseek($this->handle, 0);
	    while (! feof ($this->handle)) 
	    {
	        $line= fgets ($this->handle);
	        $line = trim(trim($line), "\t");
	        if(!empty($line))
	        {
		        if(CronApplicationJob::isApplicationJob($line))
		        {
		        	$obj = CronApplicationJob::parseFromCommand($line);
		        	if($obj !== FALSE)
		        		$this->jobs[] = $obj;
		        }
		        else
		        {
					$obj = Cronjob::parseFromCommand($line);
					if($obj !== FALSE)
	        			$this->jobs[] = $obj;
		        }
	        }
    	}		
	}
	
	
	/**
	 * Empty crontab file
	 * @return CCrontab
	 */
	protected function emptyCrontabFile()
	{
		$this->closeFile();
		$this->openFile($this->handle,$this->filename, 'w');
		$this->closeFile();
		$this->openFile($this->handle,$this->filename, 'a');
		
		return $this;
	}
	
	
	/**
	 * Close crontab file
	 */
	protected function closeFile()
	{
		fclose($this->handle);
	}
	
	/**
	 * Open crontab file
	 * @param ressource $handle
	 * @param string $filename
	 * @param string $accessType
	 */	
	protected function openFile(& $handle,$filename, $accessType = 'a+')
	{
		 return $handle = fopen($this->directory.$filename, $accessType);
		
	}	
	
}


class Cronjob 
{
	protected $minute		= NULL;
	protected $hour			= NULL;
	protected $day			= NULL;
	protected $month		= NULL;
	protected $dayofweek	= NULL;
	protected $command		= NULL;
	
	
	function Cronjob($command, $min=NULL, $hour=NULL, $day=NULL, $month=NULL, $dayofweek=NULL)
	{
		$this->setMinute($min);
		$this->setHour($hour);
		$this->setDay($day);
		$this->setMonth($month);
		$this->setDayofweek($dayofweek);
		$this->command = $command;	
	
		return $this;
	}
	
	/**
	 * Return the system command for the object
	 */
	public function getJobCommand()
	{
		return $this->minute." ".$this->hour." ".$this->day." ".$this->month." ".$this->dayofweek." ".$this->getCommand()."\n";
	}
	
	/**
	 * Return the command
	 */
	public function getCommand()
	{
		return $this->command;
	}	
	
	/**
	 * parse system job command and return an object
	 * Works only for regular entry
	 */
	static function parseFromCommand($command)
	{
		$vars = split("[ \t]",ltrim($command, " \t"), 6);
		
		if(count($vars) < 5)
			return false;
			
		$min 	 = $vars[0];
		$hour 	 = $vars[1];
		$day	 = $vars[2];
		$month	 = $vars[3];
		$dayofweek = $vars[4];
		
		$command  = $vars[5];
			
		return new Cronjob($command, $min, $hour, $day, $month, $dayofweek);
	}
	
	/* setter */
	
	public function setMinute($min)
	{
		if($min=="0")
			$this->minute=0;
		elseif($min)
			$this->minute=$min;
		else
			$this->minute="*";
	}
	
	public function setHour($hour)
	{
		if($hour=="0")
			$this->hour=0;
		elseif($hour)
			$this->hour=$hour;
		else
			$this->hour="*";	
	}
	
	public function setDay($day)
	{
		$this->day=($day) ? $day : "*";
	}
	
	public function setMonth($month)
	{
		$this->month=($month) ? $month : "*";
	}
	
	public function setdayofweek($dayofweek)
	{
		$this->dayofweek=($dayofweek) ? $dayofweek : "*";
	}
	
	/* getter */
	
	public function getMinute()
	{
		return $this->minute;
	}
	
	public function getHour()
	{
		return $this->hour;
	}

	public function getDay()
	{
		return $this->day;
	}

	public function getMonth()
	{
		return $this->month;
	}

	public function getDayofweek()
	{
		return $this->dayofweek;
	}	
	
}


class CronApplicationJob extends Cronjob
{
	protected $entryScript = NULL;
	protected $commandName = NULL;
	protected $parameters  = array();
	
	function CronApplicationJob($entryScript, $commandName, $parameters = array(), $min=NULL, $hour=NULL, $day=NULL, $month=NULL, $dayofweek=NULL)
	{
		$this->entryScript = $entryScript;
		$this->commandName = $commandName;
		$this->parameters = $parameters;
		
		$command = $this->getCommand();
			
		parent::Cronjob($command, $min, $hour, $day, $month, $dayofweek);
		
		return $this;	
	
	}
	
	/**
	 * Return the system command
	 */
	public function getJobCommand()
	{
		$command =  $this->minute." ".$this->hour." ".$this->day." ".$this->month." ".$this->dayofweek." ".$this->getCommand()."\n";

		return $command;
	}	
	
	/**
	 * Return the Application command
	 */
	public function getCommand()
	{
		$command = 'php '.Yii::getPathOfAlias('webroot').'/'.$this->entryScript . '.php ' . $this->commandName;
		
		foreach($this->parameters as $parameter)
			$command .= ' ' . $parameter;	
			
		return $command;
	}
	
	/**
	 * parse system job command and return an object
	 */
	static function parseFromCommand($command)
	{
		$vars = split("[ \t]",ltrim($command, " \t"), 6);
		
		if(count($vars) < 5)
			return false;
			
		$min 	 = $vars[0];
		$hour 		 = $vars[1];
		$day		 = $vars[2];
		$month		 = $vars[3];
		$dayofweek 	 = $vars[4];
		
		$command 	 = $vars[5];
	
		if(preg_match('|^php ([^\\\]*.php) ([^\\\]*)|', $command, $matches) > 0)
		{
			$entryScript = basename($matches[1], ".php");
			$params = explode(' ',$matches[2]);
			$commandName = $params[0];
			array_shift($params);
			$parameters = $params;	
		}
		else
			return false;
		
		return new CronApplicationJob($entryScript, $commandName, $parameters, $min, $hour, $day, $month, $dayofweek);
	}
	
	/**
	 * Check if the given command would be an ApplicationJob
	 */
	static function isApplicationJob($line)
	{
		$vars = split("[ \t]",ltrim(ltrim($line), "\t"), 6);
		
		if(count($vars) < 5)
			return false;

		return (bool)preg_match("|^php ([^\\\]*.php) ([^\\\]*)|", $vars[5]);
	}
	
	
	/* setter */
	
	public function setParams($params)
	{
		$this->parameters = $params;
	}

	public function setEntryScript($entryScript)
	{
		$this->entryScript = $entryScript;
	}	

	public function setCommandName($commandName)
	{
		$this->commandName = $commandName;
	}		
	
	
	/* getter */
	
	public function getParams()
	{
		return $this->parameters;
	}	
	
	public function getEntryScript()
	{
		return $this->entryScript;
	}

	public function getCommandName()
	{
		return $this->commandName;
	}		
	
}
