1<?php
2// contact.php -- HotCRP helper class representing system users
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class Contact_Update {
6    public $qv = [];
7    public $cdb_qf = [];
8    public $changing_email;
9    function __construct($changing_email) {
10        $this->changing_email = $changing_email;
11    }
12}
13
14class Contact {
15    static public $rights_version = 1;
16    static public $trueuser_privChair = null;
17    static public $allow_nonexistent_properties = false;
18
19    public $contactId = 0;
20    public $contactDbId = 0;
21    public $conf;
22    public $confid;
23
24    public $firstName = "";
25    public $lastName = "";
26    public $unaccentedName = "";
27    public $nameAmbiguous;
28    public $email = "";
29    public $preferredEmail = "";
30    public $sorter = "";
31    public $sort_position;
32
33    public $affiliation = "";
34    public $country;
35    public $collaborators;
36    public $phone;
37    public $birthday;
38    public $gender;
39
40    private $password = "";
41    private $passwordTime = 0;
42    private $passwordUseTime = 0;
43    private $_contactdb_user = false;
44
45    public $disabled = false;
46    public $activity_at = false;
47    private $lastLogin;
48    public $creationTime = 0;
49    private $updateTime = 0;
50    private $data = null;
51    private $_topic_interest_map;
52    private $_name_for_map = [];
53    private $_contact_sorter_map = [];
54    const WATCH_REVIEW_EXPLICIT = 1;  // only in PaperWatch
55    const WATCH_REVIEW = 2;
56    const WATCH_REVIEW_ALL = 4;
57    const WATCH_REVIEW_MANAGED = 8;
58    const WATCH_FINAL_SUBMIT_ALL = 32;
59    public $defaultWatch = self::WATCH_REVIEW;
60
61    // Roles
62    const ROLE_PC = 1;
63    const ROLE_ADMIN = 2;
64    const ROLE_CHAIR = 4;
65    const ROLE_PCLIKE = 15;
66    const ROLE_AUTHOR = 16;
67    const ROLE_REVIEWER = 32;
68    const ROLE_REQUESTER = 64;
69    private $_db_roles;
70    private $_active_roles;
71    private $_has_outstanding_review;
72    private $_is_metareviewer;
73    private $_is_lead;
74    private $_is_explicit_manager;
75    private $_dangerous_track_mask;
76    private $_can_view_pc;
77    public $is_site_contact = false;
78    private $_rights_version = 0;
79    public $roles = 0;
80    public $isPC = false;
81    public $privChair = false;
82    public $contactTags;
83    public $tracker_kiosk_state = false;
84    const CAP_AUTHORVIEW = 1;
85    private $capabilities;
86    private $_review_tokens;
87    private $_activated = false;
88    const OVERRIDE_CONFLICT = 1;
89    const OVERRIDE_TIME = 2;
90    const OVERRIDE_TAG_CHECKS = 4;
91    const OVERRIDE_EDIT_CONDITIONS = 8;
92    private $_overrides = 0;
93    public $hidden_papers;
94    private $_aucollab_matchers;
95    private $_aucollab_general_pregexes;
96    private $_authored_papers;
97
98    // Per-paper DB information, usually null
99    public $conflictType;
100    public $myReviewPermissions;
101    public $watch;
102
103    static private $status_info_cache = array();
104
105
106    function __construct($trueuser = null, Conf $conf = null) {
107        global $Conf;
108        $this->conf = $conf ? : $Conf;
109        if ($trueuser)
110            $this->merge($trueuser);
111        else if ($this->contactId || $this->contactDbId)
112            $this->db_load();
113        else if ($this->conf->opt("disableNonPC"))
114            $this->disabled = true;
115    }
116
117    static function fetch($result, Conf $conf) {
118        $user = $result ? $result->fetch_object("Contact", [null, $conf]) : null;
119        if ($user && !is_int($user->contactId)) {
120            $user->conf = $conf;
121            $user->db_load();
122        }
123        return $user;
124    }
125
126    private function merge($user) {
127        if (is_array($user))
128            $user = (object) $user;
129        if (!isset($user->dsn) || $user->dsn == $this->conf->dsn) {
130            if (isset($user->contactId))
131                $this->contactId = (int) $user->contactId;
132        }
133        if (isset($user->contactDbId))
134            $this->contactDbId = (int) $user->contactDbId;
135        if (isset($user->firstName) && isset($user->lastName))
136            $name = $user;
137        else
138            $name = Text::analyze_name($user);
139        $this->firstName = get_s($name, "firstName");
140        $this->lastName = get_s($name, "lastName");
141        if (isset($user->unaccentedName))
142            $this->unaccentedName = $user->unaccentedName;
143        else if (isset($name->unaccentedName))
144            $this->unaccentedName = $name->unaccentedName;
145        else
146            $this->unaccentedName = Text::unaccented_name($name);
147        foreach (["email", "preferredEmail", "affiliation", "phone",
148                  "country", "birthday", "gender"] as $k)
149            if (isset($user->$k))
150                $this->$k = simplify_whitespace($user->$k);
151        if (isset($user->collaborators))
152            $this->collaborators = $user->collaborators;
153        self::set_sorter($this, $this->conf);
154        if (isset($user->password))
155            $this->password = (string) $user->password;
156        if (isset($user->disabled))
157            $this->disabled = !!$user->disabled;
158        foreach (["defaultWatch", "passwordTime", "passwordUseTime",
159                  "updateTime", "creationTime"] as $k)
160            if (isset($user->$k))
161                $this->$k = (int) $user->$k;
162        if (property_exists($user, "contactTags"))
163            $this->contactTags = $user->contactTags;
164        else
165            $this->contactTags = $this->contactId ? false : null;
166        if (isset($user->activity_at))
167            $this->activity_at = (int) $user->activity_at;
168        else if (isset($user->lastLogin))
169            $this->activity_at = (int) $user->lastLogin;
170        if (isset($user->birthday))
171            $this->birthday = (int) $user->birthday;
172        if (isset($user->data) && $user->data)
173            // this works even if $user->data is a JSON string
174            // (array_to_object_recursive($str) === $str)
175            $this->data = array_to_object_recursive($user->data);
176        if (isset($user->roles) || isset($user->isPC) || isset($user->isAssistant)
177            || isset($user->isChair)) {
178            $roles = (int) get($user, "roles");
179            if (get($user, "isPC"))
180                $roles |= self::ROLE_PC;
181            if (get($user, "isAssistant"))
182                $roles |= self::ROLE_ADMIN;
183            if (get($user, "isChair"))
184                $roles |= self::ROLE_CHAIR;
185            $this->assign_roles($roles);
186        }
187        if (!$this->isPC && $this->conf->opt("disableNonPC"))
188            $this->disabled = true;
189        if (isset($user->has_review))
190            $this->has_review_ = $user->has_review;
191        if (isset($user->has_outstanding_review))
192            $this->_has_outstanding_review = $user->has_outstanding_review;
193        if (isset($user->is_site_contact))
194            $this->is_site_contact = $user->is_site_contact;
195    }
196
197    private function db_load() {
198        $this->contactId = (int) $this->contactId;
199        $this->contactDbId = (int) $this->contactDbId;
200        if ($this->unaccentedName === "")
201            $this->unaccentedName = Text::unaccented_name($this->firstName, $this->lastName);
202        self::set_sorter($this, $this->conf);
203        $this->password = (string) $this->password;
204        if (isset($this->disabled))
205            $this->disabled = !!$this->disabled;
206        foreach (["defaultWatch", "passwordTime", "passwordUseTime",
207                  "updateTime", "creationTime"] as $k)
208            $this->$k = (int) $this->$k;
209        if (!$this->activity_at && isset($this->lastLogin))
210            $this->activity_at = (int) $this->lastLogin;
211        if (isset($this->birthday))
212            $this->birthday = (int) $this->birthday;
213        if ($this->data)
214            // this works even if $user->data is a JSON string
215            // (array_to_object_recursive($str) === $str)
216            $this->data = array_to_object_recursive($this->data);
217        if (isset($this->roles))
218            $this->assign_roles((int) $this->roles);
219        if (isset($this->__isAuthor__))
220            $this->_db_roles = ((int) $this->__isAuthor__ > 0 ? self::ROLE_AUTHOR : 0)
221                | ((int) $this->__hasReview__ > 0 ? self::ROLE_REVIEWER : 0);
222        if (!$this->isPC && $this->conf->opt("disableNonPC"))
223            $this->disabled = true;
224    }
225
226    function merge_secondary_properties($x) {
227        foreach (["preferredEmail", "phone", "country", "password",
228                  "collaborators", "birthday", "gender"] as $k)
229            if (isset($x->$k))
230                $this->$k = $x->$k;
231        foreach (["passwordTime", "passwordUseTime", "creationTime",
232                  "updateTime", "defaultWatch"] as $k)
233            if (isset($x->$k))
234                $this->$k = (int) $x->$k;
235        if (isset($x->lastLogin))
236            $this->activity_at = $this->lastLogin = (int) $x->lastLogin;
237        if ($x->data)
238            $this->data = array_to_object_recursive($x->data);
239    }
240
241    function __set($name, $value) {
242        if (!self::$allow_nonexistent_properties)
243            error_log(caller_landmark(1) . ": writing nonexistent property $name");
244        $this->$name = $value;
245    }
246
247    static function set_sorter($c, Conf $conf) {
248        if (!$conf->sort_by_last && isset($c->unaccentedName)) {
249            $c->sorter = trim("$c->unaccentedName $c->email");
250            return;
251        }
252        if ($conf->sort_by_last) {
253            if (($m = Text::analyze_von($c->lastName)))
254                $c->sorter = trim("$m[1] $c->firstName $m[0] $c->email");
255            else
256                $c->sorter = trim("$c->lastName $c->firstName $c->email");
257        } else
258            $c->sorter = trim("$c->firstName $c->lastName $c->email");
259        if (preg_match('/[\x80-\xFF]/', $c->sorter))
260            $c->sorter = UnicodeHelper::deaccent($c->sorter);
261    }
262
263    static function compare($a, $b) {
264        return strnatcasecmp($a->sorter, $b->sorter);
265    }
266
267    private function assign_roles($roles) {
268        $this->roles = $roles;
269        $this->isPC = ($roles & self::ROLE_PCLIKE) != 0;
270        $this->privChair = ($roles & (self::ROLE_ADMIN | self::ROLE_CHAIR)) != 0;
271    }
272
273
274    // initialization
275
276    private function actas_user($x, $trueuser) {
277        // translate to email
278        if (is_numeric($x)) {
279            $acct = $this->conf->user_by_id($x);
280            $email = $acct ? $acct->email : null;
281        } else if ($x === "admin")
282            $email = $trueuser->email;
283        else
284            $email = $x;
285        if (!$email || strcasecmp($email, $this->email) == 0)
286            return $this;
287
288        // can always turn back into baseuser
289        $baseuser = $this;
290        if (strcasecmp($this->email, $trueuser->email) != 0
291            && ($u = $this->conf->user_by_email($trueuser->email)))
292            $baseuser = $u;
293        if (strcasecmp($email, $baseuser->email) == 0)
294            return $baseuser;
295
296        // cannot actas unless chair
297        if (!$this->privChair && !$baseuser->privChair)
298            return $this;
299
300        // new account must exist
301        $u = $this->conf->user_by_email($email);
302        if (!$u && validate_email($email) && get($this->conf->opt, "debugShowSensitiveEmail"))
303            $u = Contact::create($this->conf, null, ["email" => $email]);
304        if (!$u)
305            return $this;
306
307        // cannot turn into a manager of conflicted papers
308        if ($this->conf->setting("papermanager")) {
309            $result = $this->conf->qe("select paperId from Paper join PaperConflict using (paperId) where managerContactId!=0 and managerContactId!=? and PaperConflict.contactId=? and conflictType>0", $this->contactId, $this->contactId);
310            while (($row = $result->fetch_row()))
311                $u->hidden_papers[(int) $row[0]] = false;
312            Dbl::free($result);
313        }
314
315        // otherwise ok
316        return $u;
317    }
318
319    function activate($qreq) {
320        global $Now;
321        $this->_activated = true;
322        $trueuser = isset($_SESSION["trueuser"]) ? $_SESSION["trueuser"] : null;
323        $truecontact = null;
324
325        // Handle actas requests
326        if ($qreq && $qreq->actas && $trueuser) {
327            $actas = $qreq->actas;
328            unset($qreq->actas, $_GET["actas"], $_POST["actas"]);
329            $actascontact = $this->actas_user($actas, $trueuser);
330            if ($actascontact !== $this) {
331                if ($actascontact->email !== $trueuser->email) {
332                    hoturl_defaults(array("actas" => $actascontact->email));
333                    $_SESSION["last_actas"] = $actascontact->email;
334                }
335                if ($this->privChair)
336                    self::$trueuser_privChair = $actascontact;
337                return $actascontact->activate($qreq);
338            }
339        }
340
341        // Handle invalidate-caches requests
342        if ($qreq && $qreq->invalidatecaches && $this->privChair) {
343            unset($qreq->invalidatecaches);
344            $this->conf->invalidate_caches();
345        }
346
347        // Add capabilities from session and request
348        if (!$this->conf->opt("disableCapabilities")) {
349            if (($caps = $this->conf->session("capabilities"))) {
350                $this->capabilities = $caps;
351                ++self::$rights_version;
352            }
353            if ($qreq && (isset($qreq->cap) || isset($qreq->testcap)))
354                $this->activate_capabilities($qreq);
355        }
356
357        // Add review tokens from session
358        if (($rtokens = $this->conf->session("rev_tokens"))) {
359            $this->_review_tokens = $rtokens;
360            ++self::$rights_version;
361        }
362
363        // Maybe auto-create a user
364        if ($trueuser
365            && strcasecmp($trueuser->email, $this->email) == 0) {
366            $trueuser_aucheck = $this->conf->session("trueuser_author_check", 0);
367            if (!$this->has_database_account()
368                && $trueuser_aucheck + 600 < $Now) {
369                $this->conf->save_session("trueuser_author_check", $Now);
370                $aupapers = self::email_authored_papers($this->conf, $this->email, $this);
371                if (!empty($aupapers))
372                    $this->activate_database_account();
373            }
374            if ($this->has_database_account()
375                && $trueuser_aucheck) {
376                foreach ($_SESSION as $k => $v) {
377                    if (is_array($v)
378                        && isset($v["trueuser_author_check"])
379                        && $v["trueuser_author_check"] + 600 < $Now)
380                        unset($_SESSION[$k]["trueuser_author_check"]);
381                }
382            }
383        }
384
385        // Maybe set up the shared contacts database
386        if ($this->conf->opt("contactdb_dsn")
387            && $this->has_database_account()
388            && $this->conf->session("contactdb_roles", 0) != $this->contactdb_roles()) {
389            if ($this->contactdb_update())
390                $this->conf->save_session("contactdb_roles", $this->contactdb_roles());
391        }
392
393        // Check forceShow
394        $this->_overrides = 0;
395        if ($qreq && $qreq->forceShow && $this->privChair)
396            $this->_overrides |= self::OVERRIDE_CONFLICT;
397        if ($qreq && $qreq->override)
398            $this->_overrides |= self::OVERRIDE_TIME;
399
400        return $this;
401    }
402
403    function overrides() {
404        return $this->_overrides;
405    }
406    function set_overrides($overrides) {
407        $old_overrides = $this->_overrides;
408        if (!$this->privChair)
409            $overrides &= ~self::OVERRIDE_CONFLICT;
410        $this->_overrides = $overrides;
411        return $old_overrides;
412    }
413    function add_overrides($overrides) {
414        return $this->set_overrides($this->_overrides | $overrides);
415    }
416    function remove_overrides($overrides) {
417        return $this->set_overrides($this->_overrides & ~$overrides);
418    }
419    function call_with_overrides($overrides, $method /* , arguments... */) {
420        $old_overrides = $this->set_overrides($overrides);
421        $result = call_user_func_array([$this, $method], array_slice(func_get_args(), 2));
422        $this->_overrides = $old_overrides;
423        return $result;
424    }
425
426    function activate_database_account() {
427        assert($this->has_email());
428        if (!$this->has_database_account()
429            && ($u = Contact::create($this->conf, null, $this))) {
430            $this->merge($u);
431            $this->contactDbId = 0;
432            $this->_contactdb_user = false;
433            $this->activate(null);
434        }
435    }
436
437    function contactdb_user($refresh = false) {
438        if ($this->contactDbId && !$this->contactId)
439            return $this;
440        else if ($refresh || $this->_contactdb_user === false) {
441            $cdbu = null;
442            if ($this->has_email())
443                $cdbu = $this->conf->contactdb_user_by_email($this->email);
444            $this->_contactdb_user = $cdbu;
445        }
446        return $this->_contactdb_user;
447    }
448
449    private function _contactdb_save_roles($cdbur) {
450        global $Now;
451        Dbl::ql($this->conf->contactdb(), "insert into Roles set contactDbId=?, confid=?, roles=?, activity_at=? on duplicate key update roles=values(roles), activity_at=values(activity_at)", $cdbur->contactDbId, $cdbur->confid, $this->contactdb_roles(), $Now);
452    }
453    function contactdb_update($update_keys = null, $only_update_empty = false) {
454        global $Now;
455        if (!($cdb = $this->conf->contactdb())
456            || !$this->has_database_account()
457            || !validate_email($this->email))
458            return false;
459
460        $cdbur = $this->conf->contactdb_user_by_email($this->email);
461        $cdbux = $cdbur ? : new Contact(null, $this->conf);
462        $upd = [];
463        foreach (["firstName", "lastName", "affiliation", "country", "collaborators",
464                  "birthday", "gender"] as $k)
465            if ($this->$k !== null
466                && $this->$k !== ""
467                && (!$only_update_empty || $cdbux->$k === null || $cdbux->$k === "")
468                && (!$cdbur || in_array($k, $update_keys ? : [])))
469                $upd[$k] = $this->$k;
470        if (!$cdbur) {
471            $upd["email"] = $this->email;
472            if ($this->password
473                && $this->password !== "*"
474                && ($this->password[0] !== " " || $this->password[1] === "\$")) {
475                $upd["password"] = $this->password;
476                $upd["passwordTime"] = $this->passwordTime;
477            }
478        }
479        if (!empty($upd)) {
480            $cdbux->apply_updater($upd, true);
481            $this->_contactdb_user = false;
482        }
483        $cdbur = $cdbur ? : $this->conf->contactdb_user_by_email($this->email);
484        if ($cdbur->confid
485            && (int) $cdbur->roles !== $this->contactdb_roles())
486            $this->_contactdb_save_roles($cdbur);
487        return $cdbur ? (int) $cdbur->contactDbId : false;
488    }
489
490    function is_actas_user() {
491        return $this->_activated
492            && isset($_SESSION["trueuser"])
493            && strcasecmp($_SESSION["trueuser"]->email, $this->email) !== 0;
494    }
495
496    private function activate_capabilities($qreq) {
497        // Add capabilities from arguments
498        if (($cap_req = $qreq->cap)) {
499            foreach (preg_split(',\s+,', $cap_req) as $cap)
500                $this->apply_capability_text($cap);
501            unset($qreq->cap, $_GET["cap"], $_POST["cap"]);
502        }
503
504        // Support capability testing
505        if ($this->conf->opt("testCapabilities")
506            && ($cap_req = $qreq->testcap)
507            && preg_match_all('/([-+]?)([1-9]\d*)([A-Za-z]+)/',
508                              $cap_req, $m, PREG_SET_ORDER)) {
509            foreach ($m as $mm) {
510                $c = ($mm[3] == "a" ? self::CAP_AUTHORVIEW : 0);
511                $this->change_paper_capability((int) $mm[2], $c, $mm[1] !== "-");
512            }
513            unset($qreq->testcap, $_GET["testcap"], $_POST["testcap"]);
514        }
515    }
516
517    function is_empty() {
518        return $this->contactId <= 0 && !$this->capabilities && !$this->email;
519    }
520
521    function owns_email($email) {
522        return (string) $email !== "" && strcasecmp($email, $this->email) === 0;
523    }
524
525    function name_text() {
526        if ($this->firstName === "" || $this->lastName === "")
527            return $this->firstName . $this->lastName;
528        else
529            return $this->firstName . " " . $this->lastName;
530    }
531
532    function completion_items() {
533        $items = [];
534
535        $x = strtolower(substr($this->email, 0, strpos($this->email, "@")));
536        if ($x !== "")
537            $items[$x] = 2;
538
539        $sp = strpos($this->firstName, " ") ? : strlen($this->firstName);
540        $x = strtolower(UnicodeHelper::deaccent(substr($this->firstName, 0, $sp)));
541        if ($x !== "" && ctype_alnum($x))
542            $items[$x] = 1;
543
544        $sp = strrpos($this->lastName, " ");
545        $x = strtolower(UnicodeHelper::deaccent(substr($this->lastName, $sp ? $sp + 1 : 0)));
546        if ($x !== "" && ctype_alnum($x))
547            $items[$x] = 1;
548
549        return $items;
550    }
551
552    private function calculate_name_for($pfx, $user) {
553        if ($pfx === "u")
554            return $user;
555        if ($pfx === "t")
556            return Text::name_text($user);
557        $n = Text::name_html($user);
558        if ($pfx === "r" && isset($user->contactTags)
559            && ($colors = $this->user_color_classes_for($user)))
560            $n = '<span class="' . $colors . ' taghh">' . $n . '</span>';
561        return $n;
562    }
563
564    private function name_for($pfx, $x) {
565        $cid = is_object($x) ? $x->contactId : $x;
566        $key = $pfx . $cid;
567        if (isset($this->_name_for_map[$key]))
568            return $this->_name_for_map[$key];
569
570        if (+$cid === $this->contactId)
571            $x = $this;
572        else if (($pc = $this->conf->pc_member_by_id($cid)))
573            $x = $pc;
574
575        if (!(is_object($x) && isset($x->firstName) && isset($x->lastName) && isset($x->email))) {
576            if ($pfx === "u") {
577                $x = $this->conf->user_by_id($cid);
578                $this->_contact_sorter_map[$cid] = $x->sorter;
579            } else
580                $x = $this->name_for("u", $x);
581        }
582
583        return ($this->_name_for_map[$key] = $this->calculate_name_for($pfx, $x));
584    }
585
586    function name_html_for($x) {
587        return $this->name_for("", $x);
588    }
589
590    function name_text_for($x) {
591        return $this->name_for("t", $x);
592    }
593
594    function name_object_for($x) {
595        return $this->name_for("u", $x);
596    }
597
598    function reviewer_html_for($x) {
599        return $this->name_for($this->isPC ? "r" : "", $x);
600    }
601
602    function reviewer_text_for($x) {
603        return $this->name_for("t", $x);
604    }
605
606    function user_color_classes_for(Contact $x) {
607        return $x->viewable_color_classes($this);
608    }
609
610    function ksort_cid_array(&$a) {
611        $pcm = $this->conf->pc_members();
612        uksort($a, function ($a, $b) use ($pcm) {
613            if (isset($pcm[$a]) && isset($pcm[$b]))
614                return $pcm[$a]->sort_position - $pcm[$b]->sort_position;
615            if (isset($pcm[$a]))
616                $as = $pcm[$a]->sorter;
617            else if (isset($this->_contact_sorter_map[$a]))
618                $as = $this->_contact_sorter_map[$a];
619            else {
620                $x = $this->conf->user_by_id($a);
621                $as = $this->_contact_sorter_map[$a] = $x->sorter;
622            }
623            if (isset($pcm[$b]))
624                $bs = $pcm[$b]->sorter;
625            else if (isset($this->_contact_sorter_map[$b]))
626                $bs = $this->_contact_sorter_map[$b];
627            else {
628                $x = $this->conf->user_by_id($b);
629                $bs = $this->_contact_sorter_map[$b] = $x->sorter;
630            }
631            return strcasecmp($as, $bs);
632        });
633    }
634
635    function has_email() {
636        return !!$this->email;
637    }
638
639    static function is_anonymous_email($email) {
640        // see also PaperSearch, Mailer
641        return substr($email, 0, 9) === "anonymous"
642            && (strlen($email) === 9 || ctype_digit(substr($email, 9)));
643    }
644
645    function is_anonymous_user() {
646        return $this->email && self::is_anonymous_email($this->email);
647    }
648
649    function has_database_account() {
650        return $this->contactId > 0;
651    }
652
653    function is_admin() {
654        return $this->privChair;
655    }
656
657    function is_admin_force() {
658        return ($this->_overrides & self::OVERRIDE_CONFLICT) !== 0;
659    }
660
661    function is_pc_member() {
662        return $this->roles & self::ROLE_PC;
663    }
664
665    function is_pclike() {
666        return $this->roles & self::ROLE_PCLIKE;
667    }
668
669    function role_html() {
670        if ($this->roles & (Contact::ROLE_CHAIR | Contact::ROLE_ADMIN | Contact::ROLE_PC)) {
671            if ($this->roles & Contact::ROLE_CHAIR)
672                return '<span class="pcrole">chair</span>';
673            else if (($this->roles & (Contact::ROLE_ADMIN | Contact::ROLE_PC)) == (Contact::ROLE_ADMIN | Contact::ROLE_PC))
674                return '<span class="pcrole">PC, sysadmin</span>';
675            else if ($this->roles & Contact::ROLE_ADMIN)
676                return '<span class="pcrole">sysadmin</span>';
677            else
678                return '<span class="pcrole">PC</span>';
679        } else
680            return '';
681    }
682
683    function has_tag($t) {
684        if (($this->roles & self::ROLE_PC) && strcasecmp($t, "pc") == 0)
685            return true;
686        if ($this->contactTags)
687            return stripos($this->contactTags, " $t#") !== false;
688        if ($this->contactTags === false) {
689            trigger_error(caller_landmark(1, "/^Conf::/") . ": Contact $this->email contactTags missing " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
690            $this->contactTags = null;
691        }
692        return false;
693    }
694
695    function tag_value($t) {
696        if (($this->roles & self::ROLE_PC) && strcasecmp($t, "pc") == 0)
697            return 0.0;
698        if ($this->contactTags
699            && ($p = stripos($this->contactTags, " $t#")) !== false)
700            return (float) substr($this->contactTags, $p + strlen($t) + 2);
701        return false;
702    }
703
704    static function roles_all_contact_tags($roles, $tags) {
705        $t = "";
706        if ($roles & self::ROLE_PC)
707            $t = " pc#0";
708        if ($tags)
709            return $t . $tags;
710        else
711            return $t ? $t . " " : "";
712    }
713
714    function all_contact_tags() {
715        return self::roles_all_contact_tags($this->roles, $this->contactTags);
716    }
717
718    function viewable_tags(Contact $viewer) {
719        if ($viewer->can_view_contact_tags() || $viewer->contactId == $this->contactId) {
720            $tags = $this->all_contact_tags();
721            return $this->conf->tags()->strip_nonviewable($tags, $viewer, null);
722        } else
723            return "";
724    }
725
726    function viewable_color_classes(Contact $viewer) {
727        if ($viewer->isPC && ($tags = $this->viewable_tags($viewer)))
728            return $this->conf->tags()->color_classes($tags);
729        else
730            return "";
731    }
732
733    private function update_capabilities() {
734        ++self::$rights_version;
735        if (empty($this->capabilities))
736            $this->capabilities = null;
737        if ($this->_activated)
738            $this->conf->save_session("capabilities", $this->capabilities);
739    }
740
741    function capability($name) {
742        if ($this->capabilities !== null && isset($this->capabilities[0]))
743            return get($this->capabilities[0], $name);
744        else
745            return null;
746    }
747
748    function set_capability($name, $newval) {
749        $oldval = $this->capability($name);
750        if ($newval !== $oldval) {
751            ++self::$rights_version;
752            if ($newval !== null)
753                $this->capabilities[0][$name] = $newval;
754            else
755                unset($this->capabilities[0][$name]);
756            if (empty($this->capabilities[0]))
757                unset($this->capabilities[0]);
758            $this->update_capabilities();
759        }
760        return $newval !== $oldval;
761    }
762
763    function change_paper_capability($pid, $bit, $isset) {
764        $oldval = 0;
765        if ($this->capabilities !== null)
766            $oldval = get($this->capabilities, $pid) ? : 0;
767        $newval = ($oldval & ~$bit) | ($isset ? $bit : 0);
768        if ($newval !== $oldval) {
769            if ($newval !== 0)
770                $this->capabilities[$pid] = $newval;
771            else
772                unset($this->capabilities[$pid]);
773            $this->update_capabilities();
774        }
775        return $newval !== $oldval;
776    }
777
778    function apply_capability_text($text) {
779        if (preg_match(',\A([-+]?)0([1-9][0-9]*)(a)(\S+)\z,', $text, $m)
780            && ($result = $this->conf->ql("select paperId, capVersion from Paper where paperId=$m[2]"))
781            && ($row = edb_orow($result))) {
782            $rowcap = $this->conf->capability_text($row, $m[3]);
783            $text = substr($text, strlen($m[1]));
784            if ($rowcap === $text
785                || $rowcap === str_replace("/", "_", $text))
786                return $this->change_paper_capability((int) $m[2], self::CAP_AUTHORVIEW, $m[1] !== "-");
787        }
788        return null;
789    }
790
791    private function make_data() {
792        if (is_string($this->data))
793            $this->data = json_decode($this->data);
794        if (!$this->data)
795            $this->data = (object) array();
796    }
797
798    function data($key = null) {
799        $this->make_data();
800        if ($key)
801            return get($this->data, $key);
802        else
803            return $this->data;
804    }
805
806    private function encode_data() {
807        if ($this->data && ($t = json_encode($this->data)) !== "{}")
808            return $t;
809        else
810            return null;
811    }
812
813    function save_data($key, $value) {
814        $this->merge_and_save_data((object) array($key => array_to_object_recursive($value)));
815    }
816
817    function merge_data($data) {
818        $this->make_data();
819        object_replace_recursive($this->data, array_to_object_recursive($data));
820    }
821
822    function merge_and_save_data($data) {
823        $this->activate_database_account();
824        $this->make_data();
825        $old = $this->encode_data();
826        object_replace_recursive($this->data, array_to_object_recursive($data));
827        $new = $this->encode_data();
828        if ($old !== $new)
829            $this->conf->qe("update ContactInfo set data=? where contactId=?", $new, $this->contactId);
830    }
831
832    private function data_str() {
833        $d = null;
834        if (is_string($this->data))
835            $d = $this->data;
836        else if (is_object($this->data))
837            $d = json_encode($this->data);
838        return $d === "{}" ? null : $d;
839    }
840
841    function escape($qreq = null) {
842        global $Qreq, $Now;
843        $qreq = $qreq ? : $Qreq;
844
845        if ($qreq->ajax) {
846            if ($this->is_empty())
847                json_exit(["ok" => false, "error" => "You have been signed out.", "loggedout" => true]);
848            else
849                json_exit(["ok" => false, "error" => "You don’t have permission to access that page."]);
850        }
851
852        if ($this->is_empty()) {
853            // Preserve post values across session expiration.
854            ensure_session();
855            $x = array();
856            if (Navigation::path())
857                $x["__PATH__"] = preg_replace(",^/+,", "", Navigation::path());
858            if ($qreq->anchor)
859                $x["anchor"] = $qreq->anchor;
860            $url = SelfHref::make($qreq, $x, ["raw" => true, "site_relative" => true]);
861            $_SESSION["login_bounce"] = [$this->conf->dsn, $url, Navigation::page(), $_POST, $Now + 120];
862            if ($qreq->post_ok())
863                error_go(false, "You’ve been signed out, so your changes were not saved. After signing in, you may submit them again.");
864            else
865                error_go(false, "You must sign in to access that page.");
866        } else
867            error_go(false, "You don’t have permission to access that page.");
868    }
869
870
871    static private $cdb_fields = [
872        "firstName" => true, "lastName" => true, "affiliation" => true,
873        "country" => true, "collaborators" => true, "birthday" => true,
874        "gender" => true
875    ];
876    static private $no_clean_fields = [
877        "collaborators" => true, "defaultWatch" => true, "contactTags" => true
878    ];
879
880    private function _save_assign_field($k, $v, Contact_Update $cu) {
881        if (!isset(self::$no_clean_fields[$k])) {
882            $v = simplify_whitespace($v);
883            if ($k === "birthday" && !$v)
884                $v = null;
885        }
886        // change contactdb
887        if (isset(self::$cdb_fields[$k])
888            && ($this->$k !== $v || $cu->changing_email))
889            $cu->cdb_qf[] = $k;
890        // change local version
891        if ($this->$k !== $v || !$this->contactId)
892            $cu->qv[$k] = $v;
893        $this->$k = $v;
894    }
895
896    static function parse_roles_json($j) {
897        $roles = 0;
898        if (isset($j->pc) && $j->pc)
899            $roles |= self::ROLE_PC;
900        if (isset($j->chair) && $j->chair)
901            $roles |= self::ROLE_CHAIR | self::ROLE_PC;
902        if (isset($j->sysadmin) && $j->sysadmin)
903            $roles |= self::ROLE_ADMIN;
904        return $roles;
905    }
906
907    const SAVE_NOTIFY = 1;
908    const SAVE_ANY_EMAIL = 2;
909    const SAVE_IMPORT = 4;
910    const SAVE_NO_EXPORT = 8;
911    function save_json($cj, $actor, $flags) {
912        global $Me, $Now;
913        assert(!!$this->contactId);
914        $old_roles = $this->roles;
915        $old_email = $this->email;
916        $old_disabled = $this->disabled ? 1 : 0;
917        $changing_email = isset($cj->email) && strtolower($cj->email) !== strtolower((string) $old_email);
918        $cu = new Contact_Update($changing_email);
919
920        $aupapers = null;
921        if ($changing_email)
922            $aupapers = self::email_authored_papers($this->conf, $cj->email, $cj);
923
924        // check whether this user is changing themselves
925        $changing_other = false;
926        if ($this->conf->contactdb()
927            && $Me
928            && (strcasecmp($this->email, $Me->email) != 0 || $Me->is_actas_user()))
929            $changing_other = true;
930
931        // Main fields
932        foreach (["firstName", "lastName", "email", "affiliation", "collaborators",
933                  "preferredEmail", "country", "birthday", "gender", "phone"] as $k) {
934            if (isset($cj->$k))
935                $this->_save_assign_field($k, $cj->$k, $cu);
936        }
937        if (isset($cj->preferred_email) && !isset($cj->preferredEmail))
938            $this->_save_assign_field("preferredEmail", $cj->preferred_email, $cu);
939        $this->_save_assign_field("unaccentedName", Text::unaccented_name($this->firstName, $this->lastName), $cu);
940        self::set_sorter($this, $this->conf);
941
942        // Disabled
943        $disabled = $old_disabled;
944        if (isset($cj->disabled))
945            $disabled = $cj->disabled ? 1 : 0;
946        if ($disabled !== $old_disabled || !$this->contactId)
947            $cu->qv["disabled"] = $this->disabled = $disabled;
948
949        // Data
950        $old_datastr = $this->data_str();
951        $data = get($cj, "data", (object) array());
952        foreach (array("address", "city", "state", "zip") as $k)
953            if (isset($cj->$k) && ($x = $cj->$k)) {
954                while (is_array($x) && $x[count($x) - 1] === "")
955                    array_pop($x);
956                $data->$k = $x ? : null;
957            }
958        $this->merge_data($data);
959        $datastr = $this->data_str();
960        if ($datastr !== $old_datastr)
961            $cu->qv["data"] = $datastr;
962
963        // Changes to the above fields also change the updateTime.
964        if (!empty($cu->qv))
965            $cu->qv["updateTime"] = $this->updateTime = $Now;
966
967        // Follow
968        if (isset($cj->follow)) {
969            $w = 0;
970            if (get($cj->follow, "reviews"))
971                $w |= self::WATCH_REVIEW;
972            if (get($cj->follow, "allreviews"))
973                $w |= self::WATCH_REVIEW_ALL;
974            if (get($cj->follow, "managedreviews"))
975                $w |= self::WATCH_REVIEW_MANAGED;
976            if (get($cj->follow, "allfinal"))
977                $w |= self::WATCH_FINAL_SUBMIT_ALL;
978            $this->_save_assign_field("defaultWatch", $w, $cu);
979        }
980
981        // Tags
982        if (isset($cj->tags)) {
983            $tags = array();
984            foreach ($cj->tags as $t) {
985                list($tag, $value) = TagInfo::unpack($t);
986                if (strcasecmp($tag, "pc") != 0)
987                    $tags[$tag] = $tag . "#" . ($value ? : 0);
988            }
989            ksort($tags);
990            $t = count($tags) ? " " . join(" ", $tags) . " " : "";
991            $this->_save_assign_field("contactTags", $t, $cu);
992        }
993
994        // Initial save
995        if (count($cu->qv)) { // always true if $inserting
996            $q = "update ContactInfo set "
997                . join("=?, ", array_keys($cu->qv)) . "=?"
998                . " where contactId=$this->contactId";
999            if (!($result = $this->conf->qe_apply($q, array_values($cu->qv))))
1000                return $result;
1001            Dbl::free($result);
1002        }
1003
1004        // Topics
1005        if (isset($cj->topics)) {
1006            $tf = array();
1007            foreach ($cj->topics as $k => $v)
1008                if ($v || empty($tf))
1009                    $tf[] = "($this->contactId,$k,$v)";
1010            $this->conf->qe_raw("delete from TopicInterest where contactId=$this->contactId");
1011            if (!empty($tf))
1012                $this->conf->qe_raw("insert into TopicInterest (contactId,topicId,interest) values " . join(",", $tf));
1013            $this->_topic_interest_map = null;
1014        }
1015
1016        // Roles
1017        $roles = $old_roles;
1018        if (isset($cj->roles)) {
1019            $roles = self::parse_roles_json($cj->roles);
1020            if ($roles !== $old_roles)
1021                $this->save_roles($roles, $actor);
1022        }
1023
1024        // Update authorship
1025        if ($aupapers)
1026            $this->save_authored_papers($aupapers);
1027
1028        // Contact DB (must precede password)
1029        $cdb = $this->conf->contactdb();
1030        if ($changing_email)
1031            $this->_contactdb_user = false;
1032        if ($cdb && !($flags & self::SAVE_NO_EXPORT)
1033            && (!empty($cu->cdb_qf) || $roles !== $old_roles))
1034            $this->contactdb_update($cu->cdb_qf, $changing_other);
1035
1036        // Password
1037        if (isset($cj->new_password))
1038            $this->change_password($cj->new_password, 0);
1039
1040        // Beware PC cache
1041        if (($roles | $old_roles) & Contact::ROLE_PCLIKE)
1042            $this->conf->invalidate_caches(["pc" => 1]);
1043
1044        $actor = $actor ? : $Me;
1045        if ($actor && $this->contactId == $actor->contactId)
1046            $this->mark_activity();
1047
1048        return true;
1049    }
1050
1051    function change_email($email) {
1052        assert($this->has_database_account());
1053        $aupapers = self::email_authored_papers($this->conf, $email, $this);
1054        $this->conf->ql("update ContactInfo set email=? where contactId=?", $email, $this->contactId);
1055        $this->save_authored_papers($aupapers);
1056        if ($this->roles & Contact::ROLE_PCLIKE)
1057            $this->conf->invalidate_caches(["pc" => 1]);
1058        $this->email = $email;
1059    }
1060
1061    static function email_authored_papers(Conf $conf, $email, $reg) {
1062        $aupapers = array();
1063        $result = $conf->q("select paperId, authorInformation from Paper where authorInformation like " . Dbl::utf8ci("'%\t" . sqlq_for_like($email) . "\t%'"));
1064        while (($row = PaperInfo::fetch($result, null, $conf))) {
1065            foreach ($row->author_list() as $au) {
1066                if (strcasecmp($au->email, $email) == 0) {
1067                    $aupapers[] = $row->paperId;
1068                    if ($reg
1069                        && ($au->firstName !== "" || $au->lastName !== "")
1070                        && !isset($reg->firstName)
1071                        && !isset($reg->lastName)) {
1072                        $reg->firstName = $au->firstName;
1073                        $reg->lastName = $au->lastName;
1074                    }
1075                    if ($reg
1076                        && $au->affiliation !== ""
1077                        && !isset($reg->affiliation)) {
1078                        $reg->affiliation = $au->affiliation;
1079                    }
1080                }
1081            }
1082        }
1083        return $aupapers;
1084    }
1085
1086    private function save_authored_papers($aupapers) {
1087        if (!empty($aupapers) && $this->contactId) {
1088            $this->conf->ql("insert into PaperConflict (paperId, contactId, conflictType) values ?v on duplicate key update conflictType=greatest(conflictType, " . CONFLICT_AUTHOR . ")", array_map(function ($pid) {
1089                return [$pid, $this->contactId, CONFLICT_AUTHOR];
1090            }, $aupapers));
1091        }
1092    }
1093
1094    function save_roles($new_roles, $actor) {
1095        $old_roles = $this->roles;
1096        // ensure there's at least one system administrator
1097        if (!($new_roles & self::ROLE_ADMIN) && ($old_roles & self::ROLE_ADMIN)
1098            && !(($result = $this->conf->qe("select contactId from ContactInfo where roles!=0 and (roles&" . self::ROLE_ADMIN . ")!=0 and contactId!=" . $this->contactId . " limit 1"))
1099                 && edb_nrows($result) > 0))
1100            $new_roles |= self::ROLE_ADMIN;
1101        // log role change
1102        foreach (array(self::ROLE_PC => "pc",
1103                       self::ROLE_ADMIN => "sysadmin",
1104                       self::ROLE_CHAIR => "chair") as $role => $type)
1105            if (($new_roles & $role) && !($old_roles & $role))
1106                $this->conf->log_for($actor ? : $this, $this, "Added as $type");
1107            else if (!($new_roles & $role) && ($old_roles & $role))
1108                $this->conf->log_for($actor ? : $this, $this, "Removed as $type");
1109        // save the roles bits
1110        if ($old_roles != $new_roles) {
1111            $this->conf->qe("update ContactInfo set roles=$new_roles where contactId=$this->contactId");
1112            $this->assign_roles($new_roles);
1113        }
1114        return $old_roles != $new_roles;
1115    }
1116
1117    private function _make_create_updater($reg, $is_cdb) {
1118        $cj = [];
1119        if ($this->firstName === "" && $this->lastName === "") {
1120            if (get_s($reg, "firstName") !== "")
1121                $cj["firstName"] = (string) $reg->firstName;
1122            if (get_s($reg, "lastName") !== "")
1123                $cj["lastName"] = (string) $reg->lastName;
1124        }
1125        foreach (["affiliation", "country", "gender", "birthday",
1126                  "preferredEmail", "phone"] as $k) {
1127            if ((string) $this->$k === ""
1128                && isset($reg->$k)
1129                && $reg->$k !== "")
1130                $cj[$k] = (string) $reg->$k;
1131        }
1132        if ($is_cdb ? !$this->contactDbId : !$this->contactId)
1133            $cj["email"] = $reg->email;
1134        return $cj;
1135    }
1136
1137    function apply_updater($updater, $is_cdb) {
1138        global $Now;
1139        if ($is_cdb) {
1140            $db = $this->conf->contactdb();
1141            $idk = "contactDbId";
1142        } else {
1143            $db = $this->conf->dblink;
1144            $idk = "contactId";
1145            if (isset($updater["firstName"]) || isset($updater["lastName"])) {
1146                $updater["firstName"] = get($updater, "firstName", $this->firstName);
1147                $updater["lastName"] = get($updater, "lastName", $this->lastName);
1148                $updater["unaccentedName"] = Text::unaccented_name($updater["firstName"], $updater["lastName"]);
1149            }
1150        }
1151        if ($this->$idk) {
1152            $qv = array_values($updater);
1153            $qv[] = $this->$idk;
1154            $result = Dbl::qe_apply($db, "update ContactInfo set " . join("=?, ", array_keys($updater)) . "=? where $idk=?", $qv);
1155        } else {
1156            assert(isset($updater["email"]));
1157            if (!isset($updater["password"])) {
1158                $updater["password"] = validate_email($updater["email"]) ? self::random_password() : "*";
1159                $updater["passwordTime"] = $Now;
1160            }
1161            if (!$is_cdb)
1162                $updater["creationTime"] = $Now;
1163            $result = Dbl::qe_apply($db, "insert into ContactInfo set " . join("=?, ", array_keys($updater)) . "=? on duplicate key update firstName=firstName", array_values($updater));
1164            if ($result)
1165                $updater[$idk] = (int) $result->insert_id;
1166        }
1167        if (($ok = !!$result)) {
1168            foreach ($updater as $k => $v)
1169                $this->$k = $v;
1170        }
1171        Dbl::free($result);
1172        return $ok;
1173    }
1174
1175    static function create(Conf $conf, $actor, $reg, $flags = 0) {
1176        global $Me, $Now;
1177
1178        // clean registration
1179        if (is_array($reg))
1180            $reg = (object) $reg;
1181        assert(is_string($reg->email));
1182        $reg->email = trim($reg->email);
1183        assert($reg->email !== "");
1184        if (!isset($reg->firstName) && isset($reg->first))
1185            $reg->firstName = $reg->first;
1186        if (!isset($reg->lastName) && isset($reg->last))
1187            $reg->lastName = $reg->last;
1188        if (isset($reg->name) && !isset($reg->firstName) && !isset($reg->lastName))
1189            list($reg->firstName, $reg->lastName) = Text::split_name($reg->name);
1190        if (isset($reg->preferred_email) && !isset($reg->preferredEmail))
1191            $reg->preferredEmail = $reg->preferred_email;
1192
1193        // look up existing accounts
1194        $valid_email = validate_email($reg->email);
1195        $u = $conf->user_by_email($reg->email) ? : new Contact(null, $conf);
1196        if (($cdb = $conf->contactdb()) && $valid_email)
1197            $cdbu = $conf->contactdb_user_by_email($reg->email);
1198        else
1199            $cdbu = null;
1200        $create = !$u->contactId;
1201        $aupapers = [];
1202
1203        // if local does not exist, create it
1204        if (!$u->contactId) {
1205            if (($flags & self::SAVE_IMPORT) && !$cdbu)
1206                return null;
1207            if (!$valid_email && !($flags & self::SAVE_ANY_EMAIL))
1208                return null;
1209            if ($valid_email)
1210                // update registration from authorship information
1211                $aupapers = self::email_authored_papers($conf, $reg->email, $reg);
1212        }
1213
1214        // create or update contactdb user
1215        if ($cdb && $valid_email) {
1216            $cdbu = $cdbu ? : new Contact(null, $conf);
1217            if (($upd = $cdbu->_make_create_updater($reg, true)))
1218                $cdbu->apply_updater($upd, true);
1219        }
1220
1221        // create or update local user
1222        $upd = $u->_make_create_updater($cdbu ? : $reg, false);
1223        if (!$u->contactId) {
1224            if (($cdbu && $cdbu->disabled) || get($reg, "disabled"))
1225                $upd["disabled"] = 1;
1226            if ($cdbu) {
1227                $upd["password"] = $cdbu->password;
1228                $upd["passwordTime"] = $cdbu->passwordTime;
1229            }
1230        }
1231        if ($upd) {
1232            if (!($u->apply_updater($upd, false)))
1233                // failed because concurrent create (unlikely)
1234                $u = $conf->user_by_email($reg->email);
1235        }
1236
1237        // update paper authorship
1238        if ($aupapers) {
1239            $u->save_authored_papers($aupapers);
1240            if ($cdbu)
1241                // can't use `$cdbu` itself b/c no `confid`
1242                $u->_contactdb_save_roles($u->contactdb_user());
1243        }
1244
1245        // notify on creation
1246        if ($create) {
1247            if (($flags & self::SAVE_NOTIFY) && !$u->disabled)
1248                $u->sendAccountInfo("create", false);
1249            $type = $u->disabled ? "disabled " : "";
1250            $conf->log_for($actor && $actor->has_email() ? $actor : $u, $u, "Created {$type}account");
1251            // if ($Me && $Me->privChair)
1252            //    $conf->infoMsg("Created {$type}account for <a href=\"" . hoturl("profile", "u=" . urlencode($u->email)) . "\">" . Text::user_html_nolink($u) . "</a>.");
1253        }
1254
1255        return $u;
1256    }
1257
1258
1259    // PASSWORDS
1260    //
1261    // password "" or null: reset password (user must recreate password)
1262    // password "*": invalid password, cannot be reset by user
1263    // password starting with " ": legacy hashed password using hash_hmac
1264    //     format: " HASHMETHOD KEYID SALT[16B]HMAC"
1265    // password starting with " $": password hashed by password_hash
1266    //
1267    // PASSWORD PRINCIPLES
1268    //
1269    // - prefer contactdb password
1270    // - require contactdb password if it is newer
1271    //
1272    // PASSWORD CHECKING RULES
1273    //
1274    // if (contactdb password exists)
1275    //     check contactdb password;
1276    // if (contactdb password matches && contactdb password needs upgrade)
1277    //     upgrade contactdb password;
1278    // if (contactdb password matches && local password was from contactdb)
1279    //     set local password to contactdb password;
1280    // if (local password was not from contactdb || no contactdb)
1281    //     check local password;
1282    // if (local password matches && local password needs upgrade)
1283    //     upgrade local password;
1284    //
1285    // PASSWORD CHANGING RULES
1286    //
1287    // change(expected, new):
1288    // if (contactdb password allowed
1289    //     && (!expected || expected matches contactdb)) {
1290    //     change contactdb password and update time;
1291    //     set local password to "*";
1292    // } else
1293    //     change local password and update time;
1294
1295    static function valid_password($input) {
1296        return $input !== "" && $input !== "0" && $input !== "*"
1297            && trim($input) === $input;
1298    }
1299
1300    static function random_password($length = 14) {
1301        return hotcrp_random_password($length);
1302    }
1303
1304    static function password_storage_cleartext() {
1305        return opt("safePasswords") < 1;
1306    }
1307
1308    function allow_contactdb_password() {
1309        $cdbu = $this->contactdb_user();
1310        return $cdbu && $cdbu->password && $cdbu->password !== "*";
1311    }
1312
1313    function plaintext_password() {
1314        // Return the currently active plaintext password. This might not
1315        // equal $this->password because of the cdb.
1316        if ($this->password === "" || $this->password === "*") {
1317            if ($this->contactId
1318                && ($cdbu = $this->contactdb_user()))
1319                return $cdbu->plaintext_password();
1320            else
1321                return false;
1322        } else if ($this->password[0] === " " || $this->password === "*")
1323            return false;
1324        else
1325            return $this->password;
1326    }
1327
1328    function password_is_reset() {
1329        if (($cdbu = $this->contactdb_user()))
1330            return (string) $cdbu->password === ""
1331                && ((string) $this->password === ""
1332                    || $this->passwordTime < $cdbu->passwordTime);
1333        else
1334            return $this->password === "";
1335    }
1336
1337    function password_used() {
1338        return $this->passwordUseTime > 0;
1339    }
1340
1341
1342    // obsolete
1343    private function password_hmac_key($keyid) {
1344        if ($keyid === null)
1345            $keyid = $this->conf->opt("passwordHmacKeyid", 0);
1346        $key = $this->conf->opt("passwordHmacKey.$keyid");
1347        if (!$key && $keyid == 0)
1348            $key = $this->conf->opt("passwordHmacKey");
1349        if (!$key) /* backwards compatibility */
1350            $key = $this->conf->setting_data("passwordHmacKey.$keyid");
1351        if (!$key) {
1352            error_log("missing passwordHmacKey.$keyid, using default");
1353            $key = "NdHHynw6JwtfSZyG3NYPTSpgPFG8UN8NeXp4tduTk2JhnSVy";
1354        }
1355        return $key;
1356    }
1357
1358    private function check_hashed_password($input, $pwhash) {
1359        if ($input == ""
1360            || $input === "*"
1361            || (string) $pwhash === ""
1362            || $pwhash === "*")
1363            return false;
1364        else if ($pwhash[0] !== " ")
1365            return $pwhash === $input;
1366        else if ($pwhash[1] === "\$")
1367            return password_verify($input, substr($pwhash, 2));
1368        else {
1369            if (($method_pos = strpos($pwhash, " ", 1)) !== false
1370                && ($keyid_pos = strpos($pwhash, " ", $method_pos + 1)) !== false
1371                && strlen($pwhash) > $keyid_pos + 17
1372                && function_exists("hash_hmac")) {
1373                $method = substr($pwhash, 1, $method_pos - 1);
1374                $keyid = substr($pwhash, $method_pos + 1, $keyid_pos - $method_pos - 1);
1375                $salt = substr($pwhash, $keyid_pos + 1, 16);
1376                return hash_hmac($method, $salt . $input, $this->password_hmac_key($keyid), true)
1377                    == substr($pwhash, $keyid_pos + 17);
1378            }
1379        }
1380        return false;
1381    }
1382
1383    private function password_hash_method() {
1384        $m = $this->conf->opt("passwordHashMethod");
1385        return is_int($m) ? $m : PASSWORD_DEFAULT;
1386    }
1387
1388    private function check_password_encryption($hash, $iscdb) {
1389        $safe = $this->conf->opt($iscdb ? "contactdb_safePasswords" : "safePasswords");
1390        if ($safe < 1
1391            || ($method = $this->password_hash_method()) === false
1392            || ($hash !== "" && $hash[0] !== " " && $safe == 1))
1393            return false;
1394        else if ($hash === "" || $hash[0] !== " ")
1395            return true;
1396        else
1397            return $hash[1] !== "\$"
1398                || password_needs_rehash(substr($hash, 2), $method);
1399    }
1400
1401    function hash_password($input) {
1402        if (($method = $this->password_hash_method()) !== false)
1403            return " \$" . password_hash($input, $method);
1404        else
1405            return $input;
1406    }
1407
1408    function check_password($input) {
1409        global $Now;
1410        assert(!$this->conf->external_login());
1411        if (($this->contactId && $this->disabled)
1412            || !self::valid_password($input))
1413            return false;
1414
1415        $cdbu = $this->contactdb_user();
1416        $cdbok = false;
1417        if ($cdbu
1418            && ($hash = $cdbu->password)
1419            && $cdbu->allow_contactdb_password()
1420            && ($cdbok = $this->check_hashed_password($input, $hash))) {
1421            $updater = ["passwordUseTime" => $Now];
1422            if ($this->check_password_encryption($hash, true)) {
1423                $updater["password"] = $this->hash_password($input);
1424                $updater["passwordTime"] = $Now;
1425            }
1426            $cdbu->apply_updater($updater, true);
1427        }
1428
1429        $localok = false;
1430        if ($this->contactId
1431            && ($hash = $this->password)
1432            && ($localok = $this->check_hashed_password($input, $hash))) {
1433            if ($cdbu
1434                && !$cdbok
1435                && $this->passwordTime
1436                && $cdbu->passwordTime > $this->passwordTime)
1437                error_log($this->conf->dbname . ": " . $this->email . ": using old local password (" . post_value(true) . ")");
1438            $updater = ["passwordUseTime" => $Now];
1439            if ($this->check_password_encryption($hash, false)) {
1440                $updater["password"] = $cdbok ? $cdbu->password : $this->hash_password($input);
1441                $updater["passwordTime"] = $Now;
1442            }
1443            $this->apply_updater($updater, false);
1444        }
1445
1446        return $cdbok || $localok;
1447    }
1448
1449    const CHANGE_PASSWORD_PLAINTEXT = 1;
1450    const CHANGE_PASSWORD_ENABLE = 2;
1451    function change_password($new, $flags) {
1452        global $Now;
1453        assert(!$this->conf->external_login());
1454
1455        $cdbu = $this->contactdb_user();
1456        if (($flags & self::CHANGE_PASSWORD_ENABLE)
1457            && ($this->password !== "" || ($cdbu && (string) $cdbu->password !== "")))
1458            return false;
1459
1460        if ($new === null) {
1461            $new = self::random_password();
1462            $flags |= self::CHANGE_PASSWORD_PLAINTEXT;
1463        }
1464        assert(self::valid_password($new));
1465
1466        if ($cdbu) {
1467            $hash = $new;
1468            if ($hash
1469                && !($flags & self::CHANGE_PASSWORD_PLAINTEXT)
1470                && $this->check_password_encryption("", true))
1471                $hash = $this->hash_password($hash);
1472            $cdbu->password = $hash;
1473            $cdbu->passwordTime = $Now;
1474            Dbl::ql($this->conf->contactdb(), "update ContactInfo set password=?, passwordTime=? where contactDbId=?", $cdbu->password, $cdbu->passwordTime, $cdbu->contactDbId);
1475            if ($this->contactId && $this->password) {
1476                $this->password = "";
1477                $this->passwordTime = $cdbu->passwordTime;
1478                $this->conf->ql("update ContactInfo set password=?, passwordTime=? where contactId=?", $this->password, $this->passwordTime, $this->contactId);
1479            }
1480        } else if ($this->contactId) {
1481            $hash = $new;
1482            if ($hash
1483                && !($flags & self::CHANGE_PASSWORD_PLAINTEXT)
1484                && $this->check_password_encryption("", false))
1485                $hash = $this->hash_password($hash);
1486            $this->password = $hash;
1487            $this->passwordTime = $Now;
1488            $this->conf->ql("update ContactInfo set password=?, passwordTime=? where contactId=?", $this->password, $this->passwordTime, $this->contactId);
1489        }
1490        return true;
1491    }
1492
1493
1494    function sendAccountInfo($sendtype, $sensitive) {
1495        assert(!$this->disabled);
1496
1497        $cdbu = $this->contactdb_user();
1498        $rest = array();
1499        if ($sendtype === "create") {
1500            if ($cdbu && $cdbu->passwordUseTime)
1501                $template = "@activateaccount";
1502            else
1503                $template = "@createaccount";
1504        } else if ($sendtype === "forgot") {
1505            if ($this->conf->opt("safePasswords") <= 1 && $this->plaintext_password())
1506                $template = "@accountinfo";
1507            else {
1508                $capmgr = $this->conf->capability_manager($cdbu ? "U" : null);
1509                $rest["capability"] = $capmgr->create(CAPTYPE_RESETPASSWORD, array("user" => $this, "timeExpires" => time() + 259200));
1510                $this->conf->log_for($this, null, "Created password reset " . substr($rest["capability"], 0, 8) . "...");
1511                $template = "@resetpassword";
1512            }
1513        } else {
1514            if ($this->plaintext_password())
1515                $template = "@accountinfo";
1516            else
1517                return false;
1518        }
1519
1520        $mailer = new HotCRPMailer($this->conf, $this, null, $rest);
1521        $prep = $mailer->make_preparation($template, $rest);
1522        if ($prep->sendable
1523            || !$sensitive
1524            || $this->conf->opt("debugShowSensitiveEmail")) {
1525            $prep->send();
1526            return $template;
1527        } else {
1528            Conf::msg_error("Mail cannot be sent to " . htmlspecialchars($this->email) . " at this time.");
1529            return false;
1530        }
1531    }
1532
1533
1534    function mark_login() {
1535        global $Now;
1536        // at least one login every 30 days is marked as activity
1537        if ((int) $this->activity_at <= $Now - 2592000
1538            || (($cdbu = $this->contactdb_user())
1539                && ((int) $cdbu->activity_at <= $Now - 2592000)))
1540            $this->mark_activity();
1541    }
1542
1543    function mark_activity() {
1544        global $Now;
1545        if ((!$this->activity_at || $this->activity_at < $Now)
1546            && !$this->is_anonymous_user()) {
1547            $this->activity_at = $Now;
1548            if ($this->contactId)
1549                $this->conf->ql("update ContactInfo set lastLogin=$Now where contactId=$this->contactId");
1550            if (($cdbu = $this->contactdb_user())
1551                && $cdbu->confid
1552                && (int) $cdbu->activity_at <= $Now - 604800)
1553                $this->_contactdb_save_roles($cdbu);
1554        }
1555    }
1556
1557    function log_activity($text, $paperId = null) {
1558        $this->mark_activity();
1559        if (!$this->is_anonymous_user())
1560            $this->conf->log_for($this, $this, $text, $paperId);
1561    }
1562
1563    function log_activity_for($user, $text, $paperId = null) {
1564        $this->mark_activity();
1565        if (!$this->is_anonymous_user())
1566            $this->conf->log_for($this, $user, $text, $paperId);
1567    }
1568
1569
1570    // HotCRP roles
1571
1572    static function update_rights() {
1573        ++self::$rights_version;
1574    }
1575
1576    private function load_author_reviewer_status() {
1577        // Load from database
1578        $result = null;
1579        if ($this->contactId > 0) {
1580            $qs = ["exists (select * from PaperConflict where contactId=? and conflictType>=" . CONFLICT_AUTHOR . ")",
1581                   "exists (select * from PaperReview where contactId=?)"];
1582            $qv = [$this->contactId, $this->contactId];
1583            if ($this->isPC) {
1584                $qs[] = "exists (select * from PaperReview where requestedBy=? and contactId!=?)";
1585                array_push($qv, $this->contactId, $this->contactId);
1586            } else
1587                $qs[] = "0";
1588            if ($this->_review_tokens) {
1589                $qs[] = "exists (select * from PaperReview where reviewToken?a)";
1590                $qv[] = $this->_review_tokens;
1591            } else
1592                $qs[] = "0";
1593            $result = $this->conf->qe_apply("select " . join(", ", $qs), $qv);
1594        }
1595        $row = $result ? $result->fetch_row() : null;
1596        $this->_db_roles = ($row && $row[0] > 0 ? self::ROLE_AUTHOR : 0)
1597            | ($row && $row[1] > 0 ? self::ROLE_REVIEWER : 0)
1598            | ($row && $row[2] > 0 ? self::ROLE_REQUESTER : 0);
1599        $this->_active_roles = $this->_db_roles
1600            | ($row && $row[3] > 0 ? self::ROLE_REVIEWER : 0);
1601        Dbl::free($result);
1602
1603        // Update contact information from capabilities
1604        if ($this->capabilities) {
1605            foreach ($this->capabilities as $pid => $cap)
1606                if ($pid && ($cap & self::CAP_AUTHORVIEW))
1607                    $this->_active_roles |= self::ROLE_AUTHOR;
1608        }
1609    }
1610
1611    private function check_rights_version() {
1612        if ($this->_rights_version !== self::$rights_version) {
1613            $this->_db_roles = $this->_active_roles =
1614                $this->_has_outstanding_review = $this->_is_lead =
1615                $this->_is_explicit_manager = $this->_is_metareviewer =
1616                $this->_can_view_pc = $this->_dangerous_track_mask =
1617                $this->_authored_papers = null;
1618            $this->_rights_version = self::$rights_version;
1619        }
1620    }
1621
1622    function is_author() {
1623        $this->check_rights_version();
1624        if (!isset($this->_active_roles))
1625            $this->load_author_reviewer_status();
1626        return ($this->_active_roles & self::ROLE_AUTHOR) !== 0;
1627    }
1628
1629    function authored_papers() {
1630        $this->check_rights_version();
1631        if ($this->_authored_papers === null)
1632            $this->_authored_papers = $this->is_author() ? $this->conf->paper_set($this, ["author" => true, "tags" => true])->all() : [];
1633        return $this->_authored_papers;
1634    }
1635
1636    function has_review() {
1637        $this->check_rights_version();
1638        if (!isset($this->_active_roles))
1639            $this->load_author_reviewer_status();
1640        return ($this->_active_roles & self::ROLE_REVIEWER) !== 0;
1641    }
1642
1643    function is_reviewer() {
1644        return $this->isPC || $this->has_review();
1645    }
1646
1647    function is_metareviewer() {
1648        if (!isset($this->_is_metareviewer)) {
1649            if ($this->isPC && $this->conf->setting("metareviews"))
1650                $this->_is_metareviewer = !!$this->conf->fetch_ivalue("select exists (select * from PaperReview where contactId={$this->contactId} and reviewType=" . REVIEW_META . ")");
1651            else
1652                $this->_is_metareviewer = false;
1653        }
1654        return $this->_is_metareviewer;
1655    }
1656
1657    function contactdb_roles() {
1658        $this->is_author(); // load _db_roles
1659        return $this->roles | ($this->_db_roles & (self::ROLE_AUTHOR | self::ROLE_REVIEWER));
1660    }
1661
1662    function has_outstanding_review() {
1663        $this->check_rights_version();
1664        if ($this->_has_outstanding_review === null) {
1665            $this->_has_outstanding_review = $this->has_review()
1666                && $this->conf->fetch_ivalue("select exists (select * from PaperReview join Paper using (paperId) where Paper.timeSubmitted>0 and " . $this->act_reviewer_sql("PaperReview") . " and reviewNeedsSubmit!=0)");
1667        }
1668        return $this->_has_outstanding_review;
1669    }
1670
1671    function is_requester() {
1672        $this->check_rights_version();
1673        if (!isset($this->_active_roles))
1674            $this->load_author_reviewer_status();
1675        return ($this->_active_roles & self::ROLE_REQUESTER) !== 0;
1676    }
1677
1678    function is_discussion_lead() {
1679        $this->check_rights_version();
1680        if (!isset($this->_is_lead)) {
1681            $result = null;
1682            if ($this->contactId > 0)
1683                $result = $this->conf->qe("select exists (select * from Paper where leadContactId=?)", $this->contactId);
1684            $this->_is_lead = edb_nrows($result) > 0;
1685            Dbl::free($result);
1686        }
1687        return $this->_is_lead;
1688    }
1689
1690    function is_explicit_manager() {
1691        $this->check_rights_version();
1692        if (!isset($this->_is_explicit_manager)) {
1693            $this->_is_explicit_manager = false;
1694            if ($this->contactId > 0
1695                && $this->isPC
1696                && ($this->conf->check_any_admin_tracks($this)
1697                    || ($this->conf->has_any_manager()
1698                        && $this->conf->fetch_value("select exists (select * from Paper where managerContactId=?)", $this->contactId) > 0)))
1699                $this->_is_explicit_manager = true;
1700        }
1701        return $this->_is_explicit_manager;
1702    }
1703
1704    function is_manager() {
1705        return $this->privChair || $this->is_explicit_manager();
1706    }
1707
1708    function is_track_manager() {
1709        return $this->privChair || $this->conf->check_any_admin_tracks($this);
1710    }
1711
1712
1713    // review tokens
1714
1715    function review_tokens() {
1716        return $this->_review_tokens ? : [];
1717    }
1718
1719    function active_review_token_for(PaperInfo $prow, ReviewInfo $rrow = null) {
1720        if ($this->_review_tokens) {
1721            if ($rrow) {
1722                if ($rrow->reviewToken && in_array($rrow->reviewToken, $this->_review_tokens))
1723                    return (int) $rrow->reviewToken;
1724            } else {
1725                foreach ($prow->reviews_by_id() as $rrow)
1726                    if ($rrow->reviewToken && in_array($rrow->reviewToken, $this->_review_tokens))
1727                        return (int) $rrow->reviewToken;
1728            }
1729        }
1730        return false;
1731    }
1732
1733    function change_review_token($token, $on) {
1734        assert($token !== false || $on === false);
1735        if (!$this->_review_tokens)
1736            $this->_review_tokens = array();
1737        $old_ntokens = count($this->_review_tokens);
1738        if (!$on && $token === false)
1739            $this->_review_tokens = array();
1740        else {
1741            $pos = array_search($token, $this->_review_tokens);
1742            if (!$on && $pos !== false)
1743                array_splice($this->_review_tokens, $pos, 1);
1744            else if ($on && $pos === false && $token != 0)
1745                $this->_review_tokens[] = $token;
1746        }
1747        $new_ntokens = count($this->_review_tokens);
1748        if ($new_ntokens == 0)
1749            $this->_review_tokens = null;
1750        if ($new_ntokens != $old_ntokens)
1751            self::update_rights();
1752        if ($this->_activated && $new_ntokens != $old_ntokens)
1753            $this->conf->save_session("rev_tokens", $this->_review_tokens);
1754        return $new_ntokens != $old_ntokens;
1755    }
1756
1757
1758    // topic interests
1759
1760    function topic_interest_map() {
1761        global $Me;
1762        if ($this->_topic_interest_map !== null)
1763            return $this->_topic_interest_map;
1764        if ($this->contactId <= 0 || !$this->conf->has_topics())
1765            return array();
1766        if (($this->roles & self::ROLE_PCLIKE)
1767            && $this !== $Me
1768            && ($pcm = $this->conf->pc_members())
1769            && $this === get($pcm, $this->contactId))
1770            self::load_topic_interests($pcm);
1771        else {
1772            $result = $this->conf->qe("select topicId, interest from TopicInterest where contactId={$this->contactId} and interest!=0");
1773            $this->_topic_interest_map = Dbl::fetch_iimap($result);
1774        }
1775        return $this->_topic_interest_map;
1776    }
1777
1778    static function load_topic_interests($contacts) {
1779        if (empty($contacts))
1780            return;
1781        $cbyid = [];
1782        foreach ($contacts as $c) {
1783            $c->_topic_interest_map = [];
1784            $cbyid[$c->contactId] = $c;
1785        }
1786        $result = $c->conf->qe("select contactId, topicId, interest from TopicInterest where interest!=0 order by contactId");
1787        $c = null;
1788        while (($row = edb_row($result))) {
1789            if (!$c || $c->contactId != $row[0])
1790                $c = get($cbyid, $row[0]);
1791            if ($c)
1792                $c->_topic_interest_map[(int) $row[1]] = (int) $row[2];
1793        }
1794        Dbl::free($result);
1795    }
1796
1797
1798    // permissions policies
1799
1800    private function rights(PaperInfo $prow, $forceShow = null) {
1801        $ci = $prow->contact_info($this);
1802
1803        // check first whether administration is allowed
1804        if (!isset($ci->allow_administer)) {
1805            $ci->allow_administer = false;
1806            if (($this->contactId > 0
1807                 && (!$prow->managerContactId
1808                     || $prow->managerContactId == $this->contactId
1809                     || !$ci->conflictType)
1810                 && ($this->privChair
1811                     || $prow->managerContactId == $this->contactId
1812                     || ($this->isPC
1813                         && $this->is_track_manager()
1814                         && $this->conf->check_admin_tracks($prow, $this))))
1815                || $this->is_site_contact) {
1816                $ci->allow_administer = true;
1817            }
1818        }
1819
1820        // correct $forceShow
1821        if (!$ci->allow_administer)
1822            $forceShow = false;
1823        else if ($forceShow === null)
1824            $forceShow = ($this->_overrides & self::OVERRIDE_CONFLICT) !== 0;
1825        else if ($forceShow === "any")
1826            $forceShow = !!$ci->forced_rights_link;
1827        if ($forceShow)
1828            $ci = $ci->get_forced_rights();
1829
1830        // set other rights
1831        if ($ci->rights_forced !== $forceShow) {
1832            $ci->rights_forced = $forceShow;
1833
1834            // check current administration status
1835            $ci->can_administer = $ci->allow_administer
1836                && (!$ci->conflictType || $forceShow);
1837
1838            // check PC tracking
1839            // (see also can_accept_review_assignment*)
1840            $tracks = $this->conf->has_tracks();
1841            $am_lead = $this->contactId > 0 && isset($prow->leadContactId)
1842                && $prow->leadContactId == $this->contactId;
1843            $isPC = $this->isPC
1844                && (!$tracks
1845                    || $ci->reviewType >= REVIEW_PC
1846                    || $am_lead
1847                    || !$this->conf->check_track_view_sensitivity()
1848                    || $this->conf->check_tracks($prow, $this, Track::VIEW));
1849
1850            // check whether PC privileges apply
1851            $ci->allow_pc_broad = $ci->allow_administer || $isPC;
1852            $ci->allow_pc = $ci->can_administer
1853                || ($isPC && !$ci->conflictType);
1854
1855            // check whether this is a potential reviewer
1856            // (existing external reviewer or PC)
1857            if ($ci->reviewType > 0 || $am_lead || $ci->allow_administer)
1858                $ci->potential_reviewer = true;
1859            else if ($ci->allow_pc)
1860                $ci->potential_reviewer = !$tracks
1861                    || $this->conf->check_tracks($prow, $this, Track::UNASSREV);
1862            else
1863                $ci->potential_reviewer = false;
1864            $ci->allow_review = $ci->potential_reviewer
1865                && ($ci->can_administer || !$ci->conflictType);
1866
1867            // check author allowance
1868            $ci->act_author = $ci->conflictType >= CONFLICT_AUTHOR;
1869            $ci->allow_author = $ci->act_author || $ci->allow_administer;
1870
1871            // check author view allowance (includes capabilities)
1872            // If an author-view capability is set, then use it -- unless
1873            // this user is a PC member or reviewer, which takes priority.
1874            $ci->view_conflict_type = $ci->conflictType;
1875            if (isset($this->capabilities)
1876                && isset($this->capabilities[$prow->paperId])
1877                && ($this->capabilities[$prow->paperId] & self::CAP_AUTHORVIEW)
1878                && !$isPC
1879                && !$ci->review_status)
1880                $ci->view_conflict_type = CONFLICT_AUTHOR;
1881            $ci->act_author_view = $ci->view_conflict_type >= CONFLICT_AUTHOR;
1882            $ci->allow_author_view = $ci->act_author_view || $ci->allow_administer;
1883
1884            // check blindness
1885            $bs = $this->conf->submission_blindness();
1886            $ci->nonblind = $bs == Conf::BLIND_NEVER
1887                || ($bs == Conf::BLIND_OPTIONAL
1888                    && !$prow->blind)
1889                || ($bs == Conf::BLIND_UNTILREVIEW
1890                    && $ci->review_status > 0)
1891                || ($prow->outcome > 0
1892                    && ($isPC || $ci->allow_review)
1893                    && $this->conf->time_reviewer_view_accepted_authors());
1894
1895            // check dangerous track mask
1896            if ($ci->allow_administer && $this->_dangerous_track_mask === null)
1897                $this->_dangerous_track_mask = $this->conf->dangerous_track_mask($this);
1898        }
1899
1900        return $ci;
1901    }
1902
1903    function __rights(PaperInfo $prow, $forceShow = null) {
1904        // public access point; to be avoided
1905        return $this->rights($prow, $forceShow);
1906    }
1907
1908    function override_deadlines($rights) {
1909        if (!($this->_overrides & self::OVERRIDE_TIME))
1910            return false;
1911        if ($rights && $rights instanceof PaperInfo)
1912            $rights = $this->rights($rights);
1913        return $rights ? $rights->allow_administer : $this->privChair;
1914    }
1915
1916    function allow_administer(PaperInfo $prow = null) {
1917        if ($prow) {
1918            $rights = $this->rights($prow);
1919            return $rights->allow_administer;
1920        } else
1921            return $this->privChair;
1922    }
1923
1924    function can_meaningfully_override(PaperInfo $prow) {
1925        if ($this->is_manager()) {
1926            $rights = $this->rights($prow, "any");
1927            return $rights->allow_administer
1928                && ($rights->conflictType > 0 || $this->_dangerous_track_mask);
1929        } else
1930            return false;
1931    }
1932
1933    function can_change_password($acct) {
1934        if ($this->privChair
1935            && !$this->conf->opt("chairHidePasswords"))
1936            return true;
1937        else
1938            return $acct
1939                && $this->contactId > 0
1940                && $this->contactId == $acct->contactId
1941                && isset($_SESSION)
1942                && isset($_SESSION["trueuser"])
1943                && strcasecmp($_SESSION["trueuser"]->email, $acct->email) == 0;
1944    }
1945
1946    function can_administer(PaperInfo $prow = null, $forceShow = null) {
1947        if ($prow) {
1948            $rights = $this->rights($prow, $forceShow);
1949            return $rights->can_administer;
1950        } else
1951            return $this->privChair;
1952    }
1953
1954    private function _can_administer_for_track(PaperInfo $prow, $rights, $ttype) {
1955        return $rights->can_administer
1956            && (!($this->_dangerous_track_mask & (1 << $ttype))
1957                || $this->conf->check_tracks($prow, $this, $ttype)
1958                || ($this->_overrides & self::OVERRIDE_CONFLICT) !== 0);
1959    }
1960
1961    function can_administer_for_track(PaperInfo $prow = null, $ttype) {
1962        if ($prow)
1963            return $this->_can_administer_for_track($prow, $this->rights($prow), $ttype);
1964        else
1965            return $this->privChair;
1966    }
1967
1968    function act_pc(PaperInfo $prow = null, $forceShow = null) {
1969        if ($prow) {
1970            $rights = $this->rights($prow, $forceShow);
1971            return $rights->allow_pc;
1972        } else
1973            return $this->isPC;
1974    }
1975
1976    function can_view_pc() {
1977        $this->check_rights_version();
1978        if ($this->_can_view_pc === null) {
1979            if ($this->is_manager())
1980                $this->_can_view_pc = 2;
1981            else if ($this->isPC)
1982                $this->_can_view_pc = $this->conf->opt("secretPC") ? 0 : 2;
1983            else
1984                $this->_can_view_pc = $this->conf->opt("privatePC") ? 0 : 1;
1985        }
1986        return $this->_can_view_pc > 0;
1987    }
1988    function can_view_contact_tags() {
1989        return $this->privChair
1990            || ($this->can_view_pc() && $this->_can_view_pc > 1);
1991    }
1992
1993    function can_view_tracker() {
1994        return $this->privChair
1995            || ($this->isPC && $this->conf->check_default_track($this, Track::VIEWTRACKER))
1996            || $this->tracker_kiosk_state;
1997    }
1998
1999    function view_conflict_type(PaperInfo $prow = null) {
2000        if ($prow) {
2001            $rights = $this->rights($prow);
2002            return $rights->view_conflict_type;
2003        } else
2004            return 0;
2005    }
2006
2007    function act_author_view(PaperInfo $prow) {
2008        $rights = $this->rights($prow);
2009        return $rights->act_author_view;
2010    }
2011
2012    function act_author_view_sql($table, $only_if_complex = false) {
2013        $m = [];
2014        if (isset($this->capabilities) && !$this->isPC) {
2015            foreach ($this->capabilities as $pid => $cap)
2016                if ($pid && ($cap & Contact::CAP_AUTHORVIEW))
2017                    $m[] = "Paper.paperId=$pid";
2018        }
2019        if (empty($m) && $this->contactId && $only_if_complex)
2020            return false;
2021        if ($this->contactId)
2022            $m[] = "$table.conflictType>=" . CONFLICT_AUTHOR;
2023        if (count($m) > 1)
2024            return "(" . join(" or ", $m) . ")";
2025        else
2026            return empty($m) ? "false" : $m[0];
2027    }
2028
2029    function act_reviewer_sql($table) {
2030        $sql = $this->contactId ? "$table.contactId={$this->contactId}" : "false";
2031        if (($rev_tokens = $this->review_tokens()))
2032            $sql = "($sql or $table.reviewToken in (" . join(",", $rev_tokens) . "))";
2033        return $sql;
2034    }
2035
2036    function can_start_paper() {
2037        return $this->email
2038            && ($this->conf->timeStartPaper()
2039                || $this->override_deadlines(null));
2040    }
2041
2042    function perm_start_paper() {
2043        if ($this->can_start_paper())
2044            return null;
2045        return array("deadline" => "sub_reg", "override" => $this->privChair);
2046    }
2047
2048    function can_edit_paper(PaperInfo $prow) {
2049        $rights = $this->rights($prow, "any");
2050        return $rights->allow_administer || $prow->has_author($this);
2051    }
2052
2053    function can_update_paper(PaperInfo $prow) {
2054        $rights = $this->rights($prow, "any");
2055        return $rights->allow_author
2056            && $prow->timeWithdrawn <= 0
2057            && (($prow->outcome >= 0 && $this->conf->timeUpdatePaper($prow))
2058                || $this->override_deadlines($rights));
2059    }
2060
2061    function perm_update_paper(PaperInfo $prow) {
2062        if ($this->can_update_paper($prow))
2063            return null;
2064        $rights = $this->rights($prow, "any");
2065        $whyNot = $prow->make_whynot();
2066        if (!$rights->allow_author && $rights->allow_author_view)
2067            $whyNot["signin"] = "edit_paper";
2068        else if (!$rights->allow_author)
2069            $whyNot["author"] = 1;
2070        if ($prow->timeWithdrawn > 0)
2071            $whyNot["withdrawn"] = 1;
2072        if ($prow->outcome < 0 && $this->can_view_decision($prow))
2073            $whyNot["rejected"] = 1;
2074        if ($prow->timeSubmitted > 0 && $this->conf->setting("sub_freeze") > 0)
2075            $whyNot["updateSubmitted"] = 1;
2076        if (!$this->conf->timeUpdatePaper($prow) && !$this->override_deadlines($rights))
2077            $whyNot["deadline"] = "sub_update";
2078        if ($rights->allow_administer)
2079            $whyNot["override"] = 1;
2080        return $whyNot;
2081    }
2082
2083    function can_finalize_paper(PaperInfo $prow) {
2084        $rights = $this->rights($prow, "any");
2085        return $rights->allow_author
2086            && $prow->timeWithdrawn <= 0
2087            && ($this->conf->timeFinalizePaper($prow) || $this->override_deadlines($rights));
2088    }
2089
2090    function perm_finalize_paper(PaperInfo $prow) {
2091        if ($this->can_finalize_paper($prow))
2092            return null;
2093        $rights = $this->rights($prow, "any");
2094        $whyNot = $prow->make_whynot();
2095        if (!$rights->allow_author && $rights->allow_author_view)
2096            $whyNot["signin"] = "edit_paper";
2097        else if (!$rights->allow_author)
2098            $whyNot["author"] = 1;
2099        if ($prow->timeWithdrawn > 0)
2100            $whyNot["withdrawn"] = 1;
2101        if ($prow->timeSubmitted > 0)
2102            $whyNot["updateSubmitted"] = 1;
2103        if (!$this->conf->timeFinalizePaper($prow) && !$this->override_deadlines($rights))
2104            $whyNot["deadline"] = "sub_sub";
2105        if ($rights->allow_administer)
2106            $whyNot["override"] = 1;
2107        return $whyNot;
2108    }
2109
2110    function can_withdraw_paper(PaperInfo $prow) {
2111        $rights = $this->rights($prow, "any");
2112        return $rights->allow_author
2113            && $prow->timeWithdrawn <= 0
2114            && ($prow->outcome == 0 || $this->override_deadlines($rights));
2115    }
2116
2117    function perm_withdraw_paper(PaperInfo $prow) {
2118        if ($this->can_withdraw_paper($prow))
2119            return null;
2120        $rights = $this->rights($prow, "any");
2121        $whyNot = $prow->make_whynot();
2122        if ($prow->timeWithdrawn > 0)
2123            $whyNot["withdrawn"] = 1;
2124        if (!$rights->allow_author && $rights->allow_author_view)
2125            $whyNot["signin"] = "edit_paper";
2126        else if (!$rights->allow_author)
2127            $whyNot["author"] = 1;
2128        else if ($prow->outcome != 0 && !$this->override_deadlines($rights))
2129            $whyNot["decided"] = 1;
2130        if ($rights->allow_administer)
2131            $whyNot["override"] = 1;
2132        return $whyNot;
2133    }
2134
2135    function can_revive_paper(PaperInfo $prow) {
2136        $rights = $this->rights($prow, "any");
2137        return $rights->allow_author
2138            && $prow->timeWithdrawn > 0
2139            && ($this->conf->timeUpdatePaper($prow) || $this->override_deadlines($rights));
2140    }
2141
2142    function perm_revive_paper(PaperInfo $prow) {
2143        if ($this->can_revive_paper($prow))
2144            return null;
2145        $rights = $this->rights($prow, "any");
2146        $whyNot = $prow->make_whynot();
2147        if (!$rights->allow_author && $rights->allow_author_view)
2148            $whyNot["signin"] = "edit_paper";
2149        else if (!$rights->allow_author)
2150            $whyNot["author"] = 1;
2151        if ($prow->timeWithdrawn <= 0)
2152            $whyNot["notWithdrawn"] = 1;
2153        if (!$this->conf->timeUpdatePaper($prow) && !$this->override_deadlines($rights))
2154            $whyNot["deadline"] = "sub_update";
2155        if ($rights->allow_administer)
2156            $whyNot["override"] = 1;
2157        return $whyNot;
2158    }
2159
2160    function can_submit_final_paper(PaperInfo $prow) {
2161        // see also EditFinal_SearchTerm
2162        $rights = $this->rights($prow, "any");
2163        return $rights->allow_author
2164            && $prow->timeWithdrawn <= 0
2165            && $prow->outcome > 0
2166            && $this->conf->collectFinalPapers()
2167            && $this->can_view_decision($prow)
2168            && ($this->conf->time_submit_final_version()
2169                || $this->override_deadlines($rights));
2170    }
2171
2172    function perm_submit_final_paper(PaperInfo $prow) {
2173        if ($this->can_submit_final_paper($prow))
2174            return null;
2175        $rights = $this->rights($prow, "any");
2176        $whyNot = $prow->make_whynot();
2177        if (!$rights->allow_author && $rights->allow_author_view)
2178            $whyNot["signin"] = "edit_paper";
2179        else if (!$rights->allow_author)
2180            $whyNot["author"] = 1;
2181        if ($prow->timeWithdrawn > 0)
2182            $whyNot["withdrawn"] = 1;
2183        // NB logic order here is important elsewhere
2184        // Don’t report “rejected” error to admins
2185        if ($prow->outcome <= 0
2186            || (!$rights->allow_administer
2187                && !$this->can_view_decision($prow)))
2188            $whyNot["rejected"] = 1;
2189        else if (!$this->conf->collectFinalPapers())
2190            $whyNot["deadline"] = "final_open";
2191        else if (!$this->conf->time_submit_final_version()
2192                 && !$this->override_deadlines($rights))
2193            $whyNot["deadline"] = "final_done";
2194        if ($rights->allow_administer)
2195            $whyNot["override"] = 1;
2196        return $whyNot;
2197    }
2198
2199    function has_hidden_papers() {
2200        return $this->hidden_papers !== null;
2201    }
2202
2203    function can_view_paper(PaperInfo $prow, $pdf = false) {
2204        // hidden_papers is set when a chair with a conflicted, managed
2205        // paper “becomes” a user
2206        if ($this->hidden_papers !== null
2207            && isset($this->hidden_papers[$prow->paperId])) {
2208            $this->hidden_papers[$prow->paperId] = true;
2209            return false;
2210        }
2211        if ($this->privChair)
2212            return true;
2213        $rights = $this->rights($prow, "any");
2214        return $rights->allow_author_view
2215            || ($rights->review_status != 0
2216                // assigned reviewer can view PDF of withdrawn, but submitted, paper
2217                && (!$pdf || $prow->timeSubmitted != 0))
2218            || ($rights->allow_pc_broad
2219                && $this->conf->timePCViewPaper($prow, $pdf)
2220                && (!$pdf || $this->conf->check_tracks($prow, $this, Track::VIEWPDF)));
2221    }
2222
2223    function perm_view_paper(PaperInfo $prow, $pdf = false) {
2224        if ($this->can_view_paper($prow, $pdf))
2225            return null;
2226        $rights = $this->rights($prow, "any");
2227        $whyNot = $prow->make_whynot();
2228        $base_count = count($whyNot);
2229        if (!$rights->allow_author_view
2230            && !$rights->review_status
2231            && !$rights->allow_pc_broad)
2232            $whyNot["permission"] = "view_paper";
2233        else {
2234            if ($prow->timeWithdrawn > 0)
2235                $whyNot["withdrawn"] = 1;
2236            else if ($prow->timeSubmitted <= 0)
2237                $whyNot["notSubmitted"] = 1;
2238            if ($rights->allow_pc_broad
2239                && !$this->conf->timePCViewPaper($prow, false))
2240                $whyNot["deadline"] = "sub_sub";
2241            if ($pdf
2242                && count($whyNot) == $base_count
2243                && $this->can_view_paper($prow))
2244                $whyNot["pdfPermission"] = 1;
2245        }
2246        return $whyNot;
2247    }
2248
2249    function can_view_pdf(PaperInfo $prow) {
2250        return $this->can_view_paper($prow, true);
2251    }
2252
2253    function perm_view_pdf(PaperInfo $prow) {
2254        return $this->perm_view_paper($prow, true);
2255    }
2256
2257    function can_view_some_pdf() {
2258        return $this->privChair
2259            || $this->is_author()
2260            || $this->has_review()
2261            || ($this->isPC && $this->conf->has_any_pc_visible_pdf());
2262    }
2263
2264    function can_view_document_history(PaperInfo $prow) {
2265        if ($this->privChair)
2266            return true;
2267        $rights = $this->rights($prow, "any");
2268        return $rights->act_author || $rights->can_administer;
2269    }
2270
2271    function can_view_manager(PaperInfo $prow = null) {
2272        if ($this->privChair)
2273            return true;
2274        if (!$prow)
2275            return (!$this->conf->opt("hideManager") && $this->is_reviewer())
2276                || ($this->isPC && $this->is_explicit_manager());
2277        $rights = $this->rights($prow, "any");
2278        return $prow->managerContactId == $this->contactId
2279            || ($rights->potential_reviewer && !$this->conf->opt("hideManager"));
2280    }
2281
2282    function can_view_lead(PaperInfo $prow = null) {
2283        if ($prow) {
2284            $rights = $this->rights($prow);
2285            return $rights->can_administer
2286                || ($this->contactId > 0
2287                    && isset($prow->leadContactId)
2288                    && $prow->leadContactId == $this->contactId)
2289                || (($rights->allow_pc || $rights->allow_review)
2290                    && $this->can_view_review_identity($prow, null));
2291        } else
2292            return $this->isPC;
2293    }
2294
2295    function can_view_shepherd(PaperInfo $prow = null) {
2296        // XXX Allow shepherd view when outcome == 0 && can_view_decision.
2297        // This is a mediocre choice, but people like to reuse the shepherd field
2298        // for other purposes, and I might hear complaints.
2299        if ($prow) {
2300            return $this->act_pc($prow)
2301                || (!$this->conf->setting("shepherd_hide")
2302                    && $this->can_view_decision($prow)
2303                    && $this->can_view_review($prow, null));
2304        } else {
2305            return $this->isPC
2306                || (!$this->conf->setting("shepherd_hide")
2307                    && $this->can_view_some_decision_as_author());
2308        }
2309    }
2310
2311    /* NB caller must check can_view_paper() */
2312    function can_view_authors(PaperInfo $prow, $forceShow = null) {
2313        $rights = $this->rights($prow, $forceShow);
2314        return ($rights->nonblind
2315                && $prow->timeSubmitted != 0
2316                && ($rights->allow_pc_broad
2317                    || $rights->review_status != 0))
2318            || ($rights->nonblind
2319                && $prow->timeWithdrawn <= 0
2320                && $rights->allow_pc_broad
2321                && $this->conf->can_pc_see_all_submissions())
2322            || ($rights->allow_administer
2323                ? $rights->nonblind || $rights->rights_forced /* chair can't see blind authors unless forceShow */
2324                : $rights->act_author_view);
2325    }
2326
2327    function allow_view_authors(PaperInfo $prow) {
2328        $rights = $this->rights($prow);
2329        return $rights->allow_administer
2330            || $rights->act_author_view
2331            || ($rights->nonblind
2332                && $prow->timeSubmitted != 0
2333                && ($rights->allow_pc_broad
2334                    || $rights->review_status != 0))
2335            || ($rights->nonblind
2336                && $prow->timeWithdrawn <= 0
2337                && $rights->allow_pc_broad
2338                && $this->conf->can_pc_see_all_submissions());
2339    }
2340
2341    function can_view_some_authors() {
2342        return $this->is_manager()
2343            || $this->is_author()
2344            || ($this->is_reviewer()
2345                && ($this->conf->submission_blindness() != Conf::BLIND_ALWAYS
2346                    || $this->conf->time_reviewer_view_accepted_authors()));
2347    }
2348
2349    function can_view_conflicts(PaperInfo $prow) {
2350        $rights = $this->rights($prow);
2351        if ($rights->allow_administer || $rights->act_author_view)
2352            return true;
2353        if (!$rights->allow_pc_broad && !$rights->potential_reviewer)
2354            return false;
2355        $pccv = $this->conf->setting("sub_pcconfvis");
2356        return $pccv == 2
2357            || (!$pccv && $this->can_view_authors($prow))
2358            || (!$pccv && $this->conf->setting("tracker")
2359                && MeetingTracker::is_paper_tracked($prow)
2360                && $this->can_view_tracker());
2361    }
2362
2363    function can_view_paper_option(PaperInfo $prow, $opt) {
2364        if (!is_object($opt)
2365            && !($opt = $this->conf->paper_opts->get($opt)))
2366            return false;
2367        if (!$this->can_view_paper($prow, $opt->has_document()))
2368            return false;
2369        if ($opt->final
2370            && ($prow->outcome <= 0
2371                || !$this->can_view_decision($prow))
2372            && ($opt->id === DTYPE_FINAL
2373                ? $prow->finalPaperStorageId <= 1
2374                : !$prow->option($opt->id)))
2375            return false;
2376        if ($opt->edit_condition()
2377            && !($this->_overrides & self::OVERRIDE_EDIT_CONDITIONS)
2378            && !$opt->test_edit_condition($prow))
2379            return false;
2380        $rights = $this->rights($prow);
2381        $oview = $opt->visibility;
2382        if ($rights->allow_administer)
2383            return $oview !== "nonblind" || $this->can_view_authors($prow);
2384        else
2385            return $rights->act_author_view
2386                || (($rights->review_status != 0
2387                     || $rights->allow_pc_broad)
2388                    && (!$oview
2389                        || $oview == "rev"
2390                        || ($oview == "nonblind"
2391                            && $this->can_view_authors($prow))));
2392    }
2393
2394    function user_option_list() {
2395        if ($this->conf->has_any_accepted() && $this->can_view_some_decision())
2396            return $this->conf->paper_opts->option_list();
2397        else
2398            return $this->conf->paper_opts->nonfinal_option_list();
2399    }
2400
2401    function perm_view_paper_option(PaperInfo $prow, $opt) {
2402        if ($this->can_view_paper_option($prow, $opt))
2403            return null;
2404        if (!is_object($opt) && !($opt = $this->conf->paper_opts->get($opt)))
2405            return $prow->make_whynot();
2406        if (($whyNot = $this->perm_view_paper($prow, $opt->has_document())))
2407            return $whyNot;
2408        $whyNot = $prow->make_whynot();
2409        $rights = $this->rights($prow);
2410        $oview = $opt->visibility;
2411        if ($rights->allow_administer
2412            ? $oview === "nonblind"
2413              && !$this->can_view_authors($prow)
2414            : !$rights->act_author_view
2415              && ($oview === "admin"
2416                  || ((!$oview || $oview == "rev")
2417                      && !$rights->review_status
2418                      && !$rights->allow_pc_broad)
2419                  || ($oview == "nonblind"
2420                      && !$this->can_view_authors($prow))))
2421            $whyNot["optionPermission"] = $opt;
2422        else if ($opt->final && ($prow->outcome <= 0 || !$this->can_view_decision($prow)))
2423            $whyNot["optionNotAccepted"] = $opt;
2424        else
2425            $whyNot["optionPermission"] = $opt;
2426        return $whyNot;
2427    }
2428
2429    function can_view_some_paper_option(PaperOption $opt) {
2430        if (($opt->has_document() && !$this->can_view_some_pdf())
2431            || ($opt->final && !$this->can_view_some_decision()))
2432            return false;
2433        $oview = $opt->visibility;
2434        return $this->is_author()
2435            || ($oview == "admin" && $this->is_manager())
2436            || ((!$oview || $oview == "rev") && $this->is_reviewer())
2437            || ($oview == "nonblind" && $this->can_view_some_authors());
2438    }
2439
2440    function is_my_review(ReviewInfo $rrow = null) {
2441        return $rrow
2442            && ($rrow->contactId == $this->contactId
2443                || ($this->_review_tokens
2444                    && $rrow->reviewToken
2445                    && in_array($rrow->reviewToken, $this->_review_tokens)));
2446    }
2447
2448    function is_owned_review(ReviewInfo $rrow = null) {
2449        return $rrow
2450            && ($rrow->contactId == $this->contactId
2451                || ($this->_review_tokens && $rrow->reviewToken && in_array($rrow->reviewToken, $this->_review_tokens))
2452                || ($rrow->requestedBy == $this->contactId
2453                    && $rrow->reviewType == REVIEW_EXTERNAL
2454                    && $this->conf->setting("pcrev_editdelegate")));
2455    }
2456
2457    function can_view_review_assignment(PaperInfo $prow, $rrow) {
2458        $rights = $this->rights($prow);
2459        return $rights->allow_administer
2460            || $rights->allow_pc
2461            || $rights->review_status != 0
2462            || $this->can_view_review($prow, $rrow);
2463    }
2464
2465    static function can_some_author_respond(PaperInfo $prow) {
2466        return $prow->conf->any_response_open;
2467    }
2468
2469    static function can_some_author_view_submitted_review(PaperInfo $prow) {
2470        if (self::can_some_author_respond($prow))
2471            return true;
2472        else if ($prow->conf->au_seerev == Conf::AUSEEREV_TAGS)
2473            return $prow->has_any_tag($prow->conf->tag_au_seerev);
2474        else
2475            return $prow->conf->au_seerev != 0;
2476    }
2477
2478    private function can_view_submitted_review_as_author(PaperInfo $prow) {
2479        return self::can_some_author_respond($prow)
2480            || $this->conf->au_seerev == Conf::AUSEEREV_YES
2481            || ($this->conf->au_seerev == Conf::AUSEEREV_UNLESSINCOMPLETE
2482                && (!$this->has_review()
2483                    || !$this->has_outstanding_review()))
2484            || ($this->conf->au_seerev == Conf::AUSEEREV_TAGS
2485                && $prow->has_any_tag($this->conf->tag_au_seerev));
2486    }
2487
2488    function can_view_some_review() {
2489        return $this->is_reviewer()
2490            || ($this->is_author()
2491                && ($this->conf->au_seerev != 0
2492                    || $this->conf->any_response_open));
2493    }
2494
2495    private function seerev_setting(PaperInfo $prow, $rrow, $rights) {
2496        $round = $rrow ? $rrow->reviewRound : "max";
2497        if ($rights->allow_pc) {
2498            $rs = $this->conf->round_setting("pc_seeallrev", $round);
2499            if (!$this->conf->has_tracks())
2500                return $rs;
2501            if ($this->conf->check_required_tracks($prow, $this, Track::VIEWREVOVERRIDE))
2502                return Conf::PCSEEREV_YES;
2503            if ($this->conf->check_tracks($prow, $this, Track::VIEWREV)) {
2504                if (!$this->conf->check_tracks($prow, $this, Track::VIEWALLREV))
2505                    $rs = 0;
2506                return $rs;
2507            }
2508        } else {
2509            if ($this->conf->round_setting("extrev_view", $round))
2510                return 0;
2511        }
2512        return -1;
2513    }
2514
2515    private function seerevid_setting(PaperInfo $prow, $rrow, $rights) {
2516        $round = $rrow ? $rrow->reviewRound : "max";
2517        if ($rights->allow_pc) {
2518            if ($this->conf->check_required_tracks($prow, $this, Track::VIEWREVOVERRIDE))
2519                return Conf::PCSEEREV_YES;
2520            if ($this->conf->check_tracks($prow, $this, Track::VIEWREVID)) {
2521                $s = $this->conf->round_setting("pc_seeblindrev", $round);
2522                if ($s >= 0)
2523                    return $s ? 0 : Conf::PCSEEREV_YES;
2524            }
2525        } else {
2526            if ($this->conf->round_setting("extrev_view", $round) == 2)
2527                return 0;
2528        }
2529        return -1;
2530    }
2531
2532    function can_view_review(PaperInfo $prow, $rrow, $forceShow = null, $viewscore = null) {
2533        if (is_int($rrow)) {
2534            $viewscore = $rrow;
2535            $rrow = null;
2536        } else if ($viewscore === null)
2537            $viewscore = VIEWSCORE_AUTHOR;
2538        if ($rrow && !($rrow instanceof ReviewInfo))
2539            error_log("not ReviewInfo " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
2540        assert(!$rrow || $prow->paperId == $rrow->paperId);
2541        $rights = $this->rights($prow, $forceShow);
2542        if ($this->_can_administer_for_track($prow, $rights, Track::VIEWREV)
2543            || $rights->reviewType == REVIEW_META
2544            || ($rrow
2545                && $this->is_owned_review($rrow)
2546                && $viewscore >= VIEWSCORE_REVIEWERONLY))
2547            return true;
2548        $rrowSubmitted = !$rrow || $rrow->reviewSubmitted > 0;
2549        $seerev = $this->seerev_setting($prow, $rrow, $rights);
2550        // See also PaperInfo::can_view_review_identity_of.
2551        return ($rights->act_author_view
2552                && $rrowSubmitted
2553                && (!$rrow || $rrow->reviewOrdinal > 0)
2554                && $this->can_view_submitted_review_as_author($prow)
2555                && ($viewscore >= VIEWSCORE_AUTHOR
2556                    || ($viewscore >= VIEWSCORE_AUTHORDEC
2557                        && $prow->outcome
2558                        && $this->can_view_decision($prow, $forceShow))))
2559            || ($rights->allow_pc
2560                && $rrowSubmitted
2561                && $viewscore >= VIEWSCORE_PC
2562                && $seerev > 0
2563                && ($seerev != Conf::PCSEEREV_UNLESSANYINCOMPLETE
2564                    || !$this->has_outstanding_review())
2565                && ($seerev != Conf::PCSEEREV_UNLESSINCOMPLETE
2566                    || !$rights->review_status))
2567            || ($rights->review_status != 0
2568                && !$rights->view_conflict_type
2569                && $rrowSubmitted
2570                && $viewscore >= VIEWSCORE_PC
2571                && $prow->review_not_incomplete($this)
2572                && $seerev >= 0);
2573    }
2574
2575    function perm_view_review(PaperInfo $prow, $rrow, $forceShow = null, $viewscore = null) {
2576        if ($this->can_view_review($prow, $rrow, $forceShow, $viewscore))
2577            return null;
2578        $rrowSubmitted = !$rrow || $rrow->reviewSubmitted > 0;
2579        $rights = $this->rights($prow, $forceShow);
2580        $whyNot = $prow->make_whynot();
2581        if ((!$rights->act_author_view
2582             && !$rights->allow_pc
2583             && !$rights->review_status)
2584            || ($rights->allow_pc
2585                && !$this->conf->check_tracks($prow, $this, Track::VIEWREV)))
2586            $whyNot["permission"] = "view_review";
2587        else if ($prow->timeWithdrawn > 0)
2588            $whyNot["withdrawn"] = 1;
2589        else if ($prow->timeSubmitted <= 0)
2590            $whyNot["notSubmitted"] = 1;
2591        else if ($rights->act_author_view
2592                 && $this->conf->au_seerev == Conf::AUSEEREV_UNLESSINCOMPLETE
2593                 && $this->has_outstanding_review()
2594                 && $this->has_review())
2595            $whyNot["reviewsOutstanding"] = 1;
2596        else if ($rights->act_author_view
2597                 && !$rrowSubmitted)
2598            $whyNot["permission"] = "view_review";
2599        else if ($rights->act_author_view)
2600            $whyNot["deadline"] = "au_seerev";
2601        else if ($rights->view_conflict_type)
2602            $whyNot["conflict"] = 1;
2603        else if (!$rights->allow_pc
2604                 && $prow->review_submitted($this))
2605            $whyNot["externalReviewer"] = 1;
2606        else if (!$rrowSubmitted)
2607            $whyNot["reviewNotSubmitted"] = 1;
2608        else if ($rights->allow_pc
2609                 && $this->seerev_setting($prow, $rrow, $rights) == Conf::PCSEEREV_UNLESSANYINCOMPLETE
2610                 && $this->has_outstanding_review())
2611            $whyNot["reviewsOutstanding"] = 1;
2612        else if (!$this->conf->time_review_open())
2613            $whyNot["deadline"] = "rev_open";
2614        else
2615            $whyNot["reviewNotComplete"] = 1;
2616        if ($rights->allow_administer)
2617            $whyNot["forceShow"] = 1;
2618        return $whyNot;
2619    }
2620
2621    function can_view_review_identity(PaperInfo $prow, ReviewInfo $rrow = null, $forceShow = null) {
2622        $rights = $this->rights($prow, $forceShow);
2623        // See also PaperInfo::can_view_review_identity_of.
2624        // See also ReviewerFexpr.
2625        if ($this->_can_administer_for_track($prow, $rights, Track::VIEWREVID)
2626            || $rights->reviewType == REVIEW_META
2627            || ($rrow && $rrow->requestedBy == $this->contactId && $rights->allow_pc)
2628            || ($rrow && $this->is_owned_review($rrow)))
2629            return true;
2630        $seerevid_setting = $this->seerevid_setting($prow, $rrow, $rights);
2631        return ($rights->allow_pc
2632                && $seerevid_setting == Conf::PCSEEREV_YES)
2633            || ($rights->allow_review
2634                && $prow->review_not_incomplete($this)
2635                && $seerevid_setting >= 0)
2636            || !$this->conf->is_review_blind($rrow);
2637    }
2638
2639    function can_view_some_review_identity() {
2640        $tags = "";
2641        if (($t = $this->conf->permissive_track_tag_for($this, Track::VIEWREVOVERRIDE))
2642            || ($t = $this->conf->permissive_track_tag_for($this, Track::VIEWREVID)))
2643            $tags = " $t#0 ";
2644        if ($this->isPC)
2645            $rtype = $this->is_metareviewer() ? REVIEW_META : REVIEW_PC;
2646        else
2647            $rtype = $this->is_reviewer() ? REVIEW_EXTERNAL : 0;
2648        $prow = new PaperInfo([
2649            "conflictType" => 0, "managerContactId" => 0,
2650            "myReviewPermissions" => "$rtype 1 0",
2651            "paperId" => 1, "timeSubmitted" => 1,
2652            "blind" => false, "outcome" => 1,
2653            "paperTags" => $tags
2654        ], $this);
2655        $overrides = $this->add_overrides(self::OVERRIDE_CONFLICT);
2656        $answer = $this->can_view_review_identity($prow, null);
2657        $this->set_overrides($overrides);
2658        return $answer;
2659    }
2660
2661    function can_view_review_round(PaperInfo $prow, ReviewInfo $rrow = null) {
2662        $rights = $this->rights($prow);
2663        return $rights->can_administer
2664            || $rights->allow_pc
2665            || $rights->allow_review;
2666    }
2667
2668    function can_view_review_time(PaperInfo $prow, ReviewInfo $rrow = null) {
2669        $rights = $this->rights($prow);
2670        return !$rights->act_author_view
2671            || ($rrow && $rrow->reviewAuthorSeen
2672                && $rrow->reviewAuthorSeen <= $rrow->reviewAuthorModified);
2673    }
2674
2675    function can_view_review_requester(PaperInfo $prow, ReviewInfo $rrow = null) {
2676        $rights = $this->rights($prow);
2677        return $this->_can_administer_for_track($prow, $rights, Track::VIEWREVID)
2678            || ($rrow && $rrow->requestedBy == $this->contactId && $rights->allow_pc)
2679            || ($rrow && $this->is_owned_review($rrow))
2680            || ($rights->allow_pc && $this->can_view_review_identity($prow, $rrow));
2681    }
2682
2683    function can_request_review(PaperInfo $prow, $check_time) {
2684        $rights = $this->rights($prow);
2685        return ($rights->reviewType >= REVIEW_PC
2686                || ($this->contactId > 0
2687                    && isset($prow->leadContactId)
2688                    && $prow->leadContactId == $this->contactId)
2689                || $rights->allow_administer)
2690            && (!$check_time
2691                || $this->conf->time_review(null, false, true)
2692                || $this->override_deadlines($rights));
2693    }
2694
2695    function perm_request_review(PaperInfo $prow, $check_time) {
2696        if ($this->can_request_review($prow, $check_time))
2697            return null;
2698        $rights = $this->rights($prow);
2699        $whyNot = $prow->make_whynot();
2700        if ($rights->reviewType < REVIEW_PC
2701            && ($this->contactId <= 0
2702                || !isset($prow->leadContactId)
2703                || $prow->leadContactId != $this->contactId)
2704            && !$rights->allow_administer)
2705            $whyNot["permission"] = "request_review";
2706        else {
2707            $whyNot["deadline"] = ($rights->allow_pc ? "pcrev_hard" : "extrev_hard");
2708            if ($rights->allow_administer)
2709                $whyNot["override"] = 1;
2710        }
2711        return $whyNot;
2712    }
2713
2714    function can_review_any() {
2715        return $this->isPC
2716            && $this->conf->setting("pcrev_any") > 0
2717            && $this->conf->time_review(null, true, true)
2718            && $this->conf->check_any_tracks($this, Track::UNASSREV);
2719    }
2720
2721    function timeReview(PaperInfo $prow, ReviewInfo $rrow = null) {
2722        $rights = $this->rights($prow);
2723        if ($rights->reviewType > 0
2724            || ($rrow
2725                && $this->is_owned_review($rrow))
2726            || ($rrow
2727                && $rrow->contactId != $this->contactId
2728                && $rights->allow_administer))
2729            return $this->conf->time_review($rrow, $rights->allow_pc, true);
2730        else if ($rights->allow_review
2731                 && $this->conf->setting("pcrev_any") > 0)
2732            return $this->conf->time_review(null, true, true);
2733        else
2734            return false;
2735    }
2736
2737    function can_become_reviewer_ignore_conflict(PaperInfo $prow = null) {
2738        if (!$prow)
2739            return $this->isPC
2740                && ($this->conf->check_all_tracks($this, Track::ASSREV)
2741                    || $this->conf->check_all_tracks($this, Track::UNASSREV));
2742        $rights = $this->rights($prow);
2743        return $rights->allow_pc_broad
2744            && ($rights->reviewType > 0
2745                || $rights->allow_administer
2746                || $this->conf->check_tracks($prow, $this, Track::ASSREV)
2747                || $this->conf->check_tracks($prow, $this, Track::UNASSREV));
2748    }
2749
2750    function can_accept_review_assignment_ignore_conflict(PaperInfo $prow = null) {
2751        if (!$prow)
2752            return $this->isPC && $this->conf->check_all_tracks($this, Track::ASSREV);
2753        $rights = $this->rights($prow);
2754        return ($rights->allow_administer
2755                || $this->isPC)
2756            && ($rights->reviewType > 0
2757                || $rights->allow_administer
2758                || $this->conf->check_tracks($prow, $this, Track::ASSREV));
2759    }
2760
2761    function can_accept_review_assignment(PaperInfo $prow) {
2762        $rights = $this->rights($prow);
2763        return ($rights->allow_pc
2764                || ($this->isPC && !$rights->conflictType))
2765            && ($rights->reviewType > 0
2766                || $rights->allow_administer
2767                || $this->conf->check_tracks($prow, $this, Track::ASSREV));
2768    }
2769
2770    private function rights_owned_review($rights, $rrow) {
2771        if ($rrow)
2772            return $rights->can_administer || $this->is_owned_review($rrow);
2773        else
2774            return $rights->reviewType > 0;
2775    }
2776
2777    function can_review(PaperInfo $prow, ReviewInfo $rrow = null, $submit = false) {
2778        assert(!$rrow || $rrow->paperId == $prow->paperId);
2779        $rights = $this->rights($prow);
2780        if ($submit && !$this->can_clickthrough("review"))
2781            return false;
2782        return ($this->rights_owned_review($rights, $rrow)
2783                && $this->conf->time_review($rrow, $rights->allow_pc, true))
2784            || (!$rrow
2785                && $prow->timeSubmitted > 0
2786                && $rights->allow_review
2787                && $this->conf->setting("pcrev_any") > 0
2788                && $this->conf->time_review(null, true, true))
2789            || ($rights->can_administer
2790                && (($prow->timeSubmitted > 0 && !$submit)
2791                    || $this->override_deadlines($rights)));
2792    }
2793
2794    function perm_review(PaperInfo $prow, $rrow, $submit = false) {
2795        if ($this->can_review($prow, $rrow, $submit))
2796            return null;
2797        $rights = $this->rights($prow);
2798        $rrow_cid = $rrow ? $rrow->contactId : 0;
2799        // The "reviewNotAssigned" and "deadline" failure reasons are special.
2800        // If either is set, the system will still allow review form download.
2801        $whyNot = $prow->make_whynot();
2802        if ($rrow && $rrow_cid != $this->contactId
2803            && !$rights->allow_administer)
2804            $whyNot["differentReviewer"] = 1;
2805        else if (!$rights->allow_pc && !$this->rights_owned_review($rights, $rrow))
2806            $whyNot["permission"] = "review";
2807        else if ($prow->timeWithdrawn > 0)
2808            $whyNot["withdrawn"] = 1;
2809        else if ($prow->timeSubmitted <= 0)
2810            $whyNot["notSubmitted"] = 1;
2811        else {
2812            if ($rights->conflictType && !$rights->can_administer)
2813                $whyNot["conflict"] = 1;
2814            else if ($rights->allow_review
2815                     && !$this->rights_owned_review($rights, $rrow)
2816                     && (!$rrow || $rrow_cid == $this->contactId))
2817                $whyNot["reviewNotAssigned"] = 1;
2818            else if ($this->can_review($prow, $rrow, false)
2819                     && !$this->can_clickthrough("review"))
2820                $whyNot["clickthrough"] = 1;
2821            else
2822                $whyNot["deadline"] = ($rights->allow_pc ? "pcrev_hard" : "extrev_hard");
2823            if ($rights->allow_administer
2824                && ($rights->conflictType || $prow->timeSubmitted <= 0))
2825                $whyNot["forceShow"] = 1;
2826            if ($rights->allow_administer && isset($whyNot["deadline"]))
2827                $whyNot["override"] = 1;
2828        }
2829        return $whyNot;
2830    }
2831
2832    function perm_submit_review(PaperInfo $prow, $rrow) {
2833        return $this->perm_review($prow, $rrow, true);
2834    }
2835
2836    function can_create_review_from(PaperInfo $prow, Contact $user) {
2837        $rights = $this->rights($prow);
2838        return $rights->can_administer
2839            && ($prow->timeSubmitted > 0 || $this->override_deadlines($rights))
2840            && (!$user->isPC || $user->can_accept_review_assignment($prow))
2841            && ($this->conf->time_review(null, true, true) || $this->override_deadlines($rights));
2842    }
2843
2844    function perm_create_review_from(PaperInfo $prow, Contact $user) {
2845        if ($this->can_create_review_from($prow, $user))
2846            return null;
2847        $rights = $this->rights($prow);
2848        $whyNot = $prow->make_whynot();
2849        if (!$rights->allow_administer)
2850            $whyNot["administer"] = 1;
2851        else if ($prow->timeWithdrawn > 0)
2852            $whyNot["withdrawn"] = 1;
2853        else if ($prow->timeSubmitted <= 0)
2854            $whyNot["notSubmitted"] = 1;
2855        else {
2856            if ($user->isPC && !$user->can_accept_review_assignment($prow))
2857                $whyNot["unacceptableReviewer"] = 1;
2858            if (!$this->conf->time_review(null, true, true))
2859                $whyNot["deadline"] = ($user->isPC ? "pcrev_hard" : "extrev_hard");
2860            if ($rights->allow_administer
2861                && ($rights->conflictType || $prow->timeSubmitted <= 0))
2862                $whyNot["forceShow"] = 1;
2863            if ($rights->allow_administer && isset($whyNot["deadline"]))
2864                $whyNot["override"] = 1;
2865        }
2866        return $whyNot;
2867    }
2868
2869    function can_clickthrough($ctype) {
2870        if (!$this->privChair && $this->conf->opt("clickthrough_$ctype")) {
2871            $csha1 = sha1($this->conf->message_html("clickthrough_$ctype"));
2872            $data = $this->data("clickthrough");
2873            return $data && get($data, $csha1);
2874        } else
2875            return true;
2876    }
2877
2878    function can_view_review_ratings(PaperInfo $prow, ReviewInfo $rrow = null, $override_self = false) {
2879        $rs = $this->conf->setting("rev_ratings");
2880        $rights = $this->rights($prow);
2881        if (!$this->can_view_review($prow, $rrow)
2882            || (!$rights->allow_pc && !$rights->allow_review)
2883            || ($rs != REV_RATINGS_PC && $rs != REV_RATINGS_PC_EXTERNAL))
2884            return false;
2885        if (!$rrow
2886            || $override_self
2887            || $rrow->contactId != $this->contactId
2888            || $this->can_administer($prow)
2889            || $this->conf->setting("pc_seeallrev")
2890            || (isset($rrow->allRatings) && strpos($rrow->allRatings, ",") !== false))
2891            return true;
2892        // Do not show rating counts if rater identity is unambiguous.
2893        // See also PaperSearch::_clauseTermSetRating.
2894        $nsubraters = 0;
2895        foreach ($prow->reviews_by_id() as $rrow)
2896            if ($rrow->reviewNeedsSubmit == 0
2897                && $rrow->contactId != $this->contactId
2898                && ($rs == REV_RATINGS_PC_EXTERNAL
2899                    || ($rs == REV_RATINGS_PC && $rrow->reviewType > REVIEW_EXTERNAL)))
2900                ++$nsubraters;
2901        return $nsubraters >= 2;
2902    }
2903
2904    function can_view_some_review_ratings() {
2905        $rs = $this->conf->setting("rev_ratings");
2906        return $this->is_reviewer() && ($rs == REV_RATINGS_PC || $rs == REV_RATINGS_PC_EXTERNAL);
2907    }
2908
2909    function can_rate_review(PaperInfo $prow, $rrow) {
2910        return $this->can_view_review_ratings($prow, $rrow, true)
2911            && !$this->is_my_review($rrow);
2912    }
2913
2914
2915    function is_my_comment(PaperInfo $prow, $crow) {
2916        if ($crow->contactId == $this->contactId)
2917            return true;
2918        if ($this->_review_tokens) {
2919            foreach ($prow->reviews_of_user($crow->contactId) as $rrow)
2920                if ($rrow->reviewToken && in_array($rrow->reviewToken, $this->_review_tokens))
2921                    return true;
2922        }
2923        return false;
2924    }
2925
2926    function can_comment(PaperInfo $prow, $crow, $submit = false) {
2927        if ($crow && ($crow->commentType & COMMENTTYPE_RESPONSE))
2928            return $this->can_respond($prow, $crow, $submit);
2929        $rights = $this->rights($prow);
2930        $author = $rights->act_author
2931            && $this->conf->setting("cmt_author") > 0
2932            && $this->can_view_submitted_review_as_author($prow);
2933        return ($author
2934                || ($rights->allow_review
2935                    && ($prow->timeSubmitted > 0
2936                        || $rights->review_status != 0
2937                        || ($rights->allow_administer && $rights->rights_forced))
2938                    && ($this->conf->setting("cmt_always") > 0
2939                        || $this->conf->time_review(null, $rights->allow_pc, true)
2940                        || ($rights->allow_administer
2941                            && (!$submit || $this->override_deadlines($rights))))))
2942            && (!$crow
2943                || !$crow->contactId
2944                || $rights->allow_administer
2945                || $this->is_my_comment($prow, $crow)
2946                || ($author
2947                    && ($crow->commentType & COMMENTTYPE_BYAUTHOR)));
2948    }
2949
2950    function can_submit_comment(PaperInfo $prow, $crow) {
2951        return $this->can_comment($prow, $crow, true);
2952    }
2953
2954    function perm_comment(PaperInfo $prow, $crow, $submit = false) {
2955        if ($crow && ($crow->commentType & COMMENTTYPE_RESPONSE))
2956            return $this->perm_respond($prow, $crow, $submit);
2957        if ($this->can_comment($prow, $crow, $submit))
2958            return null;
2959        $rights = $this->rights($prow);
2960        $whyNot = $prow->make_whynot();
2961        if ($crow && $crow->contactId != $this->contactId
2962            && !$rights->allow_administer)
2963            $whyNot["differentReviewer"] = 1;
2964        else if (!$rights->allow_pc
2965                 && !$rights->allow_review
2966                 && (!$rights->act_author
2967                     || $this->conf->setting("cmt_author", 0) <= 0))
2968            $whyNot["permission"] = "comment";
2969        else if ($prow->timeWithdrawn > 0)
2970            $whyNot["withdrawn"] = 1;
2971        else if ($prow->timeSubmitted <= 0)
2972            $whyNot["notSubmitted"] = 1;
2973        else {
2974            if ($rights->conflictType > 0)
2975                $whyNot["conflict"] = 1;
2976            else
2977                $whyNot["deadline"] = ($rights->allow_pc ? "pcrev_hard" : "extrev_hard");
2978            if ($rights->allow_administer && $rights->conflictType)
2979                $whyNot["forceShow"] = 1;
2980            if ($rights->allow_administer && isset($whyNot['deadline']))
2981                $whyNot["override"] = 1;
2982        }
2983        return $whyNot;
2984    }
2985
2986    function perm_submit_comment(PaperInfo $prow, $crow) {
2987        return $this->perm_comment($prow, $crow, true);
2988    }
2989
2990    function can_respond(PaperInfo $prow, CommentInfo $crow, $submit = false) {
2991        if ($prow->timeSubmitted <= 0
2992            || !($crow->commentType & COMMENTTYPE_RESPONSE)
2993            || !($rrd = get($prow->conf->resp_rounds(), $crow->commentRound)))
2994            return false;
2995        $rights = $this->rights($prow);
2996        return ($rights->can_administer
2997                || $rights->act_author)
2998            && (($rights->allow_administer
2999                 && (!$submit || $this->override_deadlines($rights)))
3000                || $rrd->time_allowed(true))
3001            && (!$rrd->search
3002                || $rrd->search->test($prow));
3003    }
3004
3005    function perm_respond(PaperInfo $prow, CommentInfo $crow, $submit = false) {
3006        if ($this->can_respond($prow, $crow, $submit))
3007            return null;
3008        $rights = $this->rights($prow);
3009        $whyNot = $prow->make_whynot();
3010        if (!$rights->allow_administer
3011            && !$rights->act_author)
3012            $whyNot["permission"] = "respond";
3013        else if ($prow->timeWithdrawn > 0)
3014            $whyNot["withdrawn"] = 1;
3015        else if ($prow->timeSubmitted <= 0)
3016            $whyNot["notSubmitted"] = 1;
3017        else {
3018            $whyNot["deadline"] = "resp_done";
3019            if ($crow->commentRound)
3020                $whyNot["deadline"] .= "_" . $crow->commentRound;
3021            if ($rights->allow_administer && $rights->conflictType)
3022                $whyNot["forceShow"] = 1;
3023            if ($rights->allow_administer)
3024                $whyNot["override"] = 1;
3025        }
3026        return $whyNot;
3027    }
3028
3029    function preferred_resp_round_number(PaperInfo $prow) {
3030        $rights = $this->rights($prow);
3031        if ($rights->act_author)
3032            foreach ($prow->conf->resp_rounds() as $rrd)
3033                if ($rrd->time_allowed())
3034                    return $rrd->number;
3035        return false;
3036    }
3037
3038    function can_view_comment(PaperInfo $prow, $crow, $forceShow = null) {
3039        $ctype = $crow ? $crow->commentType : COMMENTTYPE_AUTHOR;
3040        $rights = $this->rights($prow, $forceShow);
3041        return ($crow && $this->is_my_comment($prow, $crow))
3042            || $rights->can_administer
3043            || ($rights->act_author_view
3044                && ($ctype & (COMMENTTYPE_BYAUTHOR | COMMENTTYPE_RESPONSE)))
3045            || ($rights->act_author_view
3046                && $ctype >= COMMENTTYPE_AUTHOR
3047                && !($ctype & COMMENTTYPE_DRAFT)
3048                && $this->can_view_submitted_review_as_author($prow))
3049            || (!$rights->view_conflict_type
3050                && !($ctype & COMMENTTYPE_DRAFT)
3051                && ($rights->allow_pc
3052                    ? $ctype >= COMMENTTYPE_PCONLY
3053                    : $ctype >= COMMENTTYPE_REVIEWER)
3054                && $this->can_view_review($prow, null, $forceShow)
3055                && ($this->conf->setting("cmt_revid")
3056                    || $ctype >= COMMENTTYPE_AUTHOR
3057                    || $this->can_view_review_identity($prow, null, $forceShow)));
3058    }
3059
3060    function can_view_new_comment_ignore_conflict(PaperInfo $prow) {
3061        // Goal: Return true if this user is part of the comment mention
3062        // completion for a new comment on $prow.
3063        // Problem: If authors are hidden, should we mention this user or not?
3064        $rights = $this->rights($prow, null);
3065        return $rights->can_administer
3066            || $rights->allow_pc;
3067    }
3068
3069    function canViewCommentReviewWheres() {
3070        if ($this->privChair
3071            || ($this->isPC && $this->conf->setting("pc_seeallrev") > 0))
3072            return array();
3073        else
3074            return array("(" . $this->act_author_view_sql("PaperConflict")
3075                         . " or MyPaperReview.reviewId is not null)");
3076    }
3077
3078    function can_view_comment_identity(PaperInfo $prow, $crow, $forceShow = null) {
3079        if ($crow && ($crow->commentType & (COMMENTTYPE_RESPONSE | COMMENTTYPE_BYAUTHOR)))
3080            return $this->can_view_authors($prow, $forceShow);
3081        $rights = $this->rights($prow, $forceShow);
3082        return $this->_can_administer_for_track($prow, $rights, Track::VIEWREVID)
3083            || ($crow && $crow->contactId == $this->contactId)
3084            || (($rights->allow_pc
3085                 || ($rights->allow_review
3086                     && $this->conf->setting("extrev_view") >= 2))
3087                && ($this->can_view_review_identity($prow, null)
3088                    || ($crow && $prow->can_view_review_identity_of($crow->commentId, $this))))
3089            || !$this->conf->is_review_blind(!$crow || ($crow->commentType & COMMENTTYPE_BLIND) != 0);
3090    }
3091
3092    function can_view_comment_time(PaperInfo $prow, $crow) {
3093        return $this->can_view_comment_identity($prow, $crow, true);
3094    }
3095
3096    function can_view_comment_tags(PaperInfo $prow, $crow) {
3097        $rights = $this->rights($prow);
3098        return $rights->allow_pc || $rights->review_status != 0;
3099    }
3100
3101    function can_view_some_draft_response() {
3102        return $this->is_manager() || $this->is_author();
3103    }
3104
3105
3106    function can_view_decision(PaperInfo $prow, $forceShow = null) {
3107        $rights = $this->rights($prow, $forceShow);
3108        return $rights->can_administer
3109            || ($rights->act_author_view
3110                && $prow->can_author_view_decision())
3111            || ($rights->allow_pc_broad
3112                && $this->conf->timePCViewDecision($rights->view_conflict_type > 0))
3113            || ($rights->review_status > 0
3114                && $this->conf->time_reviewer_view_decision());
3115    }
3116
3117    function can_view_some_decision() {
3118        return $this->is_manager()
3119            || ($this->is_author() && $this->can_view_some_decision_as_author())
3120            || ($this->isPC && $this->conf->timePCViewDecision(false))
3121            || ($this->is_reviewer() && $this->conf->time_reviewer_view_decision());
3122    }
3123
3124    function can_view_some_decision_as_author() {
3125        return $this->conf->can_some_author_view_decision();
3126    }
3127
3128    static function can_some_author_view_decision(PaperInfo $prow) {
3129        return $prow->outcome
3130            && $prow->conf->can_some_author_view_decision();
3131    }
3132
3133    function can_set_decision(PaperInfo $prow) {
3134        return $this->can_administer($prow);
3135    }
3136
3137    function can_set_some_decision() {
3138        return $this->can_administer(null);
3139    }
3140
3141    function can_view_formula(Formula $formula, $as_author = false) {
3142        $bound = $this->permissive_view_score_bound($as_author);
3143        return $formula->view_score($this) > $bound;
3144    }
3145
3146    function can_edit_formula(Formula $formula) {
3147        return $this->privChair || ($this->isPC && $formula->createdBy > 0);
3148    }
3149
3150    // A review field is visible only if its view_score > view_score_bound.
3151    function view_score_bound(PaperInfo $prow, ReviewInfo $rrow = null) {
3152        // Returns the maximum view_score for an invisible review
3153        // field. Values are:
3154        //   VIEWSCORE_ADMINONLY     admin can view
3155        //   VIEWSCORE_REVIEWERONLY  ... and review author can view
3156        //   VIEWSCORE_PC            ... and any PC/reviewer can view
3157        //   VIEWSCORE_AUTHORDEC     ... and authors can view when decisions visible
3158        //   VIEWSCORE_AUTHOR        ... and authors can view
3159        // So returning -3 means all scores are visible.
3160        // Deadlines are not considered.
3161        $rights = $this->rights($prow);
3162        if ($rights->can_administer)
3163            return VIEWSCORE_ADMINONLY - 1;
3164        else if ($rrow ? $this->is_owned_review($rrow) : $rights->allow_review)
3165            return VIEWSCORE_REVIEWERONLY - 1;
3166        else if (!$this->can_view_review($prow, $rrow))
3167            return VIEWSCORE_MAX + 1;
3168        else if ($rights->act_author_view
3169                 && $prow->outcome
3170                 && $this->can_view_decision($prow))
3171            return VIEWSCORE_AUTHORDEC - 1;
3172        else if ($rights->act_author_view)
3173            return VIEWSCORE_AUTHOR - 1;
3174        else
3175            return VIEWSCORE_PC - 1;
3176    }
3177
3178    function permissive_view_score_bound($as_author = false) {
3179        if (!$as_author && $this->is_manager()) {
3180            return VIEWSCORE_ADMINONLY - 1;
3181        } else if (!$as_author && $this->is_reviewer()) {
3182            return VIEWSCORE_REVIEWERONLY - 1;
3183        } else if (($as_author || $this->is_author())
3184                   && ($this->conf->any_response_open
3185                       || $this->conf->au_seerev != 0)) {
3186            if ($this->can_view_some_decision_as_author()) {
3187                return VIEWSCORE_AUTHORDEC - 1;
3188            } else {
3189                return VIEWSCORE_AUTHOR - 1;
3190            }
3191        } else {
3192            return VIEWSCORE_MAX + 1;
3193        }
3194    }
3195
3196    function can_view_tags(PaperInfo $prow = null) {
3197        // see also AllTags_API::alltags
3198        if (!$prow)
3199            return $this->isPC;
3200        $rights = $this->rights($prow);
3201        return $rights->allow_pc
3202            || ($rights->allow_pc_broad && $this->conf->tag_seeall)
3203            || (($this->privChair || $rights->allow_administer)
3204                && $this->conf->tags()->has_sitewide);
3205    }
3206
3207    function can_view_most_tags(PaperInfo $prow = null) {
3208        if (!$prow)
3209            return $this->isPC;
3210        $rights = $this->rights($prow);
3211        return $rights->allow_pc
3212            || ($rights->allow_pc_broad && $this->conf->tag_seeall);
3213    }
3214
3215    function can_view_hidden_tags(PaperInfo $prow = null) {
3216        if (!$prow)
3217            return $this->privChair;
3218        $rights = $this->rights($prow);
3219        return $rights->can_administer
3220            || $this->conf->check_required_tracks($prow, $this, Track::HIDDENTAG);
3221    }
3222
3223    function can_view_tag(PaperInfo $prow, $tag) {
3224        if ($this->_overrides & self::OVERRIDE_TAG_CHECKS)
3225            return true;
3226        $rights = $this->rights($prow);
3227        $tag = TagInfo::base($tag);
3228        $twiddle = strpos($tag, "~");
3229        $dt = $this->conf->tags();
3230        return ($rights->allow_pc
3231                || ($rights->allow_pc_broad && $this->conf->tag_seeall)
3232                || ($this->privChair && $dt->is_sitewide($tag)))
3233            && ($rights->allow_administer
3234                || $twiddle === false
3235                || ($twiddle === 0 && $tag[1] !== "~")
3236                || ($twiddle > 0
3237                    && (substr($tag, 0, $twiddle) == $this->contactId
3238                        || $dt->is_votish(substr($tag, $twiddle + 1)))))
3239            && ($twiddle !== false
3240                || !$dt->has_hidden
3241                || !$dt->is_hidden($tag)
3242                || $this->can_view_hidden_tags($prow));
3243    }
3244
3245    function can_view_peruser_tags(PaperInfo $prow, $tag) {
3246        return $this->can_view_tag($prow, ($this->contactId + 1) . "~$tag");
3247    }
3248
3249    function can_view_any_peruser_tags($tag) {
3250        return $this->is_manager()
3251            || ($this->isPC && $this->conf->tags()->is_votish($tag));
3252    }
3253
3254    function can_change_tag(PaperInfo $prow, $tag, $previndex, $index) {
3255        if (($this->_overrides & self::OVERRIDE_TAG_CHECKS)
3256            || $this->is_site_contact)
3257            return true;
3258        $rights = $this->rights($prow);
3259        $tagmap = $this->conf->tags();
3260        if (!($rights->allow_pc
3261              && ($rights->can_administer || $this->conf->timePCViewPaper($prow, false)))) {
3262            if ($this->privChair && $tagmap->has_sitewide) {
3263                if (!$tag)
3264                    return true;
3265                else {
3266                    $dt = $tagmap->check($tag);
3267                    return $dt && $dt->sitewide && !$dt->autosearch;
3268                }
3269            } else
3270                return false;
3271        }
3272        if (!$tag)
3273            return true;
3274        $tag = TagInfo::base($tag);
3275        $twiddle = strpos($tag, "~");
3276        if ($twiddle === 0 && $tag[1] === "~") {
3277            if (!$rights->can_administer)
3278                return false;
3279            else if (!$tagmap->has_autosearch)
3280                return true;
3281            else {
3282                $dt = $tagmap->check($tag);
3283                return !$dt || !$dt->autosearch;
3284            }
3285        }
3286        if ($twiddle > 0
3287            && substr($tag, 0, $twiddle) != $this->contactId
3288            && !$rights->can_administer)
3289            return false;
3290        if ($twiddle !== false) {
3291            $t = $this->conf->tags()->check(substr($tag, $twiddle + 1));
3292            return !($t && $t->vote && $index < 0);
3293        } else {
3294            $t = $this->conf->tags()->check($tag);
3295            if (!$t)
3296                return true;
3297            else if ($t->vote
3298                     || $t->approval
3299                     || ($t->track && !$this->privChair)
3300                     || ($t->hidden && !$this->can_view_hidden_tags($prow))
3301                     || $t->autosearch)
3302                return false;
3303            else
3304                return $rights->can_administer
3305                    || ($this->privChair && $t->sitewide)
3306                    || (!$t->readonly && !$t->rank);
3307        }
3308    }
3309
3310    function perm_change_tag(PaperInfo $prow, $tag, $previndex, $index) {
3311        if ($this->can_change_tag($prow, $tag, $previndex, $index))
3312            return null;
3313        $rights = $this->rights($prow);
3314        $whyNot = $prow->make_whynot();
3315        $whyNot["tag"] = $tag;
3316        if (!$this->isPC)
3317            $whyNot["permission"] = "change_tag";
3318        else if ($rights->conflictType > 0) {
3319            $whyNot["conflict"] = true;
3320            if ($rights->allow_administer)
3321                $whyNot["forceShow"] = true;
3322        } else if (!$this->conf->timePCViewPaper($prow, false)) {
3323            if ($prow->timeWithdrawn > 0)
3324                $whyNot["withdrawn"] = true;
3325            else
3326                $whyNot["notSubmitted"] = true;
3327        } else {
3328            $tag = TagInfo::base($tag);
3329            $twiddle = strpos($tag, "~");
3330            if ($twiddle === 0 && $tag[1] === "~")
3331                $whyNot["chairTag"] = true;
3332            else if ($twiddle > 0 && substr($tag, 0, $twiddle) != $this->contactId)
3333                $whyNot["otherTwiddleTag"] = true;
3334            else if ($twiddle !== false)
3335                $whyNot["voteTagNegative"] = true;
3336            else {
3337                $t = $this->conf->tags()->check($tag);
3338                if ($t && $t->vote)
3339                    $whyNot["voteTag"] = true;
3340                else if ($t && $t->autosearch)
3341                    $whyNot["autosearchTag"] = true;
3342                else
3343                    $whyNot["chairTag"] = true;
3344            }
3345        }
3346        return $whyNot;
3347    }
3348
3349    function can_change_some_tag(PaperInfo $prow = null) {
3350        if (!$prow)
3351            return $this->isPC;
3352        else
3353            return $this->can_change_tag($prow, null, null, null);
3354    }
3355
3356    function perm_change_some_tag(PaperInfo $prow) {
3357        return $this->perm_change_tag($prow, null, null, null);
3358    }
3359
3360    function can_change_tag_anno($tag) {
3361        if ($this->privChair)
3362            return true;
3363        $twiddle = strpos($tag, "~");
3364        $t = $this->conf->tags()->check($tag);
3365        return $this->isPC
3366            && (!$t || (!$t->readonly && !$t->hidden))
3367            && ($twiddle === false
3368                || ($twiddle === 0 && $tag[1] !== "~")
3369                || ($twiddle > 0 && substr($tag, 0, $twiddle) == $this->contactId));
3370    }
3371
3372    function can_view_reviewer_tags(PaperInfo $prow = null) {
3373        return $this->act_pc($prow);
3374    }
3375
3376
3377    function aucollab_matchers() {
3378        if ($this->_aucollab_matchers === null) {
3379            $this->_aucollab_matchers = [new AuthorMatcher($this)];
3380            if ((string) $this->collaborators !== "")
3381                foreach (explode("\n", $this->collaborators) as $co) {
3382                    if (($m = AuthorMatcher::make_collaborator_line($co)))
3383                        $this->_aucollab_matchers[] = $m;
3384                }
3385        }
3386        return $this->_aucollab_matchers;
3387    }
3388
3389    function aucollab_general_pregexes() {
3390        if ($this->_aucollab_general_pregexes === null) {
3391            $l = [];
3392            foreach ($this->aucollab_matchers() as $matcher)
3393                if (($r = $matcher->general_pregexes()))
3394                    $l[] = $r;
3395            $this->_aucollab_general_pregexes = Text::merge_pregexes($l);
3396        }
3397        return $this->_aucollab_general_pregexes;
3398    }
3399
3400    function full_matcher() {
3401        $this->aucollab_matchers();
3402        return $this->_aucollab_matchers[0];
3403    }
3404
3405    function au_general_pregexes() {
3406        return $this->full_matcher()->general_pregexes();
3407    }
3408
3409
3410    // following / email notifications
3411
3412    function following_reviews(PaperInfo $prow, $watch) {
3413        if ($watch & self::WATCH_REVIEW_EXPLICIT)
3414            return ($watch & self::WATCH_REVIEW) != 0;
3415        else
3416            return ($this->defaultWatch & self::WATCH_REVIEW_ALL)
3417                || (($this->defaultWatch & self::WATCH_REVIEW_MANAGED)
3418                    && $this->allow_administer($prow))
3419                || (($this->defaultWatch & self::WATCH_REVIEW)
3420                    && ($prow->has_author($this)
3421                        || $prow->has_reviewer($this)
3422                        || $prow->has_commenter($this)));
3423    }
3424
3425
3426    // deadlines
3427
3428    function my_deadlines($prows = null) {
3429        // Return cleaned deadline-relevant settings that this user can see.
3430        global $Now;
3431        $dl = (object) ["now" => $Now, "email" => $this->email ? : null];
3432        if ($this->privChair)
3433            $dl->is_admin = true;
3434        if ($this->is_author())
3435            $dl->is_author = true;
3436        $dl->sub = (object) [];
3437        $graces = [];
3438
3439        // submissions
3440        $sub_reg = $this->conf->setting("sub_reg");
3441        $sub_update = $this->conf->setting("sub_update");
3442        $sub_sub = $this->conf->setting("sub_sub");
3443        $dl->sub->open = +$this->conf->setting("sub_open") > 0;
3444        $dl->sub->sub = +$sub_sub;
3445        if ($dl->sub->open)
3446            $graces[] = [$dl->sub, "sub_grace"];
3447        if ($sub_reg && (!$sub_update || $sub_reg < $sub_update))
3448            $dl->sub->reg = $sub_reg;
3449        if ($sub_update && $sub_update != $sub_sub)
3450            $dl->sub->update = $sub_update;
3451
3452        $sb = $this->conf->submission_blindness();
3453        if ($sb === Conf::BLIND_ALWAYS)
3454            $dl->sub->blind = true;
3455        else if ($sb === Conf::BLIND_OPTIONAL)
3456            $dl->sub->blind = "optional";
3457        else if ($sb === Conf::BLIND_UNTILREVIEW)
3458            $dl->sub->blind = "until-review";
3459
3460        // responses
3461        if ($this->conf->setting("resp_active") > 0
3462            && ($this->isPC || $this->is_author())) {
3463            $dlresps = [];
3464            foreach ($this->conf->resp_rounds() as $rrd)
3465                if ($rrd->open
3466                    && ($this->isPC || $rrd->open < $Now)
3467                    && ($this->isPC || !$rrd->search || $rrd->search->filter($this->authored_papers()))) {
3468                    $dlresp = (object) ["open" => $rrd->open, "done" => +$rrd->done];
3469                    $dlresps[$rrd->name] = $dlresp;
3470                    $graces[] = [$dlresp, $rrd->grace];
3471                }
3472            if (!empty($dlresps))
3473                $dl->resps = $dlresps;
3474        }
3475
3476        // final copy deadlines
3477        if ($this->conf->setting("final_open") > 0) {
3478            $dl->final = (object) array("open" => true);
3479            $final_soft = +$this->conf->setting("final_soft");
3480            if ($final_soft > $Now)
3481                $dl->final->done = $final_soft;
3482            else {
3483                $dl->final->done = +$this->conf->setting("final_done");
3484                $dl->final->ishard = true;
3485            }
3486            $graces[] = [$dl->final, "final_grace"];
3487        }
3488
3489        // reviewer deadlines
3490        $revtypes = array();
3491        $rev_open = +$this->conf->setting("rev_open");
3492        $rev_open = $rev_open > 0 && $rev_open <= $Now;
3493        if ($this->is_reviewer() && $rev_open)
3494            $dl->rev = (object) ["open" => true];
3495        else if ($this->privChair)
3496            $dl->rev = (object) [];
3497        if (get($dl, "rev")) {
3498            $dl->revs = [];
3499            $k = $this->isPC ? "pcrev" : "extrev";
3500            foreach ($this->conf->defined_round_list() as $i => $round_name) {
3501                $isuf = $i ? "_$i" : "";
3502                $s = +$this->conf->setting("{$k}_soft$isuf");
3503                $h = +$this->conf->setting("{$k}_hard$isuf");
3504                $dl->revs[$round_name] = $dlround = (object) [];
3505                if ($rev_open)
3506                    $dlround->open = true;
3507                if ($h && ($h < $Now || $s < $Now)) {
3508                    $dlround->done = $h;
3509                    $dlround->ishard = true;
3510                } else if ($s)
3511                    $dlround->done = $s;
3512            }
3513            // blindness
3514            $rb = $this->conf->review_blindness();
3515            if ($rb === Conf::BLIND_ALWAYS)
3516                $dl->rev->blind = true;
3517            else if ($rb === Conf::BLIND_OPTIONAL)
3518                $dl->rev->blind = "optional";
3519        }
3520
3521        // grace periods: give a minute's notice of an impending grace
3522        // period
3523        foreach ($graces as $g) {
3524            if (($grace = $this->conf->setting($g[1])))
3525                foreach (array("reg", "update", "sub", "done") as $k)
3526                    if (get($g[0], $k) && $g[0]->$k + 60 < $Now
3527                        && $g[0]->$k + $grace >= $Now) {
3528                        $kgrace = "{$k}_ingrace";
3529                        $g[0]->$kgrace = true;
3530                    }
3531        }
3532
3533        // add meeting tracker
3534        if (($this->isPC || $this->tracker_kiosk_state)
3535            && $this->can_view_tracker()) {
3536            $tracker = MeetingTracker::lookup($this->conf);
3537            if ($tracker->trackerid
3538                && ($tinfo = MeetingTracker::info_for($this))) {
3539                $dl->tracker = $tinfo;
3540                $dl->tracker_status = MeetingTracker::tracker_status($tracker);
3541                $dl->now = microtime(true);
3542            }
3543            if ($tracker->position_at)
3544                $dl->tracker_status_at = $tracker->position_at;
3545            if (($tcs = $this->conf->opt("trackerCometSite")))
3546                $dl->tracker_site = $tcs;
3547        }
3548
3549        // permissions
3550        if ($prows) {
3551            if (is_object($prows))
3552                $prows = array($prows);
3553            $dl->perm = array();
3554            foreach ($prows as $prow) {
3555                if (!$this->can_view_paper($prow))
3556                    continue;
3557                $perm = $dl->perm[$prow->paperId] = (object) array();
3558                $rights = $this->rights($prow);
3559                $admin = $rights->allow_administer;
3560                if ($admin)
3561                    $perm->allow_administer = true;
3562                if ($rights->act_author)
3563                    $perm->act_author = true;
3564                if ($rights->act_author_view)
3565                    $perm->act_author_view = true;
3566                if ($this->can_review($prow, null, false))
3567                    $perm->can_review = true;
3568                if ($this->can_comment($prow, null, true))
3569                    $perm->can_comment = true;
3570                else if ($admin && $this->can_comment($prow, null, false))
3571                    $perm->can_comment = "override";
3572                if (get($dl, "resps")) {
3573                    foreach ($this->conf->resp_rounds() as $rrd) {
3574                        $crow = CommentInfo::make_response_template($rrd->number, $prow);
3575                        $v = false;
3576                        if ($this->can_respond($prow, $crow, true))
3577                            $v = true;
3578                        else if ($admin && $this->can_respond($prow, $crow, false))
3579                            $v = "override";
3580                        if ($v && !isset($perm->can_respond))
3581                            $perm->can_responds = [];
3582                        if ($v)
3583                            $perm->can_responds[$rrd->name] = $v;
3584                    }
3585                }
3586                if (self::can_some_author_view_submitted_review($prow))
3587                    $perm->some_author_can_view_review = true;
3588                if (self::can_some_author_view_decision($prow))
3589                    $perm->some_author_can_view_decision = true;
3590                if ($this->isPC
3591                    && !$this->conf->can_some_external_reviewer_view_comment())
3592                    $perm->default_comment_visibility = "pc";
3593                if ($this->_review_tokens) {
3594                    $tokens = [];
3595                    foreach ($prow->reviews_by_id() as $rrow) {
3596                        if ($rrow->reviewToken && in_array($rrow->reviewToken, $this->_review_tokens))
3597                            $tokens[$rrow->reviewToken] = true;
3598                    }
3599                    if (!empty($tokens))
3600                        $perm->review_tokens = array_map("encode_token", array_keys($tokens));
3601                }
3602            }
3603        }
3604
3605        return $dl;
3606    }
3607
3608    function has_reportable_deadline() {
3609        global $Now;
3610        $dl = $this->my_deadlines();
3611        if (get($dl->sub, "reg") || get($dl->sub, "update") || get($dl->sub, "sub"))
3612            return true;
3613        if (get($dl, "resps"))
3614            foreach ($dl->resps as $dlr) {
3615                if (get($dlr, "open") && $dlr->open < $Now && get($dlr, "done"))
3616                    return true;
3617            }
3618        if (get($dl, "rev") && get($dl->rev, "open") && $dl->rev->open < $Now)
3619            foreach ($dl->revs as $dlr) {
3620                if (get($dlr, "done"))
3621                    return true;
3622            }
3623        return false;
3624    }
3625
3626
3627    function setsession_api($v) {
3628        $ok = true;
3629        preg_match_all('/(?:\A|\s)(foldpaper[abpt]|foldpscollab|foldhomeactivity|(?:pl|pf|ul)display|scoresort)(|\.[^=]*)(=\S*|)(?=\s|\z)/', $v, $ms, PREG_SET_ORDER);
3630        foreach ($ms as $m) {
3631            if ($m[2]) {
3632                $on = intval(substr($m[3], 1) ? : "0") == 0;
3633                if ($m[1] === "pldisplay" || $m[1] === "pfdisplay")
3634                    PaperList::change_display($this, substr($m[1], 0, 2), substr($m[2], 1), $on);
3635                else if (preg_match('/\A\.[-a-zA-Z0-9_:]+\z/', $m[2]))
3636                    displayOptionsSet($m[1], substr($m[2], 1), $on);
3637                else
3638                    $ok = false;
3639            } else
3640                $this->conf->save_session($m[1], $m[3] ? intval(substr($m[3], 1)) : null);
3641        }
3642        return $ok;
3643    }
3644
3645
3646    // papers
3647
3648    function paper_set($pids, $options = null) {
3649        if (is_int($pids)) {
3650            $options["paperId"] = $pids;
3651        } else if (is_array($pids)
3652                   && !is_associative_array($pids)
3653                   && (!empty($pids) || $options !== null)) {
3654            $options["paperId"] = $pids;
3655        } else if (is_object($pids) && $pids instanceof SearchSelection) {
3656            $options["paperId"] = $pids->selection();
3657        } else {
3658            $options = $pids;
3659        }
3660        return $this->conf->paper_set($this, $options);
3661    }
3662
3663    function hide_reviewer_identity_pids() {
3664        $pids = [];
3665        if (!$this->privChair || $this->conf->has_any_manager()) {
3666            $overrides = $this->add_overrides(Contact::OVERRIDE_CONFLICT);
3667            foreach ($this->paper_set([]) as $prow) {
3668                if (!$this->can_view_paper($prow)
3669                    || !$this->can_view_review_assignment($prow, null)
3670                    || !$this->can_view_review_identity($prow, null))
3671                    $pids[] = $prow->paperId;
3672            }
3673            $this->set_overrides($overrides);
3674        }
3675        return $pids;
3676    }
3677
3678    function paper_status_info(PaperInfo $row, $forceShow = null) {
3679        if ($row->timeWithdrawn > 0) {
3680            return array("pstat_with", "Withdrawn");
3681        } else if ($row->outcome && $this->can_view_decision($row, $forceShow)) {
3682            $data = get(self::$status_info_cache, $row->outcome);
3683            if (!$data) {
3684                $decclass = ($row->outcome > 0 ? "pstat_decyes" : "pstat_decno");
3685
3686                $decs = $this->conf->decision_map();
3687                $decname = get($decs, $row->outcome);
3688                if ($decname) {
3689                    $trdecname = preg_replace('/[^-.\w]/', '', $decname);
3690                    if ($trdecname != "")
3691                        $decclass .= " pstat_" . strtolower($trdecname);
3692                } else
3693                    $decname = "Unknown decision #" . $row->outcome;
3694
3695                $data = self::$status_info_cache[$row->outcome] = array($decclass, $decname);
3696            }
3697            return $data;
3698        } else if ($row->timeSubmitted <= 0 && $row->paperStorageId == 1) {
3699            return array("pstat_noup", "No submission");
3700        } else if ($row->timeSubmitted > 0) {
3701            return array("pstat_sub", "Submitted");
3702        } else {
3703            return array("pstat_prog", "Not ready");
3704        }
3705    }
3706
3707
3708    private function unassigned_review_token() {
3709        while (1) {
3710            $token = mt_rand(1, 2000000000);
3711            $result = $this->conf->qe("select reviewId from PaperReview where reviewToken=$token");
3712            if (edb_nrows($result) == 0)
3713                return ", reviewToken=$token";
3714        }
3715    }
3716
3717    function assign_review($pid, $reviewer_cid, $type, $extra = array()) {
3718        global $Now;
3719        $result = $this->conf->qe("select reviewId, reviewType, reviewRound, reviewModified, reviewToken, requestedBy, reviewSubmitted from PaperReview where paperId=? and contactId=?", $pid, $reviewer_cid);
3720        $rrow = edb_orow($result);
3721        Dbl::free($result);
3722        $reviewId = $rrow ? $rrow->reviewId : 0;
3723        $type = max((int) $type, 0);
3724        $oldtype = $rrow ? (int) $rrow->reviewType : 0;
3725
3726        // can't delete a review that's in progress
3727        if ($type <= 0 && $oldtype && $rrow->reviewModified > 1) {
3728            if ($oldtype >= REVIEW_SECONDARY)
3729                $type = REVIEW_PC;
3730            else
3731                return $reviewId;
3732        }
3733        // PC members always get PC reviews
3734        if ($type == REVIEW_EXTERNAL && $this->conf->pc_member_by_id($reviewer_cid))
3735            $type = REVIEW_PC;
3736
3737        // change database
3738        if ($type && ($round = get($extra, "round_number")) === null)
3739            $round = $this->conf->assignment_round($type == REVIEW_EXTERNAL);
3740        if ($type && !$oldtype) {
3741            $qa = "";
3742            if (get($extra, "mark_notify"))
3743                $qa .= ", timeRequestNotified=$Now";
3744            if (get($extra, "token"))
3745                $qa .= $this->unassigned_review_token();
3746            $new_requester_cid = $this->contactId;
3747            if (($new_requester = get($extra, "requester_contact")))
3748                $new_requester_cid = $new_requester->contactId;
3749            $q = "insert into PaperReview set paperId=$pid, contactId=$reviewer_cid, reviewType=$type, reviewRound=$round, timeRequested=$Now$qa, requestedBy=$new_requester_cid";
3750        } else if ($type && ($oldtype != $type || $rrow->reviewRound != $round)) {
3751            $q = "update PaperReview set reviewType=$type, reviewRound=$round";
3752            if (!$rrow->reviewSubmitted)
3753                $q .= ", reviewNeedsSubmit=1";
3754            $q .= " where reviewId=$reviewId";
3755        } else if (!$type && $oldtype)
3756            $q = "delete from PaperReview where reviewId=$reviewId";
3757        else
3758            return $reviewId;
3759
3760        if (!($result = $this->conf->qe_raw($q)))
3761            return false;
3762
3763        if ($type && !$oldtype) {
3764            $reviewId = $result->insert_id;
3765            $msg = "Review $reviewId added (" . ReviewForm::$revtype_names[$type] . ")";
3766        } else if (!$type) {
3767            $msg = "Removed " . ReviewForm::$revtype_names[$oldtype] . " review";
3768            $reviewId = 0;
3769        } else
3770            $msg = "Review $reviewId changed (" . ReviewForm::$revtype_names[$oldtype] . " to " . ReviewForm::$revtype_names[$type] . ")";
3771        $this->conf->log_for($this, $reviewer_cid, $msg, $pid);
3772
3773        // on new review, update PaperReviewRefused, ReviewRequest, delegation
3774        if ($type && !$oldtype) {
3775            $this->conf->ql("delete from PaperReviewRefused where paperId=$pid and contactId=$reviewer_cid");
3776            if (($req_email = get($extra, "requested_email")))
3777                $this->conf->qe("delete from ReviewRequest where paperId=$pid and email=?", $req_email);
3778            if ($type < REVIEW_SECONDARY)
3779                $this->update_review_delegation($pid, $new_requester_cid, 1);
3780            if ($type >= REVIEW_PC
3781                && $this->conf->setting("pcrev_assigntime", 0) < $Now)
3782                $this->conf->save_setting("pcrev_assigntime", $Now);
3783        } else if (!$type) {
3784            if ($oldtype < REVIEW_SECONDARY && $rrow->requestedBy > 0)
3785                $this->update_review_delegation($pid, $rrow->requestedBy, -1);
3786            // Mark rev_tokens setting for future update by update_rev_tokens_setting
3787            if (get($rrow, "reviewToken"))
3788                $this->conf->settings["rev_tokens"] = -1;
3789        } else {
3790            if ($type == REVIEW_SECONDARY && $oldtype != REVIEW_SECONDARY
3791                && !$rrow->reviewSubmitted)
3792                $this->update_review_delegation($pid, $reviewer_cid, 0);
3793        }
3794        if ($type == REVIEW_META || $oldtype == REVIEW_META)
3795            $this->conf->update_metareviews_setting($type == REVIEW_META ? 1 : -1);
3796
3797        Contact::update_rights();
3798        return $reviewId;
3799    }
3800
3801    function update_review_delegation($pid, $cid, $direction) {
3802        if ($direction > 0) {
3803            $this->conf->qe("update PaperReview set reviewNeedsSubmit=-1 where paperId=? and reviewType=" . REVIEW_SECONDARY . " and contactId=? and reviewSubmitted is null and reviewNeedsSubmit=1", $pid, $cid);
3804        } else {
3805            $row = Dbl::fetch_first_row($this->conf->qe("select sum(contactId=$cid and reviewType=" . REVIEW_SECONDARY . " and reviewSubmitted is null), sum(reviewType<" . REVIEW_SECONDARY . " and requestedBy=$cid and reviewSubmitted is not null), sum(reviewType<" . REVIEW_SECONDARY . " and requestedBy=$cid) from PaperReview where paperId=$pid"));
3806            if ($row && $row[0]) {
3807                $rns = $row[1] ? 0 : ($row[2] ? -1 : 1);
3808                if ($direction == 0 || $rns != 0)
3809                    $this->conf->qe("update PaperReview set reviewNeedsSubmit=? where paperId=? and contactId=? and reviewSubmitted is null", $rns, $pid, $cid);
3810            }
3811        }
3812    }
3813
3814    function unsubmit_review_row($rrow) {
3815        $needsSubmit = 1;
3816        if ($rrow->reviewType == REVIEW_SECONDARY) {
3817            $row = Dbl::fetch_first_row($this->conf->qe("select count(reviewSubmitted), count(reviewId) from PaperReview where paperId=? and requestedBy=? and reviewType<" . REVIEW_SECONDARY, $rrow->paperId, $rrow->contactId));
3818            if ($row && $row[0])
3819                $needsSubmit = 0;
3820            else if ($row && $row[1])
3821                $needsSubmit = -1;
3822        }
3823        return $this->conf->qe("update PaperReview set reviewSubmitted=null, reviewNeedsSubmit=? where paperId=? and reviewId=?", $needsSubmit, $rrow->paperId, $rrow->reviewId);
3824    }
3825}
3826