【问题标题】:Showing (pdf) content of LONGBLOB in PHP在 PHP 中显示 LONGBLOB 的 (pdf) 内容
【发布时间】:2019-04-22 04:04:44
【问题描述】:

我正在尝试显示存储在我的 LONGBLOB 中的 PDF 文件。当我执行代码时,我只得到文件的名称。我想在查看器中显示 PDF 文件。

有人知道我该如何解决这个问题吗?

这是我的脚本:

          <?php
            $tpurchase_id = $_GET['tpurchase_id'];
            $conn = new mysqli("localhost","user","","db");
            $sql = "SELECT * FROM temp_purchase WHERE tpurchase_id= '$tpurchase_id'";

            $result = $conn->query($sql);
            if ($result->num_rows > 0) {
              while($row = $result->fetch_assoc()) {
                header("Content-type:application/pdf");

                $a=$row['content'];
                echo '<object data="data:application/pdf;base64,';
                echo base64_decode($a); 
                echo '" type="application/pdf" style="height:200px;width:60%"></object>';
              }
            }
            $conn->close();

          ?>

【问题讨论】:

  • 警告:使用mysqli 时,您应该使用parameterized queriesbind_param 将任何数据添加到您的查询中。 请勿使用字符串插值或连接来完成此操作,因为您创建了严重的SQL injection bug切勿$_POST$_GET任何类型的数据直接放入查询中,如果有人试图利用您的错误,这可能会非常有害。
  • 哪一步编码成base64?
  • @John - 向我们展示INSERTed content 的代码。
  • @John 您应该处理脚本的问题是您没有正确连接字符串。你应该使用 fyrye 的解决方案,它看起来不错。

标签: php mysql


【解决方案1】:

我认为这可以正常工作,提供更多标题信息

header("Content-type: application/pdf");
header('Content-disposition: attachment; filename="thing.pdf"');
header("Content-Length: " . strlen($row['content']));

print $row['content'];

【讨论】:

  • 当我执行脚本时,我只得到文件名(test.pdf)。我在浏览器中看不到内容
【解决方案2】:

一些操作顺序的改进和优化。 假设数据库正确存储了LOB 数据的全部内容。

使用ob_startob_cleanob_end_flush 的输出缓冲可以更好地控制脚本中所需的响应内容。这将有助于减少二进制输出中包含错误空格或发出的警告。 此外,这允许您控制在响应中发送哪些 header 数据。

没有必要使用while($row = $result-&gt;fetch_assoc()),因为来自数据库的响应应该包含整个单个LONGBLOB 数据行。

使用mysqli_stmt::bind_resultmysqli_stmt::fetch 将减少一些因提取到关联数组而导致的开销,因为它只需要检索内容。如果没有结果/数据,mysqli_stmt::fetch 将返回 NULL,如果出错则返回 false

我还建议使用prepared statements 来防止SQL 注入,并使用filter_var 来确保用户提供的输入是预期的数据类型并且是有效的。

使用content-disposition: inline会要求浏览器尝试加载它,如果浏览器可以理解content-type,否则会下载它。

最后,您不需要以?&gt; 结束代码,这可能会导致意外行为,而只需使用exit;。最好在 PHP 脚本文件中排除结束标记,除非您从 PHP 转换为同一文件中的纯文本或标记。

我针对我的 MySQL 数据库表测试了以下内容,该表也使用LONGBLOB 来存储 PDF 文件并且运行正常。

<?php /*line 1*/
ob_start(); //start output buffering immediately
$conn = new mysqli('localhost','user','','db');
if (mysqli_connect_errno()) {
    exit;
}
$tpurchase_id = filter_var($_GET['tpurchase_id'], FILTER_VALIDATE_INT);
$stmt = $conn->prepare('SELECT tp.content 
FROM temp_purchase AS tp 
WHERE tp.tpurchase_id = ? 
AND tp.content > ""'); //ensure content is not empty
if ($stmt && false !== $tpurchase_id) {
    $stmt->bind_param('i', $tpurchase_id);
    $stmt->execute();
    $stmt->bind_result($content);
    if ($stmt->fetch()) {
        //a record was found, change to a PDF file
        ob_clean(); //clear the buffer
        header('content-type: application/pdf');
        header('content-disposition: inline; filename="Test.pdf"');
        echo $content;
        ob_end_flush(); //output only the buffered content to the client
    }
    $stmt->close();
    unset($content, $stmt);
}
$conn->close(); //always close the connection
while (ob_get_level() > 0) {
   ob_end_clean(); //remove everything else from the buffer
}
exit;

这将导致仅将标头和内容响应发送到客户端,否则如果找不到来自数据库的结果,则会发送空白的纯文本响应。

然后可以将上述脚本用作内联对象的源代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<object data="/path/to/above_script.php?tpurchase_id=123" type="application/pdf" style="height:200px;width:60%"></object>
</body>
</html>

除了上述之外,还有其他可能导致问题的点,我们目前不知道。

  • 由网络服务器(apache、nginx、IIS 等)添加或修改的标头。
  • 上传表单或PHP处理脚本修改或未将完整的LOB数据发送到数据库。
  • 数据库正在截断或更改LOB 数据。

使用上述 PHP 脚本显示内联 object(s)。不需要输出缓冲。但是,您需要更换 base64_decode 以支持使用 base64_encode。解码采用 base64 编码字符串并将其转换为原始格式。您实际上想要从数据库中获取二进制数据并将其转换为 base64 编码字符串以供浏览器稍后解码。如果文件内容已经被上传处理脚本base64_encode'd,则不需要base64_encodebase64_decode

测试了以下内容并按预期运行。

<?php /*line 1*/
$conn = new mysqli('localhost','user','','db');
if (mysqli_connect_errno()) {
    exit;
}
$tpurchase_id = filter_var($_GET['tpurchase_id'], FILTER_VALIDATE_INT);
$stmt = $conn->prepare('SELECT tp.content 
FROM temp_purchase AS tp 
WHERE tp.tpurchase_id = ? 
AND tp.content > ""'); //ensure content is not empty
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<?php
if ($stmt && false !== $tpurchase_id) {
    $stmt->bind_param('i', $tpurchase_id);
    $stmt->execute();
    $stmt->bind_result($content);
    if ($stmt->fetch()) { ?>
        <object data="data:application/pdf;base64,<?php echo base64_encode($content); ?>" type="application/pdf" style="height:200px;width:60%"></object>
    <?php }
    $stmt->close();
    unset($content, $stmt);
}
$conn->close();
?>
</body>
</html>

为了检索多个文档,您可以选择更改 if ($stmt-&gt;fetch()) 以支持使用 while($stmt-&gt;fetch())


上传处理器建议

假设您使用问题"Auto submit is not posting data to database" 中的代码进行文件上传,我强烈建议您使用当前标准/最佳实践重写上传处理器,这也将使您的上传处理器与此答案兼容。

使用addslashes 或其他转义技术可能会导致数据库中存储的LOB 数据出现问题。我猜这就是你现在遇到的并发症的原因。

您还应该考虑 PHP 和数据库环境使用的最大数据包大小,它限制了您的应用程序可以发送或接收的数据大小,这可能导致LOB 数据被截断。由于数据包大小的限制,建议您使用send_long_data 以防止您的应用程序在传输LOB 数据时出现问题。

上传-form.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<form id="target" method="post" enctype="multipart/form-data" name="frmImage" class="frmImageUpload" action="./post.php">
   <input type="file" name="userfile" id="userfile" class="userfile"/>
</form>
</body>
</html>

post.php

<?php
$conn = new mysqli('localhost','user','','db');
if (mysqli_connect_errno()) {
    exit;
}
if (!session_id()) {
    session_start();
}
//never trust data from GLOBALS
$user_id = filter_var($_SESSION['user_id'], FILTER_VALIDATE_INT);
if (false === $user_id ||
    !isset($_FILES) ||
    !array_key_exists('userfile', $_FILES) ||
    UPLOAD_ERR_OK !== $_FILES['userfile']['error'] ||
    $_FILES['userfile']['size'] <= 0 ||
    !is_uploaded_file($_FILES['userfile']['tmp_name'])
) {
    //invalid user or file upload
    exit;
}
//params = { 0: user_id, 1: content }
$stmt = $conn->prepare('INSERT INTO temp (user_id, content) VALUES (?, ?)');
if ($stmt) {
    //bind default value as NULL
    $null = null;
    $stmt->bind_param('ib', $user_id, $null);
    //max packet size limits can lead to partial file data being inserted
    $fp = new SplFileObject($_FILES['userfile']['tmp_name'], 'rb', false);
    while (!$fp->eof()) {
        //use send_long_data to send the file data in chunks
        //be sure the first argument matches the param index for the LOB data column
        $stmt->send_long_data(1, $fp->fread(2048));
    }
    unset($fp);
    $stmt->execute();
    $stmt->close();
}
$conn->close();

作为个人推荐;多年来,我发现在数据库中存储LOB 数据会导致一些严重的问题。虽然它确实增加了应用程序内文件管理的可移植性和易用性。通过显着增加恢复数据库和硬盘驱动器 RAID 完整性所需的 I/O 时间量,它极大地阻碍了数据恢复和备份。此外,当与其他数据一起使用时,会显着增加数据库的查询和维护时间。迫使我们从SELECT * 迁移以显式避免LOB 列数据或跳过表进行优化或重新索引。最后,它还阻止了客户端缓存,而无需创建特定的 RESTful URL 来提供文件。总的来说,它变得比存储LOB 数据的努力要麻烦得多。我建议使用您的 Web 服务器来存储物理文件,并使用数据库来存储物理文件的相对路径,其中 PHP 管理物理文件的绝对路径以供查看/操作。例如,在创建可以从可公开访问的位置缓存和提供的缩略图时。

【讨论】:

    【解决方案3】:

    如果您想使用浏览器 PDF 查看器,请注意您一次只能查看一个 PDF。

    您的代码如下所示:

    <?php
      header("Content-type:application/pdf");
      $tpurchase_id = $_GET['tpurchase_id'];
      $conn = new mysqli("localhost","user","","db");
      $sql = "SELECT * FROM temp_purchase WHERE tpurchase_id= '$tpurchase_id'";
    
      $result = $conn->query($sql);
      if ($result->num_rows > 0) {
        while($row = $result->fetch_assoc())
          ob_clean();
          flush();
          echo $row['content'];
          $conn->close();
          exit();
        }
      }
    ?>
    

    【讨论】:

    • 我在浏览器中看到字符。与我在记事本中打开 PDF 文件时看到的字符相同。
    • @John 这意味着您的标题未设置。尝试将 header("Content-type:application/pdf"); 移动到文件顶部
    • 我再也看不到这些字符了。 PDF 以全屏方式打开并显示错误消息:“错误:加载 PDF 文档失败。”
    • @John 看起来你越来越近了...你在 echo $row['content']; 之前添加了 ob_clean(); flush();。还要检查 blob 是否是 PDF 类型并删除您可能拥有的任何其他标题
    【解决方案4】:

    这里有一个解决方案:它获取编码 LONGBLOB并解码它,然后在设置标题后显示它。它基于 fyrye 的回答。

    <?php
        $tpurchase_id = $_GET['tpurchase_id'];
        $connection = new mysqli("localhost","user","","db");
        $sql = "SELECT content FROM temp_purchase WHERE tpurchase_id = ?";
    
        $statement = $connection->prepare($sql);
        $statement->bind_param("s", $tpurchase_id);
        $statement->execute();
        $statement->bind_result($pdf_encoded);
    
        if($statement->fetch()){
            $pdf_decoded = base64_decode($pdf_encoded);
        }
        $statement->close();
    
        ob_start();
        ob_clean(); //Clear the buffer
        header('content-type: application/pdf');
        echo($pdf_decoded);
        ob_end_flush(); //Output only the buffered content to the client
    
    ?>
    

    【讨论】:

    • 一般你需要显式调用ob_start();,因为output buffering在PHP中默认是禁用的。
    • 你是对的。将它放在顶部还是在ob_clean(); 上方是否重要?
    • 这个概念是缓冲 ob_clean() 之前的所有内容,这样 ob_clean 会擦除缓冲区,并且在它之前没有发送任何内容。如果不在第一行调用ob_startob_start 之前的任何警告、回显、错误都将发送到客户端。根据我在回答中的解释,您还应该删除结束 PHP 标记并改用 exit。
    • 当我写 exit 而不是结束标签 ?&gt; 它不会关闭 PHP,并且在那之后我也有 HTML。你能解释一下吗?
    • 为什么在发送application/pdf 的标头内容类型后会有HTML?正如我在回答中解释的那样,除非您从 PHP 转换到另一种纯/文本或标记语言,否则您不需要结束标记。 exit 防止额外的文本被解析或发送到客户端,例如结束标记后的换行符,这往往会导致不希望的行为。如果希望包含该脚本,只需省略退出标记和结束标记,例如使用数据库连接脚本。需要解析打开/关闭标签的词法分析器的开销也更少
    猜你喜欢
    • 1970-01-01
    • 2014-02-24
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 1970-01-01
    • 2019-11-13
    • 1970-01-01
    • 2011-09-28
    相关资源
    最近更新 更多