1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4: */
3/**
4 * VersionControl_SVN_Info allows for XML formatted output. XML_Parser is used to
5 * manipulate that output.
6 *
7 * +----------------------------------------------------------------------+
8 * | This LICENSE is in the BSD license style.                            |
9 * | http://www.opensource.org/licenses/bsd-license.php                   |
10 * |                                                                      |
11 * | Redistribution and use in source and binary forms, with or without   |
12 * | modification, are permitted provided that the following conditions   |
13 * | are met:                                                             |
14 * |                                                                      |
15 * |  * Redistributions of source code must retain the above copyright    |
16 * |    notice, this list of conditions and the following disclaimer.     |
17 * |                                                                      |
18 * |  * Redistributions in binary form must reproduce the above           |
19 * |    copyright notice, this list of conditions and the following       |
20 * |    disclaimer in the documentation and/or other materials provided   |
21 * |    with the distribution.                                            |
22 * |                                                                      |
23 * |  * Neither the name of Clay Loveless nor the names of contributors   |
24 * |    may be used to endorse or promote products derived from this      |
25 * |    software without specific prior written permission.               |
26 * |                                                                      |
27 * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
28 * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
29 * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
30 * | FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE      |
31 * | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,  |
32 * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
33 * | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;     |
34 * | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER     |
35 * | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT   |
36 * | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN    |
37 * | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE      |
38 * | POSSIBILITY OF SUCH DAMAGE.                                          |
39 * +----------------------------------------------------------------------+
40 *
41 * PHP version 5
42 *
43 * @category  VersionControl
44 * @package   VersionControl_SVN
45 * @author    Clay Loveless <clay@killersoft.com>
46 * @author    Michiel Rook <mrook@php.net>
47 * @author    Alexander Opitz <opitz.alexander@gmail.com>
48 * @copyright 2004-2007 Clay Loveless
49 * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
50 * @link      http://pear.php.net/package/VersionControl_SVN
51 */
52
53require_once 'VersionControl/SVN/Exception.php';
54require_once 'System.php';
55
56/**
57 * Ground class for a SVN command.
58 *
59 * @category  VersionControl
60 * @package   VersionControl_SVN
61 * @author    Clay Loveless <clay@killersoft.com>
62 * @author    Michiel Rook <mrook@php.net>
63 * @author    Alexander Opitz <opitz.alexander@gmail.com>
64 * @copyright 2004-2007 Clay Loveless
65 * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
66 * @version   0.5.2
67 * @link      http://pear.php.net/package/VersionControl_SVN
68 */
69abstract class VersionControl_SVN_Command
70{
71    /**
72     * Indicates whether commands passed to the
73     * {@link http://www.php.net/exec exec()} function in the
74     * {@link run} method should be passed through
75     * {@link http://www.php.net/escapeshellcmd escapeshellcmd()}.
76     * NOTE: this variable is ignored on Windows machines!
77     *
78     * @var boolean $useEscapeshellcmd
79     */
80    public $useEscapeshellcmd = true;
81
82    /**
83     * Use exec or passthru to get results from command.
84     *
85     * @var bool $passthru
86     */
87    public $passthru = false;
88
89    /**
90     * Location of the svn client binary installed as part of Subversion
91     *
92     * @var string $binaryPath
93     */
94    public $binaryPath = '/usr/local/bin/svn';
95
96    /**
97     * String to prepend to command string. Helpful for setting exec()
98     * environment variables, such as:
99     *    export LANG=en_US.utf8 &&
100     * ... to support non-ASCII file and directory names.
101     *
102     * @var string $prependCmd
103     */
104    public $prependCmd = '';
105
106    /**
107     * Array of switches to use in building svn command
108     *
109     * @var array $switches
110     */
111    public $switches = array();
112
113    /**
114     * Runtime options being used.
115     *
116     * @var array $options
117     */
118    public $options = array();
119
120    /**
121     * Command-line arguments that should be passed
122     * <b>outside</b> of those specified in {@link switches}.
123     *
124     * @var array $args
125     */
126    public $args = array();
127
128    /**
129     * Preferred fetchmode. Note that not all subcommands have output available for
130     * each preferred fetchmode. The default cascade is:
131     *
132     * VERSIONCONTROL_SVN_FETCHMODE_ASSOC
133     *  VERSIONCONTROL_SVN_FETCHMODE_RAW
134     *
135     * If the specified fetchmode isn't available, raw output will be returned.
136     *
137     * @var int $fetchmode
138     */
139    public $fetchmode = VERSIONCONTROL_SVN_FETCHMODE_ASSOC;
140
141    /**
142     * Default username to use for connections.
143     *
144     * @var string $username
145     */
146    public $username = null;
147
148    /**
149     * Default password to use for connections.
150     *
151     * @var string $password
152     */
153    public $password = null;
154
155    /**
156     * Default config-dir to use for connections.
157     *
158     * @var string $configDir
159     */
160    public $configDir = null;
161
162    /**
163     * Default config-option to use for connections.
164     *
165     * @var string $configOption
166     */
167    public $configOption = null;
168
169    /**
170     * Default no-auth-cache to use for connections.
171     *
172     * @var string $noAuthCache
173     */
174    public $noAuthCache = null;
175
176    /**
177     * Default trust-server-cert to use for connections.
178     *
179     * @var string $trustServerCert
180     */
181    public $trustServerCert = false;
182
183    /**
184     * Switches required by this subcommand.
185     * See {@link http://svnbook.red-bean.com/svnbook/ Version Control with Subversion},
186     * Subversion Complete Reference for details on arguments for this subcommand.
187     *
188     * @var array $requiredSwitches
189     */
190    protected $requiredSwitches = array();
191
192    /**
193     * Minimum number of args required by this subcommand.
194     * See {@link http://svnbook.red-bean.com/svnbook/ Version Control with Subversion},
195     * Subversion Complete Reference for details on arguments for this subcommand.
196     *
197     * @var int $minArgs
198     */
199    protected $minArgs = 0;
200
201    /**
202     * SVN subcommand to run.
203     *
204     * @var string $commandName
205     */
206    protected $commandName = '';
207
208    /**
209     * Fully prepared command string.
210     *
211     * @var string $preparedCmd
212     */
213    protected $preparedCmd = '';
214
215    /**
216     * Keep track of whether XML output is available for a command
217     *
218     * @var boolean $xmlAvail
219     */
220    protected $xmlAvail = false;
221
222    /**
223     * Useable switches for command with parameters.
224     */
225    protected $validSwitchesValue = array(
226        'username',
227        'password',
228        'config-dir',
229        'config-option',
230    );
231
232    /**
233     * Useable switches for command without parameters.
234     */
235    protected $validSwitches = array(
236        'no-auth-cache',
237        'non-interactive',
238        'trust-server-cert',
239    );
240
241    /**
242     * Constructor. Can't be called directly as class is abstract.
243     */
244    public function __construct()
245    {
246        $className = get_class($this);
247        $this->commandName = strtolower(
248            substr(
249                $className,
250                strrpos($className, '_') + 1
251            )
252        );
253    }
254
255    /**
256     * Allow for overriding of previously declared options.
257     *
258     * @param array $options An associative array of option names and
259     *                       their values
260     *
261     * @return VersionControl_SVN_Command Themself.
262     * @throws VersionControl_SVN_Exception If option isn't available.
263     */
264    public function setOptions($options = array())
265    {
266        $class = new ReflectionClass($this);
267
268        foreach ($options as $option => $value) {
269            try {
270                $property = $class->getProperty($option);
271            } catch (ReflectionException $e) {
272                $property = null;
273            }
274            if (null !== $property && $property->isPublic()) {
275                $this->$option = $value;
276            } else {
277                throw new VersionControl_SVN_Exception(
278                    '"' . $option . '" is not a valid option',
279                    VersionControl_SVN_Exception::INVALID_OPTION
280                );
281            }
282        }
283
284        return $this;
285    }
286
287    /**
288     * Prepare the command switches.
289     *
290     * This function should be overloaded by the command class.
291     *
292     * @return void
293     * @throws VersionControl_SVN_Exception If preparing failed.
294     */
295    public function prepare()
296    {
297        $this->checkCommandRequirements();
298        $this->preProcessSwitches();
299
300        $invalidSwitches = array();
301        $cmdParts = array(
302            $this->binaryPath,
303            $this->commandName
304        );
305
306        foreach ($this->switches as $switch => $val) {
307            if (1 === strlen($switch)) {
308                $switchPrefix = '-';
309            } else {
310                $switchPrefix = '--';
311            }
312            if (in_array($switch, $this->validSwitchesValue)) {
313                $cmdParts[] = $switchPrefix . $switch . ' ' . escapeshellarg($val);
314            } elseif (in_array($switch, $this->validSwitches)) {
315                if (true === $val) {
316                    $cmdParts[] = $switchPrefix . $switch;
317                }
318            } else {
319                $invalidSwitches[] = $switch;
320            }
321        }
322
323        $this->postProcessSwitches($invalidSwitches);
324
325        $this->preparedCmd = implode(' ', array_merge($cmdParts, $this->args));
326    }
327
328    /**
329     * Called after handling switches.
330     *
331     * @param array $invalidSwitches Invalid switches found while processing.
332     *
333     * @return void
334     * @throws VersionControl_SVN_Exception If switch(s) is/are invalid.
335     */
336    protected function postProcessSwitches($invalidSwitches)
337    {
338        $invalid = count($invalidSwitches);
339        if ($invalid > 0) {
340            $invalides = implode(',', $invalidSwitches);
341            if ($invalid > 1) {
342                $error = '"' . $invalides . '" are invalid switches';
343            } else {
344                $error = '"' . $invalides . '" is a invalid switch';
345            }
346            $error .= ' for class "' . get_class($this) . '".';
347            throw new VersionControl_SVN_Exception(
348                $error,
349                VersionControl_SVN_Exception::INVALID_SWITCH
350            );
351        }
352    }
353
354
355    /**
356     * Called before handling switches.
357     *
358     * @return void
359     */
360    protected function preProcessSwitches()
361    {
362        if ($this->xmlAvail
363            && ($this->fetchmode == VERSIONCONTROL_SVN_FETCHMODE_ARRAY
364            || $this->fetchmode == VERSIONCONTROL_SVN_FETCHMODE_ASSOC
365            || $this->fetchmode == VERSIONCONTROL_SVN_FETCHMODE_OBJECT
366            || $this->fetchmode == VERSIONCONTROL_SVN_FETCHMODE_XML)
367        ) {
368            $this->switches['xml'] = true;
369        } else {
370            unset($this->switches['xml']);
371        }
372
373        $this->switches['non-interactive'] = true;
374
375        $this->fillSwitch('username', $this->username);
376        $this->fillSwitch('password', $this->password);
377        $this->fillSwitch('config-dir', $this->configDir);
378        $this->fillSwitch('config-option', $this->configOption);
379        $this->fillSwitch('no-auth-cache', $this->noAuthCache);
380        $this->fillSwitch('trust-server-cert', $this->trustServerCert);
381    }
382
383    /**
384     * Fills the switches array on given name with value if not already set and value is not null.
385     *
386     * @param string $switchName Name of the switch.
387     * @param string $value      Value for the switch.
388     *
389     * @return void
390     */
391    protected function fillSwitch($switchName, $value)
392    {
393        if (!isset($this->switches[$switchName])
394            && null !== $value
395        ) {
396            $this->switches[$switchName] = $value;
397        }
398    }
399
400
401    /**
402     * Standardized validation of requirements for a command class.
403     *
404     * @return void
405     * @throws VersionControl_SVN_Exception If command requirements not resolved.
406     */
407    public function checkCommandRequirements()
408    {
409        // Check for minimum arguments
410        if (sizeof($this->args) < $this->minArgs) {
411            throw new VersionControl_SVN_Exception(
412                'svn command requires at least ' . $this->minArgs . ' argument(s)',
413                VersionControl_SVN_Exception::MIN_ARGS
414            );
415        }
416
417        // Check for presence of required switches
418        if (sizeof($this->requiredSwitches) > 0) {
419            $missing    = array();
420            $switches   = $this->switches;
421            $reqsw      = $this->requiredSwitches;
422            foreach ($reqsw as $req) {
423                $found = false;
424                $good_switches = explode('|', $req);
425                foreach ($good_switches as $gsw) {
426                    if (isset($switches[$gsw])) {
427                        $found = true;
428                    }
429                }
430                if (!$found) {
431                    $missing[] = '(' . $req . ')';
432                }
433            }
434            $num_missing = count($missing);
435            if ($num_missing > 0) {
436                throw new VersionControl_SVN_Exception(
437                    'svn command requires the following switch(es): ' . implode(', ', $missing),
438                    VersionControl_SVN_Exception::SWITCH_MISSING
439                );
440            }
441        }
442    }
443
444    /**
445     * Run the command with the defined switches.
446     *
447     * @param array $args     Arguments to pass to Subversion
448     * @param array $switches Switches to pass to Subversion
449     *
450     * @return mixed $fetchmode specified output on success.
451     * @throws VersionControl_SVN_Exception If command failed.
452     */
453    public function run($args = array(), $switches = array())
454    {
455        if (!file_exists($this->binaryPath)) {
456            $system = new System();
457            $this->binaryPath = $system->which('svn');
458        }
459
460        if (sizeof($switches) > 0) {
461            $this->switches = $switches;
462        }
463        $this->args = array_map('escapeshellarg', $args);
464
465        // Always prepare, allows for obj re-use. (Request #5021)
466        $this->prepare();
467
468        $out       = array();
469        // @var integer $returnVar Return number from shell execution.
470        $returnVar = null;
471
472        $cmd = $this->preparedCmd;
473
474        // On Windows, don't use escapeshellcmd, and double-quote $cmd
475        // so it's executed as
476        // cmd /c ""C:\Program Files\SVN\bin\svn.exe" info "C:\Program Files\dev\trunk""
477        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
478            $cmd = str_replace(
479                $this->binaryPath,
480                escapeshellarg(str_replace('/', '\\', $this->binaryPath)),
481                $cmd
482            );
483
484            if (!$this->passthru) {
485                exec("cmd /c \"$cmd 2>&1\"", $out, $returnVar);
486            } else {
487                passthru("cmd /c \"$cmd 2>&1\"", $returnVar);
488            }
489        } else {
490            if ($this->useEscapeshellcmd) {
491                $cmd = escapeshellcmd($cmd);
492            }
493            if (!$this->passthru) {
494                exec("{$this->prependCmd}$cmd 2>&1", $out, $returnVar);
495            } else {
496                passthru("{$this->prependCmd}$cmd 2>&1", $returnVar);
497            }
498        }
499
500        if ($returnVar > 0) {
501            throw new VersionControl_SVN_Exception(
502                'Execution of command failed returning: ' . $returnVar
503                . "\n" . implode("\n", $out),
504                VersionControl_SVN_Exception::EXEC
505            );
506        }
507
508        return $this->parseOutput($out);
509    }
510
511    /**
512     * Handles output parsing of standard and verbose output of command.
513     *
514     * @param array $out Array of output captured by exec command in {@link run}
515     *
516     * @return mixed Returns output requested by fetchmode (if available), or
517     *               raw output if desired fetchmode is not available.
518     */
519    public function parseOutput($out)
520    {
521        $dir = realpath(dirname(__FILE__)) . '/Parser/XML';
522        switch($this->fetchmode) {
523        case VERSIONCONTROL_SVN_FETCHMODE_ARRAY:
524        case VERSIONCONTROL_SVN_FETCHMODE_ASSOC:
525        case VERSIONCONTROL_SVN_FETCHMODE_OBJECT:
526            $file = $dir . '/' . ucfirst($this->commandName) . '.php';
527            if (file_exists($file)) {
528                $class = 'VersionControl_SVN_Parser_XML_'
529                    . ucfirst($this->commandName);
530
531                include_once $file;
532                $parser = new $class;
533                $contentVar = $this->commandName;
534
535                $parsedData = $parser->getParsed(join("\n", $out));
536                if ($this->fetchmode == VERSIONCONTROL_SVN_FETCHMODE_OBJECT) {
537                    return (object) $parsedData;
538                }
539                return $parsedData;
540                break;
541            }
542        case VERSIONCONTROL_SVN_FETCHMODE_RAW:
543        case VERSIONCONTROL_SVN_FETCHMODE_XML:
544        default:
545            // What you get with VERSIONCONTROL_SVN_FETCHMODE_DEFAULT
546            return join("\n", $out);
547            break;
548        }
549    }
550}
551?>
552