Package   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Test Coverage

Coverage 89.52%

Importance

Changes 21
Bugs 1 Features 0
Metric Value
eloc 123
c 21
b 1
f 0
dl 0
loc 335
ccs 111
cts 124
cp 0.8952
rs 8.64
wmc 47

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 3
A fixBoolIfExist() 0 4 2
A __get() 0 3 1
A __isset() 0 3 1
A parseBool() 0 3 1
A __unset() 0 3 1
A __set() 0 3 1
A isCompatibleToArch() 0 4 2
A extractIfMissing() 0 24 4
A getMetadata() 0 3 1
A isBeta() 0 3 2
A hasWizardDir() 0 24 6
B collectMetadata() 0 35 9
A getThumbnails() 0 35 5
A ensureAvailableSpace() 0 5 3
A getSnapshots() 0 18 4
A isCompatibleToFirmware() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Package often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Package, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SSpkS\Package;
4
5
/**
6
 * SPK Package class
7
 *
8
 * @property string $spk Path to SPK file
9
 * @property string $spk_url URL to SPK file
10
 * @property string $displayname Pretty printed name of package (falls back to $package if not present)
11
 * @property string $package Package name
12
 * @property string $version Package version
13
 * @property string $description Package description
14
 * @property string $maintainer Package maintainer
15
 * @property string $maintainer_url URL of maintainer's web page
16
 * @property string $distributor Package distributor
17
 * @property string $distributor_url URL of distributor's web page
18
 * @property string $support_url URL of support web page 
19
 * @property array $arch List of supported architectures, or 'noarch'
20
 * @property array $thumbnail List of thumbnail files
21
 * @property array $thumbnail_url List of thumbnail URLs
22
 * @property array $snapshot List of screenshot files
23
 * @property array $snapshot_url List of screenshot URLs
24
 * @property bool $beta TRUE if this is a beta package.
25
 * @property string $firmware Minimum firmware needed on device.
26
 * @property string $install_dep_services Dependencies required by this package.
27
 * @property bool $silent_install Allow silent install
28
 * @property bool $silent_uninstall Allow silent uninstall
29
 * @property bool $silent_upgrade Allow silent upgrade
30
 * @property bool $auto_upgrade_from Allow auto upgrade if version is newer than this field
31
 * @property bool $qinst Allow silent install
32
 * @property bool $qupgrade Allow silent upgrade
33
 * @property bool $qstart Allow automatic start after install
34
 */
35
class Package
36
{
37
    private $config;
38
    private $filepath;
39
    private $filepathNoExt;
40
    private $filename;
41
    private $filenameNoExt;
42
    private $metafile;
43
    private $wizfile;
44
    private $nowizfile;
45
    private $metadata;
46
47
    /**
48
     * @param \SSpkS\Config $config Config object
49
     * @param string $filename Filename of SPK file
50
     */
51 18
    public function __construct(\SSpkS\Config $config, $filename)
52
    {
53 18
        $this->config = $config;
54 18
        if (!preg_match('/\.spk$/', $filename)) {
55 1
            throw new \Exception('File ' . $filename . ' doesn\'t have .spk extension!');
56
        }
57 17
        if (!file_exists($filename)) {
58 1
            throw new \Exception('File ' . $filename . ' not found!');
59
        }
60 16
        $this->filepath      = $filename;
61 16
        $this->filename      = basename($filename);
62 16
        $this->filenameNoExt = basename($filename, '.spk');
63 16
        $this->filepathNoExt = $this->config->paths['cache'] . $this->filenameNoExt;
64 16
        $this->metafile      = $this->filepathNoExt . '.nfo';
65 16
        $this->wizfile       = $this->filepathNoExt . '.wiz';
66 16
        $this->nowizfile     = $this->filepathNoExt . '.nowiz';
67
        // Make sure we have metadata available
68 16
        $this->collectMetadata();        
69 16
    }
70
71
    /**
72
     * Getter magic method.
73
     *
74
     * @param string $name Name of requested value.
75
     * @return mixed Requested value.
76
     */
77 10
    public function __get($name)
78
    {
79 10
        return $this->metadata[$name];
80
    }
81
82
    /**
83
     * Setter magic method.
84
     *
85
     * @param string $name Name of variable to set.
86
     * @param mixed $value Value to set.
87
     */
88 6
    public function __set($name, $value)
89
    {
90 6
        $this->metadata[$name] = $value;
91 6
    }
92
93
    /**
94
     * Isset feature magic method.
95
     *
96
     * @param string $name Name of requested value.
97
     * @return bool TRUE if value exists, FALSE otherwise.
98
     */
99 6
    public function __isset($name)
100
    {
101 6
        return isset($this->metadata[$name]);
102
    }
103
104
    /**
105
     * Unset feature magic method.
106
     *
107
     * @param string $name Name of value to unset.
108
     */
109 1
    public function __unset($name)
110
    {
111 1
        unset($this->metadata[$name]);
112 1
    }
113
114
    /**
115
     * Parses boolean value ('yes', '1', 'true') into
116
     * boolean type.
117
     *
118
     * @param mixed $value Input value
119
     * @return bool Boolean interpretation of $value.
120
     */
121 16
    public function parseBool($value)
122
    {
123 16
        return in_array($value, array('true', 'yes', '1', 1));
124
    }
125
126
    /**
127
     * Checks if given property $prop exists and converts it
128
     * into a boolean value.
129
     *
130
     * @param string $prop Property to convert
131
     */
132 16
    private function fixBoolIfExist($prop)
133
    {
134 16
        if (isset($this->metadata[$prop])) {
135 10
            $this->metadata[$prop] = $this->parseBool($this->metadata[$prop]);
136
        }
137 16
    }
138
139
    /**
140
     * Gathers metadata from package. Extracts INFO file if neccessary.
141
     */
142 16
    private function collectMetadata()
143
    {
144 16
        if (!is_null($this->metadata)) {
145
            // metadata already collected
146
            return;
147
        }
148 16
        $this->extractIfMissing('INFO', $this->metafile);
149 16
        $this->metadata = parse_ini_file($this->metafile);
150 16
        if (!isset($this->metadata['displayname'])) {
151 16
            $this->metadata['displayname'] = $this->metadata['package'];
152
        }
153 16
        $this->metadata['spk'] = $this->filepath;
154
155
        // Convert architecture(s) to array, as multiple architectures can be specified
156 16
        $this->metadata['arch'] = explode(' ', $this->metadata['arch']);
157
158 16
        $this->fixBoolIfExist('silent_install');
159 16
        $this->fixBoolIfExist('silent_uninstall');
160 16
        $this->fixBoolIfExist('silent_upgrade');
161
162 16
        if ($this->isBeta()) {
163 6
            $this->metadata['beta'] = true;
164
        } else {
165 16
            $this->metadata['beta'] = false;
166
        }
167
168 16
        $qValue = $this->hasWizardDir() ? false : true;
169 16
        $this->metadata['thumbnail'] = $this->getThumbnails();
170 16
        $this->metadata['snapshot'] = $this->getSnapshots();
171 16
        $this->metadata['qinst'] = !empty($this->metadata['qinst']) ? $this->parseBool($this->metadata['qinst']) : $qValue;
172 16
        $this->metadata['qupgrade'] = !empty($this->metadata['qupgrade']) ? $this->parseBool($this->metadata['qupgrade']) : $qValue;
173 16
        $this->metadata['qstart'] = !empty($this->metadata['qstart']) ? $this->parseBool($this->metadata['qstart']) : $qValue;
174
        
175 16
        if (isset($this->metadata['os_min_ver'])) {
176
           $this->metadata['firmware'] = $this->metadata['os_min_ver'];
177
        }
178 16
    }
179
180
    /**
181
     * Returns metadata for this package.
182
     *
183
     * @return array Metadata.
184
     */
185 3
    public function getMetadata()
186
    {
187 3
        return $this->metadata;
188
    }
189
      
190
    /**
191
     * Extracts $inPkgName from package to $targetFile, if it doesn't
192
     * already exist. Needs the phar.so extension and allow_url_fopen.
193
     *
194
     * @param string $inPkgName Filename in package
195
     * @param string $targetFile Path to destination
196
     * @throws \Exception if the file couldn't get extracted.
197
     * @return bool TRUE if successful or no action needed.
198
     */
199 16
    public function extractIfMissing($inPkgName, $targetFile)
200
    {
201 16
        if (file_exists($targetFile)) {
202
            // Everything in working order
203 9
            return true;
204
        }
205
        // Try to extract file
206 16
        $tmp_dir = sys_get_temp_dir();
207 16
        self::ensureAvailableSpace($tmp_dir, 'TMP');
208 16
        self::ensureAvailableSpace(dirname($targetFile), 'Package');
209
        try {
210 16
            $p = new \PharData($this->filepath, \Phar::CURRENT_AS_FILEINFO | \Phar::KEY_AS_FILENAME);
211
        } catch (\UnexpectedValueException $e) {
212
            rename($this->filepath, $this->filepath . '.invalid');
213
            throw new \Exception('Package ' . $this->filepath . ' not readable! Will be ignored in the future. Please try again!');
214
        }
215 16
        $tmpExtractedFilepath = $tmp_dir . DIRECTORY_SEPARATOR . $inPkgName;
216 16
        if (file_exists($tmpExtractedFilepath)) {
217
            // stale file from before - unlink first
218
            unlink($tmpExtractedFilepath);
219
        }
220 16
        $p->extractTo($tmp_dir, $inPkgName);
221 16
        rename($tmpExtractedFilepath, $targetFile);
222 16
        return true;
223
    }
224
225
    /**
226
     * Ensures there is enough free space on target drive
227
     * @param string $dir the directory whose volume has to be tested
228
     * @param string $friendlyName a nice name to display in case of error
229
     * @throws \Exception
230
     */
231 16
    private static function ensureAvailableSpace($dir, $friendlyName)
232
    {
233 16
        $free = @disk_free_space($dir);
234 16
        if (!empty($free) && $free < 2048) {
235
            throw new \Exception($friendlyName . ' folder only has ' . $free . ' Bytes available. Disk full!');
236
        }
237 16
    }
238
239
    /**
240
     * Returns a true if the package contains WIZARD_UIFILES.
241
     *
242
     * @return bool Package has a wizard
243
     */
244 16
    public function hasWizardDir()
245
    {
246 16
        if (file_exists($this->wizfile)) {
247
            return true;
248
        }
249
250 16
        if (file_exists($this->nowizfile)) {
251 5
            return false;
252
        }
253
254
        try {
255 11
            $p = new \PharData($this->filepath, \Phar::CURRENT_AS_FILEINFO | \Phar::KEY_AS_FILENAME);
256
        } catch (\UnexpectedValueException $e) {
257
            rename($this->filepath, $this->filepath . '.invalid');
258
            throw new \Exception('Package ' . $this->filepath . ' not readable! Will be ignored in the future. Please try again!');
259
        }
260 11
        foreach ($p as $file) {
261 11
            if (substr($file, strrpos($file, '/') + 1) == 'WIZARD_UIFILES') {
262
                touch($this->wizfile);
263
                return true;
264
            }
265
        }
266 11
        touch($this->nowizfile);
267 11
        return false;
268
    }
269
270
    /**
271
     * Returns a list of thumbnails for the specified package.
272
     *
273
     * @param string $pathPrefix Prefix to put before file path
274
     * @return array List of thumbnail urls
275
     */
276 16
    public function getThumbnails($pathPrefix = '')
277
    {
278
        $thumbnailSources = array(
279
            '72' => array(
280 16
                'file' => 'PACKAGE_ICON.PNG',
281
                'info' => 'package_icon',
282
            ),
283
            '120' => array(
284
                'file' => 'PACKAGE_ICON_256.PNG',
285
                'info' => 'package_icon_256',
286
            ),
287
        );
288 16
        $thumbnails = array();
289 16
        foreach ($thumbnailSources as $size => $sourceList) {
290 16
            $thumbName = $this->filepathNoExt . '_thumb_' . $size . '.png';
291
            // Try to find file in package, otherwise check if defined in INFO
292
            try {
293 16
                $this->extractIfMissing($sourceList['file'], $thumbName);
294 16
            } catch (\Exception $e) {
295
                // Check if icon is in metadata
296 16
                if (isset($this->metadata[$sourceList['info']])) {
297 12
                    file_put_contents($thumbName, base64_decode($this->metadata[$sourceList['info']]));
298
                }
299
            }
300
301
            // Use $size px thumbnail, if available
302 16
            if (file_exists($thumbName)) {
303 16
                $thumbnails[] = $pathPrefix . $thumbName;
304
            } else {
305
                // Use theme's default pictures
306 16
                $themeUrl = $this->config->paths['themes'] . $this->config->site['theme'] . '/';
307 16
                $thumbnails[] = $pathPrefix . $themeUrl . 'images/default_package_icon_' . $size . '.png';
308
            }
309
        }
310 16
        return $thumbnails;
311
    }
312
313
    /**
314
     * Returns a list of screenshots for the specified package.
315
     *
316
     * @param string $pathPrefix Prefix to put before file path
317
     * @return array List of screenshots
318
     */
319 16
    public function getSnapshots($pathPrefix = '')
320
    {
321
        /* Let's first try to extract screenshots from package (SSpkS feature) */
322 16
        $i = 1;
323 16
        while (true) {
324
            try {
325 16
                $this->extractIfMissing('screen_' . $i . '.png', $this->filepathNoExt . '_screen_' . $i . '.png');
326 9
                $i++;
327 16
            } catch (\Exception $e) {
328 16
                break;
329
            }
330
        }
331 16
        $snapshots = array();
332
        // Add screenshots, if available
333 16
        foreach (glob($this->filepathNoExt . '*_screen_*.png') as $snapshot) {
334 9
            $snapshots[] = $pathPrefix . $snapshot;
335
        }
336 16
        return $snapshots;
337
    }
338
339
    /**
340
     * Checks compatibility to the given $arch-itecture.
341
     *
342
     * @param string $arch Architecture to check against (or "noarch")
343
     * @return bool TRUE if compatible, otherwise FALSE.
344
     */
345 1
    public function isCompatibleToArch($arch)
346
    {
347
        // TODO: Check arch family, too?
348 1
        return (in_array($arch, $this->metadata['arch']) || in_array('noarch', $this->metadata['arch']));
349
    }
350
351
    /**
352
     * Checks compatibility to the given firmware $version.
353
     *
354
     * @param string $version Target firmware version.
355
     * @return bool TRUE if compatible, otherwise FALSE.
356
     */
357 1
    public function isCompatibleToFirmware($version)
358
    {
359 1
        return version_compare($this->metadata['firmware'], $version, '<=');
360
    }
361
362
    /**
363
     * Checks if this package is a beta version or not.
364
     *
365
     * @return bool TRUE if this is a beta version, FALSE otherwise.
366
     */
367 16
    public function isBeta()
368
    {
369 16
        return (isset($this->metadata['beta']) && $this->parseBool($this->metadata['beta']));
370
    }
371
}
372