xref: /web-master/manage/users.php (revision 81b302aa)
1<?php // vim: et ts=2 sw=2
2
3#TODO:
4# acls
5# handle flipping of the sort views
6
7require '../include/login.inc';
8require '../include/email-validation.inc';
9require '../include/email-templates.inc';
10
11function csrf_generate(&$mydata, $name) {
12  $mydata["CSRF"][$name] = $csrf = hash("sha512", (string)mt_rand(0,mt_getrandmax()));
13  return "$name:$csrf";
14}
15function csrf_validate(&$mydata, $name) {
16  $val = filter_input(INPUT_POST, "csrf", FILTER_UNSAFE_RAW);
17  list($which, $hash) = explode(":", $val, 2);
18
19  if ($which != $name) {
20    warn("Failed CSRF Check");
21    foot();
22    exit;
23  }
24
25  if ($mydata["CSRF"][$name] != $hash) {
26    warn("Failed CSRF Check");
27    foot();
28    exit;
29  }
30
31  csrf_generate($mydata, $name);
32  return true;
33}
34
35$indesc = [
36  "id"               => FILTER_VALIDATE_INT,
37  "rawpasswd"        => FILTER_UNSAFE_RAW,
38  "rawpasswd2"       => FILTER_UNSAFE_RAW,
39  "svnpasswd"        => FILTER_SANITIZE_STRIPPED,
40  "cvsaccess"        => ["filter" => FILTER_CALLBACK, "options" => function($v) { if ($v == "on") { return true; } return false; }],
41  "enable"           => ["filter" => FILTER_CALLBACK, "options" => function($v) { if ($v == "on") { return true; } return false; }],
42  "spamprotect"      => ["filter" => FILTER_CALLBACK, "options" => function($v) { if ($v == "on") { return true; } return false; }],
43  "greylist"         => ["filter" => FILTER_CALLBACK, "options" => function($v) { if ($v == "on") { return true; } return false; }],
44  "verified"         => FILTER_VALIDATE_INT,
45  "use_sa"           => FILTER_VALIDATE_INT,
46  "email"            => FILTER_SANITIZE_EMAIL,
47  "name"             => FILTER_SANITIZE_SPECIAL_CHARS,
48  "sshkey"           => FILTER_SANITIZE_SPECIAL_CHARS,
49  "purpose"          => FILTER_SANITIZE_SPECIAL_CHARS,
50  "profile_markdown" => FILTER_UNSAFE_RAW,
51];
52
53$rawin    = filter_input_array(INPUT_POST) ?: [];
54$in       = isset($rawin["in"]) ? filter_var_array($rawin["in"], $indesc, false) : [];
55$id       = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT) ?: 0;
56$username = filter_input(INPUT_GET, "username", FILTER_SANITIZE_STRIPPED) ?: 0;
57
58head("user administration");
59
60db_connect();
61
62# ?username=whatever will look up 'whatever' by email or username
63if ($username) {
64  $query = "SELECT userid FROM users"
65         . " WHERE username=? OR email=?";
66  $res = db_query_safe($query, [$username, $username]);
67
68  if (!($id = @mysql_result($res, 0))) {
69    warn("wasn't able to find user matching '$username'");
70  }
71}
72if ($id) {
73  $query = "SELECT * FROM users WHERE users.userid=?";
74  $res = db_query_safe($query, [$id]);
75  $userdata = mysql_fetch_array($res);
76  if (!$userdata) {
77    warn("Can't find user#$id");
78  }
79}
80
81$action = filter_input(INPUT_POST, "action", FILTER_CALLBACK, ["options" => "validateAction"]);
82if ($id && $action) {
83  csrf_validate($_SESSION, $action);
84  if (!is_admin($_SESSION["username"])) {
85    warn("you're not allowed to take actions on users.");
86    exit;
87  }
88
89  switch ($action) {
90  case 'approve':
91    user_approve((int)$id);
92    break;
93
94  case 'remove':
95    user_remove((int)$id);
96    break;
97
98  default:
99    warn("that action ('$action') is not understood.");
100  }
101}
102
103if ($in) {
104  csrf_validate($_SESSION, "useredit");
105  if (!can_modify($_SESSION["username"],$id)) {
106    warn("you're not allowed to modify this user.");
107  }
108  else {
109    if ($error = invalid_input($in)) {
110      warn($error);
111    }
112    else {
113      if (!empty($in['rawpasswd'])) {
114        $userinfo = fetch_user($id);
115        $in['svnpasswd'] = gen_svn_pass($userinfo["username"], $in['rawpasswd']);
116      }
117
118      $cvsaccess   = empty($in['cvsaccess'])   ? 0 : 1;
119      $enable      = empty($in['enable'])      ? 0 : 1;
120      $spamprotect = empty($in['spamprotect']) ? 0 : 1;
121      $use_sa      = empty($in['use_sa'])      ? 0 : (int)$in['use_sa'];
122      $greylist    = empty($in['greylist'])    ? 0 : 1;
123
124      if ($id) {
125        # update main table data
126        if (!empty($in['email']) && !empty($in['name'])) {
127          $query = new Query("UPDATE users SET name=?,email=?", [$in['name'], $in['email']]);
128          if (!empty($in['svnpasswd'])) {
129              $query->add(',svnpasswd=?', [$in['svnpasswd']]);
130          }
131          if (!empty($in['sshkey'])) {
132              $query->add(',ssh_keys=?', [html_entity_decode($in['sshkey'],ENT_QUOTES)]);
133          }
134          if (is_admin($_SESSION["username"]) && !empty($in['username'])) {
135              $query->add(',username=?', [$in['username']]);
136          }
137          if (is_admin($_SESSION["username"])) {
138              $query->add(',cvsaccess=?', [$cvsaccess]);
139          }
140          $query->add(
141            ',spamprotect=?, enable=?, use_sa=?, greylist=?',
142            [$spamprotect, $enable, $use_sa, $greylist]);
143          if (!empty($in['rawpasswd'])) {
144              $query->add(',pchanged=?', [$ts]);
145          }
146          $query->add(' WHERE userid=?', [$id]);
147          if (!empty($in['passwd'])) {
148            // Kill the session data after updates :)
149            $_SERVER["credentials"] = [];
150          }
151          db_query($query);
152
153          if(!empty($in['purpose'])) {
154              $purpose = hsc($in['purpose']);
155              $query = "INSERT INTO users_note (userid, note, entered) VALUES (?, ?, NOW())";
156              db_query_safe($query, [$id, $purpose]);
157          }
158
159          if(!empty($in['profile_markdown'])) {
160            $profile_markdown = $in['profile_markdown'];
161            $profile_html = Markdown($profile_markdown);
162            $query = "INSERT INTO users_profile (userid, markdown, html) VALUES (?, ?, ?)
163                      ON DUPLICATE KEY UPDATE markdown=?, html=?";
164            db_query_safe($query, [$id, $profile_markdown, $profile_html, $profile_markdown, $profile_html]);
165          }
166        }
167
168        warn("record $id updated");
169        $id = false;
170      }
171    }
172  }
173}
174
175if ($id) {
176?>
177<form method="post" action="users.php?id=<?php echo $userdata["userid"]?>">
178 <input type="hidden" name="csrf" value="<?php echo csrf_generate($_SESSION, "useredit") ?>" />
179<table class="useredit">
180<tbody>
181<tr>
182 <th>Name:</th>
183 <td><input type="text" name="in[name]" value="<?php echo $userdata['name'];?>" size="40" maxlength="255" /></td>
184</tr>
185<tr>
186 <th>Email:</th>
187 <td><input type="text" name="in[email]" value="<?php echo $userdata['email'];?>" size="40" maxlength="255" /><br/>
188  	<input type="checkbox" name="in[enable]"<?php echo $userdata['enable'] ? " checked" : "";?> /> Enable email for my account.
189 </td>
190</tr>
191<tr>
192 <th>VCS username:</th>
193<?php if (is_admin($_SESSION["username"])): ?>
194 <td><input type="text" name="in[username]" value="<?php echo hsc($userdata['username']);?>" size="16" maxlength="16" /></td>
195<?php else: ?>
196 <td><?php echo hsc($userdata['username']);?></td>
197<?php endif ?>
198</tr>
199<tr>
200 <td colspan="2">Leave password fields blank to leave password unchanged.</td>
201</tr>
202<tr>
203 <th>Password:</th>
204 <td><input type="password" name="in[rawpasswd]" value="" size="20" maxlength="120" /></td>
205</tr>
206<tr>
207 <th>Password (again):</th>
208 <td><input type="password" name="in[rawpasswd2]" value="" size="20" maxlength="120" /></td>
209</tr>
210<?php if (is_admin($_SESSION["username"])) {?>
211<tr>
212 <th>VCS access?</th>
213 <td><input type="checkbox" name="in[cvsaccess]"<?php echo $userdata['cvsaccess'] ? " checked" : "";?> /></td>
214</tr>
215<?php } else { ?>
216<tr>
217 <th>Has VCS access?</th>
218 <td><?php echo $userdata['cvsaccess'] ? "Yes" : "No";?></td>
219</tr>
220<?php } ?>
221<tr>
222 <th>Use Challenge/Response spam protection?</th>
223 <td><input type="checkbox" name="in[spamprotect]"<?php echo $userdata['spamprotect'] ? " checked" : "";?> />
224 <?php if ($userdata['username'] == $_SESSION["username"]) { ?>
225 <br/>
226 <a href="challenge-response.php">Show people on my quarantine list</a>
227 <?php } ?>
228 </td>
229</tr>
230<tr>
231 <th>SpamAssassin threshold</th>
232 <td>Block mail scoring <input type="text" name="in[use_sa]" value="<?php echo $userdata['use_sa'] ?>" size="4" maxlength="4"/> or higher in SpamAssassin tests.  Set to 0 to disable.</td>
233</tr>
234<tr>
235 <th>Greylist</th>
236 <td>Delay reception of your incoming mail by a minimum of one hour using a 451 response.<br/>
237  Legitimate senders will continue to try to deliver the mail, whereas
238  spammers will typically give up and move on to spamming someone else.<br/>
239  See <a href="http://projects.puremagic.com/greylisting/whitepaper.html">this whitepaper</a> for more information on greylisting.<br/>
240  <input type="checkbox" name="in[greylist]"<?php echo $userdata['greylist'] ? " checked" : "";?> /> Enable greylisting on my account</td>
241</tr>
242<tr>
243 <th>Verified?</th>
244 <td><input type="checkbox" name="in[verified]"<?php echo $userdata['verified'] ? " checked" : "";?> /> Note: Do not worry about this value. It's sometimes used to check if old-timers are still around.</td>
245</tr>
246</tbody>
247<tfoot>
248<tr>
249 <th>SSH Key</th>
250 <td><textarea name="in[sshkey]" placeholder="Paste in the contents of your id_rsa.pub"><?php echo hsc(html_entity_decode($userdata['ssh_keys'], ENT_QUOTES)); ?></textarea>
251  <p>Adding/editing the SSH key takes a few minutes to propagate to the server.<br>
252  Multiple keys are allowed, separated using a newline.</p></td>
253</tr>
254<?php
255  if ($id) {
256    $res = db_query_safe("SELECT markdown FROM users_profile WHERE userid=?", [$id]);
257    $userdata['profile_markdown'] = '';
258    if ($profile_row = mysql_fetch_assoc($res)) {
259        $userdata['profile_markdown'] = $profile_row['markdown'];
260    }
261?>
262<tr>
263 <th>People Profile<br>(<a href="http://people.php.net/user.php?username=<?php echo urlencode($userdata['username']);?>"><?php echo hsc($userdata['username']);?>'s page</a>)</th>
264 <td>
265     <p>Use <a href="http://michelf.ca/projects/php-markdown/dingus/" title="PHP Markdown: Dingus">Markdown</a>. Type as much as you like.</p>
266     <div><textarea name="in[profile_markdown]" placeholder="My PHP People page content"><?php echo hsc($userdata['profile_markdown']); ?></textarea></div>
267 </td>
268</tr>
269<?php
270  }
271?>
272<tr>
273 <th>Add Note: </th>
274 <td><textarea name="in[purpose]" placeholder="Administrative notes"></textarea></td>
275</tr>
276<tr>
277 <td colspan="2"><input type="submit" value="Update" />
278</tr>
279</tfoot>
280</table>
281</form>
282<?php
283if (is_admin($_SESSION["username"]) && !$userdata['cvsaccess']) {
284?>
285<table>
286<tr>
287<td>
288 <form method="post" action="users.php?id=<?php echo $id?>">
289  <input type="hidden" name="csrf" value="<?php echo csrf_generate($_SESSION, "remove") ?>" />
290  <input type="hidden" name="action" value="remove" />
291  <input type="submit" value="Reject" />
292 </form>
293</td>
294<td>
295<?php
296  $hash = gen_svn_pass($_SESSION["credentials"][0], $_SESSION["credentials"][1]);
297  $csrf = "approve:$hash:";
298?>
299 <form method="post" action="users.php?id=<?php echo $id?>">
300  <input type="hidden" name="csrf" value="<?php echo csrf_generate($_SESSION, "approve") ?>" />
301  <input type="hidden" name="action" value="approve" />
302  <input type="submit" value="Approve" />
303 </form>
304</td>
305</tr>
306</table>
307<?php
308}
309?>
310<h2 id="notes">Notes:</h2>
311<?php
312  $res = db_query_safe("SELECT note, UNIX_TIMESTAMP(entered) AS ts FROM users_note WHERE userid=?", [$id]);
313  while ($res && $userdata = mysql_fetch_assoc($res)) {
314    echo "<div class='note'>", date("r",$userdata['ts']), "<br />".$userdata['note']."</div>";
315  }
316  foot();
317  exit;
318}
319?>
320<?php
321
322$unapproved = filter_input(INPUT_GET, "unapproved", FILTER_VALIDATE_INT) ?: 0;
323$begin      = filter_input(INPUT_GET, "begin", FILTER_VALIDATE_INT) ?: 0;
324$max        = filter_input(INPUT_GET, "max", FILTER_VALIDATE_INT) ?: 20;
325$forward    = filter_input(INPUT_GET, "forward", FILTER_VALIDATE_INT) ?: 0;
326$search     = filter_input(INPUT_GET, "search", FILTER_UNSAFE_RAW) ?: "";
327$order      = filter_input(INPUT_GET, "order", FILTER_UNSAFE_RAW) ?: "";
328
329$query = new Query("SELECT DISTINCT SQL_CALC_FOUND_ROWS users.userid,cvsaccess,username,name,email,GROUP_CONCAT(note) note FROM users ");
330$query->add(" LEFT JOIN users_note ON users_note.userid = users.userid ");
331
332if  ($search) {
333    $query->add("WHERE (MATCH(name,email,username) AGAINST (?) OR username = ?) ", [$search, $search]);
334
335} else {
336    $query->add(' WHERE 1=1 ');
337}
338
339if ($unapproved) {
340    $query->add(' AND NOT cvsaccess ');
341}
342
343$query->add(" GROUP BY users.userid ");
344
345if ($order) {
346  if (!in_array($order, ["username", "name", "email", "note"], true)) {
347    die("Invalid order!");
348  }
349  if ($forward) {
350    $ext = "ASC";
351  } else {
352    $ext = "DESC";
353  }
354  // Safe because we checked that $order is part of a fixed set.
355  $query->add(" ORDER BY $order $ext");
356}
357$query->add(" LIMIT ?int, ?int ", [$begin, $max]);
358$res = db_query($query);
359
360$res2 = db_query_safe("SELECT FOUND_ROWS()");
361$total = (int)mysql_result($res2,0);
362
363
364$extra = [
365  "search"     => $search,
366  "order"      => $order,
367  "forward"    => $forward,
368  "begin"      => $begin,
369  "max"        => $max,
370  "unapproved" => $unapproved,
371];
372
373?>
374<h1 class="browse">Browse users<ul>
375  <li><a href="?unapproved=0">See all users</a></li>
376  <li><a href="?unapproved=1">See outstanding requests</a></li>
377  </ul></h1>
378<table id="users">
379<thead>
380<?php show_prev_next($begin,mysql_num_rows($res),$max,$total,$extra, false); ?>
381</thead>
382<tbody>
383<tr>
384  <th><a href="?<?php echo array_to_url($extra,["unapproved"=>!$unapproved]);?>"><?php echo $unapproved ? "&otimes" : "&oplus"; ?>;</a></th>
385  <th><a href="?<?php echo array_to_url($extra,["order"=>"username"]);?>">username</a></th>
386  <th><a href="?<?php echo array_to_url($extra,["order"=>"name"]);?>">name</a></th>
387<?php if (!$unapproved) { ?>
388  <th colspan="2"><a href="?<?php echo array_to_url($extra,["order"=>"email"]);?>">email</a></th>
389<?php } else { ?>
390  <th><a href="?<?php echo array_to_url($extra,["order"=>"email"]);?>">email</a></th>
391  <th><a href="?<?php echo array_to_url($extra,["order"=>"note"]);?>">note</a></th>
392<?php } ?>
393  <th> </th>
394</tr>
395<?php
396while ($userdata = mysql_fetch_array($res)) {
397?>
398  <tr class="<?php if (!$userdata["cvsaccess"]) { echo "noaccess"; }?>">
399    <td><a href="?username=<?php echo $userdata["username"];?>">edit</a></td>
400    <td><a href="https://people.php.net/?username=<?php echo hsc($userdata['username']) ?>"><?php echo hsc($userdata['username']) ?></a></td>
401    <td><?php echo hsc($userdata['name']);?></td>
402<?php if (!$unapproved) { ?>
403    <td colspan="2"><?php echo hsc($userdata['email']);?></td>
404<?php } else { ?>
405    <td><?php echo hsc($userdata['email']);?></td>
406    <td><?php echo hsc($userdata['note']) ?></td>
407<?php } ?>
408      <td> </td>
409  </tr>
410<?php
411}
412?>
413</tbody>
414<tfoot>
415<?php show_prev_next($begin,mysql_num_rows($res),$max,$total,$extra, false); ?>
416</tfoot>
417</table>
418<?php
419foot();
420
421