【问题标题】:Resumable file download with PHP in WordPress / Download files via WordPress backend在 WordPress 中使用 PHP 进行可恢复文件下载/通过 WordPress 后端下载文件
【发布时间】:2020-06-22 13:59:45
【问题描述】:

我目前正在用 PHP 开发文件下载。为此,我在服务器上创建了一个存储文件夹,并使用.htaccess 文件(全部拒绝)对其进行保护,因此无法通过在浏览器中输入路径来访问它。在这个文件夹中,我有一个文件5eecc057489de.jpeg,我现在要下载它:

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

(我的 index.php 在根 htdocs 文件夹中)

为了安全和灵活,我想进行可恢复下载,并尝试找到一个适合我和我的客户需求的好脚本。所以我做了很多研究,从 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;
}

我做了一些最小的更改,例如变量名称更改(linter)和一些格式设置,但总而言之,我保留了原来的脚本。

要立即下载文件,我已从index.php 使用以下参数调用该函数:

$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 );

但是,经过我所有的尝试、调试和日志检查(没有条目) - 下载的文件有错误。 Chrome 尝试下载它,一切看起来都很好,但每次下载在开始后 1-2 秒中止并且文件下载完全失败:

我首先删除了 .htaccess 以确保它不是问题,但它没有帮助。

接下来尝试联系开发人员并向他寻求帮助,但没有得到答复(我的意思是脚本是 2012 年的)。所以也许你们中的某个人知道这里发生了什么?如果没有 - 您是否知道更好的脚本或做过同样的事情并且可以指出我下载可恢复文件的正确方法?

如果您需要更多信息,请给我留言!

【问题讨论】:

    标签: php wordpress file plugins download


    【解决方案1】:

    我现在做了很多研究,并提出了我自己的 - 更好的功能(希望如此)。而且我也发现了一些东西!在 WordPress 中您需要替换内容类型返回码,否则在尝试下载文件时会在浏览器中出现此错误:

    ERR_INVALID_RESPONSE

    只有当你想下载它时才需要它。如果只想在浏览器中查看,则无需替换。

    这是我完整的 WordPress 下载脚本

    为了下载文件,我决定将按钮完成的任何请求重定向到浏览器中的下载 URL。因此,例如,如果您有带有以下链接的按钮:

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

    现在您需要在 WordPress 中进行一些检查以检测是否有下载请求。为此,我使用了template_redirect 钩子:

    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;
        }
    }
    

    之后,您需要我的下载功能和返回与您的文件扩展名有关的 mime 类型的功能:

    /**
     * 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';
    }
    

    所以我的下载终于成功了。但这仅适用于一个文件!如果您想下载多个文件,我建议将一组文件传递给您的函数并检查多个文件。

    在这种情况下,我会将所有内容放入一个临时的.zip 文件并请求下载这个文件。这就是要走的路。

    此外,如果允许用户下载文件或对要下载的文件进行编码,我还会添加一些检查,这样用户就无法通过知道文件名称来下载任何文件。

    【讨论】:

    • 我已经设法通过压缩文件下载了多个文件。如果有人真的需要这个,我愿意在这里扩展我的功能,以便它处理多个文件下载。
    猜你喜欢
    • 2012-07-17
    • 1970-01-01
    • 2019-06-01
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 1970-01-01
    • 2018-12-09
    • 1970-01-01
    相关资源
    最近更新 更多