> 32 ) & 0xFFFFFFFF ) . pack( 'N', $size & 0xFFFFFFFF ) . pack( 'N', max( 0, (int) $mtime ) & 0xFFFFFFFF ) . $path; } /** * Reads the next entry header at the current position. * * @param resource $fh Handle. * @return array|null Entry (path, size, mtime, data_offset) or null at end. * @throws RuntimeException When the archive is damaged. */ public static function read_header( $fh ) { $tag = fread( $fh, 3 ); if ( self::END === $tag ) { return null; } if ( self::ENTRY !== $tag ) { throw new RuntimeException( esc_html( __( 'The backup file is damaged or incomplete. Upload it again or pick another backup.', 'shiftbox' ) ) ); } $raw = fread( $fh, 14 ); if ( strlen( $raw ) !== 14 ) { throw new RuntimeException( esc_html( __( 'The backup file ends early. It was probably not fully uploaded.', 'shiftbox' ) ) ); } $h = unpack( 'nlen/Nhi/Nlo/Nmtime', $raw ); $path = $h['len'] > 0 ? fread( $fh, $h['len'] ) : ''; if ( strlen( $path ) !== $h['len'] ) { throw new RuntimeException( esc_html( __( 'The backup file ends early. It was probably not fully uploaded.', 'shiftbox' ) ) ); } return array( 'path' => $path, 'size' => ( $h['hi'] * 4294967296 ) + $h['lo'], 'mtime' => $h['mtime'], 'data_offset' => ftell( $fh ), ); } /** * Checks the magic header. * * @param string $file Archive path. * @return bool */ public static function is_archive( $file ) { $fh = @fopen( $file, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( ! $fh ) { return false; } $magic = fread( $fh, strlen( self::MAGIC ) ); fclose( $fh ); return self::MAGIC === $magic; } /** * Reads package.json, which is always the first entry. * * @param string $file Archive path. * @return array|WP_Error */ public static function read_manifest( $file ) { if ( ! self::is_archive( $file ) ) { return new WP_Error( 'shiftbox_not_archive', __( 'This is not a Shiftbox backup file (.shiftbox).', 'shiftbox' ) ); } $fh = fopen( $file, 'rb' ); fseek( $fh, strlen( self::MAGIC ) ); try { $entry = self::read_header( $fh ); } catch ( RuntimeException $e ) { fclose( $fh ); return new WP_Error( 'shiftbox_damaged', $e->getMessage() ); } if ( ! $entry || 'package.json' !== $entry['path'] || $entry['size'] > 1048576 ) { fclose( $fh ); return new WP_Error( 'shiftbox_damaged', __( 'The backup file has no package information.', 'shiftbox' ) ); } $json = $entry['size'] > 0 ? fread( $fh, $entry['size'] ) : ''; fclose( $fh ); $data = json_decode( $json, true ); if ( ! is_array( $data ) ) { return new WP_Error( 'shiftbox_damaged', __( 'The backup package information could not be read.', 'shiftbox' ) ); } return $data; } /** * Validates a path read from an archive. Returns a clean relative path or false. * * @param string $path Path. * @return string|false */ public static function clean_path( $path ) { if ( '' === $path || false !== strpos( $path, "\0" ) || false !== strpos( $path, '\\' ) ) { return false; } if ( '/' === $path[0] || preg_match( '/^[A-Za-z]:/', $path ) ) { return false; } foreach ( explode( '/', $path ) as $part ) { if ( '' === $part || '.' === $part || '..' === $part ) { return false; } } return $path; } }