Passed
Push — master ( 601cfd...7bcbf1 )
by Michiel
22:46
created

SymlinkTask::symlink()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 34
rs 8.0555
c 0
b 0
f 0
cc 9
nc 10
nop 2
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 Phing\Exception\BuildException;
23
use Phing\Io\FileSystem;
24
use Phing\Io\File;
25
use Phing\Project;
26
use Phing\Task;
27
use Phing\Type\FileSet;
28
29
/**
30
 * Generates symlinks based on a target / link combination.
31
 * Can also symlink contents of a directory, individually
32
 *
33
 * Single target symlink example:
34
 * <code>
35
 *     <symlink target="/some/shared/file" link="${project.basedir}/htdocs/my_file" />
36
 * </code>
37
 *
38
 * Symlink entire contents of directory
39
 *
40
 * This will go through the contents of "/my/shared/library/*"
41
 * and create a symlink for each entry into ${project.basedir}/library/
42
 * <code>
43
 *     <symlink link="${project.basedir}/library">
44
 *         <fileset dir="/my/shared/library">
45
 *             <include name="*" />
46
 *         </fileset>
47
 *     </symlink>
48
 * </code>
49
 *
50
 * @author  Andrei Serdeliuc <[email protected]>
51
 * @extends Task
52
 * @version $ID$
53
 * @package phing.tasks.ext
54
 */
55
class SymlinkTask extends Task
56
{
57
    /**
58
     * What we're symlinking from
59
     *
60
     * (default value: null)
61
     *
62
     * @var string
63
     */
64
    private $linkTarget = null;
65
66
    /**
67
     * Symlink location
68
     *
69
     * (default value: null)
70
     *
71
     * @var string
72
     */
73
    private $link = null;
74
75
    /**
76
     * Collection of filesets
77
     * Used when linking contents of a directory
78
     *
79
     * (default value: array())
80
     *
81
     * @var array
82
     */
83
    private $filesets = [];
84
85
    /**
86
     * Whether to override the symlink if it exists but points
87
     * to a different location
88
     *
89
     * (default value: false)
90
     *
91
     * @var boolean
92
     */
93
    private $overwrite = false;
94
95
    /**
96
     * Whether to create relative symlinks
97
     *
98
     * @var boolean
99
     */
100
    private $relative = false;
101
102
    /**
103
     * setter for linkTarget
104
     *
105
     * @param string $linkTarget
106
     * @return void
107
     */
108
    public function setTarget($linkTarget)
109
    {
110
        $this->linkTarget = $linkTarget;
111
    }
112
113
    /**
114
     * setter for _link
115
     *
116
     * @param string $link
117
     * @return void
118
     */
119
    public function setLink($link)
120
    {
121
        $this->link = $link;
122
    }
123
124
    /**
125
     * creator for _filesets
126
     *
127
     * @return FileSet
128
     */
129
    public function createFileset()
130
    {
131
        $num = array_push($this->filesets, new FileSet());
132
133
        return $this->filesets[$num - 1];
134
    }
135
136
    /**
137
     * setter for _overwrite
138
     *
139
     * @param boolean $overwrite
140
     * @return void
141
     */
142
    public function setOverwrite($overwrite)
143
    {
144
        $this->overwrite = $overwrite;
145
    }
146
147
    /**
148
     * @param boolean $relative
149
     */
150
    public function setRelative($relative)
151
    {
152
        $this->relative = $relative;
153
    }
154
155
    /**
156
     * getter for linkTarget
157
     *
158
     * @return string
159
     * @throws BuildException
160
     */
161
    public function getTarget()
162
    {
163
        if ($this->linkTarget === null) {
164
            throw new BuildException('Target not set');
165
        }
166
167
        return $this->linkTarget;
168
    }
169
170
    /**
171
     * getter for _link
172
     *
173
     * @return string
174
     * @throws BuildException
175
     */
176
    public function getLink()
177
    {
178
        if ($this->link === null) {
179
            throw new BuildException('Link not set');
180
        }
181
182
        return $this->link;
183
    }
184
185
    /**
186
     * getter for _filesets
187
     *
188
     * @return array
189
     */
190
    public function getFilesets()
191
    {
192
        return $this->filesets;
193
    }
194
195
    /**
196
     * getter for _overwrite
197
     *
198
     * @return boolean
199
     */
200
    public function getOverwrite()
201
    {
202
        return $this->overwrite;
203
    }
204
205
    /**
206
     * @return boolean
207
     */
208
    public function isRelative()
209
    {
210
        return $this->relative;
211
    }
212
213
    /**
214
     * Given an existing path, convert it to a path relative to a given starting path.
215
     *
216
     * @param string $endPath Absolute path of target
217
     * @param string $startPath Absolute path where traversal begins
218
     *
219
     * @return string Path of target relative to starting path
220
     */
221
    public function makePathRelative($endPath, $startPath)
222
    {
223
        // Normalize separators on Windows
224
        if ('\\' === DIRECTORY_SEPARATOR) {
225
            $endPath = str_replace('\\', '/', $endPath);
226
            $startPath = str_replace('\\', '/', $startPath);
227
        }
228
229
        // Split the paths into arrays
230
        $startPathArr = explode('/', trim($startPath, '/'));
231
        $endPathArr = explode('/', trim($endPath, '/'));
232
233
        // Find for which directory the common path stops
234
        $index = 0;
235
        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
236
            ++$index;
237
        }
238
239
        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
240
        $depth = count($startPathArr) - $index;
241
242
        // Repeated "../" for each level need to reach the common path
243
        $traverser = str_repeat('../', $depth);
244
245
        $endPathRemainder = implode('/', array_slice($endPathArr, $index));
246
247
        // Construct $endPath from traversing to the common path, then to the remaining $endPath
248
        $relativePath = $traverser . ('' !== $endPathRemainder ? $endPathRemainder . '/' : '');
249
250
        return '' === $relativePath ? './' : $relativePath;
251
    }
252
253
    /**
254
     * Generates an array of directories / files to be linked
255
     * If _filesets is empty, returns getTarget()
256
     *
257
     * @return array|string
258
     * @throws BuildException
259
     */
260
    protected function getMap()
261
    {
262
        $fileSets = $this->getFilesets();
263
264
        // No filesets set
265
        // We're assuming single file / directory
266
        if (empty($fileSets)) {
267
            return $this->getTarget();
268
        }
269
270
        $targets = [];
271
272
        foreach ($fileSets as $fs) {
273
            if (!($fs instanceof FileSet)) {
274
                continue;
275
            }
276
277
            // We need a directory to store the links
278
            if (!is_dir($this->getLink())) {
279
                throw new BuildException('Link must be an existing directory when using fileset');
280
            }
281
282
            $fromDir = $fs->getDir($this->getProject())->getAbsolutePath();
283
284
            if (!is_dir($fromDir)) {
285
                $this->log('Directory doesn\'t exist: ' . $fromDir, Project::MSG_WARN);
286
                continue;
287
            }
288
289
            $fsTargets = [];
290
291
            $ds = $fs->getDirectoryScanner($this->getProject());
292
293
            $fsTargets = array_merge(
294
                $fsTargets,
295
                $ds->getIncludedDirectories(),
296
                $ds->getIncludedFiles()
297
            );
298
299
            // Add each target to the map
300
            foreach ($fsTargets as $target) {
301
                if (!empty($target)) {
302
                    $targets[$target] = $fromDir . DIRECTORY_SEPARATOR . $target;
303
                }
304
            }
305
        }
306
307
        return $targets;
308
    }
309
310
    /**
311
     * Main entry point for task
312
     *
313
     * @return bool
314
     */
315
    public function main()
316
    {
317
        $map = $this->getMap();
318
319
        // Single file symlink
320
        if (is_string($map)) {
0 ignored issues
show
introduced by
The condition is_string($map) is always true.
Loading history...
321
            return $this->symlink($map, $this->getLink());
322
        }
323
324
        // Multiple symlinks
325
        foreach ($map as $name => $targetPath) {
326
            $this->symlink($targetPath, $this->getLink() . DIRECTORY_SEPARATOR . $name);
327
        }
328
329
        return true;
330
    }
331
332
    /**
333
     * Create the actual link
334
     *
335
     * @param string $target
336
     * @param string $link
337
     * @return bool
338
     */
339
    protected function symlink($target, $link)
340
    {
341
        $fs = FileSystem::getFileSystem();
342
343
        if ($this->isRelative()) {
344
            $link = (new File($link))->getAbsolutePath();
345
            $target = rtrim($this->makePathRelative($target, dirname($link)), '/');
346
        }
347
348
        if (is_link($link) && @readlink($link) == $target) {
349
            $this->log('Link exists: ' . $link, Project::MSG_INFO);
350
351
            return true;
352
        }
353
354
        if (file_exists($link) || is_link($link)) {
355
            if (!$this->getOverwrite()) {
356
                $this->log('Not overwriting existing link ' . $link, Project::MSG_ERR);
357
358
                return false;
359
            }
360
361
            if (is_link($link) || is_file($link)) {
362
                $fs->unlink($link);
363
                $this->log('Link removed: ' . $link, Project::MSG_INFO);
364
            } else {
365
                $fs->rmdir($link, true);
366
                $this->log('Directory removed: ' . $link, Project::MSG_INFO);
367
            }
368
        }
369
370
        $this->log('Linking: ' . $target . ' to ' . $link, Project::MSG_INFO);
371
372
        return $fs->symlink($target, $link);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fs->symlink($target, $link) returns the type void which is incompatible with the documented return type boolean.
Loading history...
Bug introduced by
Are you sure the usage of $fs->symlink($target, $link) targeting Phing\Io\FileSystem::symlink() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
373
    }
374
}
375