Passed
Push — 1.11.x ( 08587c...c85e98 )
by Yannick
10:54
created

ZipPackageImporter::validateH5pPackageContent()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 25
rs 8.8333
cc 7
nc 7
nop 1
1
<?php
2
3
// For licensing terms, see /license.txt
4
5
namespace Chamilo\PluginBundle\H5pImport\H5pImporter;
6
7
use Exception;
8
use Symfony\Component\Filesystem\Filesystem;
9
10
/**
11
 * Class ZipPackageImporter.
12
 */
13
class ZipPackageImporter extends H5pPackageImporter
14
{
15
    /*
16
     * Allowed file extensions
17
     * List obtained from H5P: https://h5p.org/allowed-file-extensions
18
     * */
19
    private const ALLOWED_EXTENSIONS = [
20
        'json',
21
        'png',
22
        'jpg',
23
        'jpeg',
24
        'gif',
25
        'bmp',
26
        'tif',
27
        'tiff',
28
        'svg',
29
        'eot',
30
        'ttf',
31
        'woff',
32
        'woff2',
33
        'otf',
34
        'webm',
35
        'mp4',
36
        'ogg',
37
        'mp3',
38
        'm4a',
39
        'wav',
40
        'txt',
41
        'pdf',
42
        'rtf',
43
        'doc',
44
        'docx',
45
        'xls',
46
        'xlsx',
47
        'ppt',
48
        'pptx',
49
        'odt',
50
        'ods',
51
        'odp',
52
        'xml',
53
        'csv',
54
        'diff',
55
        'patch',
56
        'swf',
57
        'md',
58
        'textile',
59
        'vtt',
60
        'webvtt',
61
        'gltf',
62
        'gl',
63
        'js',
64
        'css',
65
    ];
66
67
    /**
68
     * Import an H5P package. No DB change.
69
     *
70
     * @return string The path to the extracted package directory.
71
     *
72
     * @throws Exception When the H5P package is invalid.
73
     */
74
    public function import(): string
75
    {
76
        $zipFile = new \PclZip($this->packageFileInfo['tmp_name']);
77
        $zipContent = $zipFile->listContent();
78
79
        if ($this->validateH5pPackageContent($zipContent)) {
80
            $packageSize = array_reduce(
81
                $zipContent,
82
                function ($accumulator, $zipEntry) {
83
                    return $accumulator + $zipEntry['size'];
84
                }
85
            );
86
87
            $this->validateEnoughSpace($packageSize);
88
89
            $pathInfo = pathinfo($this->packageFileInfo['name']);
90
91
            $packageDirectoryPath = $this->generatePackageDirectory($pathInfo['filename']);
92
            $zipFile->extract($packageDirectoryPath);
93
94
            return "{$packageDirectoryPath}";
95
        }
96
97
        throw new Exception('Invalid H5P package');
98
    }
99
100
    /**
101
     * @throws Exception
102
     */
103
    protected function validateEnoughSpace(int $packageSize)
104
    {
105
        $courseSpaceQuota = \DocumentManager::get_course_quota($this->course->getCode());
106
107
        if (!enough_size($packageSize, $this->courseDirectoryPath, $courseSpaceQuota)) {
108
            throw new Exception('Not enough space to store package.');
109
        }
110
    }
111
112
    /**
113
     * Validate an H5P package.
114
     * Check if 'h5p.json' or 'content/content.json' files exist
115
     * and if the files are in a file whitelist (ALLOWED_EXTENSIONS).
116
     *
117
     * @param array $h5pPackageContent the content of the H5P package
118
     *
119
     * @return bool whether the H5P package is valid or not
120
     */
121
    private function validateH5pPackageContent(array $h5pPackageContent): bool
122
    {
123
        $validPackage = false;
124
125
        if (!empty($h5pPackageContent)) {
126
            foreach ($h5pPackageContent as $content) {
127
                $filename = $content['filename'];
128
129
                if (0 !== preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $filename)) {
130
                    // Skip any file or folder starting with a . or _
131
                    continue;
132
                }
133
134
                $fileExtension = pathinfo($filename, PATHINFO_EXTENSION);
135
136
                if (in_array($fileExtension, self::ALLOWED_EXTENSIONS)) {
137
                    $validPackage = 'h5p.json' === $filename || 'content/content.json' === $filename;
138
                    if ($validPackage) {
139
                        break;
140
                    }
141
                }
142
            }
143
        }
144
145
        return $validPackage;
146
    }
147
148
    private function generatePackageDirectory(string $name): string
149
    {
150
        $baseDirectory = $this->courseDirectoryPath.'/h5p/content/';
151
        $safeName = api_replace_dangerous_char($name);
152
        $directoryPath = $baseDirectory.$safeName;
153
154
        $fs = new Filesystem();
155
156
        if ($fs->exists($directoryPath)) {
157
            $counter = 1;
158
159
            // Add numeric suffix to the name until a unique directory name is found
160
            while ($fs->exists($directoryPath)) {
161
                $modifiedName = $safeName.'_'.$counter;
162
                $directoryPath = $baseDirectory.$modifiedName;
163
                ++$counter;
164
            }
165
        }
166
167
        $fs->mkdir(
168
            $directoryPath,
169
            api_get_permissions_for_new_directories()
170
        );
171
172
        $sharedLibrariesDir = $this->courseDirectoryPath.'/h5p/libraries';
173
174
        if (!$fs->exists($sharedLibrariesDir)) {
175
            $fs->mkdir(
176
                $sharedLibrariesDir,
177
                api_get_permissions_for_new_directories()
178
            );
179
        }
180
181
        return $directoryPath;
182
    }
183
}
184