Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP, PDO and SQLSRV executes multiple times on one INSERT statement

I've been using PDO and PHP for a while with MySQL and Apache servers. I've recently been tasked with converting a legacy web application for a business to a new setup. The old setup is a standard Linux web stack (Apache/PHP/MySQL/Filezilla) and the new setup will be a Windows server 2003 with IIS/PHP(fast cgi install)/ SQL Server 2003/No FTP.

I have nearly everything working except for a conversion of a MySQL statement to update a table with file access information. Using PDO with SQLSRV driver and executing an insert statement inside a 'file download' PHP script, inserts multiple records into the SQL table.

The download.php DOES issues multiple queries to the SQL server. Once to check for the file existence and variables in one table, and THEN updates another table with the access information.

SEE BELOW FOR download.php CODE

Debugging shows the print/echo of $count as 1. Checking the SQL server records however, always shows MORE than one inserted. Sometimes it is just one extra row for a total of two, but other times it is as high as four EXTRA statements being inserted. $count remains shown as 1, in every case.

This particular PHP script verifies information against the SQL database prior to calling this insert statement. First, the verification of authentication for the file access(succeeds), verifies the file existence(succeeds), then updates the access table with information for the download (ERROR) and finally serves the PDF to the user (succeeds).

When I issue the INSERT statement manually into Query Analyzer, it succeeds and works as expected; it inserts one row every time. The error seems to be with SQLSRV or PDO implementation of the execute().

I have searched on stackoverflow, serverfault and the almighty Google for information regarding this. The only types of results returned where users needing to execute multiple queries/inserts in one statement/execute. Where's my issue is the opposite; I wish to only execute ONE insert statement however, more than one is always executed.

Question is: Why is this happening and how can I prevent the multiple insert from happening?

UPDATE PER REQUEST

The code that accesses this file is one singular link from another web page. The page lists current files the user is allowed to access and presents links to the download.php script for the verification, update and actual serving of the PDF.

View page has a list of links (printed in a for loop) arranged like :

<a href='download.php?f={$item['name']}&t={$type}' target='_blank'>{$item['name']}</a>

When the user clicks on that link, the script below is what runs in addition to the other code above for download.php. It successfully serves the PDF file. The content is being sent by download.php as a PHP header/inline PDF:

SEE BELOW FOR CODE

Looking at the server logs shows two GET request to the download.php file:

2012-02-14 17:44:37 W3SVC1785071458 172.17.31.254 GET /download.php f=06304844-1A.pdf&t=av 4090 - 172.17.31.112 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.7+(KHTML,+like+Gecko)+Chrome/16.0.912.77+Safari/535.7 200 0 0
2012-02-14 17:44:37 W3SVC1785071458 172.17.31.254 GET /download.php f=06304844-1A.pdf&t=av 4090 - 172.17.31.112 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.7+(KHTML,+like+Gecko)+Chrome/16.0.912.77+Safari/535.7 200 0 0

I have tested in Firefox, Opera, and IE (6-9b) and the results are the same.

UPDATE TWO

Putting the entire download.php file here:

<?php
session_start();
require("cgi-bin/auth.php");

// Don't timeout when downloading large files
@ignore_user_abort();  
@set_time_limit(0);  

//error_reporting(E_ALL); 
//ini_set('display_errors',1);

function getfile() {
    require('cgi-bin/connect_db_pdf.php');
    //Verify information 
    if (!isset($_GET) || !isset($_GET['f']) || !isset($_GET['t'])) {
        echo "Nothing to do!";
        exit(0);

    }

    //Update variables
    $vuname = strtolower(trim($_SESSION['uname']));
    $file = trim($_GET['f']); //Filename we're looking for
    $type = trim($_GET['t']);//Filetype

    if (!preg_match('/^[a-zA-Z0-9_\-\.]{1,60}$/', $file) || !preg_match('/^av|ds|cr|dp$/', $type)) {
        echo "Non conforming values";
        exit(0);
    }

    try {

        $sQuery = "SELECT * FROM pdf_info WHERE PDF_name=:file AND PDF_type=:type";

        $statm = $conn->prepare($sQuery);
        $statm->execute(array(':file'=>$file,':type'=>$type));
        $result = $statm->fetch();
        $count = $statm->rowCount();
        $sQuery = null;
        $statm = null;

        if ($count == 1 ){ //File was found in the database so let them download it. Update the time as well

            $sQuery = "INSERT INTO access (PDF_name,PDF_type, PDF_time, PDF_access) VALUES (:file, :type, GetDate(), :vuname)";

            $statm = $conn->prepare($sQuery);
            $statm->execute(array( ':vuname'=>$vuname, ':file'=>$file, ':type'=>$type));
            $count = $statm->rowCount();
            $sQuery = null;
            $statm = null;

            $sQuery = "UPDATE pdf_info SET last_view=GetDate(),viewed_uname=:vuname WHERE PDF_name=:file AND PDF_type=:type";

            $statm = $conn->prepare($sQuery);
            $statm->execute(array( ':vuname'=>$vuname, ':file'=>$file, ':type'=>$type));
            $sQuery = null;
            $statm = null;

            //$result is from FIRST SELECT query outside this 'if' scope.
            $file_loc = $result['floc'];
            $file_name = $result['filename'];

            $fileh = fopen($file_loc,'rb');//Send content to browser as inline PDF
            header("Content-Type: application/pdf"); 
            header("Pragma: no-cache");  
            header("Cache-Control: must-revalidate, post-check=0, pre-check=0");  
            header("Content-Length: " . filesize($file_loc));  
            header("Accept-Ranges: bytes");
            header("Content-Disposition: inline; filename={$file_name}");  

            while (!feof($fileh)) { 
                echo(@fgets($fileh, 8192)); 
            } 

            fclose ($fileh); 
            exit(0);

            } else { //We did not find a file in the database. Redirect the user to the view page.
                header("Location: view.php");
            }

            }   catch(PDOException $err) {//PDO SQL error. 
            //echo $err;
            header('Location: error.php');
            exit(0);
         }
}

getfile();

?>
like image 342
PenguinCoder Avatar asked Feb 14 '12 15:02

PenguinCoder


2 Answers

You comment that the download script is being called twice and should exit if the correct value is not found. This may not be the case because you misuse PDOStatement's rowCount() to test for found files. This method is intended to return the number of affected rows for DELETE, UPDATE, or INSERT statements, but not SELECT.

If the last SQL statement executed by the associated PDOStatement was a SELECT statement, some databases may return the number of rows returned by that statement. However, this behaviour is not guaranteed for all databases and should not be relied on for portable applications. http://php.net/manual/en/pdostatement.rowcount.php

They continue on with:

For most databases, PDOStatement::rowCount() does not return the number of rows affected by a SELECT statement. Instead, use PDO::query() to issue a SELECT COUNT(*) statement with the same predicates as your intended SELECT statement, then use PDOStatement::fetchColumn() to retrieve the number of rows that will be returned.

If you call the download.php script twice -- 1 time with incorrect file values (that happen to pass your regex test) and 1 time with correct values, it is quite possible you are inserting twice even though you are only downloading once.

like image 73
Farray Avatar answered Nov 15 '22 11:11

Farray


If you use Apache's mod_rewrite the page is in effect loaded twice. As an example, use the following code on your index page:

<?php

  session_start();

  if (!isset($_SESSION['id']))
  {
    $_SESSION['id'] = 1;
  }

  else
  {
    $_SESSION['id'] += 1;
  }

  echo $_SESSION['id'];

?>

Without using a redirect the output increments by 1 on each refresh. Now add a .htaccess file with the following:

<IfModule mod_rewrite.c>
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php
</IfModule>

Each refresh increments the page by two. I've tried the same using a single INSERT query and the same happens: with .htaccess, two rows are inserted; without, one row is inserted.

So, if you're using mod_rewrite and it affects download.php then I believe this is the issue.

like image 1
MichaelRushton Avatar answered Nov 15 '22 12:11

MichaelRushton