3

I am trying to show the PDF file that is stored in my LONGBLOB. When I execute the code I only get the name of the file. I want to show the PDF file in a viewer.

Does someone know how I can fix this?

Here is my script:

          <?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();

          ?>
John
  • 904
  • 8
  • 22
  • 56
  • **WARNING**: When using `mysqli` you should be using [parameterized queries](http://php.net/manual/en/mysqli.quickstart.prepared-statements.php) and [`bind_param`](http://php.net/manual/en/mysqli-stmt.bind-param.php) to add any data to your query. **DO NOT** use string interpolation or concatenation to accomplish this because you have created a severe [SQL injection bug](http://bobby-tables.com/). **NEVER** put `$_POST`, `$_GET` or data *of any kind* directly into a query, it can be very harmful if someone seeks to exploit your mistake. – tadman Apr 08 '19 at 22:42
  • Possible duplicate of [Display pdf blob file from database](https://stackoverflow.com/questions/40948761/display-pdf-blob-file-from-database) – Luis Cardoza Bird Apr 08 '19 at 22:46
  • `echo $a=$row['content'];` should not be an echo there –  Apr 08 '19 at 22:51
  • I changed it. When I execute the script I see in my browser that `echo base64_decode($a); ` is returning question marks instead of the decoded variable – John Apr 08 '19 at 23:04
  • @LuisCardozaBird `base64_encode($a); ` returns `Error: Failed to load PDF document.` – John Apr 08 '19 at 23:09
  • Step 1: Stop lying to the browser. `header("Content-type:application/pdf");` followed by _HTML code_ (instead of actual, binary PDF data) makes no sense. – 04FS Apr 09 '19 at 07:03
  • I have deleted `header("Content-type:application/pdf");`. I am not lying anymore to the browser :) – John Apr 13 '19 at 20:05
  • 1
    What step encodes into base64? – Rick James Apr 14 '19 at 16:16
  • @RickJames what do you mean? – John Apr 14 '19 at 17:11
  • 2
    @John - Show us the code that `INSERTed` the `content`. – Rick James Apr 14 '19 at 19:39
  • Do you want to show multiple PDF files on a single page? – Paul Spiegel Apr 15 '19 at 08:53
  • 1
    @John you should flow problem with you script is you are not concatenating the string properly. You should use fyrye ' s solution it looks good to me. – Rahul Singh Apr 19 '19 at 09:57
  • @John if you don't mind can you use http://www.tcpdf.org for making PDF, TCPDF has many options, you should try once. – Vasant Hun Apr 20 '19 at 07:35

4 Answers4

2

I think this sould work without problem , giving more headers informations

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

print $row['content'];
Yassine CHABLI
  • 3,459
  • 2
  • 23
  • 43
  • When I execute the script I only get the name of the file (test.pdf). I cannot see the content in my browser – John Apr 13 '19 at 20:04
2

Some order of operations improvements and optimizations. Assuming the database is correctly storing the entire contents of the LOB data.

Output buffering using ob_start, ob_clean, and ob_end_flush will provide more control over the desired response content from your script. This will help to alleviate erroneous spaces or emitted warnings from being included in the binary output. Additionally this allows for you to control which header data to send in the response.

There's no need to use while($row = $result->fetch_assoc()) since the response from the database should contain the entirety of the single LONGBLOB data row.

Using mysqli_stmt::bind_result and mysqli_stmt::fetch will reduce some overhead caused by fetching into an associative array, since its only needed to retrieve the content. mysqli_stmt::fetch will return NULL if there are no results/data or false on error.

I also suggest using prepared statements, to prevent SQL injections and filter_var to ensure the user supplied input is of the expected data type and is valid.

Using content-disposition: inline will ask the browser to attempt to load it, if the browser can understand the content-type, otherwise it will download it.

Lastly you do not need to end your code with ?>, which can cause unintended behavior, instead just use exit;. It's best to exclude the closing tag in your PHP script files unless you are transitioning from PHP to plain text or markup in the same file.

I tested the below against my MySQL database table that also uses LONGBLOB to store PDF files and is functioning as desired.

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

This will result in only the headers and content response being sent to the client, otherwise if a result from the database is not found, a blank plain/text response is sent.

The above script can then be used as the source for your inline object.

<!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>

Aside from the above, there other points that can cause issues, which we are currently not aware of.

  • Headers added or modified by the webserver (apache, nginx, IIS, etc).
  • Upload form or PHP processing script modifying or not sending complete LOB data to the database.
  • Database truncating or altering the LOB data.

To use the above PHP script for displaying inline object(s). There's no need for output buffering. However you would need to swap out base64_decode in favor of using base64_encode. Decode takes a base64 encoded string and converts it to the original format. Where you actually want to take the binary data from the database and convert it to a base64 encoded string for the browser to later decode. If the file content has already been base64_encode'd by the upload processing script, neither base64_encode or base64_decode is needed.

Tested the below and is functioning as desired.

<?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>

For the purposes of retrieving multiple documents, you can optionally change if ($stmt->fetch()) in favor of using while($stmt->fetch())


Upload processor Suggestion

Assuming you're using the code in your question "Auto submit is not posting data to database" for the file uploading, I highly recommend that you rewrite the upload processor using current standards/best-practices, which will also make your upload processor compatible with this answer.

Using addslashes or other escaping techniques can cause issues with the resulting LOB data stored in the database. Which I surmise is the cause for the complications you are experiencing now.

You should also take into consideration the max packet size that is used by your PHP and database environment, that limits the data size that can be sent or received by your application, which can lead to truncated LOB data. Because of the packet size limits, it is recommended that you use send_long_data to prevent issues with your application transmitting the LOB data.

upload-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();

As a personal recommendation; over the years I have come to discover that storing LOB data within the database has caused some serious issues. While it does increase the portability and ease of file management within applications. It has greatly hindered data recovery and backup, by significantly increasing the amount of I/O time needed to recover the database and hard drive RAID integrity. Also when used with other data, significantly increases query and maintenance times with the database. Forcing us to migrate from SELECT * to explicitly avoid the LOB column data or skip the tables for optimization or re-indexing. Lastly it also prevented client-side caching without creating specific RESTful URLs to serve the files. Overall it became much more trouble than was worth the effort to store the LOB data. I suggest using your web server to store the physical file(s), and use the database to store the relative path to the physical file, where PHP manages the absolute paths to the physical file(s) for viewing/manipulation. For example when creating thumbnails that can be cached and served from a publicly accessible location.

Will B.
  • 17,883
  • 4
  • 67
  • 69
0

If you want to use the browser PDF viewer, note you can only view one PDF at a time.

Your code would look like:

<?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();
    }
  }
?>
Ikhlak S.
  • 8,578
  • 10
  • 57
  • 77
  • I see characters in my browser. The same characters as I see when I open the PDF file in Notepad. – John Apr 13 '19 at 21:56
  • @John That means your header is not set. Trying moving `header("Content-type:application/pdf");` to the top of the file – Ikhlak S. Apr 13 '19 at 23:18
  • I dont get the characters anymore. The PDF opens in a full screen and shows a error message: "Error: Failed to load PDF document." – John Apr 14 '19 at 17:11
  • @John looks like you getting closer... Did you add `ob_clean(); flush();` before `echo $row['content'];`. Also check if the blob is type of PDF and remove any other headers you might have – Ikhlak S. Apr 14 '19 at 20:27
0

Here is a solution: It gets the encoded LONGBLOB and decodes it, then displays it after setting a header. Its based on the answer from 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

?>
yuko
  • 325
  • 1
  • 8
  • Generally you need to explicitly call `ob_start();` as [output buffering](https://www.php.net/manual/en/outcontrol.configuration.php) is disabled by default in PHP. – Will B. Apr 20 '19 at 20:15
  • You're right. Does it matter if you put it at the top or just above `ob_clean();`? – yuko Apr 20 '19 at 21:12
  • The concept is to buffer everything from before the `ob_clean()` so that way ob_clean erases the buffer, and nothing is sent prior to it. Without calling `ob_start` on the first line, any warnings, echo, errors prior to `ob_start` will be sent to the client. You should also remove the closing PHP tag and use exit instead, as per my explanation in my answer. – Will B. Apr 21 '19 at 01:56
  • When I write `exit` instead of closing tag `?>` it doesn't close out the PHP, and I have HTML after that point as well. Can you explain it? – yuko Apr 21 '19 at 06:44
  • why would you have HTML after sending a header content-type of `application/pdf`? As i explained in my answer, unless you're transitioning from PHP to another plain/text or markup language, you don't need the closing tag. `exit` prevents additional text from being parsed or sent to the client, such as a newline after the closing tag, which has a tendency to cause undesired behavior. If the script is expected to be included, simply leave out both the exit and closing tag, such as with database connection scripts. It's also less overhead on the lexer for needing to parse open/close tags – Will B. Apr 21 '19 at 16:40