$file One entry from $_FILES, already moved by WordPress. * @return true|WP_Error */ public function validate( array $file ) { $path = isset( $file['tmp_name'] ) ? (string) $file['tmp_name'] : ''; $name = isset( $file['name'] ) ? (string) $file['name'] : ''; if ( '' === $path || ! is_readable( $path ) ) { return $this->fail( 'transtrix_upload_missing', __( 'No file was uploaded. Choose a .zip file and try again.', 'transtrix' ) ); } $size = (int) filesize( $path ); if ( $size <= 0 ) { return $this->fail( 'transtrix_upload_empty', __( 'The file is empty. Choose the .zip that Transtrix exported.', 'transtrix' ) ); } if ( $size > self::MAX_BYTES ) { return $this->fail( 'transtrix_upload_too_large', __( 'The package is larger than this site accepts. Ask your host to raise the upload limit.', 'transtrix' ) ); } if ( 'zip' !== strtolower( (string) pathinfo( $name, PATHINFO_EXTENSION ) ) ) { return $this->fail( 'transtrix_upload_not_zip', __( 'A package must be a .zip file.', 'transtrix' ) ); } $type = wp_check_filetype_and_ext( $path, $name, array( 'zip' => 'application/zip' ) ); if ( empty( $type['ext'] ) || 'zip' !== $type['ext'] ) { return $this->fail( 'transtrix_upload_wrong_type', __( 'This file is not a valid .zip file. Choose the .zip that Transtrix exported.', 'transtrix' ) ); } return $this->inspect_entries( $path ); } /** * Open the archive and look at every entry name and size. * * @param string $path Path to the uploaded file. * @return true|WP_Error */ private function inspect_entries( string $path ) { $zip = new ZipArchive(); if ( true !== $zip->open( $path ) ) { return $this->fail( 'transtrix_upload_not_zip', __( 'The package could not be opened. It may be damaged. Export it again.', 'transtrix' ) ); } // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- ZipArchive's own property name. $total = $zip->numFiles; if ( $total > self::MAX_ENTRIES ) { $zip->close(); return $this->fail( 'transtrix_upload_too_many_entries', __( 'This package has too many files to open safely. Use a package exported by Transtrix.', 'transtrix' ) ); } $uncompressed = 0; for ( $i = 0; $i < $total; $i++ ) { $entry = $zip->statIndex( $i ); if ( ! is_array( $entry ) ) { continue; } $name = (string) $entry['name']; if ( false !== strpos( $name, '..' ) || 0 === strpos( $name, '/' ) || 1 === preg_match( '#^[a-zA-Z]:#', $name ) ) { $zip->close(); return $this->fail( 'transtrix_upload_traversal', __( 'The package contains an unsafe file path, so it was not opened.', 'transtrix' ) ); } $uncompressed += (int) $entry['size']; if ( $uncompressed > self::MAX_UNCOMPRESSED ) { $zip->close(); return $this->fail( 'transtrix_upload_bomb', __( 'The package expands to more than this site accepts.', 'transtrix' ) ); } } $zip->close(); return true; } /** * A rejection, with a machine-readable code the UI can switch on. * * @param string $code Error code. * @param string $message Message. * @return WP_Error */ private function fail( string $code, string $message ): WP_Error { return new WP_Error( $code, $message, array( 'status' => 400 ) ); } }