xref: /web-master/manage/user-notes.php (revision 9cfe978d)
1<?php
2// $Id$
3
4// Force login before action can be taken
5include '../include/login.inc';
6include '../include/email-validation.inc';
7include '../include/note-reasons.inc';
8
9define("NOTES_MAIL", "php-notes@lists.php.net");
10define("PHP_SELF", hsc($_SERVER['PHP_SELF']));
11
12$reject_text =
13'You are receiving this email because your note posted
14to the online PHP manual has been removed by one of the editors.
15
16Read the following paragraphs carefully, because they contain
17pointers to resources better suited for requesting support or
18reporting bugs, none of which are to be included in manual notes
19because there are mechanisms and groups in place to deal with
20those issues.
21
22The user contributed notes are not an appropriate place to
23ask questions, report bugs or suggest new features; please
24use the resources listed on <http://php.net/support>
25for those purposes. This was clearly stated in the page
26you used to submit your note, please carefully re-read
27those instructions before submitting future contributions.
28
29Bug submissions and feature requests should be entered at
30<http://bugs.php.net/>. For documentation errors use the
31bug system, and classify the bug as "Documentation problem".
32Support and ways to find answers to your questions can be found
33at <http://php.net/support>.
34
35Your note has been removed from the online manual.';
36
37db_connect();
38
39$action = (isset($_REQUEST['action']) ? preg_replace('/[^\w\d\s_]/', '', $_REQUEST['action']) : '');
40$id = (isset($_REQUEST['id']) ? intval($_REQUEST['id']) : '');
41
42/*------ BEGIN SEARCH ------*/
43if (!$action) {
44  head("user notes");
45
46  // someting done before ?
47  if ($id) {
48    $str = 'Note #' . $id . ' has been ';
49    switch ($_GET['was']) {
50      case 'delete'    :
51      case 'reject'    :
52        $str .= ($_GET['was'] == 'delete') ? 'deleted' : 'rejected';
53        $str .= ' and removed from the manual';
54        break;
55      case 'edit'        :
56        $str .= ' edited';
57        break;
58      case 'resetall'    :
59        $str .= ' reset to 0 votes.';
60        break;
61      case 'resetup'     :
62        $str .= ' reset to 0 up votes.';
63        break;
64      case 'resetdown'   :
65        $str .= ' reset to 0 down votes.';
66        break;
67      case 'deletevotes' :
68        $str = 'The selected votes have been deleted!'; // INTENTIONALLY missing the concat operator
69    }
70    echo $str . '<br />';
71  }
72
73  if (isset($_REQUEST['keyword']) || isset($_REQUEST["view"])) {
74    // Pagination start
75    $page = isset($_REQUEST["page"]) ? intval($_REQUEST["page"]) : 0;
76    $NextPage = isset($_REQUEST["page"]) ? intval($_REQUEST["page"]) : 0;
77    $type = isset($_REQUEST["type"]) ? intval($_REQUEST["type"]) : 0;
78
79    if($page < 0) { $page = 0; }
80    if($NextPage < 0) { $NextPage = 0; }
81    $limit = $page * 10; $page++;
82    $limitVotes = $NextPage * 25; $NextPage++;
83    $PrevPage = ($NextPage - 2) > -1 ? $NextPage - 2 : 0;
84    // Pagination end
85
86    if(isset($_REQUEST['keyword'])) {
87      $query = new Query('SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, note.*, UNIX_TIMESTAMP(note.ts) AS ts '.
88             'FROM note '.
89             'LEFT JOIN(votes) ON (note.id = votes.note_id) '.
90             'WHERE ');
91      if (is_numeric($_REQUEST['keyword'])) {
92        $search_heading = 'Search results for #' . (int) $_REQUEST['keyword'];
93        $query->add('note.id = ?', [$_REQUEST['keyword']]);
94      } elseif (substr($_REQUEST['keyword'], 0, 5) == 'sect:') {
95        $search_heading = 'Search results for <em>' . hsc($_REQUEST['keyword']) . '</em>';
96        $section = str_replace('*', '%', substr($_REQUEST['keyword'], 5));
97        $query->add("note.sect LIKE ? GROUP BY note.id ORDER BY note.sect, note.ts LIMIT ?int, 10", [$section, $limit]);
98      } else {
99        $search_heading = 'Search results for <em>' . hsc($_REQUEST['keyword']) . '</em>';
100        $query->add(
101          "note.note LIKE ? GROUP BY note.id LIMIT ?int, 10",
102          ['%' . $_REQUEST['keyword'] . '%', $limit]);
103      }
104      $result = db_query_safe($query->get());
105    } else {
106      /* Added new voting information to be included in note from votes table. */
107      /* First notes */
108      if ($type == 1) {
109        $search_heading = 'First notes';
110        $result = db_query_safe("SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, note.*, UNIX_TIMESTAMP(note.ts) AS ts ".
111               "FROM note ".
112               "LEFT JOIN(votes) ON (note.id = votes.note_id) ".
113               "GROUP BY note.id ORDER BY note.id ASC LIMIT ?int, 10", [$limit]);
114      /* Minor notes */
115      } else if ($type == 2) {
116        $search_heading = 'Minor notes';
117        $result = db_query_safe("SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, note.*, UNIX_TIMESTAMP(note.ts) AS ts ".
118               "FROM note ".
119               "LEFT JOIN(votes) ON (note.id = votes.note_id) ".
120               "GROUP BY note.id ORDER BY LENGTH(note.note) ASC LIMIT ?int, 10", [$limit]);
121      /* Top rated notes */
122      } else if ($type == 3) {
123        $search_heading = 'Top rated notes';
124        $result = db_query_safe("SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, ".
125               "ROUND((SUM(votes.vote) / COUNT(votes.vote)) * 100) AS rate, ".
126               "(SUM(votes.vote) - (COUNT(votes.vote) - SUM(votes.vote))) AS arating, ".
127               "note.id, note.sect, note.user, note.note, UNIX_TIMESTAMP(note.ts) AS ts ".
128               "FROM note ".
129               "JOIN(votes) ON (note.id = votes.note_id) ".
130               "GROUP BY note.id ORDER BY arating DESC, up DESC, rate DESC, down DESC LIMIT ?int, 10", [$limit]);
131      /* Bottom rated notes */
132      } else if ($type == 4) {
133        $search_heading = 'Bottom rated notes';
134        $result = db_query_safe("SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, ".
135               "ROUND((SUM(votes.vote) / COUNT(votes.vote)) * 100) AS rate, ".
136               "(SUM(votes.vote) - (COUNT(votes.vote) - SUM(votes.vote))) AS arating, ".
137               "note.id, note.sect, note.user, note.note, UNIX_TIMESTAMP(note.ts) AS ts ".
138               "FROM note ".
139               "JOIN(votes) ON (note.id = votes.note_id) ".
140               "GROUP BY note.id ORDER BY arating ASC, up ASC, rate ASC, down DESC LIMIT ?int, 10", [$limit]);
141      /* Votes table view */
142      } else if ($type == 5) {
143        $search_votes = true; // set this only to change the output between votes table and notes table
144        if (!empty($_GET['votessearch'])) {
145          if (($iprange = wildcard_ip($_GET['votessearch'])) !== false) {
146            $search = html_entity_decode($_GET['votessearch'], ENT_QUOTES, 'UTF-8');
147            $start = $iprange[0];
148            $end = $iprange[1];
149            $resultCount = db_query_safe("SELECT count(votes.id) AS total_votes FROM votes JOIN (note) ON (votes.note_id = note.id) WHERE ".
150                                    "(hostip >= ? AND hostip <= ?) OR (ip >= ? AND ip <= ?)", [$start, $end, $start, $end]);
151            $resultCount = mysql_fetch_assoc($resultCount);
152            $resultCount = $resultCount['total_votes'];
153            $isSearch = '&votessearch=' . hsc($search);
154            $result = db_query_safe(
155              'SELECT votes.id, UNIX_TIMESTAMP(votes.ts) AS ts, votes.vote, votes.note_id, note.sect, votes.hostip, votes.ip '.
156              'FROM votes JOIN(note) ON (votes.note_id = note.id) '.
157              'WHERE (hostip >= ? AND hostip <= ?) OR (ip >= ? AND ip <= ?) '.
158              'ORDER BY votes.id DESC LIMIT ?int, 25',
159              [$start, $end, $start, $end, $limitVotes]);
160
161          } elseif (filter_var(html_entity_decode($_GET['votessearch'], ENT_QUOTES, 'UTF-8'), FILTER_VALIDATE_IP)) {
162            $searchip = (int) ip2long(filter_var(html_entity_decode($_GET['votessearch'], ENT_QUOTES, 'UTF-8'), FILTER_VALIDATE_IP));
163            $resultCount = db_query_safe("SELECT count(votes.id) AS total_votes FROM votes JOIN(note) ON (votes.note_id = note.id) WHERE hostip = ? OR ip = ?", [$searchip, $searchip]);
164            $resultCount = mysql_fetch_assoc($resultCount);
165            $resultCount = $resultCount['total_votes'];
166            $isSearch = '&votessearch=' . hsc(long2ip($searchip));
167            $result = db_query_safe(
168              "SELECT votes.id, UNIX_TIMESTAMP(votes.ts) AS ts, votes.vote, votes.note_id, note.sect, votes.hostip, votes.ip ".
169              "FROM votes JOIN(note) ON (votes.note_id = note.id) ".
170              "WHERE hostip = ? OR ip = ? ".
171              "ORDER BY votes.id DESC LIMIT ?int, 25",
172              [$searchip, $searchip, $limitVotes]);
173          } else {
174            $search = (int) html_entity_decode($_GET['votessearch'], ENT_QUOTES, 'UTF-8');
175            $resultCount = db_query_safe("SELECT count(votes.id) AS total_votes FROM votes JOIN(note) ON (votes.note_id = note.id) WHERE votes.note_id = ?", [$search]);
176            $resultCount = mysql_fetch_assoc($resultCount);
177            $resultCount = $resultCount['total_votes'];
178            $isSearch = '&votessearch=' . hsc($search);
179            $result = db_query_safe(
180              "SELECT votes.id, UNIX_TIMESTAMP(votes.ts) AS ts, votes.vote, votes.note_id, note.sect, votes.hostip, votes.ip ".
181              "FROM votes JOIN(note) ON (votes.note_id = note.id) ".
182              "WHERE votes.note_id = ? ".
183              "ORDER BY votes.id DESC LIMIT ?int, 25",
184              [$search, $limitVotes]);
185          }
186        } else {
187          $isSearch = null;
188          $resultCount = db_query_safe("SELECT COUNT(votes.id) AS total_votes FROM votes JOIN(note) ON (votes.note_id = note.id)");
189          $resultCount = mysql_fetch_assoc($resultCount);
190          $resultCount = $resultCount['total_votes'];
191          $result = db_query_safe(
192            "SELECT votes.id, UNIX_TIMESTAMP(votes.ts) AS ts, votes.vote, votes.note_id, note.sect, votes.hostip, votes.ip ".
193            "FROM votes JOIN(note) ON (votes.note_id = note.id) ".
194            "ORDER BY votes.id DESC LIMIT ?int, 25",
195            [$limitVotes]);
196        }
197      /* IPs with the most votes -- aggregated data */
198      } elseif ($type == 6) {
199        $votes_by_ip = true; // only set this get the table for top IPs with votes
200        $result = db_query_safe(
201          "SELECT DISTINCT(votes.ip), COUNT(votes.ip) as votes, COUNT(DISTINCT(votes.note_id)) as notes, ".
202          "INET_NTOA(votes.ip) AS ip, MIN(UNIX_TIMESTAMP(votes.ts)) AS `from`, MAX(UNIX_TIMESTAMP(votes.ts)) AS `to` ".
203          "FROM votes ".
204          "JOIN (note) ON (votes.note_id = note.id) GROUP BY votes.ip ORDER BY votes DESC LIMIT 100");
205      /* Last notes */
206      } else {
207        $search_heading = 'Last notes';
208        $result = db_query_safe(
209          "SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, note.*, UNIX_TIMESTAMP(note.ts) AS ts ".
210          "FROM note LEFT JOIN(votes) ON (note.id = votes.note_id) ".
211          "GROUP BY note.id ORDER BY note.id DESC LIMIT ?int, 10",
212          [$limit]);
213      }
214    }
215
216    if ($result) {
217      /* This is a special table only used for viewing the most recent votes */
218        $t = (isset($_GET['type']) ? '&type=' . $_GET['type'] : null);
219      if (!empty($search_votes)) {
220        $from = $limitVotes + 1;
221        $to = $NextPage * 25;
222        $to = $to > $resultCount ? $resultCount : $to;
223        if ($resultCount) {
224          echo "<p><strong>Showing $from - $to of $resultCount results.</strong></p>";
225          echo "<form method=\"POST\" action=\"" . PHP_SELF . "?action=deletevotes{$t}\" id=\"votesdeleteform\">".
226               "<table width=\"100%\">".
227               "  <thead>".
228               "    <tr style=\"text-align: center; background-color: #99C; font-size: 18px;\">\n".
229               "      <td  colspan=\"7\" width=\"100%\" style=\"padding: 5px;\"><strong>Most Recent Votes</strong></td>\n".
230               "    </tr>\n".
231               "    <tr style=\"background-color: #99C; 18px;\">\n".
232               "      <td style=\"padding: 5px;\"><input type=\"checkbox\" id=\"votesselectall\" /></td>
233                      <td style=\"padding: 5px;\"><strong>Date</strong></td>
234                      <td style=\"padding: 5px;\"><strong>Vote</strong></td>
235                      <td style=\"padding: 5px;\"><strong>Note ID</strong></td>
236                      <td style=\"padding: 5px;\"><strong>Note Section</strong></td>
237                      <td style=\"padding: 5px;\"><strong>Host IP</strong></td>
238                      <td style=\"padding: 5px;\"><strong>Client IP</strong></td>\n".
239               "    </tr>\n".
240               "  </thead>\n".
241               "  <tbody>\n";
242        } else {
243          echo "<p><strong>No results found...</strong></p>";
244        }
245      }
246      /* This is a special table only used for viewing top IPs by votes */
247      if (!empty($votes_by_ip)) {
248        echo "<form method=\"POST\" action=\"" . PHP_SELF . "?action=deletevotes{$t}\" id=\"votesdeleteform\">".
249             "<table width=\"100%\">".
250             "  <thead>".
251             "    <tr style=\"text-align: center; background-color: #99C; font-size: 18px;\">\n".
252             "      <td  colspan=\"5\" width=\"100%\" style=\"padding: 5px;\"><strong>IPs With Most Votes</strong></td>\n".
253             "    </tr>\n".
254             "    <tr style=\"background-color: #99C; 18px;\">\n".
255             "      <td style=\"padding: 5px;\"><strong>Client IP Address</strong></td>
256                    <td style=\"padding: 5px;\"><strong>Number of Votes</strong></td>
257                    <td style=\"padding: 5px;\"><strong>Number of Notes</strong></td>
258                    <td style=\"padding: 5px;\"><strong>First Vote Cast</strong></td>
259                    <td style=\"padding: 5px;\"><strong>Last Vote Cast</strong></td>\n".
260             "    </tr>\n".
261             "  </thead>\n".
262             "  <tbody>\n";
263      }
264      if (!empty($search_heading)) {
265          echo "<h2>$search_heading</h2>";
266      }
267      while ($row = mysql_fetch_assoc($result)) {
268        /*
269           I had to do this because the JOIN queries will return a single row of NULL values even when no rows match.
270           So the `if (mysql_num_rows($result))` check earlier becomes useless and as such I had to replace it with this.
271        */
272        if (mysql_num_rows($result) == 1 && !array_filter($row)) {
273          echo "<p>No results found...</p>";
274          continue;
275        }
276        $id = isset($row['id']) ? $row['id'] : null;
277        /* This div is only available in cases where the query includes the voting info */
278        if (isset($row['up']) && isset($row['down'])) {
279          $rating = isset($row['arating']) ? $row['arating'] : ($row['up'] - $row['down']);
280          if ($rating < 0) {
281            $rating = "<span style=\"color: red;\">$rating</span>";
282          } elseif ($rating > 0) {
283            $rating = "<span style=\"color: green;\">$rating</span>";
284          } else {
285            $rating = "<span style=\"color: blue;\">$rating</span>";
286          }
287
288          if (isset($row['rate'])) { // not all queries select the rate
289            $percentage = $row['rate'];
290          } else {
291            if ($row['up'] + $row['down']) { // prevents division by zero warning
292              $percentage = round(($row['up'] / ($row['up'] +$row['down'])) * 100);
293            } else {
294              $precentage = 0;
295            }
296          }
297          $percentage = sprintf('%d%%', $percentage);
298
299          echo "<div style=\"float: right; clear: both; border: 1px solid gray; padding: 5px; background-color: lightgray;\">\n".
300               "<div style=\"display: inline-block; float: left; padding: 15px;\"><strong>Up votes</strong>: {$row['up']}</div>\n".
301               "<div style=\"display: inline-block; float: left; padding: 15px;\"><strong>Down votes</strong>: {$row['down']}</div>\n".
302               "<div style=\"display: inline-block; float: left; padding: 15px;\"><strong>Rating</strong>: $rating (<em>$percentage like this</em>)</div>\n".
303               " <div style=\"padding: 15px;\">\n".
304               "  <a href=\"?action=resetall&id={$id}\">Reset all votes</a> |".
305               "  <a href=\"?action=resetup&id={$id}\">Reset up votes</a> |".
306               "  <a href=\"?action=resetdown&id={$id}\">Reset down votes</a> |".
307               "  <a href=\"?votessearch={$id}&view=notes&type=5\">See Votes</a>\n".
308               " </div>\n".
309               "</div>\n";
310        }
311        /* This is a special table only used for viewing the most recent votes */
312        if (!empty($search_votes)) {
313          $row['ts'] = date('Y-m-d H:i:s', $row['ts']);
314          $row['vote'] = '<span style="color: ' . ($row['vote'] ? 'green;">+1' : 'red;">-1') . '</span>';
315          $row['hostip'] = long2ip($row['hostip']);
316          $row['ip'] = long2ip($row['ip']);
317          $notelink = "http://php.net/{$row['sect']}#{$row['note_id']}";
318          $sectlink = "http://php.net/{$row['sect']}";
319          echo "    <tr style=\"background-color: #F0F0F0;\">\n".
320               "      <td style=\"padding: 5px;\"><input type=\"checkbox\" name=\"deletevote[]\" class=\"vdelids\" value=\"{$row['id']}\" /></td>\n".
321               "      <td style=\"padding: 5px;\">{$row['ts']}</td>\n".
322               "      <td style=\"padding: 5px;\">{$row['vote']}</td>\n".
323               "      <td style=\"padding: 5px;\"><a href=\"$notelink\" target=\"_blank\">{$row['note_id']}</a></td>\n".
324               "      <td style=\"padding: 5px;\"><a href=\"$sectlink\" target=\"_blank\">{$row['sect']}</a></td>\n".
325               "      <td style=\"padding: 5px;\">{$row['hostip']}</td>\n".
326               "      <td style=\"padding: 5px;\">{$row['ip']}</td>\n".
327               "    </tr>\n";
328        /* This is a special table only used for viewing top IPs by votes */
329        } elseif(!empty($votes_by_ip)) {
330          $from = date('Y-m-d H:i:s', $row['from']);
331          $to = date('Y-m-d H:i:s', $row['to']);
332          $ip = hsc($row['ip']);
333          echo "    <tr style=\"background-color: #F0F0F0;\">\n".
334               "      <td style=\"padding: 5px;\"><a href=\"?view=votes&type=5&votessearch=$ip\">$ip</a></td>\n".
335               "      <td style=\"padding: 5px;\">{$row['votes']}</td>\n".
336               "      <td style=\"padding: 5px;\">{$row['notes']}</td>\n".
337               "      <td style=\"padding: 5px;\">{$from}</td>\n".
338               "      <td style=\"padding: 5px;\">{$to}</td>\n".
339               "    </tr>\n";
340        /* Everything else in search should fall through here */
341        } else {
342          echo "<p class=\"notepreview\">",clean_note($row['note']),
343               "<br /><span class=\"author\">",date("d-M-Y h:i",$row['ts'])," ",
344          hsc($row['user']),"</span><br />",
345               "Note id: $id<br />\n",
346               "<a href=\"http://php.net/manual/en/{$row['sect']}.php#{$id}\" target=\"_blank\">http://php.net/manual/en/{$row['sect']}.php#{$id}</a><br />\n",
347               "<a href=\"https://master.php.net/note/edit/$id\" target=\"_blank\">Edit Note</a><br />";
348          foreach ($note_del_reasons AS $reason => $text) {
349            echo '<a href="https://master.php.net/note/delete/', $id, '/', urlencode((string)$reason), '" target=\"_blank\">', 'Delete Note: ', hsc($text), "</a><br />\n";
350          }
351          echo "<a href=\"https://master.php.net/note/delete/$id\" target=\"_blank\">Delete Note: other reason</a><br />",
352               "<a href=\"https://master.php.net/note/reject/$id\" target=\"_blank\">Reject Note</a>",
353               "</p>",
354               "<hr />";
355        }
356      }
357      /* This is a special table only used for viewing the most recent votes */
358      if (!empty($search_votes)) {
359        if ($resultCount) {
360          echo "  </tbody>\n".
361               "</table>\n".
362               "<input type=\"submit\" name=\"deletevotes\" value=\"Delete Selected Votes\" />\n".
363               "<input type=\"hidden\" name=\"votessearch\" value=\"".
364               (isset($_GET['votessearch']) ? hsc($_GET['votessearch']) : '').
365               "\" />".
366               "</form>\n";
367        }
368        echo "<form method=\"GET\" action=\"" . PHP_SELF . "\">\n".
369             "  <strong>Search for votes by IP address or Note ID</strong> - (<em>wild card searches are allowed e.g. 127.0.0.*</em>): ".
370             "<input type=\"text\" name=\"votessearch\" value=\"".
371             (isset($_GET['votessearch']) ? hsc($_GET['votessearch']) : '').
372             "\" /> <input type=\"submit\" value=\"Search\" />\n".
373             "<input type=\"hidden\" name=\"view\" value=\"notes\" />\n".
374             "<input type=\"hidden\" name=\"type\" value=\"" . (isset($_GET['type']) ? hsc($_GET['type']) : 5) . "\" />\n".
375             "</form>\n";
376      }
377      /* This is a special table only used for viewing top IPs by votes */
378      if (!empty($votes_by_ip)) {
379        echo "  </tbody>\n".
380             "</table>\n".
381             "<p>This information should only be used to determine if there are any IP addresses with an unusually high ".
382             "number of votes placed in a small timeframe to help detect spam and other potential abuse.</p>\n".
383             "<p>Also note that a <em>0.0.0.0</em> IP address indicates a client IP could not be resolved at the time of voting.</p>";
384      }
385      if((isset($_REQUEST["view"]) || isset($_REQUEST['keyword'])) && empty($search_votes)) {
386        $keyword = isset($_REQUEST['keyword']) ? '&keyword=' . urlencode($_REQUEST['keyword']) : '';
387        echo "<p><a href=\"?view=notes&page=$page&type=$type$keyword\">Next 10</a>";
388      } elseif (isset($_REQUEST["view"]) && !empty($search_votes)) {
389        echo "<p>";
390        if (isset($NextPage) && $NextPage > 1) {
391          echo "<a href=\"?view=notes&page=$PrevPage&type=$type{$isSearch}\">&lt; Prev 25</a> ";
392        }
393        if (isset($to) && isset($resultCount) && $to < $resultCount) {
394          echo " <a href=\"?view=notes&page=$NextPage&type=$type{$isSearch}\">Next 25 &gt;</a>";
395        }
396        echo "</p>";
397      }
398    }
399  }
400?>
401
402<h2>Menu</h2>
403<?php if (allow_mass_change($cuser)): ?><p><a href="<?= PHP_SELF ?>?action=mass">Mass change of sections</a></p><?php endif; ?>
404<p><a href="<?= PHP_SELF ?>?view=notes&type=0">View last 10 notes</a></p>
405<p><a href="<?= PHP_SELF ?>?view=notes&type=1">View first 10 notes</a></p>
406<p><a href="<?= PHP_SELF ?>?view=notes&type=2">View minor 10 notes</a></p>
407<p><a href="<?= PHP_SELF ?>?view=notes&type=3">View top 10 rated notes</a></p>
408<p><a href="<?= PHP_SELF ?>?view=notes&type=4">View bottom 10 rated notes</a></p>
409<p><a href="<?= PHP_SELF ?>?view=notes&type=5">View votes table</a></p>
410<p><a href="<?= PHP_SELF ?>?view=notes&type=6">IPs with the most votes</a></p>
411<p><a href="<?= PHP_SELF ?>?action=sect">Search notes within a section</a></p>
412<p><a href="<?= PHP_SELF ?>?action=voting_stats">User contributed voting statistics</a></p>
413<?php
414  foot();
415  exit;
416}
417/*------ END SEARCH ------*/
418
419
420if (preg_match("/^(.+)\\s+(\\d+)\$/", $action, $m)) {
421  $action = $m[1]; $id = $m[2];
422}
423/* hack around the rewrite rules */
424if (isset($_GET['action']) && ($_GET['action'] == 'resetall' || $_GET['action'] == 'resetup' || $_GET['action'] == 'resetdown' || $_GET['action'] == 'deletevotes')) {
425  $action = $_GET['action'];
426  $id = isset($_GET['id']) ? (int)$_GET['id'] : null;
427}
428
429switch($action) {
430case 'mass':
431  head("user notes");
432  if (!allow_mass_change($cuser)) {
433    warn("You are not allowed to take this action!");
434    foot();
435    exit;
436  }
437  $step = (isset($_REQUEST["step"]) ? (int)$_REQUEST["step"] : 0);
438  $where = new Query();
439  if (!empty($_REQUEST["old_sect"])) {
440    $where->add("sect = ?", [$_REQUEST["old_sect"]]);
441  }
442  if (!empty($_REQUEST["ids"])) {
443    if (preg_match('~^([0-9]+, *)*[0-9]+$~i', $_REQUEST["ids"])) {
444      if ($where->get() !== '') {
445        $where->add(' AND ');
446      }
447      // Safe because we checked that ids is a comma-separated list of numbers.
448      $where->add("id IN (".$_REQUEST['ids'].")");
449    } else {
450      echo "<p><b>Incorrect format of notes IDs.</b></p>\n";
451      $step = 0;
452    }
453  }
454
455  if ($step == 2) {
456    $query = new Query('UPDATE note SET sect = ? WHERE ', [$_REQUEST["new_sect"]]);
457    $query->addQuery($where);
458    db_query($query);
459    echo "<p>Mass change succeeded.</p>\n";
460  } elseif ($step == 1) {
461    if (!empty($_REQUEST["new_sect"]) && $where) {
462      $query = new Query('SELECT COUNT(*) FROM note WHERE ');
463      $query->addQuery($where);
464      $result = db_query($query);
465      if (!($count = mysql_result($result, 0, 0))) {
466        echo "<p>There are no such notes.</p>\n";
467      } else {
468        $step = 2;
469        $msg = "Are you sure to change section of <b>$count note(s)</b>";
470        $msg .= (!empty($_REQUEST["ids"]) ? " with IDs <b>" . hsc($_REQUEST['ids']) . "</b>" : "");
471        $msg .= (!empty($_REQUEST["old_sect"]) ? " from section <b>" . hsc($_REQUEST['old_sect']) . "</b>" : "");
472        $msg .= " to section <b>" . hsc($_REQUEST['new_sect']) . "</b>?";
473        echo "<p>$msg</p>\n";
474?>
475<form action="<?= PHP_SELF; ?>?action=mass" method="post">
476<input type="hidden" name="step" value="2">
477<input type="hidden" name="old_sect" value="<?= hsc($_REQUEST["old_sect"]); ?>">
478<input type="hidden" name="ids" value="<?= hsc($_REQUEST["ids"]); ?>">
479<input type="hidden" name="new_sect" value="<?= hsc($_REQUEST["new_sect"]); ?>">
480<input type="submit" value="Change">
481</form>
482<?php
483      }
484    } else {
485      if (empty($_REQUEST["new_sect"])) {
486        echo "<p><b>You have to fill-in new section.</b></p>\n";
487      }
488      if (!$where) {
489        echo "<p><b>You have to fill-in curent section or notes IDs (or both).</b></p>\n";
490      }
491    }
492  }
493  if ($step < 2) {
494?>
495<form action="<?= PHP_SELF; ?>?action=mass" method="post">
496<input type="hidden" name="step" value="1">
497<p>Change section of notes which fit these criteria:</p>
498<table>
499 <tr>
500  <th align="right">Current section:</th>
501  <td><input type="text" name="old_sect" value="<?= hsc($_REQUEST["old_sect"]); ?>" size="30" maxlength="80" /> (filename without extension)</td>
502 </tr>
503 <tr>
504  <th align="right">Notes IDs:</th>
505  <td><input type="text" name="ids" value="<?= hsc($_REQUEST["ids"]); ?>" size="30" maxlength="80" /> (comma separated list)</td>
506 </tr>
507 <tr>
508  <th align="right">Move to section:</th>
509  <td><input type="text" name="new_sect" value="<?= hsc($_REQUEST["new_sect"]); ?>" size="30" maxlength="80" /></td>
510 </tr>
511 <tr>
512  <td align="center" colspan="2">
513    <input type="submit" value="Change" />
514  </td>
515 </tr>
516</table>
517</form>
518<?php
519  }
520  echo "<p><a href='", PHP_SELF, "'>Back to notes index</a></p>\n";
521  foot();
522  exit;
523case 'approve':
524  if ($id) {
525    if ($row = note_get_by_id($id)) {
526
527      if ($row['status'] != 'na') {
528        die ("Note #$id has already been approved");
529      }
530
531      if ($row['id'] && db_query_safe("UPDATE note SET status=NULL WHERE id=?", [$id])) {
532        note_mail_on_action(
533            $cuser,
534            $id,
535            "note {$row['id']} approved from {$row['sect']} by $cuser",
536            "This note has been approved and will appear in the manual.\n\n----\n\n{$row['note']}"
537        );
538      }
539
540      print "Note #$id has been approved and will appear in the manual";
541      exit;
542    }
543  }
544case 'reject':
545case 'delete':
546  if ($id) {
547    if ($row = note_get_by_id($id)) {
548      if ($row['id'] && db_query_safe("DELETE note,votes FROM note LEFT JOIN (votes) ON (note.id = votes.note_id) WHERE note.id = ?", [$id])) {
549        $action_taken = ($action == "reject" ? "rejected" : "deleted");
550        note_mail_on_action(
551            $cuser,
552            $id,
553            "note {$row['id']} $action_taken from {$row['sect']} by $cuser",
554            "Note Submitter: " . safe_email($row['user']) .
555        (isset($reason) ? "\nReason: $reason" : " ") .
556        "\n\n----\n\n{$row['note']}");
557        if ($action == 'reject') {
558          note_mail_user($row['user'], "note $row[id] rejected and deleted from $row[sect] by notes editor $cuser",$reject_text."\n\n----- Copy of your note below -----\n\n".$row['note']);
559        }
560      }
561
562      //if we came from an email, report _something_
563      if (isset($_GET['report'])) {
564        header('Location: user-notes.php?id=' . $id . '&was=' . $action);
565        exit;
566      } else {
567        //if not, just close the window
568        echo '<script language="javascript">window.close();</script>';
569      }
570      exit;
571    }
572  }
573  /* falls through, with id not set. */
574case 'preview':
575case 'edit':
576  if ($id) {
577    $note = (isset($_POST['note']) ? $_POST['note'] : null);
578    if (!isset($note) || $action == 'preview') {
579      head("user notes");
580    }
581
582    $row = note_get_by_id($id);
583
584    $email = (isset($_POST['email']) ? html_entity_decode($_POST['email'],ENT_QUOTES) : $row['user']);
585    $sect = (isset($_POST['sect']) ? html_entity_decode($_POST['sect'],ENT_QUOTES) : $row['sect']);
586
587    if (isset($note) && $action == "edit") {
588      if (db_query_safe('UPDATE note SET note=?,user=?,sect=?,updated=NOW() WHERE id=?', [html_entity_decode($note,ENT_QUOTES), $email, $sect, $id])) {
589        note_mail_on_action(
590            $cuser,
591            $id,
592            "note {$row['id']} modified in {$row['sect']} by $cuser",
593            $note."\n\n--was--\n{$row['note']}\n\nhttp://php.net/manual/en/{$row['sect']}.php"
594        );
595        if ($row["sect"] != $sect) {
596          note_mail_user($email, "note $id moved from $row[sect] to $sect by notes editor $cuser", "----- Copy of your note below -----\n\n".$note);
597        }
598        header('Location: user-notes.php?id=' . $id . '&was=' . $action);
599        exit;
600      }
601    }
602
603    $note = isset($note) ? $note : $row['note'];
604
605    if ($action == "preview") {
606      echo "<p class=\"notepreview\">",clean_note($note),
607           "<br /><span class=\"author\">",date("d-M-Y h:i",$row['ts'])," ",
608      hsc($email),"</span></p>";
609    }
610?>
611<form method="post" action="<?= PHP_SELF ?>">
612<input type="hidden" name="id" value="<?= $id ?>" />
613<table>
614 <tr>
615  <th align="right">Section:</th>
616  <td><input type="text" name="sect" value="<?= hsc($sect) ?>" size="30" maxlength="80" /></td>
617 </tr>
618 <tr>
619  <th align="right">email:</th>
620  <td><input type="text" name="email" value="<?= hsc($email) ?>" size="30" maxlength="80" /></td>
621 </tr>
622 <tr>
623  <td colspan="2"><textarea name="note" cols="70" rows="15"><?= hsc($note) ?></textarea></td>
624 </tr>
625 <tr>
626  <td align="center" colspan="2">
627    <input type="submit" name="action" value="edit" />
628    <input type="submit" name="action" value="preview" />
629  </td>
630 </tr>
631</table>
632</form>
633<?php
634    foot();
635    exit;
636  }
637case 'resetall':
638case 'resetup':
639case 'resetdown':
640  /* Only those with privileges in allow_mass_change may use these options */
641  head('user notes');
642  if (!allow_mass_change($cuser)) {
643    warn("You do not have access to use this feature!");
644    foot();
645    exit;
646  }
647  /* Reset votes for user note -- effectively deletes votes found for that note_id in the votes table:  up/down/both */
648  if ($id) {
649    if (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') {
650      /* Make sure the note has votes before we attempt to delete them */
651      $result = db_query_safe("SELECT COUNT(id) AS id FROM votes WHERE note_id = ?", [$id]);
652      $rows = mysql_fetch_assoc($result);
653      if (!$rows['id']) {
654        echo "<p>No votes exist for Note ID ". hsc($id) ."!</p>";
655      } else {
656        if ($action == 'resetall' && isset($_POST['resetall'])) {
657          db_query_safe('DELETE FROM votes WHERE votes.note_id = ?', [$id]);
658          /* 1 for up votes */
659        } elseif ($action == 'resetup' && isset($_POST['resetup'])) {
660          db_query_safe('DELETE FROM votes WHERE votes.note_id = ? AND votes.vote = 1', [$id]);
661          /* 0 for down votes */
662        } elseif ($action == 'resetdown' && isset($_POST['resetdown'])) {
663          db_query_safe('DELETE FROM votes WHERE votes.note_id = ? AND votes.vote = 0', [$id]);
664        }
665        header('Location: user-notes.php?id=' . urlencode($id) . '&was=' . urlencode($action));
666      }
667    } else {
668      $sql = 'SELECT SUM(votes.vote) AS up, (COUNT(votes.vote) - SUM(votes.vote)) AS down, note.*, UNIX_TIMESTAMP(note.ts) AS ts '.
669             'FROM note '.
670             'JOIN(votes) ON (note.id = votes.note_id) '.
671             'WHERE note.id = ?';
672      $result = db_query_safe($sql, [$id]);
673      if (mysql_num_rows($result)) {
674        $row = mysql_fetch_assoc($result);
675        $out = "<p>\nAre you sure you want to reset all votes for <strong>Note #". hsc($row['id']) ."</strong>? ";
676        if ($action == 'resetall') {
677          $out .= "This will permanently delete all <em>". hsc($row['up']) ."</em> up votes and <em>". hsc($row['down']) ."</em> down votes for this note.\n</p>\n".
678                  "<form method=\"POST\" action=\"\">\n".
679                  "  <input type=\"submit\" value\"Yes Reset!\" name=\"resetall\" />\n".
680                  "</form>\n";
681        } elseif ($action == 'resetup') {
682          $out .= "This will permanently delete all <em>". hsc($row['up']) ."</em> up votes for this note.\n</p>\n".
683                  "<form method=\"POST\" action=\"\">\n".
684                  "  <input type=\"submit\" value\"Yes Reset!\" name=\"resetup\" />\n".
685                  "</form>\n";
686        } elseif ($action == 'resetdown') {
687          $out .= "This will permanently delete all <em>". hsc($row['down']) ."</em> down votes for this note.\n</p>\n".
688                  "<form method=\"POST\" action=\"\">\n".
689                  "  <input type=\"submit\" value\"Yes Reset!\" name=\"resetdown\" />\n".
690                  "</form>\n";
691        }
692        echo $out;
693      } else {
694        echo "<p>Note ". hsc($id) ." does not exist!</p>";
695      }
696    }
697  } else {
698    echo "<p>Note id not supplied...</p>";
699  }
700  foot();
701  exit;
702case 'deletevotes':
703  /* Only those with privileges in allow_mass_change may use these options */
704  if (!allow_mass_change($cuser)) {
705    die("You do not have access to use this feature!");
706  }
707  /* Delete votes -- effectively deletes votes found in the votes table matching all supplied ids */
708  if (empty($_POST['deletevote']) || !is_array($_POST['deletevote'])) {
709    die("No vote ids supplied!");
710  }
711  $ids = [];
712  foreach ($_POST['deletevote'] as $id) {
713    $ids[] = (int) $id;
714  }
715  $ids = implode(',',$ids);
716  // This is safe, because $ids is an array of integers.
717  if (db_query_safe("DELETE FROM votes WHERE id IN ($ids)")) {
718    header('Location: user-notes.php?id=1&view=notes&was=' . urlencode($action) .
719           (isset($_REQUEST['type']) ? ('&type=' . urlencode($_REQUEST['type'])) : null) .
720           (isset($_REQUEST['votessearch']) ? '&votessearch=' . urlencode($_REQUEST['votessearch']) : null)
721          );
722  }
723  exit;
724case 'sect':
725  head('user notes');
726?>
727<h2>Search within a section</h2>
728<p>
729  You can search notes within specified section of the PHP manual using form below or
730  by prepending your query with <em>sect:</em> in regular search form (like <em>sect:book.mysql</em>).
731</p>
732<p>
733  You can use <em>*</em> as a wildcard, like <em>mysql.*</em>. Query like <em>function.json-*</em> should
734  show all notes for JSON functions (use <em>sect:function.json-*</em> in case of generic form).
735</p>
736<form method="get" action="<?= PHP_SELF ?>">
737  <strong>Section:</strong>
738  <input type="hidden" name="action" value="sect" />
739  <input type="text" name="query" /><br />
740  <input type="submit" value="Search" />
741</form>
742<?php
743  if (isset($_GET['query'])) {
744    header('Location: user-notes.php?keyword=sect:' . $_GET['query']);
745    exit;
746  }
747
748  foot();
749  exit;
750case 'voting_stats':
751    head('user notes');
752    /* Calculate dates */
753    $today = strtotime('midnight');
754    $week = !date('w') ? strtotime('midnight') : strtotime('Last Sunday');
755    $month = strtotime('First Day of ' . date('F') . ' ' . date('Y'));
756    $yesterday = strtotime('midnight yesterday');
757    $lastweek = !date('w') ? strtotime('midnight -1 week') : strtotime('Last Sunday -1 week');
758    $lastmonth = strtotime('First Day of last month');
759    /* Handle stats queries for voting here */
760    $stats_sql = $stats = [];
761    $stats_sql['Total']       = new Query('SELECT COUNT(votes.id) AS total FROM votes');
762    $stats_sql['Total Up']    = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE votes.vote = 1');
763    $stats_sql['Total Down']  = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE votes.vote = 0');
764    $stats_sql['Today']       = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ?', [$today]);
765    $stats_sql['This Week']   = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ?', [$week]);
766    $stats_sql['This Month']  = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ?', [$month]);
767    $stats_sql['Yesterday']   = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ? AND UNIX_TIMESTAMP(votes.ts) < ?', [$yesterday, $today]);
768    $stats_sql['Last Week']   = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ? AND UNIX_TIMESTAMP(votes.ts) < ?', [$lastweek, $week]);
769    $stats_sql['Last Month']  = new Query('SELECT COUNT(votes.id) AS total FROM votes WHERE UNIX_TIMESTAMP(votes.ts) >= ? AND UNIX_TIMESTAMP(votes.ts) < ?', [$lastmonth, $month]);
770    foreach ($stats_sql as $key => $query) {
771        $result = db_query($query);
772        $row = mysql_fetch_assoc($result);
773        $stats[$key] = $row['total'];
774    }
775    ?>
776    <h2>User contributed voting statistics</h2>
777    <div style="float: left; border: 1px solid gray; padding: 5px; background-color: #C8C8C0; margin-bottom: 20px;">
778        <?php foreach (array_chunk($stats, 3, true) as $statset) { ?>
779            <?php foreach ($statset as $figure => $stat) { ?>
780                <div style="display: inline-block; float: left; padding: 15px; border-bottom: 1px solid white; color: #483D8B;"><strong><?= $figure ?></strong>: <?= $stat ?></div>
781            <?php } ?>
782            <p>&nbsp;</p>
783        <?php } ?>
784    </div>
785
786    <p style="clear: both;"><a href="<?= PHP_SELF ?>">Go back to the notes management</a></p>
787    <?php
788    // I didn't want to copy the whole menu and making it a type instead of action
789    // would mean reworking whole architecture this page has so it's an easy win tbh
790    foot();
791break;
792  /* falls through */
793default:
794  head('user notes');
795  echo "<p>'$action' is not a recognized action, or no id was specified.</p>";
796  foot();
797}
798
799// ----------------------------------------------------------------------------------
800
801// Use class names instead of colors
802ini_set('highlight.comment', 'comment');
803ini_set('highlight.default', 'default');
804ini_set('highlight.keyword', 'keyword');
805ini_set('highlight.string',  'string');
806ini_set('highlight.html',    'html');
807
808// Copied over from phpweb (should be syncronised if changed)
809function clean_note($text)
810{
811    // Highlight PHP source
812    $text = highlight_php(trim($text), TRUE);
813
814    // Turn urls into links
815    $text = preg_replace(
816        '!((mailto:|(http|ftp|nntp|news):\/\/).*?)(\s|<|\)|"|\\|\'|$)!',
817        '<a href="\1" target="_blank">\1</a>\4',
818        $text
819    );
820
821    return $text;
822}
823
824// Highlight PHP code
825function highlight_php($code, $return = FALSE)
826{
827    // Using OB, as highlight_string() only supports
828    // returning the result from 4.2.0
829    ob_start();
830    highlight_string($code);
831    $highlighted = ob_get_contents();
832    ob_end_clean();
833
834    // Fix output to use CSS classes and wrap well
835    $highlighted = '<div class="phpcode">' . str_replace(
836        [
837            '&nbsp;',
838            '<br />',
839            '<font color="',
840            '</font>',
841            "\n ",
842            '  '
843        ],
844        [
845            ' ',
846            "<br />\n",
847            '<span class="',
848            '</span>',
849            "\n&nbsp;",
850            '&nbsp; '
851        ],
852        $highlighted
853    ) . '</div>';
854
855    if ($return) { return $highlighted; }
856    else { echo $highlighted; }
857}
858
859// Send out a mail to the note submitter, with an envelope sender ignoring bounces
860function note_mail_user($mailto, $subject, $message)
861{
862    $mailto = clean_antispam($mailto);
863    if (is_emailable_address($mailto)) {
864        mail(
865            $mailto,
866            $subject,
867            $message,
868            "From: ". NOTES_MAIL,
869            "-fbounces-ignored@php.net -O DeliveryMode=b"
870        );
871    }
872}
873
874// Return data about a note by its ID
875function note_get_by_id($id)
876{
877    if ($result = db_query_safe('SELECT *, UNIX_TIMESTAMP(ts) AS ts FROM note WHERE id=?', [$id])) {
878        if (!mysql_num_rows($result)) {
879            die("Note #$id doesn't exist. It has probably been deleted/rejected already.");
880        }
881        return mysql_fetch_assoc($result);
882    }
883    return FALSE;
884}
885
886// Sends out a notification to the mailing list when
887// some action is performed on a user note.
888function note_mail_on_action($user, $id, $subject, $body)
889{
890    mail(NOTES_MAIL, $subject, $body, "From: $user@php.net\r\nIn-Reply-To: <note-$id@php.net>", "-f{$user}@php.net");
891}
892
893// Allow some users to mass change IDs in the manual
894function allow_mass_change($user)
895{
896    if (in_array(
897            $user,
898            [
899                "vrana", "goba", "nlopess", "didou", "bjori", "philip", "bobby", "danbrown", "mgdm", "googleguy", "levim",
900            ]
901        )
902    ) {
903        return TRUE;
904    } else { return FALSE; }
905}
906
907// Return safe to print version of email address
908function safe_email($mail)
909{
910    if (in_array($mail, ["php-general@lists.php.net", "user@example.com"])) {
911        return '';
912    }
913    elseif (preg_match("!(.+)@(.+)\.(.+)!", $mail)) {
914        return str_replace(['@', '.'], [' at ', ' dot '], $mail);
915    }
916    return $mail;
917}
918
919// Return a valid IPv4 range (as 0-indexed two element array) based on wildcard IP string
920function wildcard_ip($ip)
921{
922    $start = explode(".", $ip);
923    if (count($start) != 4) {
924        return false;
925    }
926    foreach ($start as $part) {
927        if ($part === "*") {
928            continue;
929        }
930        if ($part > 255 || $part < 0 || !is_numeric($part)) {
931            return false;
932        }
933    }
934    $end = [];
935    foreach (array_keys($start, "*", true) as $key) {
936        $start[$key] = "0";
937        $end[$key] = "255";
938    }
939    foreach ($start as $key => $part) {
940        if (!isset($end[$key])) {
941            $end[$key] = $start[$key];
942        }
943    }
944    ksort($end);
945    $start = ip2long(implode('.',$start));
946    $end = ip2long(implode('.',$end));
947    if ($end - $start <= 0) {
948      return false;
949    }
950    return [$start, $end];
951}
952