Passed
Push — master ( b9bc63...f1e2bc )
by Michiel
06:23
created

PropertyTask::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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\Task\System;
21
22
use Exception;
23
use Phing\Exception\BuildException;
24
use Phing\Exception\NullPointerException;
25
use Phing\Io\File;
26
use Phing\Io\FileParserFactory;
27
use Phing\Io\FileParserFactoryInterface;
28
use Phing\Io\FileReader;
29
use Phing\Io\FileUtils;
30
use Phing\Io\IOException;
31
use Phing\Io\StringReader;
32
use Phing\Project;
33
use Phing\PropertyHelper;
34
use Phing\Task;
35
use Phing\Type\Element\FilterChainAware;
36
use Phing\Type\Reference;
37
use Phing\Util\Properties;
38
use Phing\Util\StringHelper;
39
40
/**
41
 * Task for setting properties in buildfiles.
42
 *
43
 * @author  Andreas Aderhold <[email protected]>
44
 * @author  Hans Lellelid <[email protected]>
45
 * @package phing.tasks.system
46
 */
47
class PropertyTask extends Task
48
{
49
    use FilterChainAware;
50
51
    /**
52
     * @var string name of the property
53
     */
54
    protected $name;
55
56
    /**
57
     * @var string $value of the property
58
     */
59
    protected $value;
60
61
    /**
62
     * @var Reference
63
     */
64
    protected $reference;
65
66
    /**
67
     * @var string environment
68
     */
69
    protected $env;
70
71
    /**
72
     * @var File
73
     */
74
    protected $file;
75
76
    /**
77
     * @var string
78
     */
79
    protected $prefix;
80
81
    /**
82
     * @var Project
83
     */
84
    protected $fallback;
85
86
    /**
87
     * Whether to force overwrite of existing property.
88
     */
89
    protected $override = false;
90
91
    /**
92
     * Whether property should be treated as "user" property.
93
     */
94
    protected $userProperty = false;
95
96
    /**
97
     * Whether to log messages as INFO or VERBOSE
98
     */
99
    protected $logOutput = true;
100
101
    /**
102
     * @var FileParserFactoryInterface
103
     */
104
    private $fileParserFactory;
105
106
    /**
107
     * Whether a warning should be displayed when the property ismissing.
108
     */
109
    private $quiet = false;
110
111
    /**
112
     * Whether the task should fail when the property file is not found.
113
     */
114
    private $required = false;
115
116
    /**
117
     * @param FileParserFactoryInterface $fileParserFactory
118
     */
119 465
    public function __construct(FileParserFactoryInterface $fileParserFactory = null)
120
    {
121 465
        parent::__construct();
122 465
        $this->fileParserFactory = $fileParserFactory ?? new FileParserFactory();
123 465
    }
124
125
    /**
126
     * File required or not.
127
     *
128
     * @param string $d
129
     */
130 1
    public function setRequired($d)
131
    {
132 1
        $this->required = $d;
133 1
    }
134
135
    /**
136
     * @return string
137
     */
138
    public function getRequired()
139
    {
140
        return $this->required;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->required returns the type boolean which is incompatible with the documented return type string.
Loading history...
141
    }
142
143
    /**
144
     * Sets a the name of current property component
145
     *
146
     * @param string $name
147
     */
148 458
    public function setName(string $name): void
149
    {
150 458
        $this->name = $name;
151 458
    }
152
153
    /**
154
     * Get property component name.
155
     */
156 12
    public function getName()
157
    {
158 12
        return $this->name;
159
    }
160
161
    /**
162
     * Sets a the value of current property component.
163
     *
164
     * @param string $value Value of name, all scalars allowed
165
     */
166 458
    public function setValue(string $value): void
167
    {
168 458
        $this->value = $value;
169 458
    }
170
171
    /**
172
     * Sets value of property to CDATA tag contents.
173
     *
174
     * @param string $value
175
     * @since    2.2.0
176
     */
177
    public function addText(string $value): void
178
    {
179
        $this->setValue($value);
180
    }
181
182
    /**
183
     * Get the value of current property component.
184
     */
185 7
    public function getValue()
186
    {
187 7
        return $this->value;
188
    }
189
190
    /**
191
     * Set a file to use as the source for properties.
192
     *
193
     * @param $file
194
     *
195
     * @throws IOException
196
     * @throws NullPointerException
197
     */
198 18
    public function setFile($file)
199
    {
200 18
        if (is_string($file)) {
201 18
            $file = new File($file);
202
        }
203 18
        $this->file = $file;
204 18
    }
205
206
    /**
207
     * Get the PhingFile that is being used as property source.
208
     */
209 6
    public function getFile()
210
    {
211 6
        return $this->file;
212
    }
213
214
    /**
215
     * @param Reference $ref
216
     */
217
    public function setRefid(Reference $ref): void
218
    {
219
        $this->reference = $ref;
220
    }
221
222 6
    public function getRefid()
223
    {
224 6
        return $this->reference;
225
    }
226
227
    /**
228
     * Prefix to apply to properties loaded using <code>file</code>.
229
     * A "." is appended to the prefix if not specified.
230
     *
231
     * @param  string $prefix prefix string
232
     *
233
     * @since  2.0
234
     */
235 2
    public function setPrefix(string $prefix): void
236
    {
237 2
        $this->prefix = $prefix;
238 2
        if (!StringHelper::endsWith(".", $prefix)) {
239 2
            $this->prefix .= ".";
240
        }
241 2
    }
242
243
    /**
244
     * @return string
245
     * @since 2.0
246
     */
247 6
    public function getPrefix()
248
    {
249 6
        return $this->prefix;
250
    }
251
252
    /**
253
     * the prefix to use when retrieving environment variables.
254
     * Thus if you specify environment="myenv"
255
     * you will be able to access OS-specific
256
     * environment variables via property names "myenv.PATH" or
257
     * "myenv.TERM".
258
     * <p>
259
     * Note that if you supply a property name with a final
260
     * "." it will not be doubled. ie environment="myenv." will still
261
     * allow access of environment variables through "myenv.PATH" and
262
     * "myenv.TERM". This functionality is currently only implemented
263
     * on select platforms. Feel free to send patches to increase the number of platforms
264
     * this functionality is supported on ;).<br>
265
     * Note also that properties are case sensitive, even if the
266
     * environment variables on your operating system are not, e.g. it
267
     * will be ${env.Path} not ${env.PATH} on Windows 2000.
268
     *
269
     * @param string $env
270
     */
271 2
    public function setEnvironment(string $env): void
272
    {
273 2
        $this->env = $env;
274 2
    }
275
276 6
    public function getEnvironment()
277
    {
278 6
        return $this->env;
279
    }
280
281
    /**
282
     * Set whether this is a user property (ro).
283
     * This is deprecated in Ant 1.5, but the userProperty attribute
284
     * of the class is still being set via constructor, so Phing will
285
     * allow this method to function.
286
     *
287
     * @param boolean $v
288
     */
289 12
    public function setUserProperty(bool $v): void
290
    {
291 12
        $this->userProperty = $v;
292 12
    }
293
294
    /**
295
     * @return bool
296
     */
297 6
    public function getUserProperty()
298
    {
299 6
        return $this->userProperty;
300
    }
301
302
    /**
303
     * @param $v
304
     */
305 8
    public function setOverride(bool $v): void
306
    {
307 8
        $this->override = $v;
308 8
    }
309
310
    /**
311
     * @return bool
312
     */
313 6
    public function getOverride()
314
    {
315 6
        return $this->override;
316
    }
317
318
    /**
319
     * @return string
320
     */
321 1
    public function __toString()
322
    {
323 1
        return $this->value;
324
    }
325
326
    /**
327
     * @param Project $p
328
     */
329 12
    public function setFallback($p): void
330
    {
331 12
        $this->fallback = $p;
332 12
    }
333
334
    public function getFallback()
335
    {
336
        return $this->fallback;
337
    }
338
339
    /**
340
     * @param $logOutput
341
     */
342 6
    public function setLogoutput(bool $logOutput): void
343
    {
344 6
        $this->logOutput = $logOutput;
345 6
    }
346
347
    /**
348
     * @return bool
349
     */
350 6
    public function getLogoutput()
351
    {
352 6
        return $this->logOutput;
353
    }
354
355
    /**
356
     * Set quiet mode, which suppresses warnings if chmod() fails.
357
     *
358
     * @see   setFailonerror()
359
     * @param $bool
360
     */
361 6
    public function setQuiet(bool $bool): void
362
    {
363 6
        $this->quiet = $bool;
364 6
    }
365
366
    /**
367
     * @return bool
368
     */
369 6
    public function getQuiet(): bool
370
    {
371 6
        return $this->quiet;
372
    }
373
374
    /**
375
     * set the property in the project to the value.
376
     * if the task was give a file or env attribute
377
     * here is where it is loaded
378
     */
379 465
    public function main()
380
    {
381 465
        $this->validate();
382
383 464
        if ($this->name !== null && $this->value !== null) {
384 457
            $this->addProperty($this->name, $this->value);
385
        }
386
387 464
        if ($this->file !== null) {
388 17
            $this->loadFile($this->file);
389
        }
390
391 461
        if ($this->env !== null) {
392 2
            $this->loadEnvironment($this->env);
393
        }
394
395 461
        if ($this->name !== null && $this->reference !== null) {
396
            // get the refereced property
397
            try {
398
                $referencedObject = $this->reference->getReferencedObject($this->project);
399
400
                if ($referencedObject instanceof Exception) {
401
                    $reference = $referencedObject->getMessage();
402
                } else {
403
                    $reference = (string) $referencedObject;
404
                }
405
406
                $this->addProperty($this->name, $reference);
407
            } catch (BuildException $be) {
408
                if ($this->fallback !== null) {
409
                    $referencedObject = $this->reference->getReferencedObject($this->fallback);
410
411
                    if ($referencedObject instanceof Exception) {
412
                        $reference = $referencedObject->getMessage();
413
                    } else {
414
                        $reference = (string) $referencedObject;
415
                    }
416
                    $this->addProperty($this->name, $reference);
417
                } else {
418
                    throw $be;
419
                }
420
            }
421
        }
422 461
    }
423
424
    /**
425
     * @throws BuildException
426
     */
427 465
    private function validate(): void
428
    {
429 465
        if ($this->name !== null) {
430 458
            if ($this->value === null && $this->reference === null) {
431
                throw new BuildException(
432
                    "You must specify value or refid with the name attribute",
433 458
                    $this->getLocation()
434
                );
435
            }
436 19
        } elseif ($this->file === null && $this->env === null) {
437
            throw new BuildException(
438
                "You must specify file or environment when not using the name attribute",
439
                $this->getLocation()
440
            );
441
        }
442
443 465
        if ($this->file === null && $this->prefix !== null) {
444 1
            throw new BuildException('Prefix is only valid when loading from a file.', $this->getLocation());
445
        }
446 464
    }
447
448
    /**
449
     * load the environment values
450
     *
451
     * @param string $prefix prefix to place before them
452
     */
453 2
    protected function loadEnvironment(string $prefix)
454
    {
455 2
        $props = new Properties();
456 2
        if (substr($prefix, strlen($prefix) - 1) === '.') {
457
            $prefix .= ".";
458
        }
459 2
        $this->log("Loading Environment $prefix", Project::MSG_VERBOSE);
460 2
        foreach ($_ENV as $key => $value) {
461
            $props->setProperty($prefix . '.' . $key, $value);
462
        }
463 2
        $this->addProperties($props);
464 2
    }
465
466
    /**
467
     * iterate through a set of properties,
468
     * resolve them then assign them
469
     *
470
     * @param Properties $props
471
     * @throws BuildException
472
     */
473 18
    protected function addProperties($props)
474
    {
475 18
        $this->resolveAllProperties($props);
476 15
        foreach ($props->keys() as $name) {
477 13
            $value = $props->getProperty($name);
478 13
            $v = $this->project->replaceProperties($value);
479 13
            if ($this->prefix !== null) {
480 1
                $name = $this->prefix . $name;
481
            }
482 13
            $this->addProperty($name, $v);
483
        }
484 15
    }
485
486
    /**
487
     * add a name value pair to the project property set
488
     *
489
     * @param string $name name of property
490
     * @param string $value value to set
491
     */
492 460
    protected function addProperty($name, $value)
493
    {
494 460
        if ($this->file === null && count($this->filterChains) > 0) {
495 1
            $in = FileUtils::getChainedReader(new StringReader($value), $this->filterChains, $this->project);
496 1
            $value = $in->read();
497
        }
498
499 460
        $ph = PropertyHelper::getPropertyHelper($this->getProject());
500 460
        if ($this->userProperty) {
501 12
            if ($ph->getUserProperty(null, $name) === null || $this->override) {
502 10
                $ph->setInheritedProperty(null, $name, $value);
503
            } else {
504 12
                $this->log('Override ignored for ' . $name, Project::MSG_VERBOSE);
505
            }
506
        } else {
507 457
            if ($this->override) {
508 2
                $ph->setProperty(null, $name, $value, true);
509
            } else {
510 457
                $ph->setNewProperty(null, $name, $value);
511
            }
512
        }
513 460
    }
514
515
    /**
516
     * load properties from a file.
517
     *
518
     * @param  File $file
519
     * @throws BuildException
520
     */
521 15
    protected function loadFile(File $file)
522
    {
523 15
        $fileParser = $this->fileParserFactory->createParser($file->getFileExtension());
524 15
        $props = new Properties(null, $fileParser);
525 15
        $this->log("Loading " . $file->getAbsolutePath(), $this->logOutput ? Project::MSG_INFO : Project::MSG_VERBOSE);
526
        try { // try to load file
527 15
            if ($file->exists()) {
528 14
                $value = null;
529 14
                if (count($this->filterChains) > 0) {
530 1
                    $in = FileUtils::getChainedReader(new FileReader($file), $this->filterChains, $this->project);
531 1
                    $value = $in->read();
532
                }
533 14
                if ($value) {
534 1
                    foreach (array_filter(explode(PHP_EOL, $value)) as $line) {
535 1
                        [$key, $prop] = explode('=', $line);
536 1
                        $props->setProperty($key, $prop);
537
                    }
538
                } else {
539 13
                    $props->load($file);
540
                }
541 14
                $this->addProperties($props);
542
            } else {
543 1
                if ($this->required) {
544 1
                    throw new BuildException("Unable to find property file: " . $file->getAbsolutePath());
545
                }
546
547
                $this->log(
548
                    "Unable to find property file: " . $file->getAbsolutePath() . "... skipped",
549 13
                    $this->quiet ? Project::MSG_VERBOSE : Project::MSG_WARN
550
                );
551
            }
552 2
        } catch (IOException $ioe) {
553
            throw new BuildException("Could not load properties from file.", $ioe);
554
        }
555 13
    }
556
557
    /**
558
     * Given a Properties object, this method goes through and resolves
559
     * any references to properties within the object.
560
     *
561
     * @param  Properties $props The collection of Properties that need to be resolved.
562
     * @throws BuildException
563
     * @return void
564
     */
565 18
    protected function resolveAllProperties(Properties $props)
566
    {
567 18
        foreach ($props->keys() as $name) {
568
            // There may be a nice regex/callback way to handle this
569
            // replacement, but at the moment it is pretty complex, and
570
            // would probably be a lot uglier to work into a preg_replace_callback()
571
            // system.  The biggest problem is the fact that a resolution may require
572
            // multiple passes.
573
574 16
            $value = $props->getProperty($name);
575 16
            $resolved = false;
576 16
            $resolveStack = [];
577
578 16
            while (!$resolved) {
579 16
                $fragments = [];
580 16
                $propertyRefs = [];
581
582 16
                PropertyHelper::getPropertyHelper($this->project)->parsePropertyString(
583 16
                    $value,
584
                    $fragments,
585
                    $propertyRefs
586
                );
587
588 16
                $resolved = true;
589 16
                if (count($propertyRefs) === 0) {
590 13
                    continue;
591
                }
592
593 6
                $sb = "";
594
595 6
                $j = $propertyRefs;
596
597 6
                foreach ($fragments as $fragment) {
598 6
                    if ($fragment !== null) {
599 6
                        $sb .= $fragment;
600 6
                        continue;
601
                    }
602
603 6
                    $propertyName = array_shift($j);
604 6
                    if (in_array($propertyName, $resolveStack)) {
605
                        // Should we maybe just log this as an error & move on?
606
                        // $this->log("Property ".$name." was circularly defined.", Project::MSG_ERR);
607 3
                        throw new BuildException("Property " . $propertyName . " was circularly defined.");
608
                    }
609
610 6
                    $fragment = $this->getProject()->getProperty($propertyName);
611 6
                    if ($fragment !== null) {
612 2
                        $sb .= $fragment;
613 2
                        continue;
614
                    }
615
616 5
                    if ($props->containsKey($propertyName)) {
617 5
                        $fragment = $props->getProperty($propertyName);
618 5
                        if (strpos($fragment, '${') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $fragment can also be of type null; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

618
                        if (strpos(/** @scrutinizer ignore-type */ $fragment, '${') !== false) {
Loading history...
619 3
                            $resolveStack[] = $propertyName;
620 5
                            $resolved = false; // parse again (could have been replaced w/ another var)
621
                        }
622
                    } else {
623
                        $fragment = "\${" . $propertyName . "}";
624
                    }
625
626 5
                    $sb .= $fragment;
627
                }
628
629 6
                $this->log("Resolved Property \"$value\" to \"$sb\"", Project::MSG_DEBUG);
630 6
                $value = $sb;
631 6
                $props->setProperty($name, $value);
632
            } // while (!$resolved)
633
        } // while (count($keys)
634 15
    }
635
}
636