Completed
Push — master ( 3906ce...946980 )
by Iurii
01:11
created

Install::extract()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
rs 8.6845
cc 4
eloc 13
nc 5
nop 0
1
<?php
2
3
/**
4
 * @package Installer
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\installer\models;
11
12
use Exception;
13
use gplcart\core\Config;
14
use gplcart\core\helpers\Url;
15
use gplcart\core\helpers\Zip;
16
use gplcart\core\models\Job;
17
use gplcart\core\models\Module as ModuleModel;
18
use gplcart\core\models\Translation;
19
use gplcart\core\Module;
20
21
/**
22
 * Manages basic behaviors and data related to Installer module
23
 */
24
class Install
25
{
26
27
    /**
28
     * Database class instance
29
     * @var \gplcart\core\Database $db
30
     */
31
    protected $db;
32
33
    /**
34
     * Config class instance
35
     * @var \gplcart\core\Config $config
36
     */
37
    protected $config;
38
39
    /**
40
     * Module model instance
41
     * @var \gplcart\core\Module $module
42
     */
43
    protected $module;
44
45
    /**
46
     * Zip helper class instance
47
     * @var \gplcart\core\helpers\Zip $zip
48
     */
49
    protected $zip;
50
51
    /**
52
     * Url helper class instance
53
     * @var \gplcart\core\helpers\Url $url
54
     */
55
    protected $url;
56
57
    /**
58
     * Translation UI model instance
59
     * @var \gplcart\core\models\Translation $translation
60
     */
61
    protected $translation;
62
63
    /**
64
     * Job model instance
65
     * @var \gplcart\core\models\Job $job
66
     */
67
    protected $job;
68
69
    /**
70
     * Module model instance
71
     * @var \gplcart\core\models\Module $module_model
72
     */
73
    protected $module_model;
74
75
    /**
76
     * The latest validation error
77
     * @var string
78
     */
79
    protected $error;
80
81
    /**
82
     * The temporary renamed module directory
83
     * @var string
84
     */
85
    protected $tempname;
86
87
    /**
88
     * The module directory
89
     * @var string
90
     */
91
    protected $destination;
92
93
    /**
94
     * The module ID
95
     * @var string
96
     */
97
    protected $module_id;
98
99
    /**
100
     * An array of module data
101
     * @var array
102
     */
103
    protected $data;
104
105
    /**
106
     * Whether an original module has been temporary renamed
107
     * @var boolean
108
     */
109
    protected $renamed;
110
111
    /**
112
     * Install constructor.
113
     * @param Config $config
114
     * @param Module $module
115
     * @param ModuleModel $module_model
116
     * @param Translation $translation
117
     * @param Job $job
118
     * @param Zip $zip
119
     * @param Url $url
120
     */
121
    public function __construct(Config $config, Module $module, ModuleModel $module_model,
122
                                Translation $translation, Job $job, Zip $zip, Url $url)
123
    {
124
        $this->config = $config;
125
        $this->module = $module;
126
        $this->db = $this->config->getDb();
127
128
        $this->zip = $zip;
129
        $this->url = $url;
130
        $this->job = $job;
131
        $this->translation = $translation;
132
        $this->module_model = $module_model;
133
    }
134
135
    /**
136
     * Installs a module from a ZIP file
137
     * @param string $zip
138
     * @return string|bool
139
     */
140
    public function fromZip($zip)
141
    {
142
        $this->data = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $data.

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...
143
        $this->error = null;
144
        $this->renamed = false;
145
        $this->tempname = null;
146
        $this->module_id = null;
147
        $this->destination = null;
148
149
        if (!$this->setModuleId($zip)) {
150
            return $this->error;
151
        }
152
153
        if (!$this->extract()) {
154
            $this->rollback();
155
            return $this->error;
156
        }
157
158
        if (!$this->validate()) {
159
            $this->rollback();
160
            return $this->error;
161
        }
162
163
        if (!$this->backup()) {
164
            return $this->error;
165
        }
166
167
        return true;
168
    }
169
170
    /**
171
     * Install modules from multiple URLs
172
     * @param array $sources
173
     */
174
    public function fromUrl(array $sources)
175
    {
176
        $total = count($sources);
177
        $finish_message = $this->translation->text('New modules: %inserted, updated: %updated');
178
        $vars = array('@url' => $this->url->get('', array('download_errors' => true)));
179
        $errors_message = $this->translation->text('New modules: %inserted, updated: %updated, errors: %errors. <a href="@url">See error log</a>', $vars);
180
181
        $data = array(
182
            'total' => $total,
183
            'data' => array('sources' => $sources),
184
            'id' => 'installer_download_module',
185
            'log' => array('errors' => $this->getErrorLogFile()),
186
            'redirect_message' => array('finish' => $finish_message, 'errors' => $errors_message)
187
        );
188
189
        $this->job->submit($data);
190
    }
191
192
    /**
193
     * Returns path to error log file
194
     * @return string
195
     */
196
    public function getErrorLogFile()
197
    {
198
        return gplcart_file_private_temp('installer-download-errors.csv');
199
    }
200
201
    /**
202
     * Backup the previous version of the updated module
203
     */
204
    protected function backup()
205
    {
206
        if (empty($this->tempname)) {
207
            return true;
208
        }
209
210
        $module = $this->data;
211
212
        $module += array(
213
            'directory' => $this->tempname,
214
            'module_id' => $this->module_id
215
        );
216
217
        $result = $this->getBackupModule()->backup('module', $module);
218
219
        if ($result === true) {
220
            gplcart_file_delete_recursive($this->tempname);
221
            return true;
222
        }
223
224
        $this->error = $this->translation->text('Failed to backup module @id', array('@id' => $this->module_id));
225
        return false;
226
    }
227
228
    /**
229
     * Returns Backup module instance
230
     * @return \gplcart\modules\backup\Main
231
     */
232
    protected function getBackupModule()
233
    {
234
        /** @var \gplcart\modules\backup\Main $instance */
235
        $instance = $this->module->getInstance('backup');
236
        return $instance;
237
    }
238
239
    /**
240
     * Extracts module files to the system directory
241
     * @return boolean
242
     */
243
    protected function extract()
244
    {
245
        $this->destination = GC_DIR_MODULE . "/{$this->module_id}";
246
247
        if (file_exists($this->destination)) {
248
249
            $this->tempname = gplcart_file_unique($this->destination . '~');
250
251
            if (!rename($this->destination, $this->tempname)) {
252
                $this->error = $this->translation->text('Failed to rename @old to @new', array(
253
                    '@old' => $this->destination, '@new' => $this->tempname));
254
                return false;
255
            }
256
257
            $this->renamed = true;
258
        }
259
260
        if ($this->zip->extract(GC_DIR_MODULE)) {
261
            return true;
262
        }
263
264
        $this->error = $this->translation->text('Failed to extract to @name', array('@name' => $this->destination));
265
        return false;
266
    }
267
268
    /**
269
     * Restore the original module files
270
     */
271
    protected function rollback()
272
    {
273
        if (!$this->isUpdate() || ($this->isUpdate() && $this->renamed)) {
274
            gplcart_file_delete_recursive($this->destination);
275
        }
276
277
        if (isset($this->tempname)) {
278
            rename($this->tempname, $this->destination);
279
        }
280
    }
281
282
    /**
283
     * Validates a module data
284
     * @return boolean
285
     */
286
    protected function validate()
287
    {
288
        $this->data = $this->module->getInfo($this->module_id);
289
290
        if (empty($this->data)) {
291
            $this->error = $this->translation->text('Failed to read module @id', array('@id' => $this->module_id));
292
            return false;
293
        }
294
295
        try {
296
            $this->module_model->checkCore($this->data);
297
            $this->module_model->checkPhpVersion($this->data);
298
            return true;
299
        } catch (Exception $ex) {
300
            $this->error = $ex->getMessage();
301
            return false;
302
        }
303
    }
304
305
    /**
306
     * Returns an array of files from a ZIP file
307
     * @param string $file
308
     * @return array
309
     */
310
    public function getFilesFromZip($file)
311
    {
312
        try {
313
            $files = $this->zip->set($file)->getList();
314
        } catch (Exception $e) {
315
            return array();
316
        }
317
318
        return count($files) < 2 ? array() : $files;
319
    }
320
321
    /**
322
     * Set a module id
323
     * @param string $file
324
     * @return boolean
325
     */
326
    protected function setModuleId($file)
327
    {
328
        $module_id = $this->getModuleIdFromZip($file);
329
330
        try {
331
            $this->module_model->checkModuleId($module_id);
332
        } catch (Exception $ex) {
333
            $this->error = $ex->getMessage();
334
            return false;
335
        }
336
337
        // Do not deal with enabled modules as it may cause fatal results
338
        // Check if the module ID actually has enabled status in the database
339
        // Alternative system methods are based on the scanned module folders so may return incorrect results
340
        if ($this->isEnabledModule($module_id)) {
0 ignored issues
show
Security Bug introduced by
It seems like $module_id defined by $this->getModuleIdFromZip($file) on line 328 can also be of type false; however, gplcart\modules\installe...tall::isEnabledModule() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
341
            $this->error = $this->translation->text('Module @id is enabled and cannot be updated', array('@id' => $module_id));
342
            return false;
343
        }
344
345
        $this->module_id = $module_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $module_id can also be of type false. However, the property $module_id is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
346
        return true;
347
    }
348
349
    /**
350
     * Check if a module ID has enabled status in the database
351
     * @param string $module_id
352
     * @return bool
353
     */
354
    protected function isEnabledModule($module_id)
355
    {
356
        $sql = 'SELECT module_id FROM module WHERE module_id=? AND status > 0';
357
        $result = $this->db->fetchColumn($sql, array($module_id));
358
        return !empty($result);
359
    }
360
361
    /**
362
     * Returns a module id from a zip file or false on error
363
     * @param string $file
364
     * @return boolean|string
365
     */
366
    public function getModuleIdFromZip($file)
367
    {
368
        $list = $this->getFilesFromZip($file);
369
370
        if (empty($list)) {
371
            return false;
372
        }
373
374
        $folder = reset($list);
375
376
        if (strrchr($folder, '/') !== '/') {
377
            return false;
378
        }
379
380
        $nested = 0;
381
        foreach ($list as $item) {
382
            if (strpos($item, $folder) === 0) {
383
                $nested++;
384
            }
385
        }
386
387
        if (count($list) != $nested) {
388
            return false;
389
        }
390
391
        return rtrim($folder, '/');
392
    }
393
394
    /**
395
     * Whether the module files have been updated
396
     * @return bool
397
     */
398
    public function isUpdate()
399
    {
400
        return isset($this->tempname);
401
    }
402
403
}
404