xref: /web-master/entry/user-notes-vote.php (revision 9cfe978d)
1<?php
2/*
3  This script acts as the backend communication API for the user notes vote feature.
4  Requests come in here from the php.net to update the database with new votes.
5  master.php.net should respond with a JSON object (with one required property [status] and two optional properties
6  [votes] and [message]).
7  The JSON object [status] property contains a status returned by the server for that request.
8  It's value may be either a boolean true or false. If the status is true the php.net will know the vote went through successfully.
9  The optional [votes] property may then be supplied to update the php.net with the new value of the votes for that note.
10  If the status is false the php.net will know the request for voting failed and an optional [message] property may be
11  set to supply the php.net with a message string, explaining why the request failed.
12
13  Example Success:
14
15                   { "status": true, "votes": 1 }
16
17  Example Failed:
18
19                   { "status": false, "message": "You have already voted today!" }
20                   { "status": false, "message": "Invalid request..." }
21*/
22
23// Validate that the request to vote on a user note is OK (ip limits, post variables, and db info must pass validation)
24function vote_validate_request(PDO $dbh) {
25  // Initialize local variables
26  $ip = $hostip = $id = $vote = 0;
27  $ts = date("Y-m-d H:i:s");
28
29  // Validate POST variables
30  if (isset($_POST['ip']) &&
31      filter_var($_POST['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE  |
32                                                   FILTER_FLAG_NO_PRIV_RANGE |
33                                                   FILTER_FLAG_IPV4))
34  {
35      $ip = sprintf("%u", ip2long($_POST['ip']));
36  } else {
37      // If the IP can't be validated use a non routable IP for loose validation (i.e. IPv6 and clients that couldn't send back proper IPs)
38      $ip = 0;
39  }
40
41  if (isset($_SERVER['REMOTE_ADDR']) &&
42      filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE  |
43                                                              FILTER_FLAG_NO_PRIV_RANGE |
44                                                              FILTER_FLAG_IPV4))
45  {
46      $hostip = sprintf("%u", ip2long($_SERVER['REMOTE_ADDR']));
47  } else {
48      // If the IP can't be validated use a non routable IP for loose validation (i.e. IPv6 and clients that couldn't send back proper IPs)
49      $hostip = 0;
50
51  }
52
53  if (!empty($_POST['noteid']) && filter_var($_POST['noteid'], FILTER_VALIDATE_INT))
54  {
55      $id = filter_var($_POST['noteid'], FILTER_VALIDATE_INT);
56  } else {
57      return false;
58  }
59
60  if (!empty($_POST['vote']) && ($_POST['vote'] === 'up' || $_POST['vote'] === 'down'))
61  {
62      $vote = $_POST['vote'] === 'up' ? 1 : 0;
63  }
64
65  if (empty($_POST['sect'])) {
66      return false;
67  }
68
69
70  // Validate the note exists and is in the requested section
71  $noteStmt = $dbh->prepare("SELECT COUNT(*) AS num, sect FROM note WHERE id = :id");
72  if (!$noteStmt) {
73      return false;
74  }
75  if (!$noteStmt->execute(['id' => $id])) {
76      return false;
77  }
78  if (false === $noteResult = $noteStmt->fetch(PDO::FETCH_ASSOC)) {
79      return false;
80  }
81  if ($noteResult['sect'] !== $_POST['sect']) {
82      return false;
83  }
84
85  // Validate remote IP has not exceeded voting limits
86  $remoteStmt = $dbh->prepare("SELECT COUNT(*) AS num FROM votes WHERE ip = :ip AND ts >= (NOW() - INTERVAL 1 DAY) AND note_id = :id");
87  if (!$remoteStmt) {
88      return false;
89  }
90  if (!$remoteStmt->execute(['ip' => $ip, 'id' => $id])) {
91      return false;
92  }
93  if (false === $remoteResult = $remoteStmt->fetch(PDO::FETCH_ASSOC)) {
94      return false;
95  }
96  if ($remoteResult['num'] >= 1) { // Limit of 1 vote, per note, per remote IP, per day.
97      return false;
98  }
99
100  // Validate host IP has not exceeded voting limits
101  $hostStmt = $dbh->prepare("SELECT COUNT(*) AS num FROM votes WHERE hostip = :ip AND ts >= (NOW() - INTERVAL 1 HOUR) AND note_id = :id");
102  if (!$hostStmt) {
103      return false;
104  }
105  if (!$hostStmt->execute(['ip' => $ip, 'id' => $id])) {
106      return false;
107  }
108  if (false === $hostResult = $hostStmt->fetch(PDO::FETCH_ASSOC)) {
109      return false;
110  }
111  if ($hostResult['num'] >= 100) { // Limit of 100 votes, per note, per host IP, per hour.
112      return false;
113  }
114
115  // Inser the new vote
116  $voteStmt = $dbh->prepare("INSERT INTO votes(note_id,ip,hostip,ts,vote) VALUES(:id,:ip,:host,:ts,:vote)");
117  if (!$voteStmt) {
118      return false;
119  }
120  if (!$voteStmt->execute(['id' => $id, 'ip' => $ip, 'host' => $hostip, 'ts' => $ts, 'vote' => $vote])) {
121      return false;
122  }
123
124
125  // Get latest vote tallies for this note
126  $voteStmt = $dbh->prepare("SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down FROM votes WHERE votes.note_id = :id");
127  if (!$voteStmt) {
128      return false;
129  }
130  if (!$voteStmt->execute(['id' => $id])) {
131      return false;
132  }
133  if (false === $voteResult = $voteStmt->fetch(PDO::FETCH_ASSOC)) {
134      return false;
135  }
136  // Return the new vote tally for this note
137  return $voteResult['up'] - $voteResult['down'];
138}
139
140// Initialize global JSON response object
141$jsonResponse = new stdclass;
142$jsonResponse->status = false;
143
144
145// Validate the request
146if (!isset($_SERVER['REQUEST_METHOD']) || strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') {
147    $jsonResponse->message = "Invalid request...";
148    echo json_encode($jsonResponse);
149    exit;
150}
151
152// Initialize global PDO database handle
153try {
154    $dbh = new PDO('mysql:host=localhost;dbname=phpmasterdb', 'nobody', '');
155} catch(PDOException $e) {
156    $jsonResponse->message = "The server could not complete this request. Please try again later...";
157    echo json_encode($jsonResponse);
158    exit;
159}
160
161// Check master DB for hostip and clientip limits and other validations
162if (($jsonResponse->votes = vote_validate_request($dbh)) === false) {
163    $jsonResponse->message = "Unable to complete your request at this time. Please try again later...";
164    echo json_encode($jsonResponse);
165    exit;
166}
167
168// If everything passes the response should be the new jsonResponse object with updated votes and success status
169$jsonResponse->status = true;
170echo json_encode($jsonResponse);
171