1<?php
2// users.php -- HotCRP people listing/editing page
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5require_once("src/initweb.php");
6require_once("src/contactlist.php");
7
8$getaction = "";
9if (isset($Qreq->get))
10    $getaction = $Qreq->get;
11else if (isset($Qreq->getgo) && isset($Qreq->getaction))
12    $getaction = $Qreq->getaction;
13
14
15// list type
16$tOpt = array();
17if ($Me->can_view_pc())
18    $tOpt["pc"] = "Program committee";
19if ($Me->can_view_contact_tags() && count($pctags = $Conf->pc_tags())) {
20    foreach ($pctags as $t)
21        if ($t != "pc")
22            $tOpt["#$t"] = "#$t program committee";
23}
24if ($Me->isPC)
25    $tOpt["admin"] = "System administrators";
26if ($Me->privChair || ($Me->isPC && $Conf->setting("pc_seeallrev"))) {
27    $tOpt["re"] = "All reviewers";
28    $tOpt["ext"] = "External reviewers";
29    $tOpt["extsub"] = "External reviewers who completed a review";
30}
31if ($Me->isPC)
32    $tOpt["req"] = "External reviewers you requested";
33if ($Me->privChair || ($Me->isPC && $Conf->subBlindNever()))
34    $tOpt["au"] = "Contact authors of submitted papers";
35if ($Me->privChair
36    || ($Me->isPC && $Conf->timePCViewDecision(true)))
37    $tOpt["auacc"] = "Contact authors of accepted papers";
38if ($Me->privChair
39    || ($Me->isPC && $Conf->subBlindNever() && $Conf->timePCViewDecision(true)))
40    $tOpt["aurej"] = "Contact authors of rejected papers";
41if ($Me->privChair) {
42    $tOpt["auuns"] = "Contact authors of non-submitted papers";
43    $tOpt["all"] = "All users";
44}
45if (empty($tOpt))
46    $Me->escape();
47if (isset($Qreq->t) && !isset($tOpt[$Qreq->t])) {
48    if (str_starts_with($Qreq->t, "pc:")
49        && isset($tOpt["#" . substr($Qreq->t, 3)]))
50        $Qreq->t = "#" . substr($Qreq->t, 3);
51    else if (isset($tOpt["#" . $Qreq->t]))
52        $Qreq->t = "#" . $Qreq->t;
53    else if ($Qreq->t == "#pc")
54        $Qreq->t = "pc";
55    else {
56        Conf::msg_error("Unknown user collection.");
57        unset($Qreq->t);
58    }
59}
60if (!isset($Qreq->t))
61    $Qreq->t = key($tOpt);
62
63
64// paper selection and download actions
65function paperselPredicate($papersel) {
66    return "ContactInfo.contactId" . sql_in_numeric_set($papersel);
67}
68
69$Qreq->allow_a("pap");
70if (isset($Qreq->pap) && is_string($Qreq->pap))
71    $Qreq->pap = preg_split('/\s+/', $Qreq->pap);
72if ((isset($Qreq->pap) && is_array($Qreq->pap))
73    || ($getaction && !isset($Qreq->pap))) {
74    $allowed_papers = array();
75    $pl = new ContactList($Me, true);
76    // Ensure that we only select contacts we're allowed to see.
77    if (($rows = $pl->rows($Qreq->t))) {
78        foreach ($rows as $row)
79            $allowed_papers[$row->contactId] = true;
80    }
81    $papersel = array();
82    if (isset($Qreq->pap)) {
83        foreach ($Qreq->pap as $p)
84            if (($p = cvtint($p)) > 0 && isset($allowed_papers[$p]))
85                $papersel[] = $p;
86    } else
87        $papersel = array_keys($allowed_papers);
88    if (empty($papersel))
89        unset($papersel);
90}
91
92if ($getaction == "nameemail" && isset($papersel) && $Me->isPC) {
93    $result = $Conf->qe_raw("select firstName first, lastName last, email, affiliation from ContactInfo where " . paperselPredicate($papersel) . " order by lastName, firstName, email");
94    $people = edb_orows($result);
95    csv_exit($Conf->make_csvg("users")
96             ->select(["first", "last", "email", "affiliation"])
97             ->add($people));
98}
99
100function urlencode_matches($m) {
101    return urlencode($m[0]);
102}
103
104if ($getaction == "pcinfo" && isset($papersel) && $Me->privChair) {
105    $users = [];
106    $result = $Conf->qe_raw("select ContactInfo.* from ContactInfo where " . paperselPredicate($papersel));
107    while (($user = Contact::fetch($result, $Conf)))
108        $users[] = $user;
109    Dbl::free($result);
110
111    usort($users, "Contact::compare");
112    Contact::load_topic_interests($users);
113
114    // NB This format is expected to be parsed by profile.php's bulk upload.
115    $tagger = new Tagger($Me);
116    $people = [];
117    $has = (object) [];
118    foreach ($users as $user) {
119        $row = (object) ["first" => $user->firstName, "last" => $user->lastName,
120            "email" => $user->email, "phone" => $user->phone,
121            "disabled" => !!$user->disabled, "affiliation" => $user->affiliation,
122            "collaborators" => rtrim($user->collaborators)];
123        if ($user->preferredEmail && $user->preferredEmail !== $user->email)
124            $row->preferred_email = $user->preferredEmail;
125        if ($user->contactTags)
126            $row->tags = $tagger->unparse($user->contactTags);
127        foreach ($user->topic_interest_map() as $t => $i) {
128            $k = "topic$t";
129            $row->$k = $i;
130        }
131        $f = array();
132        if ($user->defaultWatch & Contact::WATCH_REVIEW)
133            $f[] = "reviews";
134        if (($user->defaultWatch & Contact::WATCH_REVIEW_ALL)
135            && ($user->roles & Contact::ROLE_PCLIKE))
136            $f[] = "allreviews";
137        if ($user->defaultWatch & Contact::WATCH_REVIEW_MANAGED)
138            $f[] = "managedreviews";
139        if ($user->defaultWatch & Contact::WATCH_FINAL_SUBMIT_ALL)
140            $f[] = "allfinal";
141        $row->follow = join(",", $f);
142        if ($user->roles & (Contact::ROLE_PC | Contact::ROLE_ADMIN | Contact::ROLE_CHAIR)) {
143            $r = array();
144            if ($user->roles & Contact::ROLE_CHAIR)
145                $r[] = "chair";
146            if ($user->roles & Contact::ROLE_PC)
147                $r[] = "pc";
148            if ($user->roles & Contact::ROLE_ADMIN)
149                $r[] = "sysadmin";
150            $row->roles = join(",", $r);
151        } else
152            $row->roles = "";
153        $people[] = $row;
154
155        foreach ((array) $row as $k => $v)
156            if ($v !== null && $v !== false && $v !== "")
157                $has->$k = true;
158    }
159
160    $header = array("first", "last", "email");
161    if (isset($has->preferred_email))
162        $header[] = "preferred_email";
163    $header[] = "roles";
164    if (isset($has->tags))
165        $header[] = "tags";
166    array_push($header, "affiliation", "collaborators", "follow");
167    if (isset($has->phone))
168        $header[] = "phone";
169    $selection = $header;
170    foreach ($Conf->topic_map() as $t => $tn) {
171        $k = "topic$t";
172        if (isset($has->$k)) {
173            $header[] = "topic: " . $tn;
174            $selection[] = $k;
175        }
176    }
177    csv_exit($Conf->make_csvg("pcinfo")->select($selection, $header)->add($people));
178}
179
180
181// modifications
182function modify_confirm($j, $ok_message, $ok_message_optional) {
183    global $Conf;
184    if (get($j, "ok") && get($j, "warnings"))
185        $Conf->warnMsg("<div>" . join("</div><div style='margin-top:0.5em'>", $j->warnings) . "</div>");
186    if (get($j, "ok") && $ok_message
187        && (!$ok_message_optional || !get($j, "warnings"))
188        && (!isset($j->users) || !empty($j->users)))
189        $Conf->confirmMsg($ok_message);
190}
191
192if ($Me->privChair && $Qreq->modifygo && $Qreq->post_ok() && isset($papersel)) {
193    if ($Qreq->modifytype == "disableaccount")
194        modify_confirm(UserActions::disable($Me, $papersel), "Accounts disabled.", true);
195    else if ($Qreq->modifytype == "enableaccount")
196        modify_confirm(UserActions::enable($Me, $papersel), "Accounts enabled.", true);
197    else if ($Qreq->modifytype == "resetpassword"
198             && $Me->can_change_password(null))
199        modify_confirm(UserActions::reset_password($Me, $papersel), "Passwords reset. <a href=\"" . hoturl_post("users", "t=" . urlencode($Qreq->t) . "&amp;modifygo=1&amp;modifytype=sendaccount&amp;pap=" . join("+", $papersel)) . "\">Send account information to those accounts</a>", false);
200    else if ($Qreq->modifytype == "sendaccount")
201        modify_confirm(UserActions::send_account_info($Me, $papersel), "Account information sent.", false);
202    unset($Qreq->modifygo, $Qreq->modifytype);
203    SelfHref::redirect($Qreq);
204}
205
206function do_tags($qreq) {
207    global $Conf, $Me, $papersel;
208    // check tags
209    $tagger = new Tagger($Me);
210    $t1 = array();
211    $errors = array();
212    foreach (preg_split('/[\s,]+/', (string) $qreq->tag) as $t) {
213        if ($t === "")
214            /* nada */;
215        else if (!($t = $tagger->check($t, Tagger::NOPRIVATE)))
216            $errors[] = $tagger->error_html;
217        else if (TagInfo::base($t) === "pc")
218            $errors[] = "The “pc” user tag is set automatically for all PC members.";
219        else
220            $t1[] = $t;
221    }
222    if (count($errors))
223        return Conf::msg_error(join("<br>", $errors));
224    else if (!count($t1))
225        return $Conf->warnMsg("Nothing to do.");
226
227    // modify database
228    Dbl::qe("lock tables ContactInfo write");
229    Conf::$no_invalidate_caches = true;
230    $users = array();
231    if ($qreq->tagtype === "s") {
232        // erase existing tags
233        $likes = array();
234        $removes = array();
235        foreach ($t1 as $t) {
236            list($tag, $index) = TagInfo::unpack($t);
237            $removes[] = $t;
238            $likes[] = "contactTags like " . Dbl::utf8ci("'% " . sqlq_for_like($tag) . "#%'");
239        }
240        foreach (Dbl::fetch_first_columns(Dbl::qe("select contactId from ContactInfo where " . join(" or ", $likes))) as $cid)
241            $users[(int) $cid] = (object) array("id" => (int) $cid, "add_tags" => [], "remove_tags" => $removes);
242    }
243    // account for request
244    $key = $qreq->tagtype === "d" ? "remove_tags" : "add_tags";
245    foreach ($papersel as $cid) {
246        if (!isset($users[(int) $cid]))
247            $users[(int) $cid] = (object) array("id" => (int) $cid, "add_tags" => [], "remove_tags" => []);
248        $users[(int) $cid]->$key = array_merge($users[(int) $cid]->$key, $t1);
249    }
250    // apply modifications
251    $us = new UserStatus($Me, ["send_email" => false]);
252    foreach ($users as $cid => $cj) {
253        $us->save($cj);
254    }
255    Dbl::qe("unlock tables");
256    Conf::$no_invalidate_caches = false;
257    $Conf->invalidate_caches(["pc" => true]);
258    // report
259    if (!$us->has_error()) {
260        $Conf->confirmMsg("Tags saved.");
261        unset($qreq->tagact, $qreq->tag);
262        SelfHref::redirect($qreq);
263    } else
264        Conf::msg_error($us->errors());
265}
266
267if ($Me->privChair && $Qreq->tagact && $Qreq->post_ok() && isset($papersel)
268    && preg_match('/\A[ads]\z/', (string) $Qreq->tagtype))
269    do_tags($Qreq);
270
271
272// set scores to view
273if (isset($Qreq->redisplay)) {
274    $Conf->save_session("uldisplay", "");
275    foreach (ContactList::$folds as $key)
276        displayOptionsSet("uldisplay", $key, $Qreq->get("show$key", 0));
277    foreach ($Conf->all_review_fields() as $f)
278        if ($f->has_options)
279            displayOptionsSet("uldisplay", $f->id, $Qreq->get("show{$f->id}", 0));
280}
281if (isset($Qreq->scoresort))
282    $Qreq->scoresort = ListSorter::canonical_short_score_sort($Qreq->scoresort);
283if (isset($Qreq->scoresort))
284    $Conf->save_session("scoresort", $Qreq->scoresort);
285
286
287if ($Qreq->t === "pc")
288    $title = "Program committee";
289else if (str_starts_with($Qreq->t, "#"))
290    $title = "#" . substr($Qreq->t, 1) . " program committee";
291else
292    $title = "Users";
293$Conf->header($title, "accounts", ["action_bar" => actionBar("account")]);
294
295
296$pl = new ContactList($Me, true, $Qreq);
297$pl_text = $pl->table_html($Qreq->t, hoturl("users", ["t" => $Qreq->t]),
298                     $tOpt[$Qreq->t], 'uldisplay.');
299
300
301// form
302echo "<div class='g'></div>\n";
303if (count($tOpt) > 1) {
304    echo "<table id='contactsform' class='tablinks1'>
305<tr><td><div class='tlx'><div class='tld1'>";
306
307    echo Ht::form(hoturl("users"), ["method" => "get"]);
308    if (isset($Qreq->sort))
309        echo Ht::hidden("sort", $Qreq->sort);
310    echo Ht::select("t", $tOpt, $Qreq->t, ["class" => "want-focus"]),
311        " &nbsp;", Ht::submit("Go"), "</form>";
312
313    echo "</div><div class='tld2'>";
314
315    // Display options
316    echo Ht::form(hoturl("users"), ["method" => "get"]);
317    foreach (array("t", "sort") as $x)
318        if (isset($Qreq[$x]))
319            echo Ht::hidden($x, $Qreq[$x]);
320
321    echo "<table><tr><td><strong>Show:</strong> &nbsp;</td>
322  <td class='pad'>";
323    foreach (array("tags" => "Tags",
324                   "aff" => "Affiliations", "collab" => "Collaborators",
325                   "topics" => "Topics") as $fold => $text)
326        if (get($pl->have_folds, $fold) !== null) {
327            $k = array_search($fold, ContactList::$folds) + 1;
328            echo Ht::checkbox("show$fold", 1, $pl->have_folds[$fold],
329                              ["data-fold-target" => "foldul#$k", "class" => "uich js-foldup"]),
330                "&nbsp;", Ht::label($text), "<br />\n";
331        }
332    echo "</td>";
333    if (isset($pl->scoreMax)) {
334        echo "<td class='pad'>";
335        $revViewScore = $Me->permissive_view_score_bound();
336        foreach ($Conf->all_review_fields() as $f)
337            if ($f->view_score > $revViewScore && $f->has_options) {
338                $checked = strpos(displayOptionsSet("uldisplay"), $f->id) !== false;
339                echo Ht::checkbox("show{$f->id}", 1, $checked),
340                    "&nbsp;", Ht::label($f->name_html), "<br />";
341            }
342        echo "</td>";
343    }
344    echo "<td>", Ht::submit("redisplay", "Redisplay"), "</td></tr>\n";
345    if (isset($pl->scoreMax)) {
346        $ss = [];
347        foreach (ListSorter::score_sort_selector_options() as $k => $v)
348            if (in_array($k, ["average", "variance", "maxmin"]))
349                $ss[$k] = $v;
350        echo "<tr><td colspan='3'><div class='g'></div><b>Sort scores by:</b> &nbsp;",
351            Ht::select("scoresort", $ss,
352                       ListSorter::canonical_long_score_sort($Conf->session("scoresort", "A"))),
353            "</td></tr>";
354    }
355    echo "</table></form>";
356
357    echo "</div></div></td></tr>\n";
358
359    // Tab selectors
360    echo "<tr><td class='tllx'><table><tr>
361  <td><div class='tll1'><a class='ui tla' href=''>User selection</a></div></td>
362  <td><div class='tll2'><a class='ui tla' href=''>Display options</a></div></td>
363</tr></table></td></tr>
364</table>\n\n";
365}
366
367
368if ($Me->privChair && $Qreq->t == "pc")
369    $Conf->infoMsg("<p><a href='" . hoturl("profile", "u=new&amp;role=pc") . "' class='btn'>Create accounts</a></p><p>Select a PC member’s name to edit their profile or remove them from the PC.</p>");
370else if ($Me->privChair && $Qreq->t == "all")
371    $Conf->infoMsg("<p><a href='" . hoturl("profile", "u=new") . "' class='btn'>Create accounts</a></p><p>Select a user to edit their profile.  Select " . Ht::img("viewas.png", "[Act as]") . " to view the site as that user would see it.</p>");
372
373
374if ($pl->any->sel) {
375    echo Ht::form(hoturl_post("users", ["t" => $Qreq->t])), "<div>";
376    if (isset($Qreq->sort))
377        echo Ht::hidden("sort", $Qreq->sort);
378}
379echo $pl_text;
380if ($pl->any->sel)
381    echo "</div></form>";
382
383
384$Conf->footer();
385