0

I have tried making a few posts about this problem, but have decided to collect everything in this final one to hopefully somehow solve it.

I am building a site where users can vote on questions from a database. There's no login and so, to make sure everyone can only vote once per question, I am using their IP together with the ID of the question.

First, I get the ID and IP address and store both, making sure they are integers:

if(isset($_GET['id']))
          {

           //Get IP address

           //Test if it is a shared client
           if (!empty($_SERVER['HTTP_CLIENT_IP'])){
            $ip=$_SERVER['HTTP_CLIENT_IP'];

           //Is it a proxy address
           }elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
            $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
           }else{
            $ip=$_SERVER['REMOTE_ADDR'];
           }

           //Save id and IP address as variables
           $id = $_GET['id'];
           $ip_long = ip2long($ip);

I then check to see if the user has already votes, using the two variables. This is where I expect the problem arises. I get a:

Notice: Trying to get property of non-object

from line 116 which is: $row_cnt = $result->num_rows.

Furthermore var_dump ($result) returns bool(false) and var_dump ($row_cnt) returns Null. Adding quotes around the two variables in the query, $ip_long and $id fixes the problem while localhost, but not on my server.

A local var_dump($result) with quotes around the variables returns the following:

object(mysqli_result)#2 (5) { ["current_field"]=> int(0) ["field_count"]=> int(1) ["lengths"]=> NULL ["num_rows"]=> int(1) ["type"]=> int(0) }

I would like to add 1 to the QuestionVotes for the specific question and then remove the option to vote on that same question for the specific IP Address.

//Save id and IP address as variables
           $id = $_GET['id'];
           $ip_long = ip2long($ip);

           ///Check to see if user already voted
           $stmt = $conn->prepare("SELECT * FROM User_Votes where UserID = ? and QuestionID = ?");
           mysqli_stmt_bind_param($stmt, 'ss', $ip_long, $id);
           $stmt->execute();
           $result = $stmt->get_result();
            if($result->num_rows){
                //The user has already voted
                echo "Already voted";
            }else{
                //Add IP Address and ID to the User_Votes table
                $stmt = $conn->prepare("INSERT INTO User_Votes (UserID, QuestionID) VALUES (?, ?)");
                mysqli_stmt_bind_param($stmt, 'ss', $ip_long, $id);
                $stmt->execute();
                $stmt = $conn->prepare("UPDATE Question SET QuestionVotes = QuestionVotes + 1 where QuestionID = ?");
                mysqli_stmt_bind_param($stmt, 's', $id);
                $stmt->execute();
            }

       }

And lastly, here is the code I use to build the html boxes containing database question information, add a voting button that displays the current votes and append, what is used as QuestionID, to the url:

// Build 4 question boxes from database Question table, including voting button
      $stmt = $conn->prepare("SELECT * FROM question ORDER BY QuestionVotes DESC LIMIT 4");
      $stmt->execute();

      $result = $stmt->get_result();
      if ($result->num_rows > 0) {
           // output data of each row
           while($row = $result->fetch_assoc()) {
               //$row["QuestionID"] to add id to url
               echo "<div class=\"col-md-3\"><h2>". $row["QuestionHeader"]. "</h2><p>". $row["QuestionText"]. "</p><p><a href=\"index.php?id=". $row["QuestionID"]. "\" class=\"btn btn-success\"> " . $row["QuestionVotes"] . "</a></p></div>";

           }
      }
      else
      {
        echo "0 results";
      }

My tables are as follows:

Question: QuestionID(int11)(pk), QuestionHeader(varchar(20)), QuestionText(text), QuestionVotes(int(5))
User_Votes: UserID(unsigned, int(39)), QuestionID(int(11))

halfer
  • 19,824
  • 17
  • 99
  • 186
Nenn
  • 477
  • 1
  • 4
  • 21
  • **SQL injection alert** Never use un-sanitized data coming from the browser (as in `$id = $_GET['id'];` followed by `$conn->query("SELECT ... QuestionID = $id");`). If I send `1; DELETE * FROM question` as value for `id`, you won't be happy. – jcaron Dec 09 '15 at 16:38
  • Your query is returning `false`, which means an error. Check the value of `$result` before using it. If `false`, log the error (`$conn->error`). – jcaron Dec 09 '15 at 16:47
  • Ah, yes, I know that my code is vulnerable to SQL injection, but will that not be fixed by using prepared statements? And var_dump on $result returns 'bool(false)'. I am very new to this however, so I am not quite sure where you want me to use '($conn->error)'? Could you direct me? Also, thanks a lot for your comments, this problem is making me quite down in the dumps. – Nenn Dec 09 '15 at 19:56
  • Log it, so you'll know why `$result` is false. And I already know `$result` was false, what I was telling you to do is to check it in your code, and only use `$result` if it's not false. – jcaron Dec 09 '15 at 20:23

3 Answers3

2

There are couple of things I would like to point out. First, your error:

I get a 'Notice: Trying to get property of non-object' from line 116 which is: $row_cnt = $result->num_rows;.

When you call mysqli->query() with a select query that finds no results then returned object is not an object but instead false.

Second, instead of COUNT(*), just use *.

So to maintain your logic, you should do something like this:

//Check to see if user already voted
$result = $conn->query("SELECT * FROM User_Votes where UserID = '$ip_long' and QuestionID = '$id'");

if ($result === false) { 
    //Add IP Address and ID to the User_Votes table
    $result = $conn->query("INSERT INTO `User_Votes` (`UserID`, `QuestionID`) VALUES ('$ip_long', '$id')");
}elseif($result && $result->num_rows) { 
    //The user has already voted
    echo "Already voted";
}

Edited:

//Check to see if user already voted
$result = $conn->query("SELECT * FROM User_Votes where UserID = '$ip_long' and QuestionID = '$id'");

if($result->num_rows){
    //The user has already voted
    echo "Already voted";
}else{
    //Add IP Address and ID to the User_Votes table
    $result = $conn->query("INSERT INTO User_Votes (UserID, QuestionID) VALUES ('$ip_long', '$id')");
}

Re-edited:

You have to call $stmt->store_result() after $stmt->execute(). And your $stmt->get_result() is unnecessary here because you're not using the selected data.

Part of a comment from the documentation:

If you do not use mysqli_stmt_store_result( ), and immediatley call this function after executing a prepared statement, this function will usually return 0 as it has no way to know how many rows are in the result set as the result set is not saved in memory yet.

So your code should be like this:

if(isset($_GET['id']) && !empty($_GET['id'])){
    $id = $_GET['id'];
    $ip_long = ip2long($ip);

    //Check to see if user already voted
    $stmt = $conn->prepare("SELECT * FROM User_Votes where UserID = ? and QuestionID = ?");
    $stmt->bind_param('ss', $ip_long, $id);
    $stmt->execute();
    $stmt->store_result();
    if($stmt->num_rows){
        //The user has already voted
        echo "Already voted";
    }else{
        //Add IP Address and ID to the User_Votes table
        $stmt = $conn->prepare("INSERT INTO User_Votes (UserID, QuestionID) VALUES (?, ?)");
        $stmt->bind_param('ss', $ip_long, $id);
        $stmt->execute();
        $stmt = $conn->prepare("UPDATE Question SET QuestionVotes = QuestionVotes + 1 where QuestionID = ?");
        $stmt->bind_param('s', $id);
        $stmt->execute();
    }
}

Sidenote: Please don't mix the procedural and object oriented style of mysqli.

Rajdeep Paul
  • 16,887
  • 3
  • 18
  • 37
  • Thank you for your answer, however, the problem is that no matter what, without the quotes around $ip_long and $ip, the query returns false every time and never true. So your change might be the correct one to follow, but there's still a problem, somehow, with the query or such connected. – Nenn Dec 09 '15 at 19:40
  • @user2304993 I've updated the queries. Try with quoted `$id` and `$ip_long`. – Rajdeep Paul Dec 09 '15 at 19:43
  • Hmm, I have just tried that and I now get 'Already voted' no matter what I do, even after changing the if to === true, cause the query returns neither false or true. – Nenn Dec 09 '15 at 19:48
  • @user2304993 The problem is definitely in and around `if` clause. – Rajdeep Paul Dec 09 '15 at 19:54
  • I think you're correct. I very much think the query is not returning what I am asking it to and I cannot seem to fix it to properly check if someone has already voted. – Nenn Dec 09 '15 at 19:58
  • @user2304993 First check if your connection is established or not. Use `$conn->connect_errno` for that. [Here's the documentation](http://php.net/manual/en/mysqli.query.php) – Rajdeep Paul Dec 09 '15 at 20:01
  • I have done that now and it seems there's no connection problems or errors. – Nenn Dec 09 '15 at 20:04
  • @user2304993 I've updated my answer. Please see the **edited** section of my answer. Now it should work fine. – Rajdeep Paul Dec 09 '15 at 20:12
  • Wow, you sound so sure and you're so right, that did fix the problem locally, I sadly cannot test server before tomorrow. But tell me, what exactly was the fix? I am not sure I understand. Also, thank you so very much for your helping answers! – Nenn Dec 09 '15 at 20:24
  • @user2304993 Actually my initial assumption was wrong. Whether it finds a row or not, it always returns an object. I just checked whether that object's property `$result->num_rows` contains any value or not. If the query doesn't any row then `$result->num_rows` will be 0, otherwise it will contain the number of rows returned. And ya, if it works on your remote server then please mark it as *accepted*. :) – Rajdeep Paul Dec 09 '15 at 20:33
  • Ah, I'm afraid we're at it again. I'm finally able to test and it does not work. I have updated my script with prepared statements and shown it in my question so you can see. When I click the 'vote' button, the whole database part disappears, how can this be? – Nenn Dec 10 '15 at 11:47
  • @user2304993 I've updated my answer. Please see the **re-edited** section of my answer. – Rajdeep Paul Dec 10 '15 at 12:40
  • Thank you very much for your edit and all your time. I am learning a lot from this and it's a pleasure. When using this code localhost, it does work, but as soon as I upload and check the code online, clicking one of the buttons make everything from the database go away. Here is the link, if it helps: http://linettemmd.dk/skodfri/ Also, no laughing, the animations and everything is still a work in progress, heh. – Nenn Dec 10 '15 at 12:50
  • @user2304993 Please explain *clicking one of the buttons make everything from the database go away*. What error are you getting? – Rajdeep Paul Dec 10 '15 at 12:54
  • None, that's what's confusing me. I'm sorry for my vague explanation. If you tru the link and click one of the query generated green buttons with numbers in, that part of the website disappears, but without giving me any errors. – Nenn Dec 10 '15 at 12:57
  • @user2304993 Make sure your database credentials are correct on your server. Check your server's error log. – Rajdeep Paul Dec 10 '15 at 13:00
  • It connects alright and I cannot find any error log, but I have checked the tables and they come back fine. Can it be because my server is PHP version 5.6 maybe? – Nenn Dec 10 '15 at 13:11
  • @user2304993 Could you include the following code in the beginning of your scripts: `error_reporting(E_ALL|E_STRICT); ini_set('display_errors', true);` – Rajdeep Paul Dec 10 '15 at 13:17
  • I have done that, wonderful little code! Here is what it returns: 'Fatal error: Call to a member function bind_param() on boolean in /customers/6/0/0/linettemmd.dk/httpd.www/skodfri/index.php on line 108' about the following code: '$stmt = $conn->prepare("SELECT * FROM User_Votes where UserID = ? and QuestionID = ?"); $stmt->bind_param('ss', $ip_long, $id);' – Nenn Dec 10 '15 at 13:19
  • @user2304993 Your query is failing to `prepare()`, and that's why it's returning false. Make sure everything is correct, table name, column names etc. – Rajdeep Paul Dec 10 '15 at 13:27
  • @user2304993 It's a good thing wrap your code inside `if` block. I've updated my answer. Please see the **re-edited** section of my answer. – Rajdeep Paul Dec 10 '15 at 13:35
  • The code looks much more structured. But it is still, sadly returning the same fatal error, for some reason a boolean is returned instead of a string. Is the query faulty for some reason? – Nenn Dec 10 '15 at 13:53
  • Ah, it works now! I was a fool, you were completely correct long ago, my names in the database were converted to lowercase, changing it did the work! Tho, my 'if($stmt->num_rows){' does not work now and doesn't check for a row it seems. – Nenn Dec 10 '15 at 14:07
  • @user2304993 So what is the issue you're facing now? – Rajdeep Paul Dec 10 '15 at 14:09
  • My new and much smaller problem seems to be that 'if($stmt->num_rows)' no matter what, returns '0', instead of '1' when the user is already in the database? – Nenn Dec 10 '15 at 14:20
  • @user2304993 Did you use `$stmt->store_result();` like I suggested in my **re-edited** section? – Rajdeep Paul Dec 10 '15 at 14:23
  • Yes, I did, the code is as follows: '$stmt->store_result(); if($stmt->num_rows){ //The user has already voted echo "Already voted"; echo ($stmt->num_rows); }else{' And returns a 0 everytime. – Nenn Dec 10 '15 at 14:28
  • Ah, it finally works, that's amazing! Thank you so very much for all your help, I cannot thank you enough! Now I just need to make it use kool-swap to Ajax the new results in, well, I'll have fun with that. I'll of course mark you answer as the answer! Again, thank you! – Nenn Dec 10 '15 at 14:34
  • @user2304993 You're very welcome! Cheers to team work. :) – Rajdeep Paul Dec 10 '15 at 14:36
  • It's sweet of you to call it teamwork, but you did most of the work here and without it I don't know what I would have done! But thank you for teaching me so much. Cheers! – Nenn Dec 10 '15 at 14:38
1

You should check the name of your table.

You use this in one of the queries User_Votes and this user_votes in another one. It might work on your development server, if it's powered by Windows, that are case insensitive, but Linux, which probably powers your production server case-sensitive is.

Check this question for more informations: Are table names in MySQL case sensitive?

Also note, that from the code above, your app looks insecure to SQL injection. You should cast the variables to int, or what do you expect them to be.

Community
  • 1
  • 1
Vojtech Kane
  • 559
  • 6
  • 21
  • The information is passed to the database correctly so I am afraid that case sensitivity is not the root of the problem. But I am glad that you directed my attention to this error, I will change it immediately. – Nenn Dec 09 '15 at 19:37
  • How would you know the information is passed to the database correctly? – jcaron Dec 09 '15 at 20:24
0

Your insert statement is using single quotes to enclose your variables. Those should be double quotes so PHP will interpret your variables to be the values instead of the literal string.

This looks to be the root cause of what's going on. Were you able to verify everything was being written to your database tables correctly before pulling them to work on? Then verify your select statement was pulling the data correctly and what the form the data took?

And jcaran's comment is correct ... some verification of the variables you grab will need to be considered.

David Soo
  • 23
  • 4
  • Everything is being sent to the database correctly, yes. So while this is good information, it sadly is not the cause of the problem. – Nenn Dec 09 '15 at 19:36