简体   繁体   中英

Resumable file download with PHP in WordPress / Download files via WordPress backend

I'm currently developing a file download in PHP. For that I've created a storage folder on my server and secured it with a .htaccess file (deny from all) so thats it's not accessible via typing in the path in the browser. In this folder I've a file 5eecc057489de.jpeg which I want to download now:

htdocs/
└── files/
    └── storage/
        ├── 5eecc057489de.jpeg
        ├── index.php
        └── .htaccess

(My index.php is in the root htdocs folder)

To be safe and flexible I wanted to go for a resumable download and tried to find a good script that fits my and my customers needs. So I've did a lot of research and found this script from Armand Niculescu - media-division.com :

/**
 * Send download file to the browser
 *
 * @param $file
 * @param string $filename
 * @param string $file_ext
 * @param bool $preview
 * @param bool $open_pdf_in_browser
 *
 * @return void
 */
private function download_file( $file, $filename, $file_ext, $preview = false, $open_pdf_in_browser = false ): void {
    while ( ob_get_level() ) {
        ob_end_clean();
    }
    ini_set( 'error_reporting', E_ALL & ~E_NOTICE );
    ini_set( 'zlib.output_compression', 'Off' );
    $is_attachment = isset( $_REQUEST['stream'] ) ? false : true;
    if ( $open_pdf_in_browser && $preview && strtolower( $file_ext ) === 'pdf' ) {
        $is_attachment = false;
    }
    if ( file_exists( $file ) ) {
        $file_size    = filesize( $file );
        $file_handler = fopen( $file, 'rb' );
        if ( $file_handler ) {
            header( 'Pragma: public' );
            header( 'Expires: -1' );
            header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' );
            header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
            if ( $is_attachment ) {
                header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
            } else {
                header( 'Content-Disposition: inline; filename="' . $filename . '"' );
            }
            header( 'Content-Type: ' . $this->mime_type( $file_ext ) );
            // todo: Apply multiple ranges
            if ( isset( $_SERVER['HTTP_RANGE'] ) ) {
                [ $size_unit, $range_orig ] = explode( '=', $_SERVER['HTTP_RANGE'], 2 );
                if ( $size_unit === 'bytes' ) {
                    [ $range ] = explode( ',', $range_orig, 2 );
                } else {
                    header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
                    exit;
                }
            } else {
                $range = '';
            }
            [ $seek_start, $seek_end ] = explode( '-', $range, 2 );
            $seek_start = ( empty( $seek_start ) || $seek_end < abs( (int) $seek_start ) ) ? 0 : max( abs( (int) $seek_start ), 0 );
            if ( $seek_start > 0 || $seek_end < ( $file_size - 1 ) ) {
                header( 'HTTP/1.1 206 Partial Content' );
                header( 'Content-Range: bytes ' . $seek_start . '-' . $seek_end . '/' . $file_size );
                header( 'Content-Length: ' . ( $seek_end - $seek_start + 1 ) );
            } else {
                header( 'Content-Length: ' . $file_size );
            }
            header( 'Accept-Ranges: bytes' );
            set_time_limit( 0 );
            fseek( $file_handler, $seek_start );
            while ( ! feof( $file_handler ) ) {
                print( fread( $file_handler, 1024 * 8 ) );
                ob_flush();
                flush();
                if ( connection_status() !== 0 ) {
                    fclose( $file_handler );
                    exit;
                }
            }
            fclose( $file_handler );
            exit;
        }
        header( 'HTTP/1.0 500 Internal Server Error' );
        exit;
    }
    header( 'HTTP/1.0 404 Not Found' );
    exit;
}

I did some minimal changes like variable name changes (linter) and some formatting but all in all I left the script as it originally was.

To download the file now, I've called the function from my index.php with the following parameters:

$file     = '/htdocs/files/storage/5eecc057489de.jpeg';
$filename = 'test.jpeg'; //This comes from the DB by doing a select with the unique file id: 5eecc057489de
$file_ext = 'jpeg';

$this->download_file( $file, $filename, $file_ext );

But with all my tries, debugging and log checking (no entries) - the downloaded file has an error. Chrome tries to download it and everything looks great but every time the download gets aborted 1-2 seconds after the start and the file download totally fails:

在此处输入图像描述

I've first removed the .htaccess to be sure its not the problem but it didn't helped.

Next tried contacting the developer and asked him for help but got no answer (I mean the script is from 2012). So maybe someone of you has an idea whats going on here? If not - do you know a better script or did the same thing and can point me to the right way doing a resumable file download?

I you need any further informations - please write me a comment!

I've did a lot of research now and came up with my own - better function (hopefully). And I also found something out, In WordPress you need to replace the content type return code: otherwise you will get this error in your browser when trying to download a file:

ERR_INVALID_RESPONSE

This is only needed if you want to download it. If you just want to view it in the browser, no replace is needed.

So this is my fully download script for WordPress

To download a file, I've decided to redirect any request done by a button to a download URL in the browser. So for example if you have this button with the following link:

<a class="button" href="https://localhost/download/?filename=test&file_ext=jpeg">Download something</a>

Now you need to do some checks in WordPress to detect that there is a download request. For that I'm using the template_redirect hook:

add_action( 'template_redirect', 'action_template_redirect', 15 );
function action_template_redirect(): void {
    $url = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );

    if ( $url === '/download/' ) { //Here we check if it's a download request
        $base_path = wp_upload_dir()['basedir'] . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR; //Here we set the base path to our folder where the file is saved in
        $filename  = $_GET['filename'];
        $file_ext  = $_GET['file_ext'];

        if ( ! empty( $filename ) && ! empty( $file_ext ) ) { //Do a check if we received a filename and the type of the file
            $full_filename = $filename . '.' . $file_ext;

            download_file( $base_path . $full_filename, $full_filename, $file_ext );
        }

        header( 'HTTP/1.0 500 Internal Server Error' );
        exit;
    }
}

After that you need my download function and a function that returns the mime type regarding to your file extension:

/**
 * Send download file to the browser
 *
 * @param string $file_path
 * @param string $filename
 * @param string $file_ext
 * @param bool $open_in_browser
 *
 * @return void
 */
function download_file( $file_path, $filename, $file_ext, $open_in_browser = false ): void {
    if ( file_exists( $file_path ) ) {
        $file = fopen( $file_path, 'rb' );

        if ( $file ) {
            $file_size  = filesize( $file_path );
            $http_range = isset( $_SERVER['HTTP_RANGE'] );
            $seek_start = 0;

            ini_set( 'zlib.output_compression', 'Off' );

            header( 'Pragma: public' );
            header( 'Expires: -1' );
            header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' );
            header( 'Accept-Ranges: bytes' );
            header( 'Content-Type: ' . $this->mime_type( $file_ext ), true, 200 ); //<-- Here we need to overwrite the WordPress default response code!!!

            if ( $open_in_browser || $file_ext === 'pdf' ) {
                header( 'Content-Disposition: inline; filename="' . $filename . '"' );
            } else {
                header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
            }

            if ( $http_range ) {
                [ $unit, $range ] = explode( '=', $_SERVER['HTTP_RANGE'], 2 );

                if ( $unit !== 'bytes' ) {
                    header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
                    exit;
                }

                if ( ! empty( $range ) ) {
                    [ $seek_start, $seek_end ] = explode( '-', $range, 2 );

                    if ( $seek_end < abs( $seek_start ) ) {
                        $seek_start = 0;
                    }

                    if ( $seek_start > 0 || $seek_end < ( $file_size - 1 ) ) {
                        header( 'HTTP/1.1 206 Partial Content' );
                        header( 'Content-Range: bytes ' . $seek_start . '-' . $seek_end . '/' . $file_size );
                        header( 'Content-Length: ' . ( $seek_end - $seek_start + 1 ) );
                    }
                }
            } else {
                header( 'Content-Length: ' . $file_size );
            }

            ignore_user_abort( true );
            @set_time_limit( 0 );
            fseek( $file, $seek_start );

            while ( ! feof( $file ) ) {
                print( fread( $file, 1024 * 8 ) );
                ob_flush();
                flush();

                if ( connection_status() !== 0 ) {
                    fclose( $file );
                    exit;
                }
            }

            fclose( $file );
            exit;
        }

        header( 'HTTP/1.0 500 Internal Server Error' );
        exit;
    }

    header( 'HTTP/1.0 404 Not Found' );
    exit;
}


/**
 * @param $ext
 *
 * @return string|null
 */
function mime_type( $ext ): ?string {
    $mime_types = [
        'swf'   => 'application/x-shockwave-flash',
        'flv'   => 'video/x-flv',
        'png'   => 'image/png',
        'jpe'   => 'image/jpeg',
        'jpeg'  => 'image/jpeg',
        'jpg'   => 'image/jpeg',
        'gif'   => 'image/gif',
        'bmp'   => 'image/bmp',
        'ico'   => 'image/vnd.microsoft.icon',
        'tiff'  => 'image/tiff',
        'tif'   => 'image/tiff',
        'svg'   => 'image/svg+xml',
        'svgz'  => 'image/svg+xml',
        'mid'   => 'audio/midi',
        'midi'  => 'audio/midi',
        'mp2'   => 'audio/mpeg',
        'mp3'   => 'audio/mpeg',
        'mpga'  => 'audio/mpeg',
        'aif'   => 'audio/x-aiff',
        'aifc'  => 'audio/x-aiff',
        'aiff'  => 'audio/x-aiff',
        'ram'   => 'audio/x-pn-realaudio',
        'rm'    => 'audio/x-pn-realaudio',
        'rpm'   => 'audio/x-pn-realaudio-plugin',
        'ra'    => 'audio/x-realaudio',
        'wav'   => 'audio/x-wav',
        'wma'   => 'audio/wma',
        'mp4'   => 'video/mp4',
        'mpeg'  => 'video/mpeg',
        'mpe'   => 'video/mpeg',
        'mpg'   => 'video/mpeg',
        'mov'   => 'video/quicktime',
        'qt'    => 'video/quicktime',
        'rv'    => 'video/vnd.rn-realvideo',
        'avi'   => 'video/x-msvideo',
        'movie' => 'video/x-sgi-movie',
        '3gp'   => 'video/3gpp',
        'webm'  => 'video/webm',
        'ogv'   => 'video/ogg',
        'pdf'   => 'application/pdf'
    ];

    if ( array_key_exists( strtolower( $ext ), $mime_types ) ) {
        return $mime_types[ strtolower( $ext ) ];
    }

    return 'application/octet-stream';
}

So finally my download works. But this only works for one file, If you want to download multiple files. I would suggest passing an array of files to your function and do a check for multiple files.

In this case I would put everything into a temp .zip file and request downloading this one. Thats the way to go.

Also I would add some checks if the user is allowed to download a file or encode the files to download so that a user can't download any file by knowing it's name.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM