Passed
Push — main ( d5ff59...b03a29 )
by Michiel
09:01
created

ManifestTask::write()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 8.021

Importance

Changes 0
Metric Value
eloc 29
dl 0
loc 48
ccs 27
cts 29
cp 0.931
rs 8.2114
c 0
b 0
f 0
cc 8
nc 11
nop 0
crap 8.021
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\System;
22
23
use Phing\Exception\BuildException;
24
use Phing\Io\File;
25
use Phing\Project;
26
use Phing\Task;
27
use Phing\Type\Element\FileSetAware;
28
29
/**
30
 * ManifestTask.
31
 *
32
 * Generates a simple Manifest file with optional checksums.
33
 *
34
 *
35
 * Manifest schema:
36
 * ...
37
 * path/to/file     CHECKSUM    [CHECKSUM2]     [CHECKSUM3]
38
 * path/to/secondfile       CHECKSUM    [CHECKSUM2]     [CHECKSUM3]
39
 * ...
40
 *
41
 * Example usage:
42
 * <manifest checksum="crc32" file="${dir_build}/Manifest">
43
 *      <fileset refid="files_build" />
44
 * </manifest>
45
 *
46
 * <manifest checksum="md5,adler32,sha256" file="${dir_build}/Manifest">
47
 *      <fileset refid="files_build" />
48
 * </manifest>
49
 *
50
 * @author David Persson <davidpersson at qeweurope dot org>
51
 *
52
 * @since 2.3.1
53
 */
54
class ManifestTask extends Task
55
{
56
    use FileSetAware;
57
58
    public $taskname = 'manifest';
59
60
    /**
61
     * Action.
62
     *
63
     * "w" for reading in files from fileSet
64
     * and writing manifest
65
     *
66
     * or
67
     *
68
     * "r" for reading in files from fileSet
69
     * and checking against manifest
70
     *
71
     * @var string "r" or "w"
72
     */
73
    private $action = 'w';
74
75
    /**
76
     * Enable/Disable checksuming or/and select algorithm
77
     * true defaults to md5
78
     * false disables checksuming
79
     * string "md5,sha256,..." enables generation of multiple checksums
80
     * string "sha256" generates sha256 checksum only.
81
     *
82
     * @var bool|string
83
     */
84
    private $checksum = false;
85
86
    /**
87
     * A string used in hashing method.
88
     *
89
     * @var string
90
     */
91
    private $salt = '';
92
93
    /**
94
     * Holds some data collected during runtime.
95
     *
96
     * @var array
97
     */
98
    private $meta = ['totalFileCount' => 0, 'totalFileSize' => 0];
99
100
    /**
101
     * @var File the target file passed in the buildfile
102
     */
103
    private $file;
104
105
    /**
106
     * The setter for the attribute "file".
107
     * This is where the manifest will be written to/read from.
108
     *
109
     * @param File $file Path to readable file
110
     */
111 1
    public function setFile(File $file)
112
    {
113 1
        $this->file = $file;
114
    }
115
116
    /**
117
     * The setter for the attribute "checksum".
118
     *
119
     * @param mixed $mixed
120
     */
121 1
    public function setChecksum($mixed)
122
    {
123 1
        if (is_string($mixed)) {
124 1
            $data = [strtolower($mixed)];
125
126 1
            if (strpos($data[0], ',')) {
127
                $data = explode(',', $mixed);
128
            }
129
130 1
            $this->checksum = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type array<integer,string> or string[] is incompatible with the declared type boolean|string of property $checksum.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
131
        } elseif (true === $mixed) {
132
            $this->checksum = ['md5'];
133
        }
134
    }
135
136
    /**
137
     * The setter for the optional attribute "salt".
138
     *
139
     * @param string $string
140
     */
141 1
    public function setSalt($string)
142
    {
143 1
        $this->salt = $string;
144
    }
145
146
    /**
147
     * The init method: Do init steps.
148
     *
149
     * {@inheritdoc}
150
     *
151
     * @internal nothing to do here
152
     */
153
    public function init()
154
    {
155
    }
156
157
    /**
158
     * Delegate the work.
159
     *
160
     * {@inheritdoc}
161
     */
162 1
    public function main()
163
    {
164 1
        $this->validateAttributes();
165
166 1
        if ('w' == $this->action) {
167 1
            $this->write();
168
        } elseif ('r' == $this->action) {
169
            $this->read();
170
        }
171
    }
172
173
    /**
174
     * Validates attributes coming in from XML.
175
     *
176
     * @throws BuildException
177
     */
178 1
    protected function validateAttributes()
179
    {
180 1
        if ('r' != $this->action && 'w' != $this->action) {
181
            throw new BuildException("'action' attribute has non valid value. Use 'r' or 'w'");
182
        }
183
184 1
        if (empty($this->salt)) {
185
            $this->log("No salt provided. Specify one with the 'salt' attribute.", Project::MSG_WARN);
186
        }
187
188 1
        if (null === $this->file && 0 === count($this->filesets)) {
189
            throw new BuildException('Specify at least sources and destination - a file or a fileset.');
190
        }
191
192 1
        if (null !== $this->file && $this->file->exists() && $this->file->isDirectory()) {
193
            throw new BuildException('Destination file cannot be a directory.');
194
        }
195
    }
196
197
    /**
198
     * Creates Manifest file
199
     * Writes to $this->file.
200
     *
201
     * @throws BuildException
202
     */
203 1
    private function write()
204
    {
205 1
        $project = $this->getProject();
206
207 1
        if (!touch($this->file->getPath())) {
208
            throw new BuildException('Unable to write to ' . $this->file->getPath() . '.');
209
        }
210
211 1
        $this->log('Writing to ' . $this->file->__toString(), Project::MSG_INFO);
212
213 1
        if (is_array($this->checksum)) {
0 ignored issues
show
introduced by
The condition is_array($this->checksum) is always false.
Loading history...
214 1
            $this->log('Using ' . implode(', ', $this->checksum) . ' for checksuming.', Project::MSG_INFO);
215
        }
216
217 1
        $manifest = [];
218
219 1
        foreach ($this->filesets as $fs) {
220 1
            $dir = $fs->getDir($this->project)->getPath();
221
222 1
            $ds = $fs->getDirectoryScanner($project);
223 1
            $fromDir = $fs->getDir($project);
0 ignored issues
show
Unused Code introduced by
The assignment to $fromDir is dead and can be removed.
Loading history...
224 1
            $srcFiles = $ds->getIncludedFiles();
0 ignored issues
show
Unused Code introduced by
The assignment to $srcFiles is dead and can be removed.
Loading history...
225 1
            $srcDirs = $ds->getIncludedDirectories();
0 ignored issues
show
Unused Code introduced by
The assignment to $srcDirs is dead and can be removed.
Loading history...
226
227 1
            foreach ($ds->getIncludedFiles() as $file_path) {
228 1
                $line = $file_path;
229 1
                if ($this->checksum) {
230 1
                    foreach ($this->checksum as $algo) {
0 ignored issues
show
Bug introduced by
The expression $this->checksum of type string|true is not traversable.
Loading history...
231 1
                        if (!$hash = $this->hashFile($dir . '/' . $file_path, $algo)) {
232
                            throw new BuildException("Hashing {$dir}/{$file_path} with {$algo} failed!");
233
                        }
234
235 1
                        $line .= "\t" . $hash;
236
                    }
237
                }
238 1
                $line .= "\n";
239 1
                $manifest[] = $line;
240 1
                $this->log('Adding file ' . $file_path, Project::MSG_VERBOSE);
241 1
                ++$this->meta['totalFileCount'];
242 1
                $this->meta['totalFileSize'] += filesize($dir . '/' . $file_path);
243
            }
244
        }
245
246 1
        file_put_contents($this->file, $manifest);
247
248 1
        $this->log(
249 1
            'Done. Total files: ' . $this->meta['totalFileCount'] . '. Total file size: ' . $this->meta['totalFileSize'] . ' bytes.',
250
            Project::MSG_INFO
251
        );
252
    }
253
254
    /**
255
     * @todo implement
256
     */
257
    private function read()
258
    {
259
        throw new BuildException('Checking against manifest not yet supported.');
260
    }
261
262
    /**
263
     * Wrapper method for hash generation
264
     * Automatically selects extension
265
     * Falls back to built-in functions.
266
     *
267
     * @see http://www.php.net/mhash
268
     * @see http://www.php.net/hash
269
     *
270
     * @param string $msg  The string that should be hashed
271
     * @param string $algo Algorithm
272
     *
273
     * @return mixed String on success, false if $algo is not available
274
     */
275 1
    private function hash($msg, $algo)
276
    {
277 1
        if (extension_loaded('hash')) {
278 1
            $algo = strtolower($algo);
279
280 1
            if (in_array($algo, hash_algos())) {
281 1
                return hash($algo, $this->salt . $msg);
282
            }
283
        }
284
285
        if (extension_loaded('mhash')) {
286
            $algo = strtoupper($algo);
287
288
            if (defined('MHASH_' . $algo)) {
289
                return mhash('MHASH_' . $algo, $this->salt . $msg);
0 ignored issues
show
Bug introduced by
'MHASH_' . $algo of type string is incompatible with the type integer expected by parameter $algo of mhash(). ( Ignorable by Annotation )

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

289
                return mhash(/** @scrutinizer ignore-type */ 'MHASH_' . $algo, $this->salt . $msg);
Loading history...
290
            }
291
        }
292
293
        switch (strtolower($algo)) {
294
            case 'md5':
295
                return md5($this->salt . $msg);
296
297
            case 'crc32':
298
                return abs(crc32($this->salt . $msg));
299
        }
300
301
        return false;
302
    }
303
304
    /**
305
     * Hash a file's contents.
306
     *
307
     * @param string $file
308
     * @param string $algo
309
     *
310
     * @return mixed String on success, false if $algo is not available
311
     */
312 1
    private function hashFile($file, $algo)
313
    {
314 1
        if (!file_exists($file)) {
315
            return false;
316
        }
317
318 1
        $msg = file_get_contents($file);
319
320 1
        return $this->hash($msg, $algo);
321
    }
322
}
323