89 $file = fopen( $fileName,
'r' );
91 return $zdr->execute();
107 return $zdr->execute();
131 private const ZIP64_EXTRA_HEADER = 0x0001;
134 private const SEGSIZE = 16384;
137 private const GENERAL_UTF8 = 11;
140 private const GENERAL_CD_ENCRYPTED = 13;
151 if ( isset( $options[
'zip64'] ) ) {
152 $this->zip64 = $options[
'zip64'];
161 private function execute() {
163 if ( !$this->file ) {
164 return Status::newFatal(
'zip-file-open-error' );
169 $this->readEndOfCentralDirectoryRecord();
170 if ( $this->zip64 ) {
171 list( $offset, $size ) = $this->findZip64CentralDirectory();
172 $this->readCentralDirectory( $offset, $size );
174 if ( $this->eocdr[
'CD size'] == 0xffffffff
175 || $this->eocdr[
'CD offset'] == 0xffffffff
176 || $this->eocdr[
'CD entries total'] == 0xffff
178 $this->error(
'zip-unsupported',
'Central directory header indicates ZIP64, ' .
179 'but we are in legacy mode. Rejecting this upload is necessary to avoid ' .
180 'opening vulnerabilities on clients using OpenJDK 7 or later.' );
183 list( $offset, $size ) = $this->findOldCentralDirectory();
184 $this->readCentralDirectory( $offset, $size );
190 fclose( $this->file );
202 private function error( $code, $debugMessage ) {
203 wfDebug( __CLASS__ .
": Fatal error: $debugMessage" );
212 private function readEndOfCentralDirectoryRecord() {
216 'CD start disk' => 2,
217 'CD entries this disk' => 2,
218 'CD entries total' => 2,
221 'file comment length' => 2,
223 $structSize = $this->getStructSize( $info );
224 $startPos = $this->getFileLength() - 65536 - $structSize;
225 if ( $startPos < 0 ) {
229 if ( $this->getFileLength() === 0 ) {
230 $this->error(
'zip-wrong-format',
"The file is empty." );
233 $block = $this->getBlock( $startPos );
234 $sigPos = strrpos( $block,
"PK\x05\x06" );
235 if ( $sigPos ===
false ) {
236 $this->error(
'zip-wrong-format',
237 "zip file lacks EOCDR signature. It probably isn't a zip file." );
240 $this->eocdr = $this->unpack( substr( $block, $sigPos ), $info );
241 $this->eocdr[
'EOCDR size'] = $structSize + $this->eocdr[
'file comment length'];
243 if ( $structSize + $this->eocdr[
'file comment length'] != strlen( $block ) - $sigPos ) {
245 $this->error(
'zip-wrong-format',
'there is a ZIP signature but it is not at ' .
246 'the end of the file. It could be an OLE file with a ZIP file embedded.' );
248 if ( $this->eocdr[
'disk'] !== 0
249 || $this->eocdr[
'CD start disk'] !== 0
251 $this->error(
'zip-unsupported',
'more than one disk (in EOCDR)' );
253 $this->eocdr += $this->unpack(
255 [
'file comment' => [
'string', $this->eocdr[
'file comment length'] ] ],
256 $sigPos + $structSize );
257 $this->eocdr[
'position'] = $startPos + $sigPos;
264 private function readZip64EndOfCentralDirectoryLocator() {
266 'signature' => [
'string', 4 ],
267 'eocdr64 start disk' => 4,
268 'eocdr64 offset' => 8,
269 'number of disks' => 4,
271 $structSize = $this->getStructSize( $info );
273 $start = $this->getFileLength() - $this->eocdr[
'EOCDR size'] - $structSize;
274 $block = $this->getBlock( $start, $structSize );
275 $this->eocdr64Locator =
$data = $this->unpack( $block, $info );
277 if (
$data[
'signature'] !==
"PK\x06\x07" ) {
281 $this->error(
'zip-bad',
'wrong signature on Zip64 end of central directory locator' );
289 private function readZip64EndOfCentralDirectoryRecord() {
290 if ( $this->eocdr64Locator[
'eocdr64 start disk'] != 0
291 || $this->eocdr64Locator[
'number of disks'] != 0
293 $this->error(
'zip-unsupported',
'more than one disk (in EOCDR64 locator)' );
297 'signature' => [
'string', 4 ],
299 'version made by' => 2,
300 'version needed' => 2,
302 'CD start disk' => 4,
303 'CD entries this disk' => 8,
304 'CD entries total' => 8,
308 $structSize = $this->getStructSize( $info );
309 $block = $this->getBlock( $this->eocdr64Locator[
'eocdr64 offset'], $structSize );
310 $this->eocdr64 =
$data = $this->unpack( $block, $info );
311 if (
$data[
'signature'] !==
"PK\x06\x06" ) {
312 $this->error(
'zip-bad',
'wrong signature on Zip64 end of central directory record' );
314 if (
$data[
'disk'] !== 0
315 ||
$data[
'CD start disk'] !== 0
317 $this->error(
'zip-unsupported',
'more than one disk (in EOCDR64)' );
327 private function findOldCentralDirectory() {
328 $size = $this->eocdr[
'CD size'];
329 $offset = $this->eocdr[
'CD offset'];
330 $endPos = $this->eocdr[
'position'];
334 if ( $offset + $size != $endPos ) {
335 $this->error(
'zip-bad',
'the central directory does not immediately precede the end ' .
336 'of central directory record' );
339 return [ $offset, $size ];
348 private function findZip64CentralDirectory() {
352 $size = $this->eocdr[
'CD size'];
353 $offset = $this->eocdr[
'CD offset'];
354 $numEntries = $this->eocdr[
'CD entries total'];
355 $endPos = $this->eocdr[
'position'];
356 if ( $size == 0xffffffff
357 || $offset == 0xffffffff
358 || $numEntries == 0xffff
360 $this->readZip64EndOfCentralDirectoryLocator();
362 if ( isset( $this->eocdr64Locator[
'eocdr64 offset'] ) ) {
363 $this->readZip64EndOfCentralDirectoryRecord();
364 if ( isset( $this->eocdr64[
'CD offset'] ) ) {
365 $size = $this->eocdr64[
'CD size'];
366 $offset = $this->eocdr64[
'CD offset'];
367 $endPos = $this->eocdr64Locator[
'eocdr64 offset'];
373 if ( $offset + $size != $endPos ) {
374 $this->error(
'zip-bad',
'the central directory does not immediately precede the end ' .
375 'of central directory record' );
378 return [ $offset, $size ];
386 private function readCentralDirectory( $offset, $size ) {
387 $block = $this->getBlock( $offset, $size );
390 'signature' => [
'string', 4 ],
391 'version made by' => 2,
392 'version needed' => 2,
394 'compression method' => 2,
398 'compressed size' => 4,
399 'uncompressed size' => 4,
401 'extra field length' => 2,
402 'comment length' => 2,
403 'disk number start' => 2,
404 'internal attrs' => 2,
405 'external attrs' => 4,
406 'local header offset' => 4,
408 $fixedSize = $this->getStructSize( $fixedInfo );
411 while ( $pos < $size ) {
412 $data = $this->unpack( $block, $fixedInfo, $pos );
415 if (
$data[
'signature'] !==
"PK\x01\x02" ) {
416 $this->error(
'zip-bad',
'Invalid signature found in directory entry' );
420 'name' => [
'string',
$data[
'name length'] ],
421 'extra field' => [
'string',
$data[
'extra field length'] ],
422 'comment' => [
'string',
$data[
'comment length'] ],
424 $data += $this->unpack( $block, $variableInfo, $pos );
425 $pos += $this->getStructSize( $variableInfo );
427 if ( $this->zip64 && (
428 $data[
'compressed size'] == 0xffffffff
429 ||
$data[
'uncompressed size'] == 0xffffffff
430 ||
$data[
'local header offset'] == 0xffffffff )
432 $zip64Data = $this->unpackZip64Extra(
$data[
'extra field'] );
438 if ( $this->testBit(
$data[
'general bits'], self::GENERAL_CD_ENCRYPTED ) ) {
439 $this->error(
'zip-unsupported',
'central directory encryption is not supported' );
445 $time =
$data[
'mod time'];
446 $date =
$data[
'mod date'];
448 $year = 1980 + ( $date >> 9 );
449 $month = ( $date >> 5 ) & 15;
451 $hour = ( $time >> 11 ) & 31;
452 $minute = ( $time >> 5 ) & 63;
453 $second = ( $time & 31 ) * 2;
454 $timestamp = sprintf(
"%04d%02d%02d%02d%02d%02d",
455 $year, $month, $day, $hour, $minute, $second );
458 if ( $this->testBit(
$data[
'general bits'], self::GENERAL_UTF8 ) ) {
459 $name =
$data[
'name'];
461 $name = iconv(
'CP437',
'UTF-8',
$data[
'name'] );
467 'mtime' => $timestamp,
468 'size' =>
$data[
'uncompressed size'],
470 call_user_func( $this->callback, $userData );
479 private function unpackZip64Extra( $extraField ) {
484 $extraHeaderSize = $this->getStructSize( $extraHeaderInfo );
487 'uncompressed size' => 8,
488 'compressed size' => 8,
489 'local header offset' => 8,
490 'disk number start' => 4,
494 while ( $extraPos < strlen( $extraField ) ) {
495 $extra = $this->unpack( $extraField, $extraHeaderInfo, $extraPos );
496 $extraPos += $extraHeaderSize;
497 $extra += $this->unpack( $extraField,
498 [
'data' => [
'string', $extra[
'size'] ] ],
500 $extraPos += $extra[
'size'];
502 if ( $extra[
'id'] == self::ZIP64_EXTRA_HEADER ) {
503 return $this->unpack( $extra[
'data'], $zip64ExtraInfo );
514 private function getFileLength() {
515 if ( $this->fileLength ===
null ) {
516 $stat = fstat( $this->file );
517 $this->fileLength = $stat[
'size'];
533 private function getBlock( $start, $length =
null ) {
536 $this->error(
'zip-bad',
"getBlock() requested position $start, " .
537 "file length is $fileLength" );
539 if ( $length ===
null ) {
542 $end = $start + $length;
544 $this->error(
'zip-bad',
"getBlock() requested end position $end, " .
545 "file length is $fileLength" );
547 $startSeg = (int)floor( $start / self::SEGSIZE );
548 $endSeg = (int)ceil( $end / self::SEGSIZE );
551 for ( $segIndex = $startSeg; $segIndex <= $endSeg; $segIndex++ ) {
552 $block .= $this->getSegment( $segIndex );
555 $block = substr( $block,
556 $start - $startSeg * self::SEGSIZE,
559 if ( strlen( $block ) < $length ) {
560 $this->error(
'zip-bad',
'getBlock() returned an unexpectedly small amount of data' );
579 private function getSegment( $segIndex ) {
580 if ( !isset( $this->buffer[$segIndex] ) ) {
581 $bytePos = $segIndex * self::SEGSIZE;
582 if ( $bytePos >= $this->getFileLength() ) {
583 $this->buffer[$segIndex] =
'';
587 if ( fseek( $this->file, $bytePos ) ) {
588 $this->error(
'zip-bad',
"seek to $bytePos failed" );
590 $seg = fread( $this->file, self::SEGSIZE );
591 if ( $seg ===
false ) {
592 $this->error(
'zip-bad',
"read from $bytePos failed" );
594 $this->buffer[$segIndex] = $seg;
597 return $this->buffer[$segIndex];
605 private function getStructSize( $struct ) {
607 foreach ( $struct as
$type ) {
608 if ( is_array(
$type ) ) {
609 list( , $fieldSize ) =
$type;
641 private function unpack( $string, $struct, $offset = 0 ) {
642 $size = $this->getStructSize( $struct );
643 if ( $offset + $size > strlen( $string ) ) {
644 $this->error(
'zip-bad',
'unpack() would run past the end of the supplied string' );
649 foreach ( $struct as $key =>
$type ) {
650 if ( is_array(
$type ) ) {
651 list( $typeName, $fieldSize ) =
$type;
652 switch ( $typeName ) {
654 $data[$key] = substr( $string, $pos, $fieldSize );
658 throw new MWException( __METHOD__ .
": invalid type \"$typeName\"" );
662 $length = intval(
$type );
667 for ( $i = $length - 1; $i >= 0; $i-- ) {
669 $value += ord( $string[$pos + $i] );
673 if ( $value > 2 ** 52 ) {
674 $this->error(
'zip-unsupported',
'number too large to be stored in a double. ' .
675 'This could happen if we tried to unpack a 64-bit structure ' .
676 'at an invalid location.' );
678 $data[$key] = $value;
694 private function testBit( $value, $bitIndex ) {
695 return (
bool)( ( $value >> $bitIndex ) & 1 );