1: <?php
2:
3: /*****************************************************************************************
4: * X2Engine Open Source Edition is a customer relationship management program developed by
5: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
6: *
7: * This program is free software; you can redistribute it and/or modify it under
8: * the terms of the GNU Affero General Public License version 3 as published by the
9: * Free Software Foundation with the addition of the following permission added
10: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
12: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13: *
14: * This program is distributed in the hope that it will be useful, but WITHOUT
15: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
17: * details.
18: *
19: * You should have received a copy of the GNU Affero General Public License along with
20: * this program; if not, see http://www.gnu.org/licenses or write to the Free
21: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22: * 02110-1301 USA.
23: *
24: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
25: * California 95067, USA. or at email address [email protected].
26: *
27: * The interactive user interfaces in modified source and object code versions
28: * of this program must display Appropriate Legal Notices, as required under
29: * Section 5 of the GNU Affero General Public License version 3.
30: *
31: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32: * these Appropriate Legal Notices must retain the display of the "Powered by
33: * X2Engine" logo. If the display of the logo is not reasonably feasible for
34: * technical reasons, the Appropriate Legal Notices must display the words
35: * "Powered by X2Engine".
36: *****************************************************************************************/
37:
38: /**
39: * Stand-alone class for running more advanced command line programs.
40: *
41: * Provides a wrapper/shortcut object for running commands using proc_open.
42: *
43: * This should ideally permit chaining of programs via pipes. An example, on
44: * Unix-like systems:
45: *
46: * $cmd->run('ps aux')->pipeTo('awk \'/foo/ {print $2}\'')->pipeTo('kill')->complete();
47: *
48: * The above would send SIGTERM to all processes matching "foo". However, this
49: * is not yet possible due to a "bug" in PHP:
50: * http://stackoverflow.com/questions/6014761/proper-shell-execution-in-php
51: *
52: * @package application.components.util
53: * @author Demitri Morgan <[email protected]>
54: */
55: class CommandUtil {
56:
57: const DEBUG = 0;
58:
59: /**
60: * Subprocess commands
61: * @var array
62: */
63: public $cmd = array();
64:
65: /**
66: * Saves input given to each program, for debugging/examination purposes
67: * @var type
68: */
69: public $inputs = array();
70:
71: /**
72: * The status of the last process that was completed.
73: */
74: public $lastReturnCode;
75:
76: /**
77: * @var string Operating system; a keyword "posix" for posix-compliant
78: * operating systems like Linux/Unix, or "dos" for Windows-based systems.
79: */
80: public $os;
81:
82: /**
83: * The stack of subprocesses inputs/outputs currently executing.
84: *
85: * Each value corresponds to an index in {@link processes}.
86: * @var array
87: */
88: public $pipeline = array();
89:
90: public $procArrays = array(
91: 'cmd',
92: 'inputs',
93: 'pipeline',
94: 'processes',
95: );
96:
97: /**
98: * All subprocess handles of the current program,
99: * @var array
100: */
101: public $processes = array();
102:
103: /**
104: * Gathers some information about the operating environment.
105: */
106: public function __construct(){
107: if(!function_exists('proc_open'))
108: throw new Exception('The function "proc_open" does not exist or is not available on this system, so running command line programs will not work.');
109: $this->os = substr(strtoupper(PHP_OS), 0, 3) == 'WIN' ? 'dos' : 'posix';
110: }
111:
112: /**
113: * Closes a process and throws an exception if it exited with error code.
114: * @param type $ind
115: * @throws Exception
116: */
117: public function close($ind){
118: if(gettype($this->processes[$ind]) != 'resource')
119: return;
120: $this->debug('Closing process at index '.$ind);
121: $err = stream_get_contents($this->pipeline[$ind][2]);
122: if($code = proc_close($this->processes[$ind]) == -1 && self::DEBUG)
123: throw new Exception("Command {$this->cmd[$ind]} exited with error status.".(empty($err) ? '' : " Error output was as follows: \n $err"));
124: $this->debug('Closed process at index '.$ind);
125: return $code;
126: }
127:
128: /**
129: * Returns true or false based on whether the named command exists on the system.
130: * @param string $cmd Name of the command
131: * @return bool
132: */
133: public function cmdExists($cmd){
134: if($this->os == 'posix'){
135: return trim($this->run("which $cmd")->output()) != null;
136: }
137: }
138:
139: /**
140: * Closes all processes and returns the return value of proc_close from the last one.
141: */
142: public function complete(){
143: $n_proc = $this->nProc();
144: $code = 0;
145: if($n_proc > 0){ // Close processes
146: foreach($this->processes as $ind => $process){
147: $codeTmp = $this->close($ind);
148: if($ind == $n_proc - 1)
149: $code = $codeTmp;
150: }
151: // Empty arrays
152: foreach($this->procArrays as $array)
153: $this->$array = array();
154: }
155: return $this->lastReturnCode = $code;
156: }
157:
158: public function debug($msg,$lvl=1) {
159: if(self::DEBUG >= $lvl) {
160: echo "[debug] $msg\n";
161: }
162: }
163:
164: /**
165: * Returns the current number of subprocesses.
166: * @return integer
167: */
168: public function nProc(){
169: return count($this->processes);
170: }
171:
172: /**
173: * Returns the output of the last command and closes/clears all processes.
174: * @return string
175: */
176: public function output(){
177: $n_proc = $this->nProc();
178: if($n_proc > 0){
179: $output = stream_get_contents($this->pipeline[$n_proc - 1][1]);
180: $this->complete();
181: return $output;
182: } else
183: return null;
184: }
185:
186: /**
187: * Wrapper for {@link pipeTo}
188: * @param type $filter PCRE regular expression
189: * @param type $cmd
190: * @param type $cwd
191: * @return CommandUtil
192: */
193: public function pipeFilteredTo($filter, $cmd, $cwd = null){
194: return $this->pipeTo($cmd, $cwd, $filter);
195: }
196:
197: /**
198: * Takes the output of the last comand and pipes it to a new command
199: * @param string $cmd
200: * @param string $cwd
201: * @param string $filter Optional regular expressions filter to restrict the input to only certain lines.
202: * @return CommandUtil
203: */
204: public function pipeTo($cmd, $cwd = null, $filter = null){
205: $n_proc = $this->nProc();
206: $this->debug('pipeTo('.$cmd.'): $n_proc = '.$n_proc);
207: if($n_proc == 0)
208: throw new Exception('Cannot pipe to subprocess; no prior processes from which to pipe have been opened.');
209: return $this->run($cmd, $n_proc - 1, $cwd, $filter);
210: }
211:
212: /**
213: * Runs a command on the command line.
214: * @param string $cmd
215: * @param resource|string $input The input for the program.
216: * @param string $cwd The directory to work in while executing.
217: * @return CommandUtil
218: */
219: public function run($cmd, $input = null, $cwd = null, $filter = null){
220: $cwd = $cwd === null ? __DIR__ : $cwd;
221: $descriptor = array(
222: 1 => array('pipe', 'w'),
223: 2 => array('pipe', 'w'),
224: );
225: // Read input
226: $inputType = gettype($input);
227: if($input !== null){
228: if($inputType == 'resource'){
229: // Interpret as a file descriptor.
230: $inputText = stream_get_contents($input);
231: $this->debug("Interpreted input as a stream resource, and read input:\n$inputText",2);
232: }else if($inputType == 'string'){
233: // Interpret as literal input
234: $inputText = $input;
235: $this->debug("Interpreted input as a string, and it is:\n$inputText",2);
236: }else if($inputType == 'integer'){
237: // Interpret as an index of a process whose output will be used as input
238: $inputText = stream_get_contents($this->pipeline[$input][1]);
239: $this->close($input);
240: }
241: $descriptor[0] = array('pipe', 'r');
242: }
243:
244: // Spawn new process
245: $procIndex = $this->nProc();
246: $this->debug("Spawning $cmd and storing process handle at index $procIndex");
247: $this->cmd[$procIndex] = $cmd;
248: $this->processes[$procIndex] = proc_open($cmd, $descriptor, $this->pipeline[$procIndex], $cwd);
249: $filter = empty($filter) ? '/.*/' : $filter;
250:
251: // Write input to process
252: if(!empty($inputText)){ // Send input to the program
253: $this->debug("Writing to input of child process $procIndex...");
254: $this->inputs[$procIndex] = $inputText;
255: foreach(explode("\n", $inputText) as $inputLine)
256: if(preg_match($filter, $inputLine))
257: fwrite($this->pipeline[$procIndex][0], $inputLine);
258: $this->debug("...done.");
259: }
260: return $this;
261: }
262:
263: //////////////////////////
264: // Cron-related methods //
265: //////////////////////////
266: //
267: // The following methods are used for creating scheduled commands. Typically
268: // these will only work in Linux/Unix environments.
269:
270: /**
271: * Loads and returns the cron table. Performs environment check first.
272: * @return string
273: */
274: public function loadCrontab() {
275: // Check to see if everything is as it should be
276: if(!$this->cmdExists('crontab'))
277: throw new Exception('The "crontab" command does not exist on this system, so there is no way to set up cron jobs.',1);
278: if($this->run('crontab -l')->complete() == -1)
279: throw new Exception('There is a cron service available on this system, but PHP is running as a system user that does not have permission to use it.',2);
280: // Get the existing crontab
281: return $this->run('crontab -l')->output();
282: }
283:
284: /**
285: * Saves the cron table.
286: *
287: * Save the table to a temporary file, sends it to cron, and deletes the
288: * temporary file.
289: *
290: * @param string $crontab The new contents of the updated cron table
291: */
292: public function saveCrontab($crontab) {
293: $tempFile = __DIR__.DIRECTORY_SEPARATOR.'crontab-'.time();
294: file_put_contents($tempFile, $crontab);
295: $status = $this->run("crontab $tempFile")->complete();
296: unlink($tempFile);
297: }
298: }
299:
300: ?>
301: