ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
ILIAS\Filesystem\Util\Archive\Unzip Class Reference
+ Collaboration diagram for ILIAS\Filesystem\Util\Archive\Unzip:

Public Member Functions

 __construct (protected UnzipOptions $options, FileStream $stream)
 Only the path of the passed stream is used, the stream itself is not kept. More...
 
 close ()
 Releases the file handle of the ZIP-file. More...
 
 getPaths ()
 
 getStreams ()
 
 getFileStreams ()
 
 getAmountOfDirectories ()
 
 getDirectories ()
 Yields the directory-paths of the currently open zip-archive. More...
 
 getAmountOfFiles ()
 
 getFiles ()
 Yields the file-paths of the currently open zip-archive. More...
 
 hasMultipleRootEntriesInZip ()
 
 isWithinLimits ()
 Guards against decompression bombs by checking the archive metadata (entry count, total uncompressed size and the uncompressed/compressed ratio) before any data is handed out. More...
 
 extract ()
 Extracting is the last operation on the archive in almost all cases, therefore the file handle of the ZIP-file is released afterwards, so the ZIP-file and its directory can be deleted immediately (which otherwise fails on e.g. More...
 
 hasZipReadingError ()
 

Data Fields

const DS_UNIX = "/"
 
const DS_WIN = "\\"
 
const BASE_DIR = '.'
 

Protected Member Functions

 pathToStreamGenerator ()
 

Protected Attributes

const URI = 'uri'
 
const DIRECTORY_SEPARATOR = DIRECTORY_SEPARATOR
 
ZipArchive $zip
 
bool $error_reading_zip = false
 
string $path_to_zip
 

Private Member Functions

 open ()
 
 calculateIsWithinLimits ()
 
 extractAndKeepOpen ()
 

Private Attributes

bool $is_open = false
 
int $amount_of_entries = 0
 
bool $within_limits = null
 

Detailed Description

Author
Fabian Schmid fabia.nosp@m.n@sr.nosp@m..solu.nosp@m.tion.nosp@m.s

Definition at line 30 of file Unzip.php.

Constructor & Destructor Documentation

◆ __construct()

ILIAS\Filesystem\Util\Archive\Unzip::__construct ( protected UnzipOptions  $options,
FileStream  $stream 
)

Only the path of the passed stream is used, the stream itself is not kept.

This way a stream which is created just for the Unzip (e.g. Streams::ofResource(fopen(...))) is closed right away and does not keep the ZIP-file open.

Definition at line 51 of file Unzip.php.

54 {
55 $this->path_to_zip = (string) $stream->getMetadata(self::URI);
56 $this->zip = new \ZipArchive();
57 if ($this->open()) {
58 $this->amount_of_entries = $this->zip->count();
59 }
60 }

References ILIAS\Filesystem\Util\Archive\Unzip\open().

+ Here is the call graph for this function:

Member Function Documentation

◆ calculateIsWithinLimits()

ILIAS\Filesystem\Util\Archive\Unzip::calculateIsWithinLimits ( )
private

Definition at line 250 of file Unzip.php.

250 : bool
251 {
252 $max_entries = $this->options->getMaxAmountOfEntries();
253 if ($max_entries > UnzipOptions::UNLIMITED && $this->amount_of_entries > $max_entries) {
254 return false;
255 }
256
257 if (!$this->open()) {
258 return false;
259 }
260
261 $total_compressed = 0;
262 $total_uncompressed = 0;
263 for ($i = 0; $i < $this->amount_of_entries; $i++) {
264 $stat = $this->zip->statIndex($i, \ZipArchive::FL_UNCHANGED);
265 if ($stat === false) {
266 continue;
267 }
268 $total_compressed += max(0, (int) ($stat['comp_size'] ?? 0));
269 $total_uncompressed += max(0, (int) ($stat['size'] ?? 0));
270 }
271
272 $max_uncompressed = $this->options->getMaxUncompressedSize();
273 if ($max_uncompressed > UnzipOptions::UNLIMITED && $total_uncompressed > $max_uncompressed) {
274 return false;
275 }
276
277 $max_ratio = $this->options->getMaxCompressionRatio();
278 if (
279 $max_ratio > UnzipOptions::UNLIMITED
280 && $total_compressed > 0
281 && $total_uncompressed > $this->options->getRatioCheckMinUncompressedSize()
282 && ($total_uncompressed / $total_compressed) > $max_ratio
283 ) {
284 return false;
285 }
286
287 return true;
288 }
const UNLIMITED
@description Disables a numeric limit (ratio, entry-count or size).

References ILIAS\Filesystem\Util\Archive\Unzip\$amount_of_entries, ILIAS\Filesystem\Util\Archive\Unzip\open(), and ILIAS\Filesystem\Util\Archive\UnzipOptions\UNLIMITED.

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\isWithinLimits().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ close()

ILIAS\Filesystem\Util\Archive\Unzip::close ( )

Releases the file handle of the ZIP-file.

extract() does this on its own, consumers which only read paths or streams can call this as soon as they are done, e.g. before deleting the ZIP-file. The archive is opened again if it is accessed after it has been closed.

Definition at line 85 of file Unzip.php.

85 : void
86 {
87 if (!$this->is_open) {
88 return;
89 }
90 $this->zip->close();
91 $this->is_open = false;
92 }

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\extract().

+ Here is the caller graph for this function:

◆ extract()

ILIAS\Filesystem\Util\Archive\Unzip::extract ( )

Extracting is the last operation on the archive in almost all cases, therefore the file handle of the ZIP-file is released afterwards, so the ZIP-file and its directory can be deleted immediately (which otherwise fails on e.g.

NFS as long as the file is open).

Definition at line 295 of file Unzip.php.

295 : bool
296 {
297 try {
298 return $this->extractAndKeepOpen();
299 } finally {
300 $this->close();
301 }
302 }
close()
Releases the file handle of the ZIP-file.
Definition: Unzip.php:85

References ILIAS\Filesystem\Util\Archive\Unzip\close(), and ILIAS\Filesystem\Util\Archive\Unzip\extractAndKeepOpen().

+ Here is the call graph for this function:

◆ extractAndKeepOpen()

ILIAS\Filesystem\Util\Archive\Unzip::extractAndKeepOpen ( )
private

Definition at line 304 of file Unzip.php.

304 : bool
305 {
306 if (!$this->open()) {
307 return false;
308 }
309
310 if (!$this->isWithinLimits()) {
311 return false;
312 }
313
314 $destination_path = $this->options->getZipOutputPath();
315 if ($destination_path === null) {
316 return false;
317 }
318
319 switch ($this->options->getDirectoryHandling()) {
320 case ZipDirectoryHandling::KEEP_STRUCTURE:
321 break;
322 case ZipDirectoryHandling::ENSURE_SINGLE_TOP_DIR:
323 // top directory with same name as the ZIP without suffix
324 $sufix = '.' . pathinfo($this->path_to_zip, PATHINFO_EXTENSION);
325 $top_directory = basename($this->path_to_zip, $sufix);
326
327 // first we check if the ZIP contains the top directory
328 $has_top_directory = true;
329 foreach ($this->getPaths() as $path) {
330 $has_top_directory = str_starts_with($path, $top_directory) && $has_top_directory;
331 }
332
333 // if not, we prepend the top directory to the destination path
334 if (!$has_top_directory) {
335 $destination_path .= self::DIRECTORY_SEPARATOR . $top_directory;
336 }
337 break;
339 if (!is_dir($destination_path) && (!mkdir($destination_path, 0777, true) && !is_dir($destination_path))) {
340 throw new \RuntimeException(sprintf('Directory "%s" was not created', $destination_path));
341 }
342
343 foreach ($this->getStreams() as $stream) {
344 $uri = $stream->getMetadata(self::URI);
345 if (substr((string) $uri, -1) === self::DIRECTORY_SEPARATOR) {
346 $stream->close();
347 continue; // Skip directories
348 }
349 $file_name = Util::sanitizeFileName($destination_path . self::DIRECTORY_SEPARATOR . basename((string) $uri));
350 file_put_contents(
351 $file_name,
352 $stream->getContents()
353 );
354 $stream->close();
355 }
356 return true; // Stop here
357 }
358
359 $this->zip->extractTo($destination_path, iterator_to_array($this->getPaths()));
360
361 return true;
362 }
isWithinLimits()
Guards against decompression bombs by checking the archive metadata (entry count, total uncompressed ...
Definition: Unzip.php:245
static sanitizeFileName(string $filename)
Definition: Util.php:48
@ FLAT_STRUCTURE
@description Will keep the top directory of the ZIP file if there is one (simple unzip).

References ILIAS\Filesystem\Util\Archive\FLAT_STRUCTURE, ILIAS\Filesystem\Util\Archive\Unzip\getPaths(), ILIAS\Filesystem\Util\Archive\Unzip\getStreams(), ILIAS\Filesystem\Util\Archive\Unzip\isWithinLimits(), ILIAS\Filesystem\Util\Archive\Unzip\open(), and ILIAS\Filesystem\Util\sanitizeFileName().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\extract().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ getAmountOfDirectories()

ILIAS\Filesystem\Util\Archive\Unzip::getAmountOfDirectories ( )

Definition at line 151 of file Unzip.php.

151 : int
152 {
153 return iterator_count($this->getDirectories());
154 }
getDirectories()
Yields the directory-paths of the currently open zip-archive.
Definition: Unzip.php:161

References ILIAS\Filesystem\Util\Archive\Unzip\getDirectories().

+ Here is the call graph for this function:

◆ getAmountOfFiles()

ILIAS\Filesystem\Util\Archive\Unzip::getAmountOfFiles ( )

Definition at line 189 of file Unzip.php.

189 : int
190 {
191 return iterator_count($this->getFiles());
192 }
getFiles()
Yields the file-paths of the currently open zip-archive.
Definition: Unzip.php:198

References ILIAS\Filesystem\Util\Archive\Unzip\getFiles().

+ Here is the call graph for this function:

◆ getDirectories()

ILIAS\Filesystem\Util\Archive\Unzip::getDirectories ( )

Yields the directory-paths of the currently open zip-archive.

This fixes the issue that win and mac zip archives have different directory structures.

Returns
\Generator|string[]

Definition at line 161 of file Unzip.php.

161 : \Generator
162 {
163 $directories = [];
164 foreach ($this->getPaths() as $path) {
165 if (substr($path, -1) === self::DS_UNIX || substr($path, -1) === self::DS_WIN) {
166 $directories[] = $path;
167 continue;
168 }
169 if ((str_contains($path, self::DS_UNIX) || str_contains($path, self::DS_WIN))) {
170 $directories[] = dirname($path) . self::DIRECTORY_SEPARATOR;
171 }
172 }
173
174 $directories_with_parents = [];
175
176 foreach ($directories as $directory) {
177 $parent = dirname($directory) . self::DIRECTORY_SEPARATOR;
178 if ($parent !== self::BASE_DIR . self::DIRECTORY_SEPARATOR && !in_array($parent, $directories, true)) {
179 $directories_with_parents[] = $parent;
180 }
181 $directories_with_parents[] = $directory;
182 }
183
184 $directories_with_parents = array_unique($directories_with_parents);
185 sort($directories_with_parents);
186 yield from $directories_with_parents;
187 }

References ILIAS\Filesystem\Util\Archive\Unzip\DIRECTORY_SEPARATOR, ILIAS\ResourceStorage\Flavour\Machine\DefaultMachines\from(), ILIAS\Filesystem\Util\Archive\Unzip\getPaths(), and ILIAS\UI\examples\Symbol\Glyph\Sort\sort().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\getAmountOfDirectories(), and ILIAS\Filesystem\Util\Archive\Unzip\hasMultipleRootEntriesInZip().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ getFiles()

ILIAS\Filesystem\Util\Archive\Unzip::getFiles ( )

Yields the file-paths of the currently open zip-archive.

Returns
\Generator|string[]

Definition at line 198 of file Unzip.php.

198 : \Generator
199 {
200 $files = [];
201 foreach ($this->getPaths() as $path) {
202 if (substr($path, -1) !== self::DS_UNIX && substr($path, -1) !== self::DS_WIN) {
203 $files[] = $path;
204 }
205 }
206 sort($files);
207 yield from $files;
208 }

References $files, ILIAS\ResourceStorage\Flavour\Machine\DefaultMachines\from(), ILIAS\Filesystem\Util\Archive\Unzip\getPaths(), and ILIAS\UI\examples\Symbol\Glyph\Sort\sort().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\getAmountOfFiles(), ILIAS\Filesystem\Util\Archive\Unzip\getFileStreams(), ILIAS\Filesystem\Util\Archive\Unzip\getStreams(), and ILIAS\Filesystem\Util\Archive\Unzip\hasMultipleRootEntriesInZip().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ getFileStreams()

ILIAS\Filesystem\Util\Archive\Unzip::getFileStreams ( )
Returns
\Generator|FileStream[]

Definition at line 144 of file Unzip.php.

144 : \Generator
145 {
146 $paths_to_stream_generator = $this->pathToStreamGenerator();
147
148 yield from $paths_to_stream_generator($this->getFiles());
149 }

References ILIAS\ResourceStorage\Flavour\Machine\DefaultMachines\from(), ILIAS\Filesystem\Util\Archive\Unzip\getFiles(), and ILIAS\Filesystem\Util\Archive\Unzip\pathToStreamGenerator().

+ Here is the call graph for this function:

◆ getPaths()

ILIAS\Filesystem\Util\Archive\Unzip::getPaths ( )
Returns
\Generator<bool|string>

Definition at line 114 of file Unzip.php.

114 : \Generator
115 {
116 if (!$this->error_reading_zip && $this->isWithinLimits() && $this->open()) {
117 for ($i = 0, $i_max = $this->amount_of_entries; $i < $i_max; $i++) {
118 $path = $this->zip->getNameIndex($i, \ZipArchive::FL_UNCHANGED);
119 if ($this->isPathIgnored($path, $this->options)) {
120 continue;
121 }
122 yield $path;
123 }
124 }
125 }

References ILIAS\Filesystem\Util\Archive\Unzip\isWithinLimits(), and ILIAS\Filesystem\Util\Archive\Unzip\open().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\extractAndKeepOpen(), ILIAS\Filesystem\Util\Archive\Unzip\getDirectories(), ILIAS\Filesystem\Util\Archive\Unzip\getFiles(), and ILIAS\Filesystem\Util\Archive\Unzip\getStreams().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ getStreams()

ILIAS\Filesystem\Util\Archive\Unzip::getStreams ( )
Returns
\Generator|FileStream[]

Definition at line 130 of file Unzip.php.

130 : \Generator
131 {
132 $paths_to_stream_generator = $this->pathToStreamGenerator();
133
134 if ($this->options->getDirectoryHandling() === ZipDirectoryHandling::FLAT_STRUCTURE) {
135 yield from $paths_to_stream_generator($this->getFiles());
136 } else {
137 yield from $paths_to_stream_generator($this->getPaths());
138 }
139 }

References ILIAS\Filesystem\Util\Archive\FLAT_STRUCTURE, ILIAS\ResourceStorage\Flavour\Machine\DefaultMachines\from(), ILIAS\Filesystem\Util\Archive\Unzip\getFiles(), ILIAS\Filesystem\Util\Archive\Unzip\getPaths(), and ILIAS\Filesystem\Util\Archive\Unzip\pathToStreamGenerator().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\extractAndKeepOpen().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ hasMultipleRootEntriesInZip()

ILIAS\Filesystem\Util\Archive\Unzip::hasMultipleRootEntriesInZip ( )

Definition at line 210 of file Unzip.php.

210 : bool
211 {
212 $amount = 0;
213 foreach ($this->getDirectories() as $zip_directory) {
214 $dirname = dirname($zip_directory);
215 if ($dirname === self::BASE_DIR) {
216 $amount++;
217 }
218 if ($amount > 1) {
219 return true;
220 }
221 }
222 foreach ($this->getFiles() as $zip_file) {
223 $dirname = dirname($zip_file);
224 if ($dirname === self::BASE_DIR) {
225 $amount++;
226 }
227 if ($amount > 1) {
228 return true;
229 }
230 }
231 return false;
232 }

References ILIAS\Filesystem\Util\Archive\Unzip\getDirectories(), and ILIAS\Filesystem\Util\Archive\Unzip\getFiles().

+ Here is the call graph for this function:

◆ hasZipReadingError()

ILIAS\Filesystem\Util\Archive\Unzip::hasZipReadingError ( )

Definition at line 364 of file Unzip.php.

364 : bool
365 {
367 }

References ILIAS\Filesystem\Util\Archive\Unzip\$error_reading_zip.

◆ isWithinLimits()

ILIAS\Filesystem\Util\Archive\Unzip::isWithinLimits ( )

Guards against decompression bombs by checking the archive metadata (entry count, total uncompressed size and the uncompressed/compressed ratio) before any data is handed out.

The sizes are read from the central directory via statIndex(); a crafted archive that lies about these sizes fails during the subsequent extraction anyway.

An archive that exceeds the limits behaves like an empty one: extract() returns false and all generators (getPaths(), getFiles(), getStreams(), getFileStreams(), ...) yield nothing. Callers that consume the streams themselves should ask this method first, so they can tell a rejected archive from an empty one and report it to the user.

Definition at line 245 of file Unzip.php.

245 : bool
246 {
247 return $this->within_limits ??= $this->calculateIsWithinLimits();
248 }

References ILIAS\Filesystem\Util\Archive\Unzip\calculateIsWithinLimits().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\extractAndKeepOpen(), and ILIAS\Filesystem\Util\Archive\Unzip\getPaths().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ open()

ILIAS\Filesystem\Util\Archive\Unzip::open ( )
private

Definition at line 62 of file Unzip.php.

62 : bool
63 {
64 if ($this->is_open) {
65 return true;
66 }
67 if ($this->error_reading_zip) {
68 return false;
69 }
70 try {
71 $this->is_open = $this->zip->open($this->path_to_zip, \ZipArchive::RDONLY) === true;
72 } catch (\Throwable) {
73 $this->is_open = false;
74 }
75 $this->error_reading_zip = !$this->is_open;
76
77 return $this->is_open;
78 }

References ILIAS\Filesystem\Util\Archive\Unzip\$is_open.

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\__construct(), ILIAS\Filesystem\Util\Archive\Unzip\calculateIsWithinLimits(), ILIAS\Filesystem\Util\Archive\Unzip\extractAndKeepOpen(), ILIAS\Filesystem\Util\Archive\Unzip\getPaths(), and ILIAS\Filesystem\Util\Archive\Unzip\pathToStreamGenerator().

+ Here is the caller graph for this function:

◆ pathToStreamGenerator()

ILIAS\Filesystem\Util\Archive\Unzip::pathToStreamGenerator ( )
protected
Returns
\Closure

Definition at line 97 of file Unzip.php.

97 : \Closure
98 {
99 return function (\Generator $paths): \Generator {
100 foreach ($paths as $path) {
101 if (!$this->open()) {
102 return;
103 }
104 $resource = $this->zip->getStream($path);
105
106 yield Streams::ofResource($resource);
107 }
108 };
109 }
static ofResource($resource)
Wraps an already created resource with the stream abstraction.
Definition: Streams.php:64

References ILIAS\Filesystem\Stream\Streams\ofResource(), and ILIAS\Filesystem\Util\Archive\Unzip\open().

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\getFileStreams(), and ILIAS\Filesystem\Util\Archive\Unzip\getStreams().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

Field Documentation

◆ $amount_of_entries

int ILIAS\Filesystem\Util\Archive\Unzip::$amount_of_entries = 0
private

◆ $error_reading_zip

bool ILIAS\Filesystem\Util\Archive\Unzip::$error_reading_zip = false
protected

Definition at line 40 of file Unzip.php.

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\hasZipReadingError().

◆ $is_open

bool ILIAS\Filesystem\Util\Archive\Unzip::$is_open = false
private

Definition at line 42 of file Unzip.php.

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\open().

◆ $path_to_zip

string ILIAS\Filesystem\Util\Archive\Unzip::$path_to_zip
protected

Definition at line 41 of file Unzip.php.

◆ $within_limits

bool ILIAS\Filesystem\Util\Archive\Unzip::$within_limits = null
private

Definition at line 44 of file Unzip.php.

◆ $zip

ZipArchive ILIAS\Filesystem\Util\Archive\Unzip::$zip
protected

Definition at line 39 of file Unzip.php.

◆ BASE_DIR

const ILIAS\Filesystem\Util\Archive\Unzip::BASE_DIR = '.'

Definition at line 38 of file Unzip.php.

◆ DIRECTORY_SEPARATOR

const ILIAS\Filesystem\Util\Archive\Unzip::DIRECTORY_SEPARATOR = DIRECTORY_SEPARATOR
protected

Definition at line 35 of file Unzip.php.

Referenced by ILIAS\Filesystem\Util\Archive\Unzip\getDirectories().

◆ DS_UNIX

const ILIAS\Filesystem\Util\Archive\Unzip::DS_UNIX = "/"

Definition at line 36 of file Unzip.php.

◆ DS_WIN

const ILIAS\Filesystem\Util\Archive\Unzip::DS_WIN = "\\"

Definition at line 37 of file Unzip.php.

◆ URI

const ILIAS\Filesystem\Util\Archive\Unzip::URI = 'uri'
protected

Definition at line 34 of file Unzip.php.


The documentation for this class was generated from the following file: