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

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

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