Passed
Push — master ( e1f86a...4e1a3a )
by Siad
05:23
created

ExecTask::isValidOs()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 3
nop 0
dl 0
loc 21
ccs 11
cts 12
cp 0.9167
crap 5.0144
rs 9.6111
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
namespace Phing\Tasks\System;
21
22
use Phing\Exception\BuildException;
23
use Phing\Exception\NullPointerException;
24
use Phing\Io\FileUtils;
25
use Phing\Io\IOException;
26
use Phing\Io\File;
27
use Phing\Phing;
28
use Phing\Project;
29
use Phing\Task;
30
use Phing\Tasks\System\Condition\OsCondition;
31
use Phing\Tasks\System\Element\LogLevelAware;
32
use Phing\Type\Commandline;
33
use Phing\Type\CommandlineArgument;
34
use Phing\Type\Environment;
35
use Phing\Type\EnvVariable;
36
use Phing\Type\Path;
37
use Phing\Util\StringHelper;
38
39
/**
40
 * Executes a command on the shell.
41
 *
42
 * @author  Andreas Aderhold <[email protected]>
43
 * @author  Hans Lellelid <[email protected]>
44
 * @author  Christian Weiske <[email protected]>
45
 * @package phing.tasks.system
46
 */
47
class ExecTask extends Task
48
{
49
    use LogLevelAware;
50
51
    public const INVALID = PHP_INT_MAX;
52
53
    private $exitValue = self::INVALID;
54
55
    /**
56
     * Command to be executed
57
     *
58
     * @var string
59
     */
60
    protected $realCommand;
61
62
    /**
63
     * Commandline managing object
64
     *
65
     * @var Commandline
66
     */
67
    protected $commandline;
68
69
    /**
70
     * Working directory.
71
     *
72
     * @var File
73
     */
74
    protected $dir;
75
76
    protected $currdir;
77
78
    /**
79
     * Operating system.
80
     *
81
     * @var string
82
     */
83
    protected $os;
84
85
    /**
86
     * Whether to escape shell command using escapeshellcmd().
87
     *
88
     * @var boolean
89
     */
90
    protected $escape = false;
91
92
    /**
93
     * Where to direct output.
94
     *
95
     * @var File
96
     */
97
    protected $output;
98
99
    /**
100
     * Whether to use PHP's passthru() function instead of exec()
101
     *
102
     * @var boolean
103
     */
104
    protected $passthru = false;
105
106
    /**
107
     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
108
     *
109
     * @var boolean
110
     */
111
    protected $logOutput = false;
112
113
    /**
114
     * Where to direct error output.
115
     *
116
     * @var File
117
     */
118
    protected $error;
119
120
    /**
121
     * If spawn is set then [unix] programs will redirect stdout and add '&'.
122
     *
123
     * @var boolean
124
     */
125
    protected $spawn = false;
126
127
    /**
128
     * Property name to set with return value from exec call.
129
     *
130
     * @var string
131
     */
132
    protected $returnProperty;
133
134
    /**
135
     * Property name to set with output value from exec call.
136
     *
137
     * @var string
138
     */
139
    protected $outputProperty;
140
141
    /**
142
     * Whether to check the return code.
143
     *
144
     * @var boolean
145
     */
146
    protected $checkreturn = false;
147
148
    private $osFamily;
149
    private $executable;
150
    private $resolveExecutable = false;
151
    private $searchPath = false;
152
    private $env;
153
154
    /**
155
     * @throws \Phing\Exception\BuildException
156
     */
157 87
    public function __construct()
158
    {
159 87
        parent::__construct();
160 87
        $this->commandline = new Commandline();
161 87
        $this->env = new Environment();
162 87
    }
163
164
    /**
165
     * Main method: wraps execute() command.
166
     *
167
     * @throws \Phing\Exception\BuildException
168
     */
169 32
    public function main()
170
    {
171 32
        if (!$this->isValidOs()) {
172 1
            return null;
173
        }
174
175
        try {
176 30
            $this->commandline->setExecutable($this->resolveExecutable($this->executable, $this->searchPath));
177
        } catch (IOException | NullPointerException $e) {
178
            throw new BuildException($e);
179
        }
180
181 30
        $this->prepare();
182 28
        $this->buildCommand();
183 28
        [$return, $output] = $this->executeCommand();
184 28
        $this->cleanup($return, $output);
185
186 27
        return $return;
187
    }
188
189
    /**
190
     * Prepares the command building and execution, i.e.
191
     * changes to the specified directory.
192
     *
193
     * @throws BuildException
194
     * @return void
195
     */
196 30
    protected function prepare()
197
    {
198 30
        if ($this->dir === null) {
199 27
            $this->dir = $this->getProject()->getBasedir();
200
        }
201
202 30
        if ($this->commandline->getExecutable() === null) {
0 ignored issues
show
introduced by
The condition $this->commandline->getExecutable() === null is always false.
Loading history...
203 1
            throw new BuildException(
204 1
                'ExecTask: Please provide "executable"'
205
            );
206
        }
207
208
        // expand any symbolic links first
209
        try {
210 29
            if (!$this->dir->getCanonicalFile()->exists()) {
211 1
                throw new BuildException(
212 1
                    "The directory '" . (string) $this->dir . "' does not exist"
213
                );
214
            }
215 28
            if (!$this->dir->getCanonicalFile()->isDirectory()) {
216
                throw new BuildException(
217 28
                    "'" . (string) $this->dir . "' is not a directory"
218
                );
219
            }
220 1
        } catch (IOException $e) {
221
            throw new BuildException(
222
                "'" . (string) $this->dir . "' is not a readable directory"
223
            );
224
        }
225 28
        $this->currdir = getcwd();
226 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

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

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