1<?php
2// profile.php -- HotCRP profile management page
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5require_once("src/initweb.php");
6
7// check for change-email capabilities
8function change_email_by_capability($Qreq) {
9    global $Conf, $Me;
10    $capmgr = $Conf->capability_manager();
11    $capdata = $capmgr->check($Qreq->changeemail);
12    if (!$capdata
13        || $capdata->capabilityType != CAPTYPE_CHANGEEMAIL
14        || !($capdata->data = json_decode($capdata->data))
15        || !get($capdata->data, "uemail"))
16        error_go(false, "That email change code has expired, or you didn’t enter it correctly.");
17
18    if ($capdata->contactId)
19        $Acct = $Conf->user_by_id($capdata->contactId);
20    else
21        error_go(false, "That email change code was created improperly due to a server error. Please create another email change code, or sign out of your current account and create a new account using your preferred email address.");
22
23    if (!$Acct)
24        error_go(false, "No such account.");
25    else if (isset($capdata->data->oldemail)
26             && strcasecmp($Acct->email, $capdata->data->oldemail))
27        error_go(false, "You have changed your email address since creating that email change code.");
28
29    $email = $capdata->data->uemail;
30    if ($Conf->user_id_by_email($email))
31        error_go(false, "Email address “" . htmlspecialchars($email) . "” is already in use. You may want to <a href=\"" . hoturl("mergeaccounts") . "\">merge these accounts</a>.");
32
33    $Acct->change_email($email);
34    $capmgr->delete($capdata);
35
36    $Conf->confirmMsg("Your email address has been changed.");
37    if (!$Me->has_database_account() || $Me->contactId == $Acct->contactId)
38        $Me = $Acct->activate($Qreq);
39}
40if ($Qreq->changeemail)
41    change_email_by_capability($Qreq);
42
43if (!$Me->has_email())
44    $Me->escape();
45$newProfile = false;
46$useRequest = false;
47$UserStatus = new UserStatus($Me);
48
49if ($Qreq->u === null) {
50    if ($Qreq->user)
51        $Qreq->u = $Qreq->user;
52    else if ($Qreq->contact)
53        $Qreq->u = $Qreq->contact;
54    else if (preg_match(',\A/(?:new|[^\s/]+)\z,i', Navigation::path()))
55        $Qreq->u = substr(Navigation::path(), 1);
56}
57if ($Me->privChair && $Qreq->new)
58    $Qreq->u = "new";
59
60
61// Load user.
62$Acct = $Me;
63if ($Me->privChair && ($Qreq->u || $Qreq->search)) {
64    if ($Qreq->u === "new") {
65        $Acct = new Contact(null, $Conf);
66        $newProfile = true;
67    } else if (($id = cvtint($Qreq->u)) > 0)
68        $Acct = $Conf->user_by_id($id);
69    else if ($Qreq->u === "" && $Qreq->search)
70        Navigation::redirect_site("users");
71    else {
72        $Acct = $Conf->user_by_email($Qreq->u);
73        if (!$Acct && $Qreq->search) {
74            $cs = new ContactSearch(ContactSearch::F_USER, $Qreq->u, $Me);
75            if ($cs->ids) {
76                $Acct = $Conf->user_by_id($cs->ids[0]);
77                $list = new SessionList("u/all/" . urlencode($Qreq->search), $cs->ids, "“" . htmlspecialchars($Qreq->u) . "”", hoturl_site_relative_raw("users", ["t" => "all"]));
78                $list->set_cookie();
79                $Qreq->u = $Acct->email;
80                SelfHref::redirect($Qreq);
81            }
82        }
83    }
84}
85
86// Redirect if requested user isn't loaded user.
87if (!$Acct
88    || ($Qreq->u !== null
89        && $Qreq->u !== (string) $Acct->contactId
90        && strcasecmp($Qreq->u, $Acct->email)
91        && ($Acct->contactId || $Qreq->u !== "new"))
92    || (isset($Qreq->profile_contactid)
93        && $Qreq->profile_contactid !== (string) $Acct->contactId)) {
94    if (!$Acct)
95        Conf::msg_error("Invalid user.");
96    else if (isset($Qreq->save) || isset($Qreq->savebulk))
97        Conf::msg_error("You’re logged in as a different user now, so your changes were ignored.");
98    unset($Qreq->u, $Qreq->save, $Qreq->savebulk);
99    SelfHref::redirect($Qreq);
100}
101
102$need_highlight = false;
103if (($Acct->contactId != $Me->contactId || !$Me->has_database_account())
104    && $Acct->has_email()
105    && !$Acct->firstName && !$Acct->lastName && !$Acct->affiliation
106    && !$Qreq->post) {
107    $result = $Conf->qe_raw("select Paper.paperId, authorInformation from Paper join PaperConflict on (PaperConflict.paperId=Paper.paperId and PaperConflict.contactId=$Acct->contactId and PaperConflict.conflictType>=" . CONFLICT_AUTHOR . ")");
108    while (($prow = PaperInfo::fetch($result, $Me)))
109        foreach ($prow->author_list() as $au)
110            if (strcasecmp($au->email, $Acct->email) == 0
111                && ($au->firstName || $au->lastName || $au->affiliation)) {
112                if (!$Acct->firstName && $au->firstName) {
113                    $Acct->firstName = $au->firstName;
114                    $need_highlight = true;
115                }
116                if (!$Acct->lastName && $au->lastName) {
117                    $Acct->lastName = $au->lastName;
118                    $need_highlight = true;
119                }
120                if (!$Acct->affiliation && $au->affiliation) {
121                    $Acct->affiliation = $au->affiliation;
122                    $need_highlight = true;
123                }
124            }
125}
126
127
128function save_user($cj, $user_status, $Acct, $allow_modification) {
129    global $Conf, $Me, $Now, $newProfile;
130    if ($newProfile)
131        $Acct = null;
132
133    // check for missing fields
134    UserStatus::normalize_name($cj);
135    if ($newProfile && !isset($cj->email)) {
136        $user_status->error_at("email", "Email address required.");
137        return false;
138    }
139
140    // check email
141    if ($newProfile || strcasecmp($cj->email, $Acct->email)) {
142        if ($Acct && $Acct->data("locked"))
143            return $user_status->error_at("email", "This account is locked, so you can’t change its email address.");
144        else if (($new_acct = $Conf->user_by_email($cj->email))) {
145            if ($allow_modification)
146                $cj->id = $new_acct->contactId;
147            else {
148                $msg = "Email address “" . htmlspecialchars($cj->email) . "” is already in use.";
149                if ($Me->privChair)
150                    $msg = str_replace("an account", "<a href=\"" . hoturl("profile", "u=" . urlencode($cj->email)) . "\">an account</a>", $msg);
151                if (!$newProfile)
152                    $msg .= " You may want to <a href=\"" . hoturl("mergeaccounts") . "\">merge these accounts</a>.";
153                return $user_status->error_at("email", $msg);
154            }
155        } else if ($Conf->external_login()) {
156            if ($cj->email === "")
157                return $user_status->error_at("email", "Not a valid username.");
158        } else if ($cj->email === "") {
159            return $user_status->error_at("email", "You must supply an email address.");
160        } else if (!validate_email($cj->email)) {
161            return $user_status->error_at("email", "“" . htmlspecialchars($cj->email) . "” is not a valid email address.");
162        } else if ($Acct && !$Acct->has_database_account()) {
163            return $user_status->error_at("email", "Your current account is only active on other HotCRP.com sites. Due to a server limitation, you can’t change your email until activating your account on this site.");
164        }
165        if (!$newProfile && !$Me->privChair) {
166            $old_preferredEmail = $Acct->preferredEmail;
167            $Acct->preferredEmail = $cj->email;
168            $capmgr = $Conf->capability_manager();
169            $rest = array("capability" => $capmgr->create(CAPTYPE_CHANGEEMAIL, array("user" => $Acct, "timeExpires" => $Now + 259200, "data" => json_encode_db(array("oldemail" => $Acct->email, "uemail" => $cj->email)))));
170            $mailer = new HotCRPMailer($Conf, $Acct, null, $rest);
171            $prep = $mailer->make_preparation("@changeemail", $rest);
172            if ($prep->sendable) {
173                $prep->send();
174                $Conf->warnMsg("Mail has been sent to " . htmlspecialchars($cj->email) . ". Use the link it contains to confirm your email change request.");
175            } else
176                Conf::msg_error("Mail cannot be sent to " . htmlspecialchars($cj->email) . " at this time. Your email address was unchanged.");
177            // Save changes *except* for new email, by restoring old email.
178            $cj->email = $Acct->email;
179            $Acct->preferredEmail = $old_preferredEmail;
180        }
181    }
182
183    // save account
184    return $user_status->save($cj, $Acct);
185}
186
187
188function parseBulkFile($text, $filename) {
189    global $Conf, $Me;
190    $text = cleannl(convert_to_utf8($text));
191    $filename = $filename ? "$filename:" : "line ";
192    $success = $errors = array();
193
194    if (!preg_match('/\A[^\r\n]*(?:,|\A)(?:user|email)(?:[,\r\n]|\z)/', $text)
195        && !preg_match('/\A[^\r\n]*,[^\r\n]*,/', $text)) {
196        $tarr = CsvParser::split_lines($text);
197        foreach ($tarr as &$t) {
198            if (($t = trim($t)) && $t[0] !== "#" && $t[0] !== "%")
199                $t = CsvGenerator::quote($t);
200            $t .= "\n";
201        }
202        unset($t);
203        $text = join("", $tarr);
204    }
205
206    $csv = new CsvParser($text);
207    $csv->set_comment_chars("#%");
208    if (($line = $csv->next())) {
209        $lcline = array_map(function ($a) { return strtolower(trim($a)); }, $line);
210        if (array_search("email", $lcline) !== false
211            || array_search("user", $lcline) !== false)
212            $csv->set_header($lcline);
213        else if (count($line) == 1) {
214            $csv->set_header(["user"]);
215            $csv->unshift($line);
216        } else {
217            // interpolate a likely header
218            $lcline = [];
219            for ($i = 0; $i < count($line); ++$i)
220                if (validate_email($line[$i]) && array_search("email", $lcline) === false)
221                    $lcline[] = "email";
222                else if (strpos($line[$i], " ") !== false && array_search("name", $lcline) === false)
223                    $lcline[] = "name";
224                else if (array_search($line[$i], ["pc", "chair"]) !== false && array_search("roles", $lcline) === false)
225                    $lcline[] = "roles";
226                else if (array_search("name", $lcline) !== false && array_search("affiliation", $lcline) === false)
227                    $lcline[] = "affiliation";
228                else
229                    $lcline[] = "unknown";
230            $csv->set_header($lcline);
231            $csv->unshift($line);
232            $errors[] = "<span class='lineno'>" . $filename . $csv->lineno() . ":</span> Header missing, assuming “<code>" . join(",", $lcline) . "</code>”";
233        }
234
235    }
236
237    $saved_users = [];
238    $ustatus = new UserStatus($Me, ["send_email" => true, "no_deprivilege_self" => true]);
239
240    while (($line = $csv->next()) !== false) {
241        $ustatus->set_user(new Contact(null, $Conf));
242        $ustatus->clear_messages();
243        $cj = (object) ["id" => null];
244        $ustatus->parse_csv_group("", $cj, $line);
245
246        if (isset($cj->email) && isset($saved_users[strtolower($cj->email)])) {
247            $errors[] = '<span class="lineno">' . $filename . $csv->lineno() . ":</span> Already saved a user with email “" . htmlspecialchars($cj->email) . "”.";
248            $errors[] = '<span class="lineno">' . $filename . $saved_users[strtolower($cj->email)] . ":</span> (That user was saved here.)";
249            continue;
250        }
251
252        if (isset($cj->email) && $cj->email !== "")
253            $saved_users[strtolower($cj->email)] = $csv->lineno();
254        if (($saved_user = save_user($cj, $ustatus, null, true)))
255            $success[] = "<a href=\"" . hoturl("profile", "u=" . urlencode($saved_user->email)) . "\">"
256                . Text::user_html_nolink($saved_user) . "</a>";
257        foreach ($ustatus->errors() as $e)
258            $errors[] = '<span class="lineno">' . $filename . $csv->lineno() . ":</span> " . $e;
259    }
260
261    if (!empty($ustatus->unknown_topics))
262        $errors[] = "There were unrecognized topics (" . htmlspecialchars(commajoin(array_keys($ustatus->unknown_topics))) . ").";
263    if (count($success) == 1)
264        $successMsg = "Saved account " . $success[0] . ".";
265    else if (count($success))
266        $successMsg = "Saved " . plural($success, "account") . ": " . commajoin($success) . ".";
267    if (count($errors))
268        $errorMsg = "<div class='parseerr'><p>" . join("</p>\n<p>", $errors) . "</p></div>";
269    if (count($success) && count($errors))
270        $Conf->confirmMsg($successMsg . "<br />$errorMsg");
271    else if (count($success))
272        $Conf->confirmMsg($successMsg);
273    else if (count($errors))
274        Conf::msg_error($errorMsg);
275    else
276        $Conf->warnMsg("Nothing to do.");
277    return empty($errors);
278}
279
280if (!$Qreq->post_ok())
281    /* do nothing */;
282else if ($Qreq->savebulk && $newProfile && $Qreq->has_file("bulk")) {
283    if (($text = $Qreq->file_contents("bulk")) === false)
284        Conf::msg_error("Internal error: cannot read file.");
285    else
286        parseBulkFile($text, $Qreq->file_filename("bulk"));
287    $Qreq->bulkentry = "";
288    SelfHref::redirect($Qreq, ["anchor" => "bulk"]);
289} else if ($Qreq->savebulk && $newProfile) {
290    $success = true;
291    if ($Qreq->bulkentry && $Qreq->bulkentry !== "Enter users one per line")
292        $success = parseBulkFile($Qreq->bulkentry, "");
293    if (!$success)
294        $Conf->save_session("profile_bulkentry", array($Now, $Qreq->bulkentry));
295    SelfHref::redirect($Qreq, ["anchor" => "bulk"]);
296} else if (isset($Qreq->save)) {
297    assert($Acct->is_empty() === $newProfile);
298    $cj = (object) ["id" => $Acct->has_database_account() ? $Acct->contactId : "new"];
299    $UserStatus->set_user($Acct);
300    $UserStatus->parse_request_group("", $cj, $Qreq);
301    if ($newProfile)
302        $UserStatus->send_email = true;
303    $saved_user = save_user($cj, $UserStatus, $Acct, false);
304    if (!$UserStatus->has_error()) {
305        if ($UserStatus->has_messages())
306            $Conf->msg($UserStatus->problem_status(), $UserStatus->messages());
307        if ($newProfile) {
308            $Conf->msg("xconfirm", "Created an account for <a href=\"" . hoturl("profile", "u=" . urlencode($saved_user->email)) . "\">" . Text::user_html_nolink($saved_user) . "</a>. A password has been emailed to that address. You may now create another account.");
309        } else {
310            $Conf->msg("xconfirm", "Profile updated.");
311            if ($Acct->contactId != $Me->contactId)
312                $Qreq->u = $Acct->email;
313        }
314        if (isset($Qreq->redirect))
315            go(hoturl("index"));
316        else {
317            $xcj = [];
318            if ($newProfile) {
319                foreach (["roles", "follow", "tags"] as $k)
320                    if (isset($cj->$k))
321                        $xcj[$k] = $cj->$k;
322            }
323            if ($UserStatus->has_warning())
324                $xcj["warning_fields"] = $UserStatus->problem_fields();
325            $Conf->save_session("profile_redirect", $xcj);
326            SelfHref::redirect($Qreq);
327        }
328    }
329} else if (isset($Qreq->merge) && !$newProfile
330           && $Acct->contactId == $Me->contactId)
331    go(hoturl("mergeaccounts"));
332
333function databaseTracks($who) {
334    global $Conf;
335    $tracks = (object) array("soleAuthor" => array(),
336                             "author" => array(),
337                             "review" => array(),
338                             "comment" => array());
339
340    // find authored papers
341    $result = $Conf->qe_raw("select Paper.paperId, count(pc.contactId)
342        from Paper
343        join PaperConflict c on (c.paperId=Paper.paperId and c.contactId=$who and c.conflictType>=" . CONFLICT_AUTHOR . ")
344        join PaperConflict pc on (pc.paperId=Paper.paperId and pc.conflictType>=" . CONFLICT_AUTHOR . ")
345        group by Paper.paperId order by Paper.paperId");
346    while (($row = edb_row($result))) {
347        if ($row[1] == 1)
348            $tracks->soleAuthor[] = $row[0];
349        $tracks->author[] = $row[0];
350    }
351
352    // find reviews
353    $result = $Conf->qe_raw("select paperId from PaperReview
354        where PaperReview.contactId=$who
355        group by paperId order by paperId");
356    while (($row = edb_row($result)))
357        $tracks->review[] = $row[0];
358
359    // find comments
360    $result = $Conf->qe_raw("select paperId from PaperComment
361        where PaperComment.contactId=$who
362        group by paperId order by paperId");
363    while (($row = edb_row($result)))
364        $tracks->comment[] = $row[0];
365
366    return $tracks;
367}
368
369function textArrayPapers($pids) {
370    return commajoin(preg_replace('/(\d+)/', "<a href='" . hoturl("paper", "p=\$1&amp;ls=" . join("+", $pids)) . "'>\$1</a>", $pids));
371}
372
373if (isset($Qreq->delete) && !Dbl::has_error() && $Qreq->post_ok()) {
374    if (!$Me->privChair)
375        Conf::msg_error("Only administrators can delete accounts.");
376    else if ($Acct->contactId == $Me->contactId)
377        Conf::msg_error("You aren’t allowed to delete your own account.");
378    else if ($Acct->has_database_account()) {
379        $tracks = databaseTracks($Acct->contactId);
380        if (!empty($tracks->soleAuthor))
381            Conf::msg_error("This account can’t be deleted since it is sole contact for " . pluralx($tracks->soleAuthor, "paper") . " " . textArrayPapers($tracks->soleAuthor) . ". You will be able to delete the account after deleting those papers or adding additional paper contacts.");
382        else if ($Acct->data("locked"))
383            Conf::msg_error("This account is locked and can’t be deleted.");
384        else {
385            $Conf->q("insert into DeletedContactInfo set contactId=?, firstName=?, lastName=?, unaccentedName=?, email=?", $Acct->contactId, $Acct->firstName, $Acct->lastName, $Acct->unaccentedName, $Acct->email);
386            foreach (array("ContactInfo",
387                           "PaperComment", "PaperConflict", "PaperReview",
388                           "PaperReviewPreference", "PaperReviewRefused",
389                           "PaperWatch", "ReviewRating", "TopicInterest")
390                     as $table)
391                $Conf->qe_raw("delete from $table where contactId=$Acct->contactId");
392            // delete twiddle tags
393            $assigner = new AssignmentSet($Me, true);
394            $assigner->parse("paper,tag\nall,{$Acct->contactId}~all#clear\n");
395            $assigner->execute();
396            // clear caches
397            if ($Acct->isPC || $Acct->privChair)
398                $Conf->invalidate_caches(["pc" => 1]);
399            // done
400            $Conf->confirmMsg("Permanently deleted account " . htmlspecialchars($Acct->email) . ".");
401            $Me->log_activity_for($Acct, "Permanently deleted account " . htmlspecialchars($Acct->email));
402            go(hoturl("users", "t=all"));
403        }
404    }
405}
406
407function echo_modes($hlbulk) {
408    global $Me, $Acct, $newProfile;
409    echo '<div class="psmode">',
410        '<div class="', ($hlbulk == 0 ? "papmodex" : "papmode"), '">',
411        Ht::link($newProfile || $Me->email == $Acct->email ? "Your profile" : "Profile", selfHref(["u" => null])),
412        '</div><div class="', ($hlbulk == 1 ? "papmodex" : "papmode"), '">';
413    if ($newProfile)
414        echo Ht::link("New account", "", ["class" => "ui tla has-focus-history", "data-fold-target" => "9c"]);
415    else
416        echo Ht::link("New account", hoturl("profile", "u=new"));
417    echo '</div><div class="', ($hlbulk == 2 ? "papmodex" : "papmode"), '">';
418    if ($newProfile)
419        echo Ht::link("Bulk update", "#bulk", ["class" => "ui tla has-focus-history", "data-fold-target" => "9o"]);
420    else
421        echo Ht::link("Bulk update", hoturl("profile", "u=new#bulk"));
422    echo '</div></div><hr class="c" style="margin-bottom:24px" />', "\n";
423}
424
425
426// set session list
427if (!$newProfile
428    && isset($_COOKIE["hotlist-info"])
429    && ($list = SessionList::decode_info_string($_COOKIE["hotlist-info"]))
430    && $list->list_type() === "u"
431    && $list->set_current_id($Acct->contactId)) {
432    $Conf->set_active_list($list);
433}
434
435if ($newProfile) {
436    $title = "User update";
437} else if (strcasecmp($Me->email, $Acct->email) == 0) {
438    $title = "Profile";
439} else {
440    $title = $Me->name_html_for($Acct) . " profile";
441}
442$Conf->header($title, "account", ["action_bar" => actionBar("account")]);
443
444$useRequest = !$Acct->has_database_account() && isset($Qreq->watchreview);
445if ($UserStatus->has_error())
446    $need_highlight = $useRequest = true;
447
448if (!$UserStatus->has_error() && $Conf->session("freshlogin") === "redirect")
449    $Conf->save_session("freshlogin", null);
450// Set warnings
451if (!$newProfile) {
452    if (!$Acct->firstName && !$Acct->lastName) {
453        $UserStatus->warning_at("firstName", "Please enter your name.");
454        $UserStatus->warning_at("lastName", false);
455    }
456    if (!$Acct->affiliation)
457        $UserStatus->warning_at("affiliation", "Please enter your affiliation (use “None” or “Unaffiliated” if you have none).");
458    if ($Acct->is_pc_member()) {
459        if (!$Acct->collaborators)
460            $UserStatus->warning_at("collaborators", "Please enter your recent collaborators and other affiliations. This information can help detect conflicts of interest. Enter “None” if you have none.");
461        if ($Conf->topic_map() && !$Acct->topic_interest_map())
462            $UserStatus->warning_at("topics", "Please enter your topic interests. We use topic interests to improve the paper assignment process.");
463    }
464}
465
466
467$UserStatus->set_user($Acct);
468$userj = $UserStatus->user_json(["include_password" => true]);
469if (!$useRequest && $Me->privChair && $Acct->is_empty()
470    && ($Qreq->role === "chair" || $Qreq->role === "pc")) {
471    $userj->roles = (object) [$Qreq->role => true];
472}
473
474if ($useRequest) {
475    $UserStatus->ignore_msgs = true;
476    $formcj = (object) ["id" => $Acct->has_database_account() ? $Acct->contactId : "new"];
477    $UserStatus->parse_request_group("", $formcj, $Qreq);
478} else {
479    $formcj = $userj;
480}
481if (($prdj = $Conf->session("profile_redirect"))) {
482    $Conf->save_session("profile_redirect", null);
483    foreach ($prdj as $k => $v) {
484        if ($k === "warning_fields") {
485            foreach ($v as $k)
486                $UserStatus->warning_at($k, null);
487        } else {
488            $formcj->$k = $v;
489        }
490    }
491}
492
493$form_params = array();
494if ($newProfile)
495    $form_params[] = "u=new";
496else if ($Me->contactId != $Acct->contactId)
497    $form_params[] = "u=" . urlencode($Acct->email);
498if (isset($Qreq->ls))
499    $form_params[] = "ls=" . urlencode($Qreq->ls);
500if ($newProfile)
501    echo '<div id="foldbulk" class="fold9' . ($Qreq->savebulk ? "o" : "c") . ' js-fold-focus"><div class="fn9">';
502
503echo Ht::form(hoturl_post("profile", join("&amp;", $form_params)),
504              array("id" => "profile-form")),
505    // Don't want chrome to autofill the password changer.
506    // But chrome defaults to autofilling the password changer
507    // unless we supply an earlier password input.
508    Ht::password("chromefooler", "", ["class" => "ignore-diff hidden"]),
509    Ht::hidden("profile_contactid", $Acct->contactId);
510if (isset($Qreq->redirect))
511    echo Ht::hidden("redirect", $Qreq->redirect);
512if ($Me->privChair)
513    echo Ht::hidden("whichpassword", "");
514
515if ($newProfile)
516    echo_modes(1);
517else if ($Me->privChair)
518    echo_modes(0);
519
520if ($UserStatus->has_messages()) {
521    $status = 0;
522    $msgs = [];
523    foreach ($UserStatus->messages(true) as $m) {
524        $status = max($m[2], $status);
525        $msgs[] = '<div class="mmm">' . $m[1] . '</div>';
526    }
527    echo '<div class="msgs-wide">', Ht::xmsg($status, join("", $msgs)), "</div>\n";
528}
529
530echo '<div id="foldaccount" class="';
531if (isset($formcj->roles)
532    && (isset($formcj->roles->pc) || isset($formcj->roles->chair)))
533    echo "fold1o fold2o";
534else if (isset($formcj->roles) && isset($formcj->roles->sysadmin))
535    echo "fold1c fold2o";
536else
537    echo "fold1c fold2c";
538echo "\">\n";
539
540
541$UserStatus->set_user($Acct);
542$UserStatus->render_group("", $userj, $formcj);
543
544if ($UserStatus->global_user() && false) {
545    echo '<div class="profile-g"><div class="checki"><label><span class="checkc">',
546        Ht::checkbox("saveglobal", 1, $useRequest ? !!$Qreq->saveglobal : true, ["class" => "ignore-diff"]),
547        '</span>Update global profile</label></div></div>';
548}
549
550$buttons = [Ht::submit("save", $newProfile ? "Create account" : "Save changes", ["class" => "btn btn-primary"]),
551    Ht::submit("cancel", "Cancel", ["class" => "btn"])];
552
553if ($Me->privChair && !$newProfile && $Me->contactId != $Acct->contactId) {
554    $tracks = databaseTracks($Acct->contactId);
555    $args = ["class" => "btn ui"];
556    if (!empty($tracks->soleAuthor)) {
557        $args["class"] .= " js-cannot-delete-user";
558        $args["data-sole-author"] = pluralx($tracks->soleAuthor, "paper") . " " . textArrayPapers($tracks->soleAuthor);
559    } else {
560        $args["class"] .= " js-delete-user";
561        $x = $y = array();
562        if (!empty($tracks->author)) {
563            $x[] = "contact for " . pluralx($tracks->author, "paper") . " " . textArrayPapers($tracks->author);
564            $y[] = "delete " . pluralx($tracks->author, "this") . " " . pluralx($tracks->author, "authorship association");
565        }
566        if (!empty($tracks->review)) {
567            $x[] = "reviewer for " . pluralx($tracks->review, "paper") . " " . textArrayPapers($tracks->review);
568            $y[] = "<strong>permanently delete</strong> " . pluralx($tracks->review, "this") . " " . pluralx($tracks->review, "review");
569        }
570        if (!empty($tracks->comment)) {
571            $x[] = "commenter for " . pluralx($tracks->comment, "paper") . " " . textArrayPapers($tracks->comment);
572            $y[] = "<strong>permanently delete</strong> " . pluralx($tracks->comment, "this") . " " . pluralx($tracks->comment, "comment");
573        }
574        if (!empty($x)) {
575            $args["data-delete-info"] = "<p>This user is " . commajoin($x) . ". Deleting the user will also " . commajoin($y) . ".</p>";
576        }
577    }
578    $buttons[] = "";
579    $buttons[] = [Ht::button("Delete user", $args), "(admin only)"];
580}
581if (!$newProfile && $Acct->contactId == $Me->contactId)
582    array_push($buttons, "", Ht::submit("merge", "Merge with another account"));
583
584echo Ht::actions($buttons, ["class" => "aab aabr aabig"]);
585
586echo "</div>\n", // foldaccount
587    "</form>\n";
588
589if ($newProfile) {
590    echo '</div><div class="fx9">';
591    echo Ht::form(hoturl_post("profile", join("&amp;", $form_params))),
592        "<div class='profiletext", ($UserStatus->has_error() ? " alert" : ""), "'>\n",
593        // Don't want chrome to autofill the password changer.
594        // But chrome defaults to autofilling the password changer
595        // unless we supply an earlier password input.
596        Ht::password("chromefooler", "", ["class" => "ignore-diff hidden"]);
597    echo_modes(2);
598
599    $bulkentry = $Qreq->bulkentry;
600    if ($bulkentry === null
601        && ($session_bulkentry = $Conf->session("profile_bulkentry"))
602        && is_array($session_bulkentry) && $session_bulkentry[0] > $Now - 5) {
603        $bulkentry = $session_bulkentry[1];
604        $Conf->save_session("profile_bulkentry", null);
605    }
606    echo '<div class="f-i">',
607        Ht::textarea("bulkentry", $bulkentry,
608                     ["rows" => 1, "cols" => 80, "placeholder" => "Enter users one per line", "class" => "want-focus need-autogrow"]),
609        '</div>';
610
611    echo '<div class="g"><strong>OR</strong> &nbsp;',
612        '<input type="file" name="bulk" size="30" /></div>';
613
614    echo '<div>', Ht::submit("savebulk", "Save accounts", ["class" => "btn btn-primary"]), '</div>';
615
616    echo "<p>Enter or upload CSV user data with header. For example:</p>\n",
617        '<pre class="entryexample">
618name,email,affiliation,roles
619John Adams,john@earbox.org,UC Berkeley,pc
620"Adams, John Quincy",quincy@whitehouse.gov
621</pre>', "\n",
622        '<p>Or just enter an email address per line.</p>',
623        '<p>Supported CSV fields include:</p><table>',
624        '<tr><td class="lmcaption"><code>name</code></td>',
625          '<td>User name</td></tr>',
626        '<tr><td class="lmcaption"><code>first</code></td>',
627          '<td>First name</td></tr>',
628        '<tr><td class="lmcaption"><code>last</code></td>',
629          '<td>Last name</td></tr>',
630        '<tr><td class="lmcaption"><code>affiliation</code></td>',
631          '<td>Affiliation</td></tr>',
632        '<tr><td class="lmcaption"><code>roles</code></td>',
633          '<td>User roles: blank, “<code>pc</code>”, “<code>chair</code>”, or “<code>sysadmin</code>”</td></tr>',
634        '<tr><td class="lmcaption"><code>tags</code></td>',
635          '<td>PC tags (space-separated)</td></tr>',
636        '<tr><td class="lmcaption"><code>add_tags</code>, <code>remove_tags</code></td>',
637          '<td>PC tags to add or remove</td></tr>',
638        '<tr><td class="lmcaption"><code>collaborators</code></td>',
639          '<td>Collaborators</td></tr>',
640        '<tr><td class="lmcaption"><code>follow</code></td>',
641          '<td>Email notification: blank, “<code>reviews</code>”, “<code>allreviews</code>”</td></tr>',
642        "</table>\n";
643
644    echo '</div></form></div></div>';
645}
646
647
648Ht::stash_script('focus_within($("#profile-form"))');
649if ($newProfile)
650    Ht::stash_script("focus_fold.hash(true)");
651else
652    Ht::stash_script('hiliter_children("#profile-form",true)');
653$Conf->footer();
654