Passed
Push — main ( 4c3994...11b1e9 )
by Michiel
13:02 queued 05:33
created

ZipTask::archiveIsUpToDate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\Ext\Archive;
22
23
use Exception;
24
use Phing\Exception\BuildException;
25
use Phing\Io\IOException;
26
use Phing\Io\File;
27
use Phing\Io\SourceFileScanner;
28
use Phing\Mapper\MergeMapper;
29
use Phing\Project;
30
use Phing\Task\System\MatchingTask;
31
use Phing\Type\FileSet;
32
use ZipArchive;
33
34
/**
35
 * Creates a zip archive using PHP ZipArchive extension/
36
 *
37
 * @author  Michiel Rook <[email protected]>
38
 * @package phing.tasks.ext
39
 * @since   2.1.0
40
 */
41
class ZipTask extends MatchingTask
42
{
43
    /**
44
     * @var File
45
     */
46
    private $zipFile;
47
48
    /**
49
     * @var File
50
     */
51
    private $baseDir;
52
53
    /**
54
     * Whether to include empty dirs in the archive.
55
     */
56
    private $includeEmpty = true;
57
58
    private $filesets = [];
59
60
    private $ignoreLinks = false;
61
62
    private $saveFileAttributes = false;
63
64
    /**
65
     * File path prefix in zip archive
66
     *
67
     * @var string
68
     */
69
    private $prefix = null;
70
71
    /**
72
     * Comment for zip archive.
73
     *
74
     * @var string $comment
75
     */
76
    private $comment = '';
77
    private string $mtimeDummy;
78
79
    /**
80
     * Removes all external attributes from the Zip archive.
81
     *
82
     * @param \ZipArchive $zip The Zip archive
83
     *
84
     * @return void
85
     */
86 6
    private static function clearExternalAttributes(\ZipArchive $zip)
87
    {
88 6
        for ($i = 0, $count = $zip->count(); $i < $count; ++$i) {
89 6
            $zip->setExternalAttributesIndex($i, \ZipArchive::OPSYS_DOS, 0);
90
        }
91
    }
92
93
    /**
94
     * Add a new fileset.
95
     *
96
     * @return ZipFileSet
97
     */
98 3
    public function createFileSet()
99
    {
100 3
        $this->fileset = new ZipFileSet();
101 3
        $this->filesets[] = $this->fileset;
102
103 3
        return $this->fileset;
104
    }
105
106
    /**
107
     * Add a new fileset.
108
     *
109
     * @param ZipFileSet $fileset
110
     */
111 1
    public function addZipFileSet(ZipFileSet $fileset)
112
    {
113 1
        $this->filesets[] = $fileset;
114
    }
115
116
    /**
117
     * Set is the name/location of where to create the zip file.
118
     *
119
     * @param File $destFile The output of the zip
120
     */
121 6
    public function setDestFile(File $destFile)
122
    {
123 6
        $this->zipFile = $destFile;
124
    }
125
126
    /**
127
     * This is the base directory to look in for things to zip.
128
     *
129
     * @param File $baseDir
130
     */
131 2
    public function setBasedir(File $baseDir)
132
    {
133 2
        $this->baseDir = $baseDir;
134
    }
135
136
    /**
137
     * Sets the file path prefix for file in the zip file.
138
     *
139
     * @param string $prefix Prefix
140
     *
141
     * @return void
142
     */
143
    public function setPrefix($prefix)
144
    {
145
        $this->prefix = $prefix;
146
    }
147
148
    /**
149
     * Set the include empty dirs flag.
150
     *
151
     * @param  boolean  Flag if empty dirs should be tarred too
0 ignored issues
show
Bug introduced by
The type Phing\Task\Ext\Archive\Flag was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
152
     * @return void
153
     */
154
    public function setIncludeEmptyDirs($bool)
155
    {
156
        $this->includeEmpty = (bool) $bool;
157
    }
158
159
    /**
160
     * Set the ignore symlinks flag.
161
     *
162
     * @param  boolean $bool Flag if symlinks should be ignored
163
     * @return void
164
     */
165
    public function setIgnoreLinks($bool)
166
    {
167
        $this->ignoreLinks = (bool) $bool;
168
    }
169
170
    /**
171
     * Set the save file attributes flag.
172
     *
173
     * @param  bool $bool Flag if file attributes should be saved
174
     * @return void
175
     */
176
    public function setSaveFileAttributes($bool)
177
    {
178
        $this->saveFileAttributes = (bool) $bool;
179
    }
180
181
    /**
182
     * Add a comment to the zip archive.
183
     *
184
     * @param string $text
185
     *
186
     * @return void
187
     */
188
    public function setComment($text)
189
    {
190
        $this->comment = $text;
191
    }
192
193
    /**
194
     * do the work
195
     *
196
     * @throws BuildException
197
     */
198 6
    public function main()
199
    {
200 6
        if (!extension_loaded('zip')) {
201
            throw new BuildException("Zip extension is required");
202
        }
203
204 6
        if ($this->zipFile === null) {
205
            throw new BuildException("zipfile attribute must be set!", $this->getLocation());
206
        }
207
208 6
        if ($this->zipFile->exists() && $this->zipFile->isDirectory()) {
209
            throw new BuildException("zipfile is a directory!", $this->getLocation());
210
        }
211
212 6
        if ($this->zipFile->exists() && !$this->zipFile->canWrite()) {
213
            throw new BuildException("Can not write to the specified zipfile!", $this->getLocation());
214
        }
215
216
        try {
217 6
            if ($this->baseDir !== null) {
218 2
                if (!$this->baseDir->exists()) {
219
                    throw new BuildException(
220
                        "basedir '" . (string) $this->baseDir . "' does not exist!",
221
                        $this->getLocation()
222
                    );
223
                }
224
225 2
                if (empty($this->filesets)) {
226
                    // add the main fileset to the list of filesets to process.
227 2
                    $mainFileSet = new ZipFileSet($this->fileset);
228 2
                    $mainFileSet->setDir($this->baseDir);
229 2
                    $this->filesets[] = $mainFileSet;
230
                }
231
            }
232
233 6
            if (empty($this->filesets)) {
234
                throw new BuildException(
235
                    "You must supply either a basedir "
236
                    . "attribute or some nested filesets.",
237
                    $this->getLocation()
238
                );
239
            }
240
241
            // check if zip is out of date with respect to each
242
            // fileset
243 6
            if ($this->areFilesetsUpToDate()) {
244
                $this->log("Nothing to do: " . $this->zipFile->__toString() . " is up to date.", Project::MSG_INFO);
245
246
                return;
247
            }
248
249 6
            $this->log("Building zip: " . $this->zipFile->__toString(), Project::MSG_INFO);
250
251 6
            if (false === $this->mtimeDummy = tempnam(sys_get_temp_dir(), 'mtimeDummy')) {
252
                throw new Exception('Could not create temp file');
253
            }
254
255 6
            $zip = new ZipArchive();
256 6
            $res = $zip->open($this->zipFile->getAbsolutePath(), ZipArchive::CREATE);
257
258 6
            if ($res !== true) {
259
                throw new Exception("ZipArchive::open() failed with code " . $res);
260
            }
261
262 6
            if ($this->comment !== '') {
263
                $isCommented = $zip->setArchiveComment($this->comment);
264
                if ($isCommented === false) {
265
                    $this->log("Could not add a comment for the Archive.", Project::MSG_INFO);
266
                }
267
            }
268
269 6
            $this->addFilesetsToArchive($zip);
270
271 6
            if (!$this->saveFileAttributes) {
272 6
                self::clearExternalAttributes($zip);
273
            }
274
275 6
            $zip->close();
276 6
            unlink($this->mtimeDummy);
277
        } catch (IOException $ioe) {
278
            $msg = "Problem creating ZIP: " . $ioe->getMessage();
279
            throw new BuildException($msg, $ioe, $this->getLocation());
280
        }
281
    }
282
283
    /**
284
     * @param  array $files array of filenames
285
     * @param  File $dir
286
     * @return boolean
287
     */
288 6
    private function archiveIsUpToDate($files, $dir)
289
    {
290 6
        $sfs = new SourceFileScanner($this);
291 6
        $mm = new MergeMapper();
292 6
        $mm->setTo($this->zipFile->getAbsolutePath());
293
294 6
        return count($sfs->restrict($files, $dir, null, $mm)) == 0;
295
    }
296
297
    /**
298
     * @return array
299
     * @throws BuildException
300
     */
301 6
    public function areFilesetsUpToDate()
302
    {
303
        /**
304
         * @var FileSet $fs
305
         */
306 6
        foreach ($this->filesets as $fs) {
307 6
            $files = $fs->getIterator($this->includeEmpty);
308 6
            if (!$this->archiveIsUpToDate($files, $fs->getDir($this->project))) {
309 6
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
310
            }
311
            for ($i = 0, $fcount = count($files); $i < $fcount; $i++) {
312
                if ($this->zipFile->equals(new File($fs->getDir($this->project), $files[$i]))) {
313
                    throw new BuildException("A zip file cannot include itself", $this->getLocation());
314
                }
315
            }
316
        }
317
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
318
    }
319
320
    /**
321
     * @param $zip
322
     */
323 6
    private function addFilesetsToArchive($zip)
324
    {
325 6
        foreach ($this->filesets as $fs) {
326 6
            $fsBasedir = (null != $this->baseDir) ? $this->baseDir :
327 4
                $fs->getDir($this->project);
328
329 6
            $files = $fs->getIterator($this->includeEmpty);
330
331 6
            foreach ($files as $file) {
332 6
                $f = new File($fsBasedir, $file);
333
334 6
                $pathInZip = $this->prefix
335 6
                    . $f->getPathWithoutBase($fsBasedir);
336
337 6
                $pathInZip = str_replace('\\', '/', $pathInZip);
338
339 6
                if ($this->ignoreLinks && $f->isLink()) {
340
                    continue;
341
                }
342
343 6
                if ($f->isDirectory()) {
344 6
                    if ($pathInZip != '.') {
345 2
                        $this->addDirToZip($zip, $f->getAbsolutePath(), $pathInZip . '/');
346
                    }
347
                } else {
348 6
                    $zip->addFile($f->getAbsolutePath(), $pathInZip);
349
                }
350 6
                $this->log("Adding " . $f->getPath() . " as " . $pathInZip . " to archive.", Project::MSG_VERBOSE);
351
            }
352
        }
353
    }
354
355
    /**
356
     * @param \ZipArchive $zip
357
     * @param string      $dirPath
358
     * @param string      $entryName
359
     *
360
     * @return void
361
     */
362 2
    private function addDirToZip(\ZipArchive $zip, string $dirPath, string $entryName)
363
    {
364 2
        touch($this->mtimeDummy, filemtime($dirPath)); // Save directory's mtime to dummmy
365 2
        $zip->addFile($this->mtimeDummy, $entryName); // Add empty dummy as a directory
366 2
        if (false !== $filePerms = fileperms($dirPath)) { // filePerms supported
367 2
            $zip->setExternalAttributesName($entryName, \ZipArchive::OPSYS_UNIX, $filePerms << 16);
368
        }
369
    }
370
}
371