Passed
Push — master ( 51d982...2a6e37 )
by Siad
11:06
created

ExecTask::maybeSetReturnPropertyValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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

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

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