Test Failed
Push — master ( 342b63...d215d6 )
by Michiel
05:23
created

ExecTask::setExecutable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
use Phing\Exception\BuildException;
21
use Phing\Exception\NullPointerException;
22
use Phing\Io\FileUtils;
23
use Phing\Io\IOException;
24
use Phing\Io\File;
25
use Phing\Phing;
26
use Phing\Project;
27
use Phing\Type\Commandline;
28
use Phing\Type\CommandlineArgument;
29
use Phing\Type\Environment;
30
use Phing\Type\EnvVariable;
31
use Phing\Type\Path;
32
use Phing\Util\StringHelper;
33
34
/**
35
 * Executes a command on the shell.
36
 *
37
 * @author  Andreas Aderhold <[email protected]>
38
 * @author  Hans Lellelid <[email protected]>
39
 * @author  Christian Weiske <[email protected]>
40
 * @package phing.tasks.system
41
 */
42
class ExecTask extends Task
43
{
44
    use LogLevelAware;
45
46
    public const INVALID = PHP_INT_MAX;
47
48
    private $exitValue = self::INVALID;
49
50
    /**
51
     * Command to be executed
52
     *
53
     * @var string
54
     */
55
    protected $realCommand;
56
57
    /**
58
     * Commandline managing object
59
     *
60
     * @var Commandline
61
     */
62
    protected $commandline;
63
64
    /**
65
     * Working directory.
66
     *
67
     * @var File
68
     */
69
    protected $dir;
70
71
    protected $currdir;
72
73
    /**
74
     * Operating system.
75
     *
76
     * @var string
77
     */
78
    protected $os;
79
80
    /**
81
     * Whether to escape shell command using escapeshellcmd().
82
     *
83
     * @var boolean
84
     */
85
    protected $escape = false;
86
87
    /**
88
     * Where to direct output.
89
     *
90
     * @var File
91
     */
92
    protected $output;
93
94
    /**
95
     * Whether to use PHP's passthru() function instead of exec()
96
     *
97
     * @var boolean
98
     */
99
    protected $passthru = false;
100
101
    /**
102
     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
103
     *
104
     * @var boolean
105
     */
106
    protected $logOutput = false;
107
108
    /**
109
     * Where to direct error output.
110
     *
111
     * @var File
112
     */
113
    protected $error;
114
115
    /**
116
     * If spawn is set then [unix] programs will redirect stdout and add '&'.
117
     *
118
     * @var boolean
119
     */
120
    protected $spawn = false;
121
122
    /**
123
     * Property name to set with return value from exec call.
124
     *
125
     * @var string
126
     */
127
    protected $returnProperty;
128
129
    /**
130
     * Property name to set with output value from exec call.
131
     *
132
     * @var string
133
     */
134
    protected $outputProperty;
135
136
    /**
137
     * Whether to check the return code.
138
     *
139
     * @var boolean
140
     */
141
    protected $checkreturn = false;
142
143
    private $osFamily;
144
    private $executable;
145
    private $resolveExecutable = false;
146
    private $searchPath = false;
147
    private $env;
148
149
    /**
150
     * @throws \Phing\Exception\BuildException
151
     */
152 87
    public function __construct()
153
    {
154 87
        parent::__construct();
155 87
        $this->commandline = new Commandline();
156 87
        $this->env = new Environment();
157 87
    }
158
159
    /**
160
     * Main method: wraps execute() command.
161
     *
162
     * @throws \Phing\Exception\BuildException
163
     */
164 32
    public function main()
165
    {
166 32
        if (!$this->isValidOs()) {
167 1
            return null;
168
        }
169
170
        try {
171 30
            $this->commandline->setExecutable($this->resolveExecutable($this->executable, $this->searchPath));
172
        } catch (IOException | NullPointerException $e) {
173
            throw new BuildException($e);
174
        }
175
176 30
        $this->prepare();
177 28
        $this->buildCommand();
178 28
        [$return, $output] = $this->executeCommand();
179 28
        $this->cleanup($return, $output);
180
181 27
        return $return;
182
    }
183
184
    /**
185
     * Prepares the command building and execution, i.e.
186
     * changes to the specified directory.
187
     *
188
     * @throws BuildException
189
     * @return void
190
     */
191 30
    protected function prepare()
192
    {
193 30
        if ($this->dir === null) {
194 27
            $this->dir = $this->getProject()->getBasedir();
195
        }
196
197 30
        if ($this->commandline->getExecutable() === null) {
0 ignored issues
show
introduced by
The condition $this->commandline->getExecutable() === null is always false.
Loading history...
198 1
            throw new BuildException(
199 1
                'ExecTask: Please provide "executable"'
200
            );
201
        }
202
203
        // expand any symbolic links first
204
        try {
205 29
            if (!$this->dir->getCanonicalFile()->exists()) {
206 1
                throw new BuildException(
207 1
                    "The directory '" . (string) $this->dir . "' does not exist"
208
                );
209
            }
210 28
            if (!$this->dir->getCanonicalFile()->isDirectory()) {
211
                throw new BuildException(
212 28
                    "'" . (string) $this->dir . "' is not a directory"
213
                );
214
            }
215 1
        } catch (IOException $e) {
216
            throw new BuildException(
217
                "'" . (string) $this->dir . "' is not a readable directory"
218
            );
219
        }
220 28
        $this->currdir = getcwd();
221 28
        @chdir($this->dir->getPath());
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

221
        /** @scrutinizer ignore-unhandled */ @chdir($this->dir->getPath());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
222
223 28
        $this->commandline->setEscape($this->escape);
224 28
    }
225
226
    /**
227
     * @param int $exitValue
228
     * @return bool
229
     */
230 1
    public function isFailure($exitValue = null)
231
    {
232 1
        if ($exitValue === null) {
233
            $exitValue = $this->getExitValue();
234
        }
235
236 1
        return $exitValue !== 0;
237
    }
238
239
    /**
240
     * Builds the full command to execute and stores it in $command.
241
     *
242
     * @throws BuildException
243
     * @return void
244
     * @uses   $command
245
     */
246 28
    protected function buildCommand()
247
    {
248 28
        if ($this->error !== null) {
249 1
            $this->realCommand .= ' 2> ' . escapeshellarg($this->error->getPath());
250 1
            $this->log(
251 1
                'Writing error output to: ' . $this->error->getPath(),
252 1
                $this->logLevel
253
            );
254
        }
255
256 28
        if ($this->output !== null) {
257 1
            $this->realCommand .= ' 1> ' . escapeshellarg($this->output->getPath());
258 1
            $this->log(
259 1
                'Writing standard output to: ' . $this->output->getPath(),
260 1
                $this->logLevel
261
            );
262 27
        } elseif ($this->spawn) {
263 1
            $this->realCommand .= ' 1>/dev/null';
264 1
            $this->log('Sending output to /dev/null', $this->logLevel);
265
        }
266
267
        // If neither output nor error are being written to file
268
        // then we'll redirect error to stdout so that we can dump
269
        // it to screen below.
270
271 28
        if ($this->output === null && $this->error === null && $this->passthru === false) {
272 25
            $this->realCommand .= ' 2>&1';
273
        }
274
275
        // we ignore the spawn boolean for windows
276 28
        if ($this->spawn) {
277 1
            $this->realCommand .= ' &';
278
        }
279
280 28
        $envString = '';
281 28
        $environment = $this->env->getVariables();
282 28
        if ($environment !== null) {
0 ignored issues
show
introduced by
The condition $environment !== null is always true.
Loading history...
283 3
            foreach ($environment as $variable) {
284 3
                if ($this->isPath($variable)) {
285 1
                    continue;
286
                }
287 2
                $this->log('Setting environment variable: ' . $variable, Project::MSG_VERBOSE);
288 2
                if (OsCondition::isOS(OsCondition::FAMILY_WINDOWS)) {
289
                    $envString .= 'set ' . $variable . '& ';
290
                } else {
291 2
                    $envString .= 'export ' . $variable . '; ';
292
                }
293
            }
294
        }
295
296 28
        $this->realCommand = $envString . $this->commandline . $this->realCommand;
297 28
    }
298
299
    /**
300
     * Executes the command and returns return code and output.
301
     *
302
     * @return array array(return code, array with output)
303
     * @throws \Phing\Exception\BuildException
304
     */
305 46
    protected function executeCommand()
306
    {
307 46
        $cmdl = $this->realCommand;
308
309 46
        $this->log('Executing command: ' . $cmdl, $this->logLevel);
310
311 46
        $output = [];
312 46
        $return = null;
313
314 46
        if ($this->passthru) {
315 2
            passthru($cmdl, $return);
316
        } else {
317 44
            exec($cmdl, $output, $return);
318
        }
319
320 46
        return [$return, $output];
321
    }
322
323
    /**
324
     * Runs all tasks after command execution:
325
     * - change working directory back
326
     * - log output
327
     * - verify return value
328
     *
329
     * @param integer $return Return code
330
     * @param array $output Array with command output
331
     *
332
     * @throws BuildException
333
     * @return void
334
     */
335 28
    protected function cleanup($return, $output): void
336
    {
337 28
        if ($this->dir !== null) {
338 28
            @chdir($this->currdir);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

338
            /** @scrutinizer ignore-unhandled */ @chdir($this->currdir);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
339
        }
340
341 28
        $outloglevel = $this->logOutput ? Project::MSG_INFO : Project::MSG_VERBOSE;
342 28
        foreach ($output as $line) {
343 21
            $this->log($line, $outloglevel);
344
        }
345
346 28
        $this->maybeSetReturnPropertyValue($return);
347
348 28
        if ($this->outputProperty) {
349 5
            $this->project->setProperty(
350 5
                $this->outputProperty,
351 5
                implode("\n", $output)
352
            );
353
        }
354
355 28
        $this->setExitValue($return);
356
357 28
        if ($return !== 0) {
358 11
            if ($this->checkreturn) {
359 1
                throw new BuildException($this->getTaskType() . ' returned: ' . $return, $this->getLocation());
360
            }
361 10
            $this->log('Result: ' . $return, Project::MSG_ERR);
362
        }
363 27
    }
364
365
    /**
366
     * Set the exit value.
367
     *
368
     * @param int $value exit value of the process.
369
     */
370 28
    protected function setExitValue($value): void
371
    {
372 28
        $this->exitValue = $value;
373 28
    }
374
375
    /**
376
     * Query the exit value of the process.
377
     *
378
     * @return int the exit value or self::INVALID if no exit value has
379
     *             been received.
380
     */
381
    public function getExitValue(): int
382
    {
383
        return $this->exitValue;
384
    }
385
386
    /**
387
     * The command to use.
388
     *
389
     * @param string $command String or string-compatible (e.g. w/ __toString()).
390
     *
391
     * @return void
392
     * @throws \Phing\Exception\BuildException
393
     */
394 32
    public function setCommand($command): void
395
    {
396 32
        $this->log(
397 32
            "The command attribute is deprecated.\nPlease use the executable attribute and nested arg elements.",
398 32
            Project::MSG_WARN
399
        );
400 32
        $this->commandline = new Commandline($command);
401 32
        $this->executable = $this->commandline->getExecutable();
402 32
    }
403
404
    /**
405
     * The executable to use.
406
     *
407
     * @param string|bool $value String or string-compatible (e.g. w/ __toString()).
408
     *
409
     * @return void
410
     */
411 54
    public function setExecutable($value): void
412
    {
413 54
        if (is_bool($value)) {
414 3
            $value = $value === true ? 'true' : 'false';
415
        }
416 54
        $this->executable = $value;
417 54
        $this->commandline->setExecutable($value);
418 54
    }
419
420
    /**
421
     * Whether to use escapeshellcmd() to escape command.
422
     *
423
     * @param boolean $escape If the command shall be escaped or not
424
     *
425
     * @return void
426
     */
427 7
    public function setEscape(bool $escape): void
428
    {
429 7
        $this->escape = $escape;
430 7
    }
431
432
    /**
433
     * Specify the working directory for executing this command.
434
     *
435
     * @param File $dir Working directory
436
     *
437
     * @return void
438
     */
439 9
    public function setDir(File $dir): void
440
    {
441 9
        $this->dir = $dir;
442 9
    }
443
444
    /**
445
     * Specify OS (or multiple OS) that must match in order to execute this command.
446
     *
447
     * @param string $os Operating system string (e.g. "Linux")
448
     *
449
     * @return void
450
     */
451 6
    public function setOs($os): void
452
    {
453 6
        $this->os = (string) $os;
454 6
    }
455
456
    /**
457
     * List of operating systems on which the command may be executed.
458
     */
459
    public function getOs(): string
460
    {
461
        return $this->os;
462
    }
463
464
    /**
465
     * Restrict this execution to a single OS Family
466
     *
467
     * @param string $osFamily the family to restrict to.
468
     */
469 2
    public function setOsFamily($osFamily): void
470
    {
471 2
        $this->osFamily = strtolower($osFamily);
472 2
    }
473
474
    /**
475
     * Restrict this execution to a single OS Family
476
     */
477
    public function getOsFamily()
478
    {
479
        return $this->osFamily;
480
    }
481
482
    /**
483
     * File to which output should be written.
484
     *
485
     * @param File $f Output log file
486
     *
487
     * @return void
488
     */
489 6
    public function setOutput(File $f): void
490
    {
491 6
        $this->output = $f;
492 6
    }
493
494
    /**
495
     * File to which error output should be written.
496
     *
497
     * @param File $f Error log file
498
     *
499
     * @return void
500
     */
501 4
    public function setError(File $f): void
502
    {
503 4
        $this->error = $f;
504 4
    }
505
506
    /**
507
     * Whether to use PHP's passthru() function instead of exec()
508
     *
509
     * @param boolean $passthru If passthru shall be used
510
     *
511
     * @return void
512
     */
513 4
    public function setPassthru($passthru): void
514
    {
515 4
        $this->passthru = $passthru;
516 4
    }
517
518
    /**
519
     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
520
     *
521
     * @param boolean $logOutput If output shall be logged visibly
522
     *
523
     * @return void
524
     */
525 1
    public function setLogoutput($logOutput): void
526
    {
527 1
        $this->logOutput = $logOutput;
528 1
    }
529
530
    /**
531
     * Whether to suppress all output and run in the background.
532
     *
533
     * @param boolean $spawn If the command is to be run in the background
534
     *
535
     * @return void
536
     */
537 4
    public function setSpawn($spawn): void
538
    {
539 4
        $this->spawn = $spawn;
540 4
    }
541
542
    /**
543
     * Whether to check the return code.
544
     *
545
     * @param boolean $checkreturn If the return code shall be checked
546
     *
547
     * @return void
548
     */
549 13
    public function setCheckreturn($checkreturn): void
550
    {
551 13
        $this->checkreturn = $checkreturn;
552 13
    }
553
554
    /**
555
     * The name of property to set to return value from exec() call.
556
     *
557
     * @param string $prop Property name
558
     *
559
     * @return void
560
     */
561 5
    public function setReturnProperty($prop): void
562
    {
563 5
        $this->returnProperty = $prop;
564 5
    }
565
566 46
    protected function maybeSetReturnPropertyValue(int $return)
567
    {
568 46
        if ($this->returnProperty) {
569 3
            $this->getProject()->setNewProperty($this->returnProperty, $return);
570
        }
571 46
    }
572
573
    /**
574
     * The name of property to set to output value from exec() call.
575
     *
576
     * @param string $prop Property name
577
     *
578
     * @return void
579
     */
580 9
    public function setOutputProperty($prop): void
581
    {
582 9
        $this->outputProperty = $prop;
583 9
    }
584
585
    /**
586
     * Add an environment variable to the launched process.
587
     *
588
     * @param EnvVariable $var new environment variable.
589
     */
590 3
    public function addEnv(EnvVariable $var)
591
    {
592 3
        $this->env->addVariable($var);
593 3
    }
594
595
    /**
596
     * Creates a nested <arg> tag.
597
     *
598
     * @return CommandlineArgument Argument object
599
     */
600 25
    public function createArg()
601
    {
602 25
        return $this->commandline->createArgument();
603
    }
604
605
    /**
606
     * Is this the OS the user wanted?
607
     *
608
     * @return boolean.
0 ignored issues
show
Documentation Bug introduced by
The doc comment boolean. at position 0 could not be parsed: Unknown type name 'boolean.' at position 0 in boolean..
Loading history...
609
     * <ul>
610
     * <li>
611
     * <li><code>true</code> if the os and osfamily attributes are null.</li>
612
     * <li><code>true</code> if osfamily is set, and the os family and must match
613
     * that of the current OS, according to the logic of
614
     * {@link Os#isOs(String, String, String, String)}, and the result of the
615
     * <code>os</code> attribute must also evaluate true.
616
     * </li>
617
     * <li>
618
     * <code>true</code> if os is set, and the system.property os.name
619
     * is found in the os attribute,</li>
620
     * <li><code>false</code> otherwise.</li>
621
     * </ul>
622
     */
623 51
    protected function isValidOs(): bool
624
    {
625
        //hand osfamily off to OsCondition class, if set
626 51
        if ($this->osFamily !== null && !OsCondition::isFamily($this->osFamily)) {
627
            return false;
628
        }
629
        //the Exec OS check is different from Os.isOs(), which
630
        //probes for a specific OS. Instead it searches the os field
631
        //for the current os.name
632 50
        $myos = Phing::getProperty("os.name");
633 50
        $this->log("Current OS is " . $myos, Project::MSG_VERBOSE);
634 50
        if (($this->os !== null) && (strpos($this->os, $myos) === false)) {
635
            // this command will be executed only on the specified OS
636 2
            $this->log(
637 2
                "This OS, " . $myos
638 2
                . " was not found in the specified list of valid OSes: " . $this->os,
639 2
                Project::MSG_VERBOSE
640
            );
641 2
            return false;
642
        }
643 48
        return true;
644
    }
645
646
    /**
647
     * Set whether to attempt to resolve the executable to a file.
648
     *
649
     * @param bool $resolveExecutable if true, attempt to resolve the
650
     * path of the executable.
651
     */
652 1
    public function setResolveExecutable($resolveExecutable): void
653
    {
654 1
        $this->resolveExecutable = $resolveExecutable;
655 1
    }
656
657
    /**
658
     * Set whether to search nested, then
659
     * system PATH environment variables for the executable.
660
     *
661
     * @param bool $searchPath if true, search PATHs.
662
     */
663 1
    public function setSearchPath($searchPath): void
664
    {
665 1
        $this->searchPath = $searchPath;
666 1
    }
667
668
    /**
669
     * Indicates whether to attempt to resolve the executable to a
670
     * file.
671
     *
672
     * @return bool the resolveExecutable flag
673
     */
674
    public function getResolveExecutable(): bool
675
    {
676
        return $this->resolveExecutable;
677
    }
678
679
    /**
680
     * The method attempts to figure out where the executable is so that we can feed
681
     * the full path. We first try basedir, then the exec dir, and then
682
     * fallback to the straight executable name (i.e. on the path).
683
     *
684
     * @param string $exec the name of the executable.
685
     * @param bool $mustSearchPath if true, the executable will be looked up in
686
     *                               the PATH environment and the absolute path
687
     *                               is returned.
688
     *
689
     * @return string the executable as a full path if it can be determined.
690
     * @throws \Phing\Exception\BuildException
691
     * @throws IOException
692
     * @throws NullPointerException
693
     */
694 30
    protected function resolveExecutable($exec, $mustSearchPath): ?string
695
    {
696 30
        if (!$this->resolveExecutable) {
697 29
            return $exec;
698
        }
699
        // try to find the executable
700 1
        $executableFile = $this->getProject()->resolveFile($exec);
701 1
        if ($executableFile->exists()) {
702
            return $executableFile->getAbsolutePath();
703
        }
704
        // now try to resolve against the dir if given
705 1
        if ($this->dir !== null) {
706
            $executableFile = (new FileUtils())->resolveFile($this->dir, $exec);
707
            if ($executableFile->exists()) {
708
                return $executableFile->getAbsolutePath();
709
            }
710
        }
711
        // couldn't find it - must be on path
712 1
        if ($mustSearchPath) {
713 1
            $p = null;
714 1
            $environment = $this->env->getVariables();
715 1
            if ($environment !== null) {
0 ignored issues
show
introduced by
The condition $environment !== null is always true.
Loading history...
716 1
                foreach ($environment as $env) {
717 1
                    if ($this->isPath($env)) {
718 1
                        $p = new Path($this->getProject(), $this->getPath($env));
719 1
                        break;
720
                    }
721
                }
722
            }
723 1
            if ($p === null) {
724
                $p = new Path($this->getProject(), getenv('path'));
725
            }
726 1
            if ($p !== null) {
727 1
                $dirs = $p->listPaths();
728 1
                foreach ($dirs as $dir) {
729 1
                    $executableFile = (new FileUtils())->resolveFile(new File($dir), $exec);
730 1
                    if ($executableFile->exists()) {
731 1
                        return $executableFile->getAbsolutePath();
732
                    }
733
                }
734
            }
735
        }
736
737
        return $exec;
738
    }
739
740 3
    private function isPath($line)
741
    {
742 3
        return StringHelper::startsWith('PATH=', $line) || StringHelper::startsWith('Path=', $line);
743
    }
744
745 1
    private function getPath($value)
746
    {
747 1
        if (is_string($value)) {
748 1
            return StringHelper::substring($value, strlen("PATH="));
749
        }
750
751
        if (is_array($value)) {
752
            $p = $value['PATH'];
753
            return $p ?? $value['Path'];
754
        }
755
756
        throw new InvalidArgumentException('$value should be of type array or string.');
757
    }
758
}
759