Completed
Push — master ( be20c9...3906ce )
by Iurii
01:15
created

Install::validate()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 4
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 as UrlHelper;
15
use gplcart\core\helpers\Zip as ZipHelper;
16
use gplcart\core\models\Job as JobModel;
17
use gplcart\core\models\Module as ModuleModel;
18
use gplcart\core\models\Translation as TranslationModel;
19
use gplcart\core\Module;
20
use gplcart\modules\backup\models\Backup as ModuleBackupModel;
21
22
/**
23
 * Manages basic behaviors and data related to Installer module
24
 */
25
class Install
26
{
27
28
    /**
29
     * Database class instance
30
     * @var \gplcart\core\Database $db
31
     */
32
    protected $db;
33
34
    /**
35
     * Config class instance
36
     * @var \gplcart\core\Config $config
37
     */
38
    protected $config;
39
40
    /**
41
     * Module model instance
42
     * @var \gplcart\core\Module $module
43
     */
44
    protected $module;
45
46
    /**
47
     * Zip helper class instance
48
     * @var \gplcart\core\helpers\Zip $zip
49
     */
50
    protected $zip;
51
52
    /**
53
     * Url helper class instance
54
     * @var \gplcart\core\helpers\Url $url
55
     */
56
    protected $url;
57
58
    /**
59
     * Translation UI model instance
60
     * @var \gplcart\core\models\Translation $translation
61
     */
62
    protected $translation;
63
64
    /**
65
     * Job model instance
66
     * @var \gplcart\core\models\Job $job
67
     */
68
    protected $job;
69
70
    /**
71
     * Module model instance
72
     * @var \gplcart\core\models\Module $module_model
73
     */
74
    protected $module_model;
75
76
    /**
77
     * Backup model instance
78
     * @var \gplcart\modules\backup\models\Backup $backup
79
     */
80
    protected $backup;
81
82
    /**
83
     * The latest validation error
84
     * @var string
85
     */
86
    protected $error;
87
88
    /**
89
     * The temporary renamed module directory
90
     * @var string
91
     */
92
    protected $tempname;
93
94
    /**
95
     * The module directory
96
     * @var string
97
     */
98
    protected $destination;
99
100
    /**
101
     * The module ID
102
     * @var string
103
     */
104
    protected $module_id;
105
106
    /**
107
     * An array of module data
108
     * @var array
109
     */
110
    protected $data;
111
112
    /**
113
     * Whether an original module has been temporary renamed
114
     * @var boolean
115
     */
116
    protected $renamed;
117
118
    /**
119
     * @param Config $config
120
     * @param Module $module
121
     * @param ModuleModel $module_model
122
     * @param TranslationModel $translation
123
     * @param ModuleBackupModel $backup
124
     * @param JobModel $job
125
     * @param ZipHelper $zip
126
     * @param UrlHelper $url
127
     */
128
    public function __construct(Config $config, Module $module,
129
                                ModuleModel $module_model, TranslationModel $translation, ModuleBackupModel $backup,
130
                                JobModel $job, ZipHelper $zip, UrlHelper $url)
131
    {
132
        $this->config = $config;
133
        $this->module = $module;
134
        $this->db = $this->config->getDb();
135
136
        $this->zip = $zip;
137
        $this->url = $url;
138
        $this->job = $job;
139
        $this->backup = $backup;
140
        $this->translation = $translation;
141
        $this->module_model = $module_model;
142
    }
143
144
    /**
145
     * Installs a module from a ZIP file
146
     * @param string $zip
147
     * @return string|bool
148
     */
149
    public function fromZip($zip)
150
    {
151
        $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...
152
        $this->error = null;
153
        $this->renamed = false;
154
        $this->tempname = null;
155
        $this->module_id = null;
156
        $this->destination = null;
157
158
        if (!$this->setModuleId($zip)) {
159
            return $this->error;
160
        }
161
162
        if (!$this->extract()) {
163
            $this->rollback();
164
            return $this->error;
165
        }
166
167
        if (!$this->validate()) {
168
            $this->rollback();
169
            return $this->error;
170
        }
171
172
        if (!$this->backup()) {
173
            return $this->error;
174
        }
175
176
        return true;
177
    }
178
179
    /**
180
     * Install modules from multiple URLs
181
     * @param array $sources
182
     */
183
    public function fromUrl(array $sources)
184
    {
185
        $total = count($sources);
186
        $finish_message = $this->translation->text('New modules: %inserted, updated: %updated');
187
        $vars = array('@url' => $this->url->get('', array('download_errors' => true)));
188
        $errors_message = $this->translation->text('New modules: %inserted, updated: %updated, errors: %errors. <a href="@url">See error log</a>', $vars);
189
190
        $data = array(
191
            'total' => $total,
192
            'data' => array('sources' => $sources),
193
            'id' => 'installer_download_module',
194
            'log' => array('errors' => $this->getErrorLogFile()),
195
            'redirect_message' => array('finish' => $finish_message, 'errors' => $errors_message)
196
        );
197
198
        $this->job->submit($data);
199
    }
200
201
    /**
202
     * Returns path to error log file
203
     * @return string
204
     */
205
    public function getErrorLogFile()
206
    {
207
        return gplcart_file_private_temp('installer-download-errors.csv');
208
    }
209
210
    /**
211
     * Backup the previous version of the updated module
212
     */
213
    protected function backup()
214
    {
215
        if (empty($this->tempname)) {
216
            return true;
217
        }
218
219
        $module = $this->data;
220
221
        $module += array(
222
            'directory' => $this->tempname,
223
            'module_id' => $this->module_id
224
        );
225
226
        $result = $this->backup->backup('module', $module);
227
228
        if ($result === true) {
229
            gplcart_file_delete_recursive($this->tempname);
230
            return true;
231
        }
232
233
        $this->error = $this->translation->text('Failed to backup module @id', array('@id' => $this->module_id));
234
        return false;
235
    }
236
237
    /**
238
     * Extracts module files to the system directory
239
     * @return boolean
240
     */
241
    protected function extract()
242
    {
243
        $this->destination = GC_DIR_MODULE . "/{$this->module_id}";
244
245
        if (file_exists($this->destination)) {
246
            $this->tempname = gplcart_file_unique($this->destination . '~');
247
            if (!rename($this->destination, $this->tempname)) {
248
                $this->error = $this->translation->text('Failed to rename @old to @new', array('@old' => $this->destination, '@new' => $this->tempname));
249
                return false;
250
            }
251
            $this->renamed = true;
252
        }
253
254
        if ($this->zip->extract(GC_DIR_MODULE)) {
255
            return true;
256
        }
257
258
        $this->error = $this->translation->text('Failed to extract to @name', array('@name' => $this->destination));
259
        return false;
260
    }
261
262
    /**
263
     * Restore the original module files
264
     */
265
    protected function rollback()
266
    {
267
        if (!$this->isUpdate() || ($this->isUpdate() && $this->renamed)) {
268
            gplcart_file_delete_recursive($this->destination);
269
        }
270
271
        if (isset($this->tempname)) {
272
            rename($this->tempname, $this->destination);
273
        }
274
    }
275
276
    /**
277
     * Validates a module data
278
     * @return boolean
279
     */
280
    protected function validate()
281
    {
282
        $this->data = $this->module->getInfo($this->module_id);
283
284
        if (empty($this->data)) {
285
            $this->error = $this->translation->text('Failed to read module @id', array('@id' => $this->module_id));
286
            return false;
287
        }
288
289
        try {
290
            $this->module_model->checkCore($this->data);
291
            $this->module_model->checkPhpVersion($this->data);
292
            return true;
293
        } catch (Exception $ex) {
294
            $this->error = $ex->getMessage();
295
            return false;
296
        }
297
    }
298
299
    /**
300
     * Returns an array of files from a ZIP file
301
     * @param string $file
302
     * @return array
303
     */
304
    public function getFilesFromZip($file)
305
    {
306
        try {
307
            $files = $this->zip->set($file)->getList();
308
        } catch (Exception $e) {
309
            return array();
310
        }
311
312
        return count($files) < 2 ? array() : $files;
313
    }
314
315
    /**
316
     * Set a module id
317
     * @param string $file
318
     * @return boolean
319
     */
320
    protected function setModuleId($file)
321
    {
322
        $module_id = $this->getModuleIdFromZip($file);
323
324
        try {
325
            $this->module_model->checkModuleId($module_id);
326
        } catch (Exception $ex) {
327
            $this->error = $ex->getMessage();
328
            return false;
329
        }
330
331
        // Do not deal with enabled modules as it may cause fatal results
332
        // Check if the module ID actually has enabled status in the database
333
        // Alternative system methods are based on the scanned module folders so may return incorrect results
334
        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 322 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...
335
            $this->error = $this->translation->text('Module @id is enabled and cannot be updated', array('@id' => $module_id));
336
            return false;
337
        }
338
339
        $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...
340
        return true;
341
    }
342
343
    /**
344
     * Check if a module ID has enabled status in the database
345
     * @param string $module_id
346
     * @return bool
347
     */
348
    protected function isEnabledModule($module_id)
349
    {
350
        $sql = 'SELECT module_id FROM module WHERE module_id=? AND status > 0';
351
        $result = $this->db->fetchColumn($sql, array($module_id));
352
        return !empty($result);
353
    }
354
355
    /**
356
     * Returns a module id from a zip file or false on error
357
     * @param string $file
358
     * @return boolean|string
359
     */
360
    public function getModuleIdFromZip($file)
361
    {
362
        $list = $this->getFilesFromZip($file);
363
364
        if (empty($list)) {
365
            return false;
366
        }
367
368
        $folder = reset($list);
369
370
        if (strrchr($folder, '/') !== '/') {
371
            return false;
372
        }
373
374
        $nested = 0;
375
        foreach ($list as $item) {
376
            if (strpos($item, $folder) === 0) {
377
                $nested++;
378
            }
379
        }
380
381
        if (count($list) != $nested) {
382
            return false;
383
        }
384
385
        return rtrim($folder, '/');
386
    }
387
388
    /**
389
     * Whether the module files have been updated
390
     * @return bool
391
     */
392
    public function isUpdate()
393
    {
394
        return isset($this->tempname);
395
    }
396
397
}
398