UploadModule::uploadModuleFromZip()   F
last analyzed

Complexity

Conditions 20
Paths 152

Size

Total Lines 128
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 0
Metric Value
cc 20
eloc 59
nc 152
nop 0
dl 0
loc 128
ccs 0
cts 78
cp 0
crap 420
rs 3.7333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Backend\Modules\Extensions\Actions;
4
5
use Backend\Core\Engine\Base\ActionAdd as BackendBaseActionAdd;
6
use Backend\Core\Engine\Model as BackendModel;
7
use Backend\Core\Engine\Form as BackendForm;
8
use Backend\Core\Language\Language as BL;
9
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
10
use Symfony\Component\Filesystem\Filesystem;
11
12
/**
13
 * This is the module upload-action.
14
 * It will install a module via a compressed zip file.
15
 */
16
class UploadModule extends BackendBaseActionAdd
17
{
18 1
    public function execute(): void
19
    {
20
        // call parent, this will probably add some general CSS/JS or other required files
21 1
        parent::execute();
22
23
        // zip extension is required for module upload
24 1
        if (!extension_loaded('zlib')) {
25
            $this->template->assign('zlibIsMissing', true);
26
        }
27
28 1
        if (!$this->isWritable()) {
29
            // we need write rights to upload files
30
            $this->template->assign('notWritable', true);
31
        } else {
32
            // everything allright, we can upload
33 1
            $this->buildForm();
34 1
            $this->validateForm();
35 1
            $this->parse();
36
        }
37
38
        // display the page
39 1
        $this->display();
40 1
    }
41
42
    /**
43
     * Process the zip-file & install the module
44
     *
45
     * @return string|null
46
     */
47
    private function uploadModuleFromZip(): ?string
48
    {
49
        // list of validated files (these files will actually be unpacked)
50
        $files = [];
51
52
        // shorten field variables
53
        /** @var $fileFile \SpoonFormFile */
54
        $fileFile = $this->form->getField('file');
55
56
        // create \ziparchive instance
57
        $zip = new \ZipArchive();
58
59
        // try and open it
60
        if ($zip->open($fileFile->getTempFileName()) !== true) {
61
            $fileFile->addError(BL::getError('CorruptedFile'));
62
        }
63
64
        // zip file needs to contain some files
65
        if ($zip->numFiles == 0) {
66
            $fileFile->addError(BL::getError('FileIsEmpty'));
67
68
            return null;
69
        }
70
71
        // directories we are allowed to upload to
72
        $allowedDirectories = [
73
            'src/Backend/Modules/',
74
            'src/Frontend/Modules/',
75
        ];
76
77
        // name of the module we are trying to upload
78
        $moduleName = null;
79
80
        // has the module zip one level of folders too much?
81
        $prefix = '';
82
83
        // check every file in the zip
84
        for ($i = 0; $i < $zip->numFiles; ++$i) {
85
            // get the file name
86
            $file = $zip->statIndex($i);
87
            $fileName = $file['name'];
88
89
            if ($i === 0) {
90
                $prefix = $this->extractPrefix($file['name']);
91
            }
92
93
            // check if the file is in one of the valid directories
94
            foreach ($allowedDirectories as $directory) {
95
                // yay, in a valid directory
96
                if (mb_stripos($fileName, $prefix . $directory) === 0) {
97
                    // extract the module name from the url
98
                    $tmpName = trim(str_ireplace($prefix . $directory, '', $fileName), '/');
0 ignored issues
show
Bug introduced by
It seems like str_ireplace($prefix . $directory, '', $fileName) can also be of type array; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

98
                    $tmpName = trim(/** @scrutinizer ignore-type */ str_ireplace($prefix . $directory, '', $fileName), '/');
Loading history...
99
                    if ($tmpName == '') {
100
                        break;
101
                    }
102
                    $chunks = explode('/', $tmpName);
103
                    $tmpName = $chunks[0];
104
105
                    // ignore hidden files
106
                    if (mb_substr(basename($fileName), 0, 1) == '.') {
107
                        break;
108
                    } elseif ($moduleName === null) {
109
                        // first module we find, store the name
110
                        $moduleName = $tmpName;
111
                    } elseif ($moduleName !== $tmpName) {
112
                        // the name does not match the previous module we found, skip the file
113
                        break;
114
                    }
115
116
                    // passed all our tests, store it for extraction
117
                    $files[] = $fileName;
118
119
                    // go to next file
120
                    break;
121
                }
122
            }
123
        }
124
125
        // after filtering no files left (nothing useful found)
126
        if (count($files) == 0) {
127
            $fileFile->addError(BL::getError('FileContentsIsUseless'));
128
129
            return null;
130
        }
131
132
        // module already exists on the filesystem
133
        if (BackendExtensionsModel::existsModule($moduleName)) {
0 ignored issues
show
Bug introduced by
$moduleName of type null is incompatible with the type string expected by parameter $module of Backend\Modules\Extensio...e\Model::existsModule(). ( Ignorable by Annotation )

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

133
        if (BackendExtensionsModel::existsModule(/** @scrutinizer ignore-type */ $moduleName)) {
Loading history...
134
            $fileFile->addError(sprintf(BL::getError('ModuleAlreadyExists'), $moduleName));
135
136
            return null;
137
        }
138
139
        // installer in array?
140
        if (!in_array($prefix . 'src/Backend/Modules/' . $moduleName . '/Installer/Installer.php', $files)) {
141
            $fileFile->addError(sprintf(BL::getError('NoInstallerFile'), $moduleName));
142
143
            return null;
144
        }
145
146
        // unpack module files
147
        $zip->extractTo($this->getContainer()->getParameter('site.path_www'), $files);
0 ignored issues
show
Bug introduced by
It seems like $this->getContainer()->g...ameter('site.path_www') can also be of type array; however, parameter $destination of ZipArchive::extractTo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

147
        $zip->extractTo(/** @scrutinizer ignore-type */ $this->getContainer()->getParameter('site.path_www'), $files);
Loading history...
148
149
        // place all the items in the prefixed folders in the right folders
150
        if (!empty($prefix)) {
151
            $filesystem = new Filesystem();
152
            foreach ($files as &$file) {
153
                $fullPath = $this->getContainer()->getParameter('site.path_www') . '/' . $file;
154
                $newPath = str_replace(
155
                    $this->getContainer()->getParameter('site.path_www') . '/' . $prefix,
156
                    $this->getContainer()->getParameter('site.path_www') . '/',
157
                    $fullPath
158
                );
159
160
                if ($filesystem->exists($fullPath) && is_dir($fullPath)) {
161
                    $filesystem->mkdir($newPath);
162
                } elseif ($filesystem->exists($fullPath) && is_file($fullPath)) {
163
                    $filesystem->copy(
164
                        $fullPath,
165
                        $newPath
166
                    );
167
                }
168
            }
169
170
            $filesystem->remove($this->getContainer()->getParameter('site.path_www') . '/' . $prefix);
171
        }
172
173
        // return the files
174
        return $moduleName;
175
    }
176
177
    /**
178
     * Try to extract a prefix if a module has been zipped with unexpected
179
     * paths.
180
     *
181
     * @param string $file
182
     *
183
     * @return string
184
     */
185
    private function extractPrefix(string $file): string
186
    {
187
        $name = explode(PATH_SEPARATOR, $file);
188
        $prefix = [];
189
190
        foreach ($name as $element) {
191
            if ($element == 'src') {
192
                return implode(PATH_SEPARATOR, $prefix);
193
            }
194
195
            $prefix[] = $element;
196
        }
197
198
        // If the zip has a top-level single directory, eg
199
        // /myModuleName/, then we should just assume that is the prefix.
200
        return $file;
201
    }
202
203
    /**
204
     * Do we have write rights to the modules folders?
205
     *
206
     * @return bool
207
     */
208 1
    private function isWritable(): bool
209
    {
210 1
        if (!BackendExtensionsModel::isWritable(FRONTEND_MODULES_PATH)) {
211
            return false;
212
        }
213
214 1
        return BackendExtensionsModel::isWritable(BACKEND_MODULES_PATH);
215
    }
216
217 1
    private function buildForm(): void
218
    {
219
        // create form
220 1
        $this->form = new BackendForm('upload');
221
222
        // create and add elements
223 1
        $this->form->addFile('file');
224 1
    }
225
226 1
    private function validateForm(): void
227
    {
228 1
        if (!$this->form->isSubmitted()) {
229 1
            return;
230
        }
231
232
        // shorten field variables
233
        $fileFile = $this->form->getField('file');
234
235
        // validate the file
236
        $fileFile->isFilled(BL::err('FieldIsRequired'));
237
        $fileFile->isAllowedExtension(['zip'], sprintf(BL::getError('ExtensionNotAllowed'), 'zip'));
238
239
        // passed all validation
240
        if (!$this->form->isCorrect()) {
241
            return;
242
        }
243
244
        $moduleName = $this->uploadModuleFromZip();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $moduleName is correct as $this->uploadModuleFromZip() targeting Backend\Modules\Extensio...::uploadModuleFromZip() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
245
246
        // redirect to the install url, this is needed for doctrine modules because the container needs to
247
        // load this module as an allowed module to get the entities working
248
        $this->redirect(BackendModel::createUrlForAction('InstallModule') . '&module=' . $moduleName);
249
    }
250
}
251