WC_Download_Handler::resolve_filename_from_response_headers
Given the raw HTTP response header lines collected while opening a remote file (possibly spanning a redirect chain) and the filename derived from the URL, determine the most appropriate filename for serving the download.
The filename derived from the URL wins when it already has an extension. Otherwise the filename announced by the remote server in the Content-Disposition header of the final response is used, falling back to appending an extension derived from its Content-Type header. This makes downloads hosted on URLs without a filename in their path (for example Google Drive links) save under a usable name instead of a bare URL segment like uc.
Method of the class: WC_Download_Handler{}
No Hooks.
Returns
string.
Usage
$result = WC_Download_Handler::resolve_filename_from_response_headers( $response_headers, $filename, $preserve_filename ): string;
- $response_headers(array) (required)
- Raw HTTP response header lines, e.g. from
stream_get_meta_data()['wrapper_data']. - $filename(mixed) (required)
- Filename derived from the URL. Documented as a string, but a misbehaving woocommerce_file_download_filename callback may produce any type. Anything renderable as a string (scalars, and objects declaring
__toString()) is rendered, matching what string concatenation used to do with it; anything else (null, arrays) becomes''instead of fataling. A non-string value triggers an incorrect usage notice identifying its type. - $preserve_filename(true|false)
- When true,
$filenameis kept (it was customized deliberately, e.g. via the woocommerce_file_download_filename and only completed with an extension derived from the response headers, instead of being replaced by the remote-announced filename. Ignored when$filenamerenders empty, since there is then nothing to preserve.
Default:false
Changelog
| Since 11.1.0 | Introduced. |
WC_Download_Handler::resolve_filename_from_response_headers() WC Download Handler::resolve filename from response headers code WC 11.1.2
public static function resolve_filename_from_response_headers( array $response_headers, $filename, bool $preserve_filename = false ): string {
if ( ! is_string( $filename ) ) {
$original_type = gettype( $filename );
// `is_scalar()` is false for objects, so callable `__toString()` implementations are admitted separately.
// `instanceof Stringable` cannot be used here: it is PHP 8.0+, and WooCommerce supports PHP 7.4.
$filename = is_scalar( $filename ) || ( is_object( $filename ) && is_callable( array( $filename, '__toString' ) ) )
? (string) $filename
: '';
wc_doing_it_wrong(
__METHOD__,
sprintf(
'The woocommerce_file_download_filename filter should return a string; %s returned.',
$original_type
),
'11.1.0'
);
}
// An empty filename carries nothing to preserve, so let the remote-announced filename win.
$preserve_filename = $preserve_filename && '' !== $filename;
if ( '' !== pathinfo( $filename, PATHINFO_EXTENSION ) ) {
return $filename;
}
// Only the headers of the final response in a redirect chain describe the actual file.
$headers = array();
foreach ( $response_headers as $header ) {
if ( ! is_string( $header ) ) {
continue;
}
if ( 0 === stripos( $header, 'HTTP/' ) ) {
$headers = array();
continue;
}
$parts = explode( ':', $header, 2 );
if ( 2 === count( $parts ) ) {
$headers[ strtolower( trim( $parts[0] ) ) ] = trim( $parts[1] );
}
}
$remote_filename = '';
if ( isset( $headers['content-disposition'] ) ) {
// The RFC 5987 encoded `filename*` parameter takes precedence over the plain `filename` one.
// Matches e.g. `filename*=UTF-8''My%20Report.pdf`: a charset, a language tag between the quotes, then the percent-encoded value.
if ( preg_match( '/filename\*\s*=\s*[^\']*\'[^\']*\'(?<encoded_value>[^;]+)/i', $headers['content-disposition'], $matches ) ) {
$remote_filename = rawurldecode( trim( $matches['encoded_value'], " \t\"" ) );
} elseif ( preg_match( '/filename\s*=\s*(?:"(?<quoted_value>[^"]*)"|(?<bare_value>[^;]+))/i', $headers['content-disposition'], $matches ) ) {
// Matches `filename="My Report.pdf"` (quoted) or `filename=report.pdf` (bare token, terminated by `;`).
$remote_filename = isset( $matches['bare_value'] ) ? trim( $matches['bare_value'] ) : $matches['quoted_value'];
}
$remote_filename = sanitize_file_name( $remote_filename );
}
if ( '' !== $remote_filename ) {
if ( $preserve_filename ) {
$remote_extension = pathinfo( $remote_filename, PATHINFO_EXTENSION );
if ( '' !== $remote_extension ) {
$filename .= '.' . $remote_extension;
}
} else {
$filename = $remote_filename;
}
}
if ( '' === pathinfo( $filename, PATHINFO_EXTENSION ) && isset( $headers['content-type'] ) ) {
$mime_type = strtolower( trim( (string) strtok( $headers['content-type'], ';' ) ) );
// application/octet-stream is generic and would map to an arbitrary extension.
if ( '' !== $mime_type && 'application/octet-stream' !== $mime_type ) {
$extension = wp_get_default_extension_for_mime_type( $mime_type );
if ( $extension ) {
$filename .= '.' . $extension;
}
}
}
return $filename;
}