1<?php
2// papertable.php -- HotCRP helper class for producing paper tables
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class PaperTable {
6    public $conf;
7    public $prow;
8    private $_prow;
9    public $user;
10    private $all_rrows = null;
11    public $viewable_rrows = null;
12    var $crows = null;
13    private $mycrows;
14    private $can_view_reviews = false;
15    var $rrow = null;
16    var $editrrow = null;
17    var $mode;
18    private $prefer_approvable = false;
19    private $allreviewslink;
20    private $edit_status = null;
21
22    public $editable;
23    public $edit_fields;
24    public $edit_fields_position;
25
26    private $qreq;
27    private $useRequest;
28    private $review_values;
29    private $npapstrip = 0;
30    private $npapstrip_tag_entry;
31    private $allFolded;
32    private $matchPreg;
33    private $entryMatches;
34    private $canUploadFinal;
35
36    private $allow_admin;
37    private $admin;
38    private $view_authors = 0;
39    private $view_options = [];
40
41    private $cf = null;
42    private $quit = false;
43
44    static private $textAreaRows = array("title" => 1, "abstract" => 5, "authorInformation" => 5, "collaborators" => 5);
45
46    function __construct($prow, $qreq, $mode = null) {
47        global $Conf, $Me;
48
49        $this->conf = $Conf;
50        $this->prow = $prow;
51        $this->_prow = $this->prow ? : new PaperInfo(null, null, $this->conf);
52        $this->user = $user = $Me;
53        $this->allow_admin = $user->allow_administer($prow);
54        $this->admin = $user->can_administer($prow);
55        $this->qreq = $qreq;
56
57        $this->canUploadFinal = $this->prow
58            && $this->prow->outcome > 0
59            && $this->user->call_with_overrides(Contact::OVERRIDE_TIME, "can_submit_final_paper", $this->prow);
60
61        if (!$this->prow) {
62            $this->mode = "edit";
63            return;
64        }
65
66        // calculate visibility of authors and options
67        // 0: not visible; 1: fold (admin only); 2: visible
68        $new_overrides = 0;
69        if ($this->allow_admin)
70            $new_overrides |= Contact::OVERRIDE_CONFLICT;
71        if ($this->mode === "edit")
72            $new_overrides |= Contact::OVERRIDE_EDIT_CONDITIONS;
73        $overrides = $user->add_overrides($new_overrides);
74
75        if ($user->can_view_authors($prow))
76            $this->view_authors = 2;
77        $olist = $this->conf->paper_opts->option_list_type(!$this->canUploadFinal);
78        foreach ($olist as $o) {
79            if ($user->can_view_paper_option($prow, $o))
80                $this->view_options[$o->id] = 2;
81        }
82
83        if ($this->allow_admin) {
84            $user->remove_overrides(Contact::OVERRIDE_CONFLICT);
85            if ($this->view_authors && !$user->can_view_authors($prow))
86                $this->view_authors = 1;
87            foreach ($olist as $o)
88                if (isset($this->view_options[$o->id])
89                    && !$user->can_view_paper_option($prow, $o))
90                    $this->view_options[$o->id] = 1;
91        }
92
93        $user->set_overrides($overrides);
94
95        // enumerate allowed modes
96        $ms = array();
97        if ($user->can_view_review($prow, null)
98            || $prow->review_submitted($user))
99            $this->can_view_reviews = $ms["p"] = true;
100        else if ($prow->timeWithdrawn > 0 && !$this->conf->timeUpdatePaper($prow))
101            $ms["p"] = true;
102        if ($user->can_review($prow, null))
103            $ms["re"] = true;
104        if ($user->can_view_paper($prow) && $this->allow_admin)
105            $ms["p"] = true;
106        if ($prow->has_author($user)
107            && ($this->conf->timeFinalizePaper($prow) || $prow->timeSubmitted <= 0))
108            $ms["edit"] = true;
109        if ($user->can_view_paper($prow))
110            $ms["p"] = true;
111        if ($prow->has_author($user) || $this->allow_admin)
112            $ms["edit"] = true;
113        if ($prow->review_type($user) >= REVIEW_SECONDARY || $this->allow_admin)
114            $ms["assign"] = true;
115        if (!$mode)
116            $mode = $this->qreq->m ? : $this->qreq->mode;
117        if ($mode === "pe")
118            $mode = "edit";
119        else if ($mode === "view" || $mode === "r")
120            $mode = "p";
121        else if ($mode === "rea") {
122            $mode = "re";
123            $this->prefer_approvable = true;
124        }
125        if ($mode && isset($ms[$mode]))
126            $this->mode = $mode;
127        else
128            $this->mode = key($ms);
129        if (isset($ms["re"]) && isset($this->qreq->reviewId))
130            $this->mode = "re";
131
132        // choose list
133        if (!$this->conf->has_active_list())
134            $this->conf->set_active_list($this->find_session_list($prow->paperId));
135        else {
136            $list = $this->conf->active_list();
137            assert($list && ($list->set_current_id($prow->paperId) || $list->digest));
138        }
139
140        $this->matchPreg = [];
141        if (($list = $this->conf->active_list()) && $list->highlight
142            && preg_match('_\Ap/([^/]*)/([^/]*)(?:/|\z)_', $list->listid, $m)) {
143            $hlquery = is_string($list->highlight) ? $list->highlight : urldecode($m[2]);
144            $ps = new PaperSearch($user, ["t" => $m[1], "q" => $hlquery]);
145            foreach ($ps->field_highlighters() as $k => $v)
146                $this->matchPreg[$k] = $v;
147        }
148        if (empty($this->matchPreg))
149            $this->matchPreg = null;
150    }
151    private function find_session_list($pid) {
152        if (isset($_COOKIE["hotlist-info"])
153            && ($list = SessionList::decode_info_string($_COOKIE["hotlist-info"]))
154            && $list->list_type() === "p"
155            && ($list->set_current_id($pid) || $list->digest))
156            return $list;
157
158        // look up list description
159        $list = null;
160        $listdesc = $this->qreq->ls;
161        if ($listdesc) {
162            if (($opt = PaperSearch::unparse_listid($listdesc)))
163                $list = $this->try_list($opt, $pid);
164            if (!$list && preg_match('{\A(all|s):(.*)\z}s', $listdesc, $m))
165                $list = $this->try_list(["t" => $m[1], "q" => $m[2]], $pid);
166            if (!$list && preg_match('{\A[a-z]+\z}', $listdesc))
167                $list = $this->try_list(["t" => $listdesc], $pid);
168            if (!$list)
169                $list = $this->try_list(["q" => $listdesc], $pid);
170        }
171
172        // default lists
173        if (!$list)
174            $list = $this->try_list([], $pid);
175        if (!$list && $this->user->privChair)
176            $list = $this->try_list(["t" => "all"], $pid);
177
178        return $list;
179    }
180    private function try_list($opt, $pid) {
181        $srch = new PaperSearch($this->user, $opt);
182        $list = $srch->session_list_object();
183        return $list->set_current_id($pid);
184    }
185
186    private static function _combine_match_preg($m1, $m) {
187        if (is_object($m))
188            $m = get_object_vars($m);
189        if (!is_array($m))
190            $m = ["abstract" => $m, "title" => $m,
191                  "authorInformation" => $m, "collaborators" => $m];
192        foreach ($m as $k => $v)
193            if (!isset($m1[$k]) || !$m1[$k])
194                $m1[$k] = $v;
195        return $m1;
196    }
197
198    function initialize($editable, $useRequest) {
199        $this->editable = $editable;
200        $this->useRequest = $useRequest;
201        $this->allFolded = $this->mode === "re" || $this->mode === "assign"
202            || ($this->mode !== "edit"
203                && (!empty($this->all_rrows) || !empty($this->crows)));
204    }
205
206    function set_edit_status(PaperStatus $status) {
207        $this->edit_status = $status;
208    }
209
210    function set_review_values(ReviewValues $rvalues = null) {
211        $this->review_values = $rvalues;
212    }
213
214    function can_view_reviews() {
215        return $this->can_view_reviews;
216    }
217
218    static function do_header($paperTable, $id, $action_mode, $qreq) {
219        global $Conf, $Me;
220        $prow = $paperTable ? $paperTable->prow : null;
221        $format = 0;
222
223        $t = '<div id="header_page" class="header_page_submission"><div id="header_page_submission_inner"><h1 class="paptitle';
224
225        if (!$paperTable && !$prow) {
226            if (($pid = $qreq->paperId) && ctype_digit($pid))
227                $title = "#$pid";
228            else
229                $title = $Conf->_c("paper_title", "Submission");
230            $t .= '">' . $title;
231        } else if (!$prow) {
232            $title = $Conf->_c("paper_title", "New submission");
233            $t .= '">' . $title;
234        } else {
235            $title = "#" . $prow->paperId;
236            $viewable_tags = $prow->viewable_tags($Me);
237            if ($viewable_tags || $Me->can_view_tags($prow)) {
238                $t .= ' has-tag-classes';
239                if (($color = $prow->conf->tags()->color_classes($viewable_tags)))
240                    $t .= ' ' . $color;
241            }
242            $t .= '"><a class="q" href="' . hoturl("paper", array("p" => $prow->paperId, "ls" => null))
243                . '"><span class="taghl"><span class="pnum">' . $title . '</span>'
244                . ' &nbsp; ';
245
246            $highlight_text = null;
247            $title_matches = 0;
248            if ($paperTable && $paperTable->matchPreg
249                && ($highlight = get($paperTable->matchPreg, "title")))
250                $highlight_text = Text::highlight($prow->title, $highlight, $title_matches);
251
252            if (!$title_matches && ($format = $prow->title_format()))
253                $t .= '<span class="ptitle need-format" data-format="' . $format . '">';
254            else
255                $t .= '<span class="ptitle">';
256            if ($highlight_text)
257                $t .= $highlight_text;
258            else if ($prow->title === "")
259                $t .= "[No title]";
260            else
261                $t .= htmlspecialchars($prow->title);
262
263            $t .= '</span></span></a>';
264            if ($viewable_tags && $Conf->tags()->has_decoration) {
265                $tagger = new Tagger($Me);
266                $t .= $tagger->unparse_decoration_html($viewable_tags);
267            }
268        }
269
270        $t .= '</h1></div></div>';
271        if ($paperTable && $prow)
272            $t .= $paperTable->_paptabBeginKnown();
273
274        $Conf->header($title, $id, [
275            "action_bar" => actionBar($action_mode, $qreq),
276            "title_div" => $t, "class" => "paper", "paperId" => $qreq->paperId
277        ]);
278        if ($format)
279            echo Ht::unstash_script("render_text.on_page()");
280    }
281
282    private function abstract_foldable($abstract) {
283        return strlen($abstract) > 190;
284    }
285
286    private function echoDivEnter() {
287        $folds = ["a" => true, "p" => $this->allFolded, "b" => $this->allFolded, "t" => $this->allFolded];
288        foreach (["a", "p", "b", "t"] as $k)
289            if (!$this->conf->session("foldpaper$k", 1))
290                $folds[$k] = false;
291
292        // if highlighting, automatically unfold abstract/authors
293        if ($this->prow && $folds["b"]) {
294            $abstract = $this->entryData("abstract");
295            if ($this->entryMatches || !$this->abstract_foldable($abstract))
296                $folds["b"] = false;
297        }
298        if ($this->matchPreg && $this->prow && $folds["a"]) {
299            $this->entryData("authorInformation");
300            if ($this->entryMatches)
301                $folds["a"] = $folds["p"] = false;
302        }
303
304        // collect folders
305        $folders = array("clearfix");
306        if ($this->prow) {
307            if ($this->view_authors == 1)
308                $folders[] = $folds["a"] ? "fold8c" : "fold8o";
309            if ($this->view_authors && $this->allFolded)
310                $folders[] = $folds["p"] ? "fold9c" : "fold9o";
311        }
312        $folders[] = $folds["t"] ? "fold5c" : "fold5o";
313        $folders[] = $folds["b"] ? "fold6c" : "fold6o";
314
315        // echo div
316        echo '<div id="foldpaper" class="', join(" ", $folders), '" data-fold-session="'
317            . htmlspecialchars(json_encode_browser([
318                "5" => "foldpapert", "6" => "foldpaperb",
319                "8" => "foldpapera", "9" => "foldpaperp"
320            ])) . '">';
321    }
322
323    private function echoDivExit() {
324        echo "</div>";
325    }
326
327    function has_problem_at($f) {
328        if ($this->edit_status) {
329            if (str_starts_with($f, "au")) {
330                if ($f === "authorInformation")
331                    $f = "authors";
332                else if (preg_match('/\A.*?(\d+)\z/', $f, $m)
333                         && $this->edit_status->has_problem_at("author$m[1]"))
334                    return true;
335            }
336            return $this->edit_status->has_problem_at($f);
337        } else
338            return false;
339    }
340
341    function error_class($f) {
342        return $this->has_problem_at($f) ? " error" : "";
343    }
344    function has_error_class($f) {
345        return $this->has_problem_at($f) ? " has-error" : "";
346    }
347
348    private function editable_papt($what, $heading, $extra = [], PaperOption $opt = null) {
349        $for = get($extra, "for", false);
350        $t = '<div class="papeg';
351        if ($opt && $opt->edit_condition()) {
352            $t .= ' has-edit-condition';
353            if (!$opt->test_edit_condition($this->_prow))
354                $t .= ' hidden';
355            $t .= '" data-edit-condition="' . htmlspecialchars(json_encode($opt->compile_edit_condition($this->_prow)));
356            Ht::stash_script('$(edit_paper_ui.edit_condition)', 'edit_condition');
357        }
358        $t .= '"><div class="papet' . $this->error_class($what);
359        if ($for === "checkbox")
360            $t .= ' checki';
361        if (($id = get($extra, "id")))
362            $t .= '" id="' . $id;
363        return $t . '">' . Ht::label($heading, $for === "checkbox" ? false : $for, ["class" => "papfn"]) . '</div>';
364    }
365
366    function messages_for($field) {
367        if ($this->edit_status && ($ms = $this->edit_status->messages_at($field, true))) {
368            $status = array_reduce($ms, function ($c, $m) { return max($c, $m[2]); }, 0);
369            return Ht::xmsg($status, array_map(function ($m) { return $m[1]; }, $ms));
370        } else
371            return "";
372    }
373
374    private function papt($what, $name, $extra = array()) {
375        $fold = defval($extra, "fold", false);
376        $editfolder = defval($extra, "editfolder", false);
377        if ($fold || $editfolder) {
378            $foldnum = defval($extra, "foldnum", 0);
379            $foldnumclass = $foldnum ? " data-fold-target=\"$foldnum\"" : "";
380        }
381
382        if (get($extra, "type") === "ps")
383            list($divclass, $hdrclass) = array("pst", "psfn");
384        else
385            list($divclass, $hdrclass) = array("pavt", "pavfn");
386
387        $c = "<div class=\"$divclass" . $this->error_class($what);
388        if (($fold || $editfolder) && !get($extra, "float"))
389            $c .= " ui js-foldup\"" . $foldnumclass . ">";
390        else
391            $c .= "\">";
392        $c .= "<span class=\"$hdrclass\">";
393        if (!$fold) {
394            $n = (is_array($name) ? $name[0] : $name);
395            if ($editfolder)
396                $c .= "<a class=\"q fn ui js-foldup\" "
397                    . "href=\"" . SelfHref::make($this->qreq, ["atab" => $what])
398                    . "\"" . $foldnumclass . ">" . $n
399                    . "</a><span class=\"fx\">" . $n . "</span>";
400            else
401                $c .= $n;
402        } else {
403            $c .= '<a class="q ui js-foldup" href=""' . $foldnumclass;
404            if (($title = defval($extra, "foldtitle")))
405                $c .= ' title="' . $title . '"';
406            $c .= '>' . expander(null, $foldnum);
407            if (!is_array($name))
408                $name = array($name, $name);
409            if ($name[0] !== $name[1])
410                $c .= '<span class="fn' . $foldnum . '">' . $name[1] . '</span><span class="fx' . $foldnum . '">' . $name[0] . '</span>';
411            else
412                $c .= $name[0];
413            $c .= '</a>';
414        }
415        $c .= "</span>";
416        if ($editfolder) {
417            $c .= "<span class=\"pstedit fn\">"
418                . "<a class=\"ui xx need-tooltip js-foldup\" href=\""
419                . SelfHref::make($this->qreq, ["atab" => $what])
420                . "\"" . $foldnumclass . " data-tooltip=\"Edit\">"
421                . "<span class=\"psteditimg\">"
422                . Ht::img("edit48.png", "[Edit]", "editimg")
423                . "</span>&nbsp;<u class=\"x\">Edit</u></a></span>";
424        }
425        if (isset($extra["float"]))
426            $c .= $extra["float"];
427        $c .= "</div>";
428        return $c;
429    }
430
431    private function editable_textarea($fieldName) {
432        $js = ["id" => $fieldName,
433               "class" => "papertext need-autogrow" . $this->has_error_class($fieldName),
434               "rows" => self::$textAreaRows[$fieldName], "cols" => 60];
435        if ($fieldName === "abstract")
436            $js["spellcheck"] = true;
437        $value = $pvalue = $this->prow ? $this->prow->$fieldName : "";
438        if ($this->useRequest && isset($this->qreq[$fieldName])) {
439            $value = cleannl($this->qreq[$fieldName]);
440            if (self::$textAreaRows[$fieldName] === 1)
441                $value = trim($value);
442            if ($value !== $pvalue)
443                $js["data-default-value"] = $pvalue;
444        }
445        return Ht::textarea($fieldName, $value, $js);
446    }
447
448    private function entryData($fieldName, $table_type = false) {
449        $this->entryMatches = 0;
450        $text = $this->prow ? $this->prow->$fieldName : "";
451        if ($this->matchPreg
452            && isset(self::$textAreaRows[$fieldName])
453            && isset($this->matchPreg[$fieldName]))
454            $text = Text::highlight($text, $this->matchPreg[$fieldName], $this->entryMatches);
455        else
456            $text = htmlspecialchars($text);
457        return $table_type === "col" ? nl2br($text) : $text;
458    }
459
460    private function field_name($name) {
461        return $this->conf->_c("paper_field/edit", $name);
462    }
463
464    private function field_hint($name, $itext = "") {
465        $args = array_merge(["paper_edit_description"], func_get_args());
466        if (count($args) === 2)
467            $args[] = "";
468        $t = call_user_func_array([$this->conf->ims(), "xci"], $args);
469        if ($t !== "")
470            return '<div class="paphint">' . $t . '</div>';
471        return "";
472    }
473
474    private function echo_editable_title() {
475        echo $this->editable_papt("title", $this->field_name("Title"), ["for" => "title"]),
476            $this->messages_for("title"),
477            $this->field_hint("Title"),
478            '<div class="papev">', $this->editable_textarea("title"), "</div></div>\n\n";
479    }
480
481    static function pdf_stamps_html($data, $options = null) {
482        global $Conf;
483        $tooltip = !$options || !get($options, "notooltip");
484
485        $t = array();
486        $tm = defval($data, "timestamp", defval($data, "timeSubmitted", 0));
487        if ($tm > 0)
488            $t[] = ($tooltip ? '<span class="nb need-tooltip" data-tooltip="Time of PDF upload">' : '<span class="nb">')
489                . '<svg width="12" height="12" viewBox="0 0 96 96" style="vertical-align:-2px"><path style="fill:#333" d="M48 6a42 42 0 1 1 0 84 42 42 0 1 1 0-84zm0 10a32 32 0 1 0 0 64 32 32 0 1 0 0-64z"/><path style="fill:#333" d="M48 19A5 5 0 0 0 43 24V46c0 2.352.37 4.44 1.464 5.536l12 12c4.714 4.908 12-2.36 7-7L53 46V24A5 5 0 0 0 43 24z"/></svg>'
490                . " " . $Conf->unparse_time_full($tm) . "</span>";
491        if (($hash = defval($data, "sha1")) != "")
492            $hash = Filer::hash_as_text($hash);
493        if ($hash) {
494            list($xhash, $pfx, $alg) = Filer::analyze_hash($hash);
495            $x = '<span class="nb checksum';
496            if ($tooltip) {
497                $x .= ' need-tooltip" data-tooltip="';
498                if ($alg === "sha1")
499                    $x .= "SHA-1 checksum";
500                else if ($alg === "sha256")
501                    $x .= "SHA-256 checksum";
502            }
503            $x .= '"><svg width="12" height="12" viewBox="0 0 48 48" style="vertical-align:-2px"><path style="fill:#333" d="M19 32l-8-8-7 7 14 14 26-26-6-6-19 19z"/><path style="fill:#333" d="M15 3V10H8v5h7v7h5v-7H27V10h-7V3h-5z"/></svg> '
504                . '<span class="checksum-overflow">' . $xhash . '</span>'
505                . '<span class="checksum-abbreviation">' . substr($xhash, 0, 8) . '</span></span>';
506            $t[] = $x;
507        }
508        if (!empty($t))
509            return '<span class="hint">' . join(" <span class='barsep'>·</span> ", $t) . "</span>";
510        else
511            return "";
512    }
513
514    private function paptabDownload() {
515        assert(!$this->editable);
516        $prow = $this->prow;
517        $out = array();
518
519        // download
520        if ($this->user->can_view_pdf($prow)) {
521            $dprefix = "";
522            $dtype = $prow->finalPaperStorageId > 1 ? DTYPE_FINAL : DTYPE_SUBMISSION;
523            if (($doc = $prow->document($dtype)) && $doc->paperStorageId > 1) {
524                if (($stamps = self::pdf_stamps_html($doc)))
525                    $stamps = "<span class='sep'></span>" . $stamps;
526                if ($dtype == DTYPE_FINAL)
527                    $dname = $this->conf->_c("paper_field", "Final version");
528                else if ($prow->timeSubmitted != 0)
529                    $dname = $this->conf->_c("paper_field", "Submission");
530                else
531                    $dname = $this->conf->_c("paper_field", "Draft submission");
532                $out[] = '<p class="xd">' . $dprefix . $doc->link_html('<span class="pavfn">' . $dname . '</span>', DocumentInfo::L_REQUIREFORMAT) . $stamps . '</p>';
533            }
534
535            foreach ($prow ? $prow->options() : [] as $ov) {
536                $o = $ov->option;
537                if ($o->display() === PaperOption::DISP_SUBMISSION
538                    && get($this->view_options, $o->id)
539                    && ($oh = $this->unparse_option_html($ov))) {
540                    $out = array_merge($out, $oh);
541                }
542            }
543
544            if ($prow->finalPaperStorageId > 1 && $prow->paperStorageId > 1)
545                $out[] = '<p class="xd"><small>' . $prow->document(DTYPE_SUBMISSION)->link_html("Submission version", DocumentInfo::L_SMALL | DocumentInfo::L_NOSIZE) . "</small></p>";
546        }
547
548        // conflicts
549        if ($this->user->isPC && !$prow->has_conflict($this->user)
550            && $this->conf->timeUpdatePaper($prow)
551            && $this->mode !== "assign"
552            && $this->mode !== "contact"
553            && $prow->outcome >= 0)
554            $out[] = Ht::xmsg("warning", 'The authors still have <a href="' . hoturl("deadlines") . '">time</a> to make changes.');
555
556        echo join("", $out);
557    }
558
559    private function is_ready() {
560        return $this->is_ready_checked() && ($this->prow || $this->conf->opt("noPapers"));
561    }
562
563    private function is_ready_checked() {
564        if ($this->useRequest)
565            return !!$this->qreq->submitpaper;
566        else if ($this->prow && $this->prow->timeSubmitted > 0)
567            return true;
568        else
569            return !$this->conf->setting("sub_freeze")
570                && (!$this->prow
571                    || (!$this->conf->opt("noPapers") && $this->prow->paperStorageId <= 1));
572    }
573
574    private function echo_editable_complete() {
575        if ($this->canUploadFinal) {
576            echo Ht::hidden("submitpaper", 1);
577            return;
578        }
579
580        $checked = $this->is_ready_checked();
581        echo '<div class="ready-container ',
582            (($this->prow && $this->prow->paperStorageId > 1)
583             || $this->conf->opt("noPapers") ? "foldo" : "foldc"),
584            '"><div class="checki fx"><span class="checkc">',
585            Ht::checkbox("submitpaper", 1, $checked, ["class" => "js-check-submittable"]),
586            " </span>";
587        if ($this->conf->setting("sub_freeze"))
588            echo Ht::label("<strong>" . $this->conf->_("The submission is complete.") . "</strong>"),
589                '<p class="settings-ap hint">You must complete your submission before the deadline or it will not be reviewed. Completed submissions are frozen and cannot be changed further.</p>';
590        else
591            echo Ht::label("<strong>" . $this->conf->_("The submission is ready for review.") . "</strong>");
592        echo "</div></div>\n";
593    }
594
595    static function document_upload_input($inputid, $dtype, $accepts) {
596        $t = '<input id="' . $inputid . '" type="file" name="' . $inputid . '"';
597        if ($accepts !== null && count($accepts) == 1)
598            $t .= ' accept="' . $accepts[0]->mimetype . '"';
599        $t .= ' size="30" class="';
600        $k = ["document-uploader"];
601        if ($dtype == DTYPE_SUBMISSION || $dtype == DTYPE_FINAL)
602            $k[] = "js-check-submittable";
603        return $t . join(" ", $k) . '" />';
604    }
605
606    function echo_editable_document(PaperOption $docx, $storageId) {
607        $dtype = $docx->id;
608        if ($dtype == DTYPE_SUBMISSION || $dtype == DTYPE_FINAL) {
609            $noPapers = $this->conf->opt("noPapers");
610            if ($noPapers === 1 || $noPapers === true)
611                return;
612        }
613        $inputid = $dtype > 0 ? "opt" . $dtype : "paperUpload";
614
615        $accepts = $docx->mimetypes();
616        $field = $docx->field_key();
617        $doc = null;
618        if ($this->prow && $this->user->can_view_pdf($this->prow) && $storageId > 1)
619            $doc = $this->prow->document($dtype, $storageId, true);
620
621        $msgs = [];
622        if ($accepts)
623            $msgs[] = htmlspecialchars(Mimetype::description($accepts));
624        $msgs[] = "max " . ini_get("upload_max_filesize") . "B";
625        $heading = $this->field_name(htmlspecialchars($docx->title)) . ' <span class="n">(' . join(", ", $msgs) . ")</span>";
626        echo $this->editable_papt($field, $heading, ["for" => $doc ? false : $inputid], $docx),
627            $this->field_hint(htmlspecialchars($docx->title), $docx->description),
628            $this->messages_for($field);
629
630        echo '<div class="papev has-document" data-dtype="', $dtype,
631            '" data-document-name="', $docx->field_key(), '"';
632        if ($doc)
633            echo ' data-docid="', $doc->paperStorageId, '"';
634        echo '>';
635        if ($dtype > 0)
636            echo Ht::hidden("has_opt" . $dtype, 1);
637
638        $upload_input = self::document_upload_input($inputid, $dtype, $accepts);
639
640        // current version, if any
641        $has_cf = false;
642        if ($doc) {
643            if ($doc->mimetype === "application/pdf") {
644                if (!$this->cf)
645                    $this->cf = new CheckFormat($this->conf, CheckFormat::RUN_NO);
646                $spec = $this->conf->format_spec($dtype);
647                $has_cf = $spec && !$spec->is_empty();
648                if ($has_cf)
649                    $this->cf->check_document($this->prow, $doc);
650            }
651
652            echo '<div class="document-file nameless">', $doc->link_html(), '</div>',
653                '<div class="document-upload hidden">', $upload_input, '</div>',
654                '<div class="document-stamps">';
655            if (($stamps = self::pdf_stamps_html($doc)))
656                echo $stamps;
657            echo '</div><div class="document-actions">',
658                Ht::button("Replace", ["class" => "btn ui js-replace-document document-action"]);
659            if ($dtype > 0)
660                '<a href="" class="ui js-remove-document document-action">Delete</a>';
661            if ($has_cf && ($this->cf->failed || $this->cf->need_run || $this->cf->possible_run)) {
662                echo '<a href="" class="ui js-check-format document-action">',
663                    ($this->cf->failed || $this->cf->need_run ? "Check format" : "Recheck format"),
664                    '</a>';
665            } else if ($has_cf && !$this->cf->has_problem()) {
666                echo '<span class="document-action dim">Format OK</span>';
667            }
668            echo '</div>';
669            if ($has_cf) {
670                echo '<div class="document-format">';
671                if (!$this->cf->failed && $this->cf->has_problem())
672                    echo $this->cf->document_report($this->prow, $doc);
673                echo '</div>';
674            }
675        } else {
676            echo '<div class="document-upload">', $upload_input, '</div>';
677        }
678
679        echo "</div>";
680    }
681
682    private function echo_editable_submission() {
683        if ($this->canUploadFinal)
684            $this->echo_editable_document($this->conf->paper_opts->get(DTYPE_FINAL), $this->prow ? $this->prow->finalPaperStorageId : 0);
685        else
686            $this->echo_editable_document($this->conf->paper_opts->get(DTYPE_SUBMISSION), $this->prow ? $this->prow->paperStorageId : 0);
687        echo "</div>\n\n";
688    }
689
690    private function echo_editable_abstract() {
691        $noAbstract = $this->conf->opt("noAbstract");
692        if ($noAbstract !== 1 && $noAbstract !== true) {
693            $title = $this->field_name("Abstract");
694            if ($noAbstract === 2)
695                $title .= ' <span class="n">(optional)</span>';
696            echo $this->editable_papt("abstract", $title, ["for" => "abstract"]),
697                $this->field_hint("Abstract"),
698                $this->messages_for("abstract"),
699                '<div class="papev abstract">';
700            if (($fi = $this->conf->format_info($this->prow ? $this->prow->paperFormat : null)))
701                echo $fi->description_preview_html();
702            echo $this->editable_textarea("abstract"),
703                "</div></div>\n\n";
704        }
705    }
706
707    private function paptabAbstract() {
708        $text = $this->entryData("abstract");
709        if (trim($text) === "") {
710            if ($this->conf->opt("noAbstract"))
711                return false;
712            else
713                $text = "[No abstract]";
714        }
715        $extra = [];
716        if ($this->allFolded && $this->abstract_foldable($text))
717            $extra = ["fold" => "paper", "foldnum" => 6,
718                      "foldtitle" => "Toggle full abstract"];
719        echo '<div class="paperinfo-cl"><div class="paperinfo-abstract"><div class="pg">',
720            $this->papt("abstract", $this->conf->_c("paper_field", "Abstract"), $extra),
721            '<div class="pavb abstract';
722        if ($this->prow
723            && !$this->entryMatches
724            && ($format = $this->prow->format_of($text))) {
725            echo ' need-format" data-format="', $format, '.abs">', $text;
726            Ht::stash_script('$(render_text.on_page)', 'render_on_page');
727        } else
728            echo ' format0">', Ht::format0($text);
729        echo "</div></div></div>";
730        if ($extra)
731            echo '<div class="fn6 fx7 longtext-fader"></div>',
732                '<div class="fn6 fx7 longtext-expander"><a class="ui x js-foldup" href="" data-fold-target="6">[more]</a></div>';
733        echo "</div>\n";
734        if ($extra)
735            echo Ht::unstash_script("render_text.on_page()");
736        return true;
737    }
738
739    private function editable_author_component_entry($n, $pfx, $au) {
740        $auval = "";
741        if ($pfx === "auname") {
742            $js = ["size" => "35", "placeholder" => "Name", "autocomplete" => "off"];
743            if ($au && $au->firstName && $au->lastName && !preg_match('@^\s*(v[oa]n\s+|d[eu]\s+)?\S+(\s+jr.?|\s+sr.?|\s+i+)?\s*$@i', $au->lastName))
744                $auval = $au->lastName . ", " . $au->firstName;
745            else if ($au)
746                $auval = $au->name();
747        } else if ($pfx === "auemail") {
748            $js = ["size" => "30", "placeholder" => "Email", "autocomplete" => "off"];
749            $auval = $au ? $au->email : "";
750        } else {
751            $js = ["size" => "32", "placeholder" => "Affiliation", "autocomplete" => "off"];
752            $auval = $au ? $au->affiliation : "";
753        }
754
755        $val = $auval;
756        if ($this->useRequest) {
757            $val = ($pfx === '$' ? "" : (string) $this->qreq["$pfx$n"]);
758        }
759
760        $js["class"] = "need-autogrow js-autosubmit e$pfx" . $this->has_error_class("$pfx$n");
761        if ($val !== $auval) {
762            $js["data-default-value"] = $auval;
763        }
764        return Ht::entry("$pfx$n", $val, $js);
765    }
766    private function editable_authors_tr($n, $au, $max_authors) {
767        $t = '<tr>';
768        if ($max_authors != 1)
769            $t .= '<td class="rxcaption">' . $n . '.</td>';
770        return $t . '<td class="lentry">'
771            . $this->editable_author_component_entry($n, "auname", $au) . ' '
772            . $this->editable_author_component_entry($n, "auemail", $au) . ' '
773            . $this->editable_author_component_entry($n, "auaff", $au)
774            . '<span class="nb btnbox aumovebox"><a href="" class="ui btn qx need-tooltip row-order-ui moveup" data-tooltip="Move up" tabindex="-1">'
775            . Icons::ui_triangle(0)
776            . '</a><a href="" class="ui btn qx need-tooltip row-order-ui movedown" data-tooltip="Move down" tabindex="-1">'
777            . Icons::ui_triangle(2)
778            . '</a><a href="" class="ui btn qx need-tooltip row-order-ui delete" data-tooltip="Delete" tabindex="-1">✖</a></span></td></tr>';
779    }
780
781    private function echo_editable_authors() {
782        $max_authors = (int) $this->conf->opt("maxAuthors");
783        $min_authors = $max_authors > 0 ? min(5, $max_authors) : 5;
784
785        $sb = $this->conf->submission_blindness();
786        $title = $this->conf->_c("paper_field/edit", "Authors", $max_authors);
787        if ($sb === Conf::BLIND_ALWAYS)
788            $title .= " (blind)";
789        else if ($sb === Conf::BLIND_UNTILREVIEW)
790            $title .= " (blind until review)";
791        echo $this->editable_papt("authors", $title);
792        $hint = "List the authors, including email addresses and affiliations.";
793        if ($sb === Conf::BLIND_ALWAYS)
794            $hint .= " Submission is blind, so reviewers will not see author information.";
795        else if ($sb === Conf::BLIND_UNTILREVIEW)
796            $hint .= " Reviewers will not see author information until they submit a review.";
797        echo $this->field_hint("Authors", $hint, $sb),
798            $this->messages_for("authors"),
799            '<div class="papev"><table id="auedittable" class="auedittable js-row-order">',
800            '<tbody class="need-row-order-autogrow" data-min-rows="', $min_authors, '" ',
801            ($max_authors > 0 ? 'data-max-rows="' . $max_authors . '" ' : ''),
802            'data-row-template="', htmlspecialchars($this->editable_authors_tr('$', null, $max_authors)), '">';
803
804        $aulist = $this->prow ? $this->prow->author_list() : [];
805        if ($this->useRequest) {
806            $n = $nonempty_n = 0;
807            while (1) {
808                $auname = $this->qreq["auname" . ($n + 1)];
809                $auemail = $this->qreq["auemail" . ($n + 1)];
810                $auaff = $this->qreq["auaff" . ($n + 1)];
811                if ($auname === null && $auemail === null && $auaff === null) {
812                    break;
813                }
814                ++$n;
815                if ((string) $auname !== "" || (string) $auemail !== "" || (string) $auaff !== "") {
816                    $nonempty_n = $n;
817                }
818            }
819            while (count($aulist) < $nonempty_n) {
820                $aulist[] = null;
821            }
822        }
823
824        for ($n = 1; $n <= count($aulist); ++$n) {
825            echo $this->editable_authors_tr($n, get($aulist, $n - 1), $max_authors);
826        }
827        if ($max_authors <= 0 || $n <= $max_authors) {
828            do {
829                echo $this->editable_authors_tr($n, null, $max_authors);
830                ++$n;
831            } while ($n <= $min_authors);
832        }
833        echo "</tbody></table></div></div>\n\n";
834    }
835
836    private function authorData($table, $type, $viewAs = null) {
837        if ($this->matchPreg && isset($this->matchPreg["authorInformation"]))
838            $highpreg = $this->matchPreg["authorInformation"];
839        else
840            $highpreg = false;
841        $this->entryMatches = 0;
842        $names = [];
843
844        if (empty($table)) {
845            return "[No authors]";
846        } else if ($type === "last") {
847            foreach ($table as $au) {
848                $n = Text::abbrevname_text($au);
849                $names[] = Text::highlight($n, $highpreg, $nm);
850                $this->entryMatches += $nm;
851            }
852            return join(", ", $names);
853        } else {
854            foreach ($table as $au) {
855                $nm1 = $nm2 = $nm3 = 0;
856                $n = $e = $t = "";
857                $n = trim(Text::highlight("$au->firstName $au->lastName", $highpreg, $nm1));
858                if ($au->email !== "") {
859                    $e = Text::highlight($au->email, $highpreg, $nm2);
860                    $e = '&lt;<a href="mailto:' . htmlspecialchars($au->email)
861                        . '" class="mailto">' . $e . '</a>&gt;';
862                }
863                $t = ($n === "" ? $e : $n);
864                if ($au->affiliation !== "")
865                    $t .= ' <span class="auaff">(' . Text::highlight($au->affiliation, $highpreg, $nm3) . ')</span>';
866                if ($n !== "" && $e !== "")
867                    $t .= " " . $e;
868                $this->entryMatches += $nm1 + $nm2 + $nm3;
869                $t = trim($t);
870                if ($au->email !== "" && $au->contactId
871                    && $viewAs !== null && $viewAs->email !== $au->email && $viewAs->privChair)
872                    $t .= " <a href=\"" . SelfHref::make($this->qreq, ["actas" => $au->email]) . "\">" . Ht::img("viewas.png", "[Act as]", array("title" => "Act as " . Text::name_text($au))) . "</a>";
873                $names[] = '<p class="odname">' . $t . '</p>';
874            }
875            return join("\n", $names);
876        }
877    }
878
879    private function _analyze_authors() {
880        // clean author information
881        $aulist = $this->prow->author_list();
882
883        // find contact author information, combine with author table
884        $result = $this->conf->qe("select firstName, lastName, '' affiliation, email, contactId from ContactInfo where contactId?a", array_keys($this->prow->contacts()));
885        $contacts = array();
886        while ($result && ($row = $result->fetch_object("Author"))) {
887            $match = -1;
888            for ($i = 0; $match < 0 && $i < count($aulist); ++$i)
889                if (strcasecmp($aulist[$i]->email, $row->email) == 0)
890                    $match = $i;
891            if (($row->firstName !== "" || $row->lastName !== "") && $match < 0) {
892                $contact_n = $row->firstName . " " . $row->lastName;
893                $contact_preg = str_replace("\\.", "\\S*", "{\\b" . preg_quote($row->firstName) . "\\b.*\\b" . preg_quote($row->lastName) . "\\b}i");
894                for ($i = 0; $match < 0 && $i < count($aulist); ++$i) {
895                    $f = $aulist[$i]->firstName;
896                    $l = $aulist[$i]->lastName;
897                    if (($f !== "" || $l !== "") && $aulist[$i]->email === "") {
898                        $author_n = $f . " " . $l;
899                        $author_preg = str_replace("\\.", "\\S*", "{\\b" . preg_quote($f) . "\\b.*\\b" . preg_quote($l) . "\\b}i");
900                        if (preg_match($contact_preg, $author_n)
901                            || preg_match($author_preg, $contact_n))
902                            $match = $i;
903                    }
904                }
905            }
906            if ($match >= 0) {
907                $au = $aulist[$match];
908                if ($au->email === "")
909                    $au->email = $row->email;
910            } else {
911                $contacts[] = $au = $row;
912                $au->nonauthor = true;
913            }
914            $au->contactId = (int) $row->contactId;
915            Contact::set_sorter($au, $this->conf);
916        }
917        Dbl::free($result);
918
919        uasort($contacts, "Contact::compare");
920        return array($aulist, $contacts);
921    }
922
923    private function paptabAuthors($skip_contacts) {
924        if ($this->view_authors == 0) {
925            echo '<div class="pg">',
926                $this->papt("authorInformation", $this->conf->_c("paper_field", "Authors", 0)),
927                '<div class="pavb"><i>Hidden for blind review</i></div>',
928                "</div>\n\n";
929            return;
930        }
931
932        // clean author information
933        list($aulist, $contacts) = $this->_analyze_authors();
934
935        // "author" or "authors"?
936        $auname = $this->conf->_c("paper_field", "Authors", count($aulist));
937        if ($this->view_authors == 1)
938            $auname .= " (deblinded)";
939        else if ($this->user->act_author_view($this->prow)) {
940            $sb = $this->conf->submission_blindness();
941            if ($sb === Conf::BLIND_ALWAYS
942                || ($sb === Conf::BLIND_OPTIONAL && $this->prow->blind))
943                $auname .= " (blind)";
944            else if ($sb === Conf::BLIND_UNTILREVIEW)
945                $auname .= " (blind until review)";
946        }
947
948        // header with folding
949        echo '<div class="pg">',
950            '<div class="pavt ui js-aufoldup', $this->error_class("authors"),
951            '"><span class="pavfn">';
952        if ($this->view_authors == 1 || $this->allFolded)
953            echo '<a class="q ui js-aufoldup" href="" title="Toggle author display">';
954        if ($this->view_authors == 1)
955            echo '<span class="fn8">', $this->conf->_c("paper_field", "Authors", 0), '</span><span class="fx8">';
956        if ($this->allFolded)
957            echo expander(null, 9);
958        else if ($this->view_authors == 1)
959            echo expander(false);
960        echo $auname;
961        if ($this->view_authors == 1)
962            echo '</span>';
963        if ($this->view_authors == 1 || $this->allFolded)
964            echo '</a>';
965        echo '</span></div>';
966
967        // contents
968        echo '<div class="pavb">';
969        if ($this->view_authors == 1)
970            echo '<a class="q fn8 ui js-aufoldup" href="" title="Toggle author display">',
971                '+&nbsp;<i>Hidden for blind review</i>',
972                '</a><div class="fx8">';
973        if ($this->allFolded)
974            echo '<div class="fn9">',
975                $this->authorData($aulist, "last", null),
976                ' <a class="ui js-aufoldup" href="">[details]</a>',
977                '</div><div class="fx9">';
978        echo $this->authorData($aulist, "col", $this->user);
979        if ($this->allFolded)
980            echo '</div>';
981        if ($this->view_authors == 1)
982            echo '</div>';
983        echo "</div></div>\n\n";
984
985        // contacts
986        if (count($contacts) > 0 && !$skip_contacts) {
987            echo '<div class="pg fx9', ($this->view_authors > 1 ? "" : " fx8"), '">',
988                $this->papt("authorInformation",
989                            $this->conf->_c("paper_field", "Contacts", count($contacts))),
990                '<div class="pavb">',
991                $this->authorData($contacts, "col", $this->user),
992                "</div></div>\n\n";
993        }
994    }
995
996    private function unparse_option_html(PaperOptionValue $ov) {
997        $o = $ov->option;
998        $phtml = $o->unparse_page_html($this->prow, $ov);
999        if (!$phtml || count($phtml) <= 1)
1000            return [];
1001        $phtype = array_shift($phtml);
1002        $aufold = $this->view_options[$o->id] == 1;
1003
1004        $ts = [];
1005        if ($o->display() === PaperOption::DISP_SUBMISSION) {
1006            $div = $aufold ? '<div class="xd fx8">' : '<div class="xd">';
1007            if ($phtype === PaperOption::PAGE_HTML_NAME) {
1008                foreach ($phtml as $p)
1009                    $ts[] = $div . '<span class="pavfn">' . $p . "</span></div>\n";
1010            } else if ($phtype === PaperOption::PAGE_HTML_FULL) {
1011                foreach ($phtml as $p)
1012                    $ts[] = $div . $p . "</div>\n";
1013            } else {
1014                $x = $div . '<span class="pavfn">' . htmlspecialchars($o->title) . '</span>';
1015                foreach ($phtml as $p)
1016                    $x .= '<div class="pavb">' . $p . '</div>';
1017                $ts[] = $x . "</div>\n";
1018            }
1019        } else if ($o->display() !== PaperOption::DISP_TOPICS) {
1020            $div = $aufold ? '<div class="pgsm fx8">' : '<div class="pgsm">';
1021            if ($phtype === PaperOption::PAGE_HTML_NAME) {
1022                foreach ($phtml as $p)
1023                    $ts[] = $div . '<div class="pavt"><span class="pavfn">' . $p . "</span></div></div>\n";
1024            } else if ($phtype === PaperOption::PAGE_HTML_FULL) {
1025                foreach ($phtml as $p)
1026                    $ts[] = $div . $p . "</div>\n";
1027            } else {
1028                $x = $div . '<div class="pavt"><span class="pavfn">' . htmlspecialchars($o->title) . '</span></div>';
1029                foreach ($phtml as $p)
1030                    $x .= '<div class="pavb">' . $p . '</div>';
1031                $ts[] = $x . "</div>\n";
1032            }
1033        } else {
1034            $div = $aufold ? '<div class="fx8">' : '<div>';
1035            if ($phtype === PaperOption::PAGE_HTML_NAME) {
1036                foreach ($phtml as $p)
1037                    $ts[] = $div . '<span class="papon">' . $p . "</span></div>\n";
1038            } else if ($phtype === PaperOption::PAGE_HTML_FULL) {
1039                foreach ($phtml as $p)
1040                    $ts[] = $div . $p . "</div>\n";
1041            } else {
1042                foreach ($phtml as $p) {
1043                    if (!empty($ts)
1044                        || $p === ""
1045                        || $p[0] !== "<"
1046                        || !preg_match('/\A((?:<(?:div|p).*?>)*)([\s\S]*)\z/', $p, $cm))
1047                        $cm = [null, "", $p];
1048                    $ts[] = $div . $cm[1] . '<span class="papon">' . htmlspecialchars($o->title) . ':</span> ' . $cm[2] . "</div>\n";
1049                }
1050            }
1051        }
1052        return $ts;
1053    }
1054
1055    private function paptab_topics() {
1056        if (!($tmap = $this->prow->named_topic_map()))
1057            return "";
1058        $interests = $this->user->topic_interest_map();
1059        $k = 0;
1060        $ts = [];
1061        foreach ($tmap as $tid => $tname) {
1062            $t = '<li class="topictp';
1063            if (($i = get($interests, $tid)))
1064                $t .= ' topic' . $i;
1065            $x = htmlspecialchars($tname);
1066            if ($this->user->isPC && strpos($tname, "\"") === false)
1067                $x = Ht::link($x, hoturl("search", ["q" => "topic:\"$tname\""]), ["class" => "qq"]);
1068            $ts[] = $t . '">' . $x . '</li>';
1069            if ($k < 2 && strlen($tname) > 50 && UnicodeHelper::utf8_glyphlen($tname) > 50)
1070                $k = 2;
1071            else if ($k < 1 && strlen($tname) > 20 && UnicodeHelper::utf8_glyphlen($tname) > 20)
1072                $k = 1;
1073        }
1074        if (count($ts) < 4)
1075            $k = "long";
1076        else
1077            $k = get(["short", "medium", "long"], $k);
1078        return '<ul class="topict topict-' . $k . '">' . join("", $ts) . '</ul>';
1079    }
1080
1081    private function paptabTopicsOptions() {
1082        $topicdata = $this->paptab_topics();
1083        $optt = $optp = [];
1084        $optp_nfold = $optt_ndoc = $optt_nfold = 0;
1085
1086        foreach ($this->prow->options() as $ov) {
1087            $o = $ov->option;
1088            if ($o->display() !== PaperOption::DISP_SUBMISSION
1089                && $o->display() >= 0
1090                && get($this->view_options, $o->id)
1091                && ($oh = $this->unparse_option_html($ov))) {
1092                $aufold = $this->view_options[$o->id] == 1;
1093                if ($o->display() === PaperOption::DISP_TOPICS) {
1094                    $optt = array_merge($optt, $oh);
1095                    if ($aufold)
1096                        $optt_nfold += count($oh);
1097                    if ($o->has_document())
1098                        $optt_ndoc += count($oh);
1099                } else {
1100                    $optp = array_merge($optp, $oh);
1101                    if ($aufold)
1102                        $optp_nfold += count($oh);
1103                }
1104            }
1105        }
1106
1107        if (!empty($optp)) {
1108            $div = count($optp) === $optp_nfold ? '<div class="pg fx8">' : '<div class="pg">';
1109            echo $div, join("", $optp), "</div>\n";
1110        }
1111
1112        if ($topicdata !== "" || !empty($optt)) {
1113            $infotypes = array();
1114            if ($optt_ndoc > 0)
1115                $infotypes[] = "Attachments";
1116            if (count($optt) !== $optt_ndoc)
1117                $infotypes[] = "Options";
1118            $options_name = commajoin($infotypes);
1119            if ($topicdata !== "")
1120                array_unshift($infotypes, "Topics");
1121            $tanda = commajoin($infotypes);
1122
1123            if ($this->allFolded) {
1124                $extra = array("fold" => "paper", "foldnum" => 5,
1125                               "foldtitle" => "Toggle " . strtolower($tanda));
1126                $eclass = " fx5";
1127            } else {
1128                $extra = null;
1129                $eclass = "";
1130            }
1131
1132            if ($topicdata !== "") {
1133                echo "<div class='pg'>",
1134                    $this->papt("topics", array("Topics", $tanda), $extra),
1135                    "<div class='pavb$eclass'>", $topicdata, "</div></div>\n\n";
1136                $extra = null;
1137                $tanda = $options_name;
1138            }
1139
1140            if (!empty($optt)) {
1141                echo '<div class="pg', ($extra ? "" : $eclass),
1142                    (count($optt) === $optt_nfold ? " fx8" : ""), '">',
1143                    $this->papt("options", array($options_name, $tanda), $extra),
1144                    "<div class=\"pavb$eclass\">", join("", $optt), "</div></div>\n\n";
1145            }
1146        }
1147    }
1148
1149    private function editable_newcontact_row($num) {
1150        if ($num === '$') {
1151            $checked = true;
1152            $name = $email = "";
1153            $cerror = false;
1154        } else {
1155            $checked = !$this->useRequest || $this->qreq["newcontact_active_{$num}"];
1156            $email = (string) ($this->useRequest ? $this->qreq["newcontact_email_{$num}"] : "");
1157            $name = (string) ($this->useRequest ? $this->qreq["newcontact_name_{$num}"] : "");
1158            $cerror = $this->has_problem_at("contactAuthor") || $this->has_problem_at("contacts");
1159        }
1160        $email = $email === "Email" ? "" : $email;
1161        $name = $name === "Name" ? "" : $name;
1162
1163        return '<div class="checki"><span class="checkc">'
1164                . Ht::checkbox("newcontact_active_{$num}", 1, $checked, ["data-default-checked" => 1])
1165                . ' </span>'
1166                . Ht::entry("newcontact_name_{$num}", $name, ["size" => 30, "placeholder" => "Name", "class" => ($cerror ? "has-error " : "") . "want-focus js-autosubmit", "autocomplete" => "off"])
1167                . '  '
1168                . Ht::entry("newcontact_email_{$num}", $email, ["size" => 20, "placeholder" => "Email", "class" => ($cerror ? "has-error " : "") . "js-autosubmit", "autocomplete" => "off"])
1169                . '</div>';
1170    }
1171
1172    private function echo_editable_contact_author() {
1173        if ($this->prow) {
1174            list($aulist, $contacts) = $this->_analyze_authors();
1175            $contacts = array_merge($aulist, $contacts);
1176        } else if (!$this->admin) {
1177            $contacts = [new Author($this->user)];
1178            $contacts[0]->contactId = $this->user->contactId;
1179            Contact::set_sorter($contacts[0], $this->conf);
1180        } else
1181            $contacts = [];
1182        usort($contacts, "Contact::compare");
1183
1184        $cerror = $this->has_problem_at("contactAuthor") || $this->has_problem_at("contacts");
1185        echo Ht::hidden("has_contacts", 1),
1186            '<div id="foldcontactauthors" class="papeg">',
1187            '<div class="papet',
1188            ($cerror ? " error" : ""),
1189            '"><span class="papfn">',
1190            $this->field_name("Contacts"),
1191            '</span></div>';
1192
1193        // Editable version
1194        echo $this->field_hint("Contacts", "These users can edit the submission and view reviews. All listed authors with site accounts are contacts. You can add contacts who aren’t in the author list or create accounts for authors who haven’t yet logged in.", !!$this->prow),
1195            '<div class="papev js-row-order"><div>';
1196
1197        $req_cemail = [];
1198        if ($this->useRequest) {
1199            for ($cidx = 1; isset($this->qreq["contact_email_{$cidx}"]); ++$cidx)
1200                if ($this->qreq["contact_active_{$cidx}"])
1201                    $req_cemail[strtolower($this->qreq["contact_email_{$cidx}"])] = true;
1202        }
1203
1204        $cidx = 1;
1205        foreach ($contacts as $au) {
1206            if ($au->nonauthor
1207                && (strcasecmp($this->user->email, $au->email) != 0 || $this->allow_admin)) {
1208                $ctl = Ht::hidden("contact_email_{$cidx}", $au->email)
1209                    . Ht::checkbox("contact_active_{$cidx}", 1, !$this->useRequest || isset($req_cemail[strtolower($au->email)]), ["data-default-checked" => true]);
1210            } else if ($au->contactId) {
1211                $ctl = Ht::hidden("contact_email_{$cidx}", $au->email)
1212                    . Ht::hidden("contact_active_{$cidx}", 1)
1213                    . Ht::checkbox(null, null, true, ["disabled" => true]);
1214            } else if ($au->email && validate_email($au->email)) {
1215                $ctl = Ht::hidden("contact_email_{$cidx}", $au->email)
1216                    . Ht::checkbox("contact_active_{$cidx}", 1, $this->useRequest && isset($req_cemail[strtolower($au->email)]), ["data-default-checked" => ""]);
1217            } else
1218                continue;
1219            echo '<div class="checki"><label><span class="checkc">', $ctl, ' </span>',
1220                Text::user_html_nolink($au);
1221            if ($au->nonauthor)
1222                echo ' (<em>non-author</em>)';
1223            if ($this->user->privChair
1224                && $au->contactId
1225                && $au->contactId != $this->user->contactId)
1226                echo '&nbsp;', actas_link($au);
1227            echo '</label></div>';
1228            ++$cidx;
1229        }
1230        echo '</div><div data-row-template="',
1231            htmlspecialchars($this->editable_newcontact_row('$')),
1232            '">';
1233        if ($this->useRequest) {
1234            for ($i = 1; isset($this->qreq["newcontact_email_{$i}"]); ++$i)
1235                echo $this->editable_newcontact_row($i);
1236        }
1237        echo '</div><div class="ug">',
1238            Ht::button("Add contact", ["class" => "ui btn row-order-ui addrow"]),
1239            "</div></div></div>\n\n";
1240    }
1241
1242    private function echo_editable_anonymity() {
1243        if ($this->conf->submission_blindness() != Conf::BLIND_OPTIONAL
1244            || $this->editable !== "f")
1245            return;
1246        $pblind = !$this->prow || $this->prow->blind;
1247        $blind = $this->useRequest ? !!$this->qreq->blind : $pblind;
1248        $heading = '<span class="checkc">' . Ht::checkbox("blind", 1, $blind, ["data-default-checked" => $pblind]) . " </span>" . $this->field_name("Anonymous submission");
1249        echo $this->editable_papt("blind", $heading, ["for" => "checkbox"]),
1250            $this->field_hint("Anonymous submission", "Check this box to submit anonymously (reviewers won’t be shown the author list). Make sure you also remove your name from the submission itself!"),
1251            $this->messages_for("blind"),
1252            "</div>\n\n";
1253    }
1254
1255    private function echo_editable_collaborators() {
1256        if (!$this->conf->setting("sub_collab")
1257            || ($this->editable === "f" && !$this->admin))
1258            return;
1259        $sub_pcconf = $this->conf->setting("sub_pcconf");
1260
1261        echo $this->editable_papt("collaborators", $this->field_name($sub_pcconf ? "Other conflicts" : "Potential conflicts"), ["for" => "collaborators"]),
1262            '<div class="paphint"><div class="mmm">';
1263        if ($this->conf->setting("sub_pcconf"))
1264            echo "List <em>other</em> people and institutions with which
1265        the authors have conflicts of interest.  This will help us avoid
1266        conflicts when assigning external reviews.  No need to list people
1267        at the authors’ own institutions.";
1268        else
1269            echo "List people and institutions with which the authors have
1270        conflicts of interest. ", $this->conf->message_html("conflictdef"), "
1271        Be sure to include conflicted <a href='", hoturl("users", "t=pc"), "'>PC members</a>.
1272        We use this information when assigning PC and external reviews.";
1273        echo "</div><div class=\"mmm\"><strong>List one conflict per line</strong>, using parentheses for affiliations and institutions. Examples: “Jelena Markovic (EPFL)”, “All (University of Southern California)”.</div></div>",
1274            $this->messages_for("collaborators"),
1275            '<div class="papev">',
1276            $this->editable_textarea("collaborators"),
1277            "</div></div>\n\n";
1278    }
1279
1280    private function papstrip_tags_background_classes($viewable) {
1281        $t = "has-tag-classes pscopen";
1282        if (($color = $this->prow->conf->tags()->styles($viewable, TagMap::STYLE_BG))) {
1283            TagMap::mark_pattern_fill($color);
1284            $t .= " " . join(" ", $color);
1285        }
1286        return $t;
1287    }
1288
1289    private function _papstripBegin($foldid = null, $folded = null, $extra = null) {
1290        if (!$this->npapstrip) {
1291            echo '<div class="pspcard_container"><div class="pspcard">',
1292                '<div class="pspcard_body"><div class="pspcard_fold">',
1293                '<div style="float:right;margin-left:1em"><span class="psfn">More ', expander(true), '</span></div>';
1294
1295            if ($this->prow && ($viewable = $this->prow->viewable_tags($this->user))) {
1296                $tagger = new Tagger($this->user);
1297                echo '<div class="', $this->papstrip_tags_background_classes($viewable), '">',
1298                    '<span class="psfn">Tags:</span> ',
1299                    $tagger->unparse_and_link($viewable),
1300                    '</div>';
1301            } else
1302                echo '<hr class="c" />';
1303
1304            echo '</div><div class="pspcard_open">';
1305            Ht::stash_script('$(".pspcard_fold").click(function(evt){$(".pspcard_fold").hide();$(".pspcard_open").show();evt.preventDefault()})');
1306        }
1307        echo '<div';
1308        if ($foldid)
1309            echo " id=\"fold$foldid\"";
1310        echo ' class="psc';
1311        if (!$this->npapstrip)
1312            echo " psc1";
1313        if ($foldid)
1314            echo " fold", ($folded ? "c" : "o");
1315        if ($extra) {
1316            if (isset($extra["class"]))
1317                echo " ", $extra["class"];
1318            foreach ($extra as $k => $v)
1319                if ($k !== "class")
1320                    echo "\" $k=\"", str_replace("\"", "&quot;", $v);
1321        }
1322        echo '">';
1323        ++$this->npapstrip;
1324    }
1325
1326    private function papstripCollaborators() {
1327        if (!$this->conf->setting("sub_collab") || !$this->prow->collaborators
1328            || strcasecmp(trim($this->prow->collaborators), "None") == 0)
1329            return;
1330        $name = $this->conf->setting("sub_pcconf") ? "Other conflicts" : "Potential conflicts";
1331        $fold = $this->conf->session("foldpscollab", 1) ? 1 : 0;
1332
1333        $data = $this->entryData("collaborators", "col");
1334        if ($this->entryMatches || !$this->allFolded)
1335            $fold = 0;
1336
1337        $this->_papstripBegin("pscollab", $fold, ["data-fold-session" => "foldpscollab"]);
1338        echo $this->papt("collaborators", $name,
1339                         ["type" => "ps", "fold" => "pscollab", "folded" => $fold]),
1340            "<div class='psv'><div class='fx'>", $data,
1341            "</div></div></div>\n\n";
1342    }
1343
1344    private function echo_editable_topics() {
1345        if (!$this->conf->has_topics())
1346            return;
1347        echo $this->editable_papt("topics", $this->field_name("Topics")),
1348            $this->field_hint("Topics", "Select any topics that apply to your submission."),
1349            $this->messages_for("topics"),
1350            '<div class="papev">',
1351            Ht::hidden("has_topics", 1),
1352            '<div class="ctable">';
1353        $ptopics = $this->prow ? $this->prow->topic_map() : [];
1354        foreach ($this->conf->topic_map() as $tid => $tname) {
1355            $pchecked = isset($ptopics[$tid]);
1356            $checked = $this->useRequest ? isset($this->qreq["top$tid"]) : $pchecked;
1357            echo '<div class="ctelt"><div class="ctelti checki"><label><span class="checkc">',
1358                Ht::checkbox("top$tid", 1, $checked, ["data-default-checked" => $pchecked, "data-range-type" => "topic", "class" => "uix js-range-click topic-entry"]),
1359                ' </span>', $tname, '</label></div></div>';
1360        }
1361        echo "</div></div></div>\n\n";
1362    }
1363
1364    function echo_editable_option_papt(PaperOption $o, $heading = null, $for = null) {
1365        if (!$heading)
1366            $heading = $this->field_name(htmlspecialchars($o->title));
1367        if ($for === null || $for === true)
1368            $for = $o->formid;
1369        echo $this->editable_papt($o->formid, $heading, ["id" => "{$o->formid}_div", "for" => $for], $o),
1370            $this->field_hint(htmlspecialchars($o->title), $o->description),
1371            $this->messages_for($o->formid), Ht::hidden("has_{$o->formid}", 1);
1372    }
1373
1374    private function echo_editable_pc_conflicts() {
1375        if (!$this->conf->setting("sub_pcconf")
1376            || ($this->editable === "f" && !$this->admin))
1377            return;
1378        $pcm = $this->conf->full_pc_members();
1379        if (empty($pcm))
1380            return;
1381
1382        $selectors = $this->conf->setting("sub_pcconfsel");
1383        $show_colors = $this->user->can_view_reviewer_tags($this->prow);
1384
1385        if ($selectors) {
1386            $ctypes = Conflict::$type_descriptions;
1387            $extra = array("class" => "pcconf-selector");
1388            if ($this->admin) {
1389                $ctypes["xsep"] = null;
1390                $ctypes[CONFLICT_CHAIRMARK] = "Confirmed conflict";
1391                $extra["optionstyles"] = array(CONFLICT_CHAIRMARK => "font-weight:bold");
1392            }
1393            foreach ($ctypes as &$ctype)
1394                $ctype = $this->conf->_c("conflict_type", $ctype);
1395            unset($ctype);
1396            $author_ctype = $this->conf->_c("conflict_type", "Author");
1397        }
1398
1399        echo $this->editable_papt("pcconf", $this->field_name("PC conflicts")),
1400            "<div class='paphint'>Select the PC members who have conflicts of interest with this submission. ", $this->conf->message_html("conflictdef"), "</div>\n",
1401            $this->messages_for("pcconf"),
1402            '<div class="papev">',
1403            Ht::hidden("has_pcconf", 1),
1404            '<div class="pc_ctable">';
1405        foreach ($pcm as $id => $p) {
1406            $pct = $this->prow ? $this->prow->conflict_type($p) : 0;
1407            if ($this->useRequest)
1408                $ct = Conflict::constrain_editable($this->qreq["pcc$id"], $this->admin);
1409            else
1410                $ct = $pct;
1411            $pcconfmatch = null;
1412            if ($this->prow && $pct < CONFLICT_AUTHOR)
1413                $pcconfmatch = $this->prow->potential_conflict_html($p, $pct <= 0);
1414
1415            $label = '<span class="taghl">' . $this->user->name_html_for($p) . '</span>';
1416            if ($p->affiliation)
1417                $label .= '<span class="pcconfaff">' . htmlspecialchars(UnicodeHelper::utf8_abbreviate($p->affiliation, 60)) . '</span>';
1418
1419            echo '<div class="ctelt"><div class="ctelti';
1420            if (!$selectors)
1421                echo ' checki';
1422            echo ' clearfix';
1423            if ($show_colors && ($classes = $p->viewable_color_classes($this->user)))
1424                echo ' ', $classes;
1425            if ($pct)
1426                echo ' boldtag';
1427            if ($pcconfmatch)
1428                echo ' need-tooltip" data-tooltip-class="gray" data-tooltip="', str_replace('"', '&quot;', $pcconfmatch[1]);
1429            echo '"><label>';
1430
1431            $js = ["id" => "pcc$id"];
1432            $disabled = $pct >= CONFLICT_AUTHOR
1433                || ($pct > 0 && !$this->admin && !Conflict::is_author_mark($pct));
1434            if ($selectors) {
1435                echo '<span class="pcconf-editselector">';
1436                if ($disabled) {
1437                    echo '<strong>', ($pct >= CONFLICT_AUTHOR ? $author_ctype : "Conflict"), '</strong>',
1438                        Ht::hidden("pcc$id", $pct, ["class" => "conflict-entry"]);
1439                } else {
1440                    $js["class"] = "conflict-entry";
1441                    $js["data-default-value"] = Conflict::constrain_editable($pct, $this->admin);
1442                    echo Ht::select("pcc$id", $ctypes, Conflict::constrain_editable($ct, $this->admin), $js);
1443                }
1444                echo '</span>', $label;
1445            } else {
1446                $js["disabled"] = $disabled;
1447                $js["data-default-checked"] = $pct > 0;
1448                $js["data-range-type"] = "pcc";
1449                $js["class"] = "uix js-range-click conflict-entry";
1450                echo '<span class="checkc">',
1451                    Ht::checkbox("pcc$id", $ct > 0 ? $ct : CONFLICT_AUTHORMARK,
1452                                 $ct > 0, $js),
1453                    ' </span>', $label;
1454            }
1455            echo "</label>";
1456            if ($pcconfmatch)
1457                echo $pcconfmatch[0];
1458            echo "</div></div>";
1459        }
1460        echo "</div>\n</div></div>\n\n";
1461    }
1462
1463    private function papstripPCConflicts() {
1464        assert(!$this->editable);
1465        if (!$this->prow)
1466            return;
1467
1468        $pcconf = array();
1469        $pcm = $this->conf->pc_members();
1470        foreach ($this->prow->pc_conflicts() as $id => $x) {
1471            $p = $pcm[$id];
1472            $text = "<p class=\"odname\">" . $this->user->name_html_for($p) . "</p>";
1473            if ($this->user->isPC && ($classes = $p->viewable_color_classes($this->user)))
1474                $text = "<div class=\"pscopen $classes taghh\">$text</div>";
1475            $pcconf[$p->sort_position] = $text;
1476        }
1477        ksort($pcconf);
1478        if (!count($pcconf))
1479            $pcconf[] = "<p class=\"odname\">None</p>";
1480        $this->_papstripBegin();
1481        echo $this->papt("pcconflict", "PC conflicts", array("type" => "ps")),
1482            "<div class='psv psconf'>", join("", $pcconf), "</div></div>\n";
1483    }
1484
1485    private function _papstripLeadShepherd($type, $name, $showedit, $wholefold) {
1486        $editable = ($type === "manager" ? $this->user->privChair : $this->admin);
1487
1488        $field = $type . "ContactId";
1489        if ($this->prow->$field == 0 && !$editable)
1490            return;
1491        $value = $this->prow->$field;
1492
1493        if ($wholefold === null)
1494            $this->_papstripBegin($type, true);
1495        else {
1496            echo '<div id="fold', $type, '" class="foldc">';
1497            $this->_papstripBegin(null, true);
1498        }
1499        echo $this->papt($type, $name, array("type" => "ps", "fold" => $editable ? $type : false, "folded" => true)),
1500            '<div class="psv">';
1501        $p = $this->conf->pc_member_by_id($value);
1502        $n = $p ? $this->user->name_html_for($p) : ($value ? "Unknown!" : "");
1503        $text = '<p class="fn odname js-psedit-result">' . $n . '</p>';
1504        echo '<div class="pcopen taghh';
1505        if ($p && ($classes = $this->user->user_color_classes_for($p)))
1506            echo ' ', $classes;
1507        echo '">', $text, '</div>';
1508
1509        if ($editable) {
1510            $selopt = [0];
1511            foreach ($this->conf->pc_members() as $p)
1512                if (!$this->prow
1513                    || $p->can_accept_review_assignment($this->prow)
1514                    || $p->contactId == $value)
1515                    $selopt[] = $p->contactId;
1516            $this->conf->stash_hotcrp_pc($this->user);
1517            echo '<form class="submit-ui fx"><div>',
1518                Ht::select($type, [], 0, ["class" => "psc-select need-pcselector want-focus", "style" => "width:99%", "data-pcselector-options" => join(" ", $selopt), "data-pcselector-selected" => $value]),
1519                '</div></form>';
1520            Ht::stash_script('edit_paper_ui.prepare_psedit.call($$("fold' . $type . '"),{p:' . $this->prow->paperId . ',fn:"' . $type . '"})');
1521        }
1522
1523        if ($wholefold === null)
1524            echo "</div></div>\n";
1525        else
1526            echo "</div></div></div>\n";
1527    }
1528
1529    private function papstripLead($showedit) {
1530        $this->_papstripLeadShepherd("lead", "Discussion lead", $showedit || $this->qreq->atab === "lead", null);
1531    }
1532
1533    private function papstripShepherd($showedit, $fold) {
1534        $this->_papstripLeadShepherd("shepherd", "Shepherd", $showedit || $this->qreq->atab === "shepherd", $fold);
1535    }
1536
1537    private function papstripManager($showedit) {
1538        $this->_papstripLeadShepherd("manager", "Paper administrator", $showedit || $this->qreq->atab === "manager", null);
1539    }
1540
1541    private function papstripTags() {
1542        if (!$this->prow || !$this->user->can_view_tags($this->prow))
1543            return;
1544        $tags = $this->prow->all_tags_text();
1545        $is_editable = $this->user->can_change_some_tag($this->prow);
1546        if ($tags === "" && !$is_editable)
1547            return;
1548
1549        // Note that tags MUST NOT contain HTML special characters.
1550        $tagger = new Tagger($this->user);
1551        $viewable = $this->prow->viewable_tags($this->user);
1552
1553        $tx = $tagger->unparse_and_link($viewable);
1554        $unfolded = $is_editable && ($this->has_problem_at("tags") || $this->qreq->atab === "tags");
1555
1556        $this->_papstripBegin("tags", true);
1557        echo '<div class="', $this->papstrip_tags_background_classes($viewable), '">';
1558
1559        if ($is_editable) {
1560            echo Ht::form(hoturl("paper", "p=" . $this->prow->paperId), ["data-pid" => $this->prow->paperId, "data-no-tag-report" => $unfolded ? 1 : null]);
1561            Ht::stash_script('edit_paper_ui.prepare_pstags.call($$("foldtags"))');
1562        }
1563
1564        echo $this->papt("tags", "Tags", array("type" => "ps", "editfolder" => ($is_editable ? "tags" : 0))),
1565            '<div class="psv">';
1566        if ($is_editable) {
1567            // tag report form
1568            $treport = PaperApi::tagreport($this->user, $this->prow);
1569            $tm0 = $tm1 = [];
1570            $tms = 0;
1571            foreach ($treport->tagreport as $tr) {
1572                $search = isset($tr->search) ? $tr->search : "#" . $tr->tag;
1573                $tm = Ht::link("#" . $tr->tag, hoturl("search", ["q" => $search]), ["class" => "q"]) . ": " . $tr->message;
1574                $tms = max($tms, $tr->status);
1575                $tm0[] = $tm;
1576                if ($tr->status > 0 && $this->prow->has_tag($tagger->expand($tr->tag)))
1577                    $tm1[] = $tm;
1578            }
1579
1580            // uneditable
1581            echo '<div class="fn want-tag-report-warnings">';
1582            if (!empty($tm1))
1583                echo Ht::xmsg("warning", $tm1);
1584            echo '</div><div class="fn js-tag-result">',
1585                ($tx === "" ? "None" : $tx), '</div>';
1586
1587            echo '<div class="fx js-tag-editor"><div class="want-tag-report">';
1588            if (!empty($tm0))
1589                echo Ht::xmsg($tms, $tm0);
1590            echo "</div>";
1591            $editable = $tags;
1592            if ($this->prow)
1593                $editable = $this->prow->editable_tags($this->user);
1594            echo '<div style="position:relative">',
1595                '<textarea cols="20" rows="4" name="tags" style="width:97%;margin:0" class="want-focus">',
1596                $tagger->unparse($editable),
1597                "</textarea></div>",
1598                '<div class="aab aabr"><div class="aabut">',
1599                Ht::submit("save", "Save", ["class" => "btn btn-primary"]),
1600                '</div><div class="aabut">',
1601                Ht::submit("cancel", "Cancel"),
1602                "</div></div>",
1603                "<span class='hint'><a href='", hoturl("help", "t=tags"), "'>Learn more</a> <span class='barsep'>·</span> <strong>Tip:</strong> Twiddle tags like &ldquo;~tag&rdquo; are visible only to you.</span>",
1604                "</div>";
1605        } else
1606            echo '<div class="js-tag-result">', ($tx === "" ? "None" : $tx), '</div>';
1607        echo "</div>";
1608
1609        if ($is_editable)
1610            echo "</form>";
1611        if ($unfolded)
1612            echo Ht::unstash_script('fold("tags",0)');
1613        echo "</div></div>\n";
1614    }
1615
1616    function papstripOutcomeSelector() {
1617        $this->_papstripBegin("decision", $this->qreq->atab !== "decision");
1618        echo $this->papt("decision", "Decision", array("type" => "ps", "fold" => "decision")),
1619            '<div class="psv"><form class="submit-ui fx"><div>';
1620        if (isset($this->qreq->forceShow))
1621            echo Ht::hidden("forceShow", $this->qreq->forceShow ? 1 : 0);
1622        echo decisionSelector($this->prow->outcome, null, " class=\"want-focus\" style=\"width:99%\""),
1623            '</div></form><p class="fn odname js-psedit-result">',
1624            htmlspecialchars($this->conf->decision_name($this->prow->outcome)),
1625            "</p></div></div>\n";
1626        Ht::stash_script('edit_paper_ui.prepare_psedit.call($$("folddecision"),{p:' . $this->prow->paperId . ',fn:"decision"})');
1627    }
1628
1629    function papstripReviewPreference() {
1630        $this->_papstripBegin("revpref");
1631        echo $this->papt("revpref", "Review preference", array("type" => "ps")),
1632            "<div class=\"psv\"><form class=\"ui\"><div>";
1633        $rp = unparse_preference($this->prow);
1634        $rp = ($rp == "0" ? "" : $rp);
1635        echo "<input id=\"revprefform_d\" type=\"text\" name=\"revpref", $this->prow->paperId,
1636            "\" size=\"4\" value=\"$rp\" class=\"revpref want-focus want-select\" />",
1637            "</div></form></div></div>\n";
1638        Ht::stash_script("add_revpref_ajax(\"#revprefform_d\",true);shortcut(\"revprefform_d\").add()");
1639    }
1640
1641    private function papstrip_tag_entry($id, $folds) {
1642        if (!$this->npapstrip_tag_entry)
1643            $this->_papstripBegin(null, null, ["class" => "psc_te"]);
1644        ++$this->npapstrip_tag_entry;
1645        echo '<div', ($id ? " id=\"fold{$id}\"" : ""),
1646            ' class="pste', ($folds ? " $folds" : ""), '">';
1647    }
1648
1649    private function papstrip_tag_float($tag, $kind, $type) {
1650        $class = "is-nonempty-tags floatright";
1651        if (($totval = $this->prow->tag_value($tag)) === false) {
1652            $totval = "";
1653            $class .= " hidden";
1654        }
1655        $reverse = $type !== "rank";
1656        $extradiv = "";
1657        if ($type === "vote" || $type === "approval") {
1658            $class .= " need-tooltip";
1659            $extradiv = ' data-tooltip-dir="h" data-tooltip-info="votereport" data-tag="' . htmlspecialchars($tag) . '"';
1660        }
1661        return '<div class="' . $class . '"' . $extradiv
1662            . '><a class="qq" href="' . hoturl("search", "q=" . urlencode("show:#$tag sort:" . ($reverse ? "-" : "") . "#$tag")) . '">'
1663            . '<span class="is-tag-index" data-tag-base="' . $tag . '">' . $totval . '</span> ' . $kind . '</a></div>';
1664    }
1665
1666    private function papstrip_tag_entry_title($start, $tag, $value) {
1667        $title = $start . '<span class="fn is-nonempty-tags';
1668        if ($value === "")
1669            $title .= ' hidden';
1670        return $title . '">: <span class="is-tag-index" data-tag-base="' . $tag . '">' . $value . '</span></span>';
1671    }
1672
1673    private function papstripRank($tag) {
1674        $id = "rank_" . html_id_encode($tag);
1675        if (($myval = $this->prow->tag_value($this->user->contactId . "~$tag")) === false)
1676            $myval = "";
1677        $totmark = $this->papstrip_tag_float($tag, "overall", "rank");
1678
1679        $this->papstrip_tag_entry($id, "foldc fold2c");
1680        echo Ht::form("", ["id" => "{$id}form", "data-pid" => $this->prow->paperId]);
1681        if (isset($this->qreq->forceShow))
1682            echo Ht::hidden("forceShow", $this->qreq->forceShow);
1683        echo $this->papt($id, $this->papstrip_tag_entry_title("#$tag rank", "~$tag", $myval),
1684                         array("type" => "ps", "fold" => $id, "float" => $totmark)),
1685            '<div class="psv"><div class="fx">',
1686            Ht::entry("tagindex", $myval,
1687                      array("size" => 4, "class" => "is-tag-index want-focus",
1688                            "data-tag-base" => "~$tag")),
1689            ' <span class="barsep">·</span> ',
1690            '<a href="', hoturl("search", "q=" . urlencode("editsort:#~$tag")), '">Edit all</a>',
1691            " <div class='hint' style='margin-top:4px'><strong>Tip:</strong> <a href='", hoturl("search", "q=" . urlencode("editsort:#~$tag")), "'>Search “editsort:#~{$tag}”</a> to drag and drop your ranking, or <a href='", hoturl("offline"), "'>use offline reviewing</a> to rank many papers at once.</div>",
1692            "</div></div></form></div>\n";
1693        Ht::stash_script('edit_paper_ui.prepare_pstagindex.call($$("' . $id . 'form"))');
1694    }
1695
1696    private function papstripVote($tag, $allotment) {
1697        $id = "vote_" . html_id_encode($tag);
1698        if (($myval = $this->prow->tag_value($this->user->contactId . "~$tag")) === false)
1699            $myval = "";
1700        $totmark = $this->papstrip_tag_float($tag, "total", "vote");
1701
1702        $this->papstrip_tag_entry($id, "foldc fold2c");
1703        echo Ht::form("", ["id" => "{$id}form", "data-pid" => $this->prow->paperId]);
1704        if (isset($this->qreq->forceShow))
1705            echo Ht::hidden("forceShow", $this->qreq->forceShow);
1706        echo $this->papt($id, $this->papstrip_tag_entry_title("#$tag votes", "~$tag", $myval),
1707                         array("type" => "ps", "fold" => $id, "float" => $totmark)),
1708            '<div class="psv"><div class="fx">',
1709            Ht::entry("tagindex", $myval,
1710                      array("size" => 4, "class" => "is-tag-index want-focus",
1711                            "data-tag-base" => "~$tag")),
1712            " &nbsp;of $allotment",
1713            ' <span class="barsep">·</span> ',
1714            '<a href="', hoturl("search", "q=" . urlencode("editsort:-#~$tag")), '">Edit all</a>',
1715            "</div></div></form></div>\n";
1716        Ht::stash_script('edit_paper_ui.prepare_pstagindex.call($$("' . $id . 'form"))');
1717    }
1718
1719    private function papstripApproval($tag) {
1720        $id = "approval_" . html_id_encode($tag);
1721        if (($myval = $this->prow->tag_value($this->user->contactId . "~$tag")) === false)
1722            $myval = "";
1723        $totmark = $this->papstrip_tag_float($tag, "total", "approval");
1724
1725        $this->papstrip_tag_entry(null, null);
1726        echo Ht::form("", ["id" => "{$id}form", "data-pid" => $this->prow->paperId]);
1727        if (isset($this->qreq->forceShow))
1728            echo Ht::hidden("forceShow", $this->qreq->forceShow);
1729        echo $this->papt($id,
1730                         Ht::checkbox("tagindex", "0", $myval !== "",
1731                                      array("class" => "is-tag-index want-focus",
1732                                            "data-tag-base" => "~$tag",
1733                                            "style" => "padding-left:0;margin-left:0;margin-top:0"))
1734                         . "&nbsp;" . Ht::label("#$tag vote"),
1735                         array("type" => "ps", "float" => $totmark)),
1736            "</form></div>\n\n";
1737        Ht::stash_script('edit_paper_ui.prepare_pstagindex.call($$("' . $id . 'form"))');
1738    }
1739
1740    private function papstripWatch() {
1741        $prow = $this->prow;
1742        $conflictType = $prow->conflict_type($this->user);
1743        if (!($prow->timeSubmitted > 0
1744              && ($conflictType >= CONFLICT_AUTHOR
1745                  || $conflictType <= 0
1746                  || $this->user->is_admin_force())
1747              && $this->user->contactId > 0))
1748            return;
1749        // watch note
1750        $watch = $this->conf->fetch_ivalue("select watch from PaperWatch where paperId=? and contactId=?", $prow->paperId, $this->user->contactId);
1751
1752        $this->_papstripBegin();
1753
1754        echo '<form class="submit-ui"><div>',
1755            $this->papt("watch",
1756                        Ht::checkbox("follow", 1,
1757                                     $this->user->following_reviews($prow, $watch),
1758                                     ["class" => "js-follow-change",
1759                                      "style" => "padding-left:0;margin-left:0"])
1760                        . "&nbsp;" . Ht::label("Email notification"),
1761                        array("type" => "ps")),
1762            '<div class="pshint">Select to receive email on updates to reviews and comments.</div>',
1763            "</div></form></div>\n\n";
1764        Ht::stash_script('$(".js-follow-change").on("change", handle_ui)');
1765    }
1766
1767
1768    // Functions for editing
1769
1770    function deadlineSettingIs($dname) {
1771        $deadline = $this->conf->printableTimeSetting($dname, "span");
1772        if ($deadline === "N/A")
1773            return "";
1774        else if (time() < $this->conf->setting($dname))
1775            return " The deadline is $deadline.";
1776        else
1777            return " The deadline was $deadline.";
1778    }
1779
1780    private function _deadline_override_message() {
1781        if ($this->admin)
1782            return " As an administrator, you can make changes anyway.";
1783        else
1784            return $this->_forceShow_message();
1785    }
1786    private function _forceShow_message() {
1787        if (!$this->admin && $this->allow_admin)
1788            return " " . Ht::link("(Override your conflict)", SelfHref::make($this->qreq, ["forceShow" => 1]), ["class" => "nw"]);
1789        else
1790            return "";
1791    }
1792
1793    private function _edit_message_new_paper() {
1794        global $Now;
1795        $startDeadline = $this->deadlineSettingIs("sub_reg");
1796        $msg = "";
1797        if (!$this->conf->timeStartPaper()) {
1798            $sub_open = $this->conf->setting("sub_open");
1799            if ($sub_open <= 0 || $sub_open > $Now)
1800                $msg = "The conference site is not open for submissions." . $this->_deadline_override_message();
1801            else
1802                $msg = 'The <a href="' . hoturl("deadlines") . '">deadline</a> for registering submissions has passed.' . $startDeadline . $this->_deadline_override_message();
1803            if (!$this->admin) {
1804                $this->quit = true;
1805                return '<div class="merror">' . $msg . '</div>';
1806            }
1807            $msg = Ht::xmsg("info", $msg);
1808        }
1809        $t1 = $this->conf->_("Enter information about your paper.");
1810        if ($startDeadline && !$this->conf->setting("sub_freeze"))
1811            $t2 = "You can make changes until the deadline, but thereafter incomplete submissions will not be considered.";
1812        else if (!$this->conf->opt("noPapers"))
1813            $t2 = "You don’t have to upload the PDF right away, but incomplete submissions will not be considered.";
1814        else
1815            $t2 = "Incomplete submissions will not be considered.";
1816        $t2 = $this->conf->_($t2);
1817        $msg .= Ht::xmsg("info", space_join($t1, $t2, $startDeadline));
1818        if (($v = $this->conf->message_html("submit")))
1819            $msg .= Ht::xmsg("info", $v);
1820        return $msg;
1821    }
1822
1823    private function _edit_message_for_author(PaperInfo $prow) {
1824        $can_view_decision = $prow->outcome != 0 && $this->user->can_view_decision($prow);
1825        if ($can_view_decision && $prow->outcome < 0) {
1826            return Ht::xmsg("warning", "The submission was not accepted." . $this->_forceShow_message());
1827        } else if ($prow->timeWithdrawn > 0) {
1828            if ($this->user->can_revive_paper($prow))
1829                return Ht::xmsg("warning", "The submission has been withdrawn, but you can still revive it." . $this->deadlineSettingIs("sub_update"));
1830            else
1831                return Ht::xmsg("warning", "The submission has been withdrawn." . $this->_forceShow_message());
1832        } else if ($prow->timeSubmitted <= 0) {
1833            $whyNot = $this->user->perm_update_paper($prow);
1834            if (!$whyNot) {
1835                if ($this->conf->setting("sub_freeze"))
1836                    $t = "This submission must be completed before it can be reviewed.";
1837                else if ($prow->paperStorageId <= 1 && !$this->conf->opt("noPapers"))
1838                    $t = "This submission is not ready for review and will not be considered as is, but you can still make changes.";
1839                else
1840                    $t = "This submission is not ready for review and will not be considered as is, but you can still mark it ready for review and make other changes if appropriate.";
1841                return Ht::xmsg("warning", $t . $this->deadlineSettingIs("sub_update"));
1842            } else if (isset($whyNot["updateSubmitted"])
1843                       && $this->user->can_finalize_paper($prow)) {
1844                return Ht::xmsg("warning", 'The submission is not ready for review. Although you cannot make any further changes, the current version can be still be submitted for review.' . $this->deadlineSettingIs("sub_sub") . $this->_deadline_override_message());
1845            } else if (isset($whyNot["deadline"])) {
1846                if ($this->conf->deadlinesBetween("", "sub_sub", "sub_grace")) {
1847                    return Ht::xmsg("warning", 'The site is not open for updates at the moment.' . $this->_deadline_override_message());
1848                } else {
1849                    return Ht::xmsg("warning", 'The <a href="' . hoturl("deadlines") . '">submission deadline</a> has passed and the submission will not be reviewed.' . $this->deadlineSettingIs("sub_sub") . $this->_deadline_override_message());
1850                }
1851            } else {
1852                return Ht::xmsg("warning", 'The submission is not ready for review and can’t be changed further. It will not be reviewed.' . $this->_deadline_override_message());
1853            }
1854        } else if ($this->user->can_update_paper($prow)) {
1855            if ($this->mode === "edit")
1856                return Ht::xmsg("confirm", 'The submission is ready and will be considered for review. You do not need to take any further action. However, you can still make changes if you wish.' . $this->deadlineSettingIs("sub_update"));
1857        } else if ($this->conf->collectFinalPapers()
1858                   && $prow->outcome > 0
1859                   && $can_view_decision) {
1860            if ($this->user->can_submit_final_paper($prow)) {
1861                if (($t = $this->conf->message_html("finalsubmit", array("deadline" => $this->deadlineSettingIs("final_soft")))))
1862                    return Ht::xmsg("info", $t);
1863            } else if ($this->mode === "edit") {
1864                return Ht::xmsg("warning", "The deadline for updating final versions has passed. You can still change contact information." . $this->_deadline_override_message());
1865            }
1866        } else if ($this->mode === "edit") {
1867            $t = "";
1868            if ($this->user->can_withdraw_paper($prow))
1869                $t = " or withdraw it from consideration";
1870            return Ht::xmsg("info", "The submission is under review and can’t be changed, but you can change its contacts$t." . $this->_deadline_override_message());
1871        }
1872        return "";
1873    }
1874
1875    private function editMessage() {
1876        if (!($prow = $this->prow))
1877            return $this->_edit_message_new_paper();
1878
1879        $m = "";
1880        $has_author = $prow->has_author($this->user);
1881        $can_view_decision = $prow->outcome != 0 && $this->user->can_view_decision($prow);
1882        if ($has_author)
1883            $m .= $this->_edit_message_for_author($prow);
1884        else if ($this->conf->collectFinalPapers()
1885                 && $prow->outcome > 0 && !$prow->can_author_view_decision())
1886            $m .= Ht::xmsg("info", "The submission has been accepted, but its authors can’t see that yet. Once decisions are visible, the system will allow accepted authors to upload final versions.");
1887        else
1888            $m .= Ht::xmsg("info", "You aren’t a contact for this submission, but as an administrator you can still make changes.");
1889        if ($this->user->call_with_overrides(Contact::OVERRIDE_TIME, "can_update_paper", $prow)
1890            && ($v = $this->conf->message_html("submit")))
1891            $m .= Ht::xmsg("info", $v);
1892        if ($this->edit_status && $this->edit_status->has_problem()
1893            && ($this->edit_status->has_problem_at("contacts") || $this->editable))
1894            $m .= Ht::xmsg("warning", "There may be problems with this submission. Please scroll through the form and fix the problems if appropriate.");
1895        return $m;
1896    }
1897
1898    function _collectActionButtons() {
1899        $pid = $this->prow ? $this->prow->paperId : "new";
1900
1901        // Withdrawn papers can be revived
1902        if ($this->_prow->timeWithdrawn > 0) {
1903            $revivable = $this->conf->timeFinalizePaper($this->_prow);
1904            if ($revivable)
1905                $b = Ht::submit("revive", "Revive submission", ["class" => "btn btn-primary"]);
1906            else {
1907                $b = "The <a href='" . hoturl("deadlines") . "'>deadline</a> for reviving withdrawn submissions has passed. Are you sure you want to override it?";
1908                if ($this->admin)
1909                    $b = array(Ht::button("Revive submission", ["class" => "btn ui js-override-deadlines", "data-override-text" => $b, "data-override-submit" => "revive"]), "(admin only)");
1910            }
1911            return array($b);
1912        }
1913
1914        $buttons = array();
1915
1916        if ($this->mode === "edit") {
1917            // check whether we can save
1918            $old_overrides = $this->user->set_overrides(0);
1919            if ($this->canUploadFinal) {
1920                $updater = "submitfinal";
1921                $whyNot = $this->user->perm_submit_final_paper($this->prow);
1922            } else if ($this->prow) {
1923                $updater = "update";
1924                $whyNot = $this->user->perm_update_paper($this->prow);
1925            } else {
1926                $updater = "update";
1927                $whyNot = $this->user->perm_start_paper();
1928            }
1929            $this->user->set_overrides($old_overrides);
1930            // produce button
1931            $save_name = $this->is_ready() ? "Save and resubmit" : "Save draft";
1932            if (!$whyNot)
1933                $buttons[] = array(Ht::submit($updater, $save_name, ["class" => "btn btn-primary btn-savepaper"]), "");
1934            else if ($this->admin) {
1935                $revWhyNot = filter_whynot($whyNot, ["deadline", "rejected"]);
1936                $x = whyNotText($revWhyNot) . " Are you sure you want to override the deadline?";
1937                $buttons[] = array(Ht::button($save_name, ["class" => "btn btn-primary btn-savepaper ui js-override-deadlines", "data-override-text" => $x, "data-override-submit" => $updater]), "(admin only)");
1938            } else if (isset($whyNot["updateSubmitted"])
1939                       && $this->user->can_finalize_paper($this->_prow)) {
1940                $buttons[] = array(Ht::submit("update", $save_name, ["class" => "btn btn-savepaper"]));
1941            } else if ($this->prow) {
1942                $buttons[] = array(Ht::submit("updatecontacts", "Save contacts", ["class" => "btn"]), "");
1943            }
1944            if (!empty($buttons)) {
1945                $buttons[] = [Ht::submit("cancel", "Cancel", ["class" => "btn"])];
1946                $buttons[] = "";
1947            }
1948        }
1949
1950        // withdraw button
1951        if (!$this->prow
1952            || !$this->user->call_with_overrides(Contact::OVERRIDE_TIME, "can_withdraw_paper", $this->prow))
1953            $b = null;
1954        else if ($this->prow->timeSubmitted <= 0)
1955            $b = Ht::submit("withdraw", "Withdraw");
1956        else {
1957            $args = ["class" => "btn ui js-withdraw"];
1958            if ($this->user->can_withdraw_paper($this->prow))
1959                $args["data-withdrawable"] = "true";
1960            if (($this->admin && !$this->prow->has_author($this->user))
1961                || $this->conf->timeFinalizePaper($this->prow))
1962                $args["data-revivable"] = "true";
1963            $b = Ht::button("Withdraw", $args);
1964        }
1965        if ($b) {
1966            if (!$this->user->can_withdraw_paper($this->prow))
1967                $b = array($b, "(admin only)");
1968            $buttons[] = $b;
1969        }
1970
1971        return $buttons;
1972    }
1973
1974    function echoActions($top) {
1975        if ($this->admin && !$top) {
1976            $v = (string) $this->qreq->emailNote;
1977            echo '<div class="checki"><span class="checkc">', Ht::checkbox("doemail", 1, true, ["class" => "ignore-diff"]), " </span>",
1978                Ht::label("Email authors, including:"), "&nbsp; ",
1979                Ht::entry("emailNote", $v, ["id" => "emailNote", "size" => 30, "placeholder" => "Optional explanation", "class" => "ignore-diff js-autosubmit"]),
1980                "</div>\n";
1981        }
1982
1983        $buttons = $this->_collectActionButtons();
1984
1985        if ($this->admin && $this->prow)
1986            $buttons[] = array(Ht::button("Delete", ["class" => "btn ui js-delete-paper"]), "(admin only)");
1987
1988        echo Ht::actions($buttons, array("class" => "aab aabr aabig"));
1989    }
1990
1991
1992    // Functions for overall paper table viewing
1993
1994    function _papstrip() {
1995        $prow = $this->prow;
1996        if (($prow->managerContactId || ($this->user->privChair && $this->mode === "assign"))
1997            && $this->user->can_view_manager($prow))
1998            $this->papstripManager($this->user->privChair);
1999        $this->papstripTags();
2000        $this->npapstrip_tag_entry = 0;
2001        foreach ($this->conf->tags() as $ltag => $t)
2002            if ($this->user->can_change_tag($prow, "~$ltag", null, 0)) {
2003                if ($t->approval)
2004                    $this->papstripApproval($t->tag);
2005                else if ($t->vote)
2006                    $this->papstripVote($t->tag, $t->vote);
2007                else if ($t->rank)
2008                    $this->papstripRank($t->tag);
2009            }
2010        if ($this->npapstrip_tag_entry)
2011            echo "</div>";
2012        $this->papstripWatch();
2013        if ($this->user->can_view_conflicts($prow) && !$this->editable)
2014            $this->papstripPCConflicts();
2015        if ($this->user->allow_view_authors($prow) && !$this->editable)
2016            $this->papstripCollaborators();
2017
2018        $foldShepherd = $this->user->can_set_decision($prow) && $prow->outcome <= 0
2019            && $prow->shepherdContactId == 0 && $this->mode !== "assign";
2020        if ($this->user->can_set_decision($prow))
2021            $this->papstripOutcomeSelector();
2022        if ($this->user->can_view_lead($prow))
2023            $this->papstripLead($this->mode === "assign");
2024        if ($this->user->can_view_shepherd($prow))
2025            $this->papstripShepherd($this->mode === "assign", $foldShepherd);
2026
2027        if ($this->user->can_accept_review_assignment($prow)
2028            && $this->conf->timePCReviewPreferences()
2029            && ($this->user->roles & (Contact::ROLE_PC | Contact::ROLE_CHAIR)))
2030            $this->papstripReviewPreference();
2031        echo Ht::unstash_script("$(\".need-pcselector\").each(populate_pcselector)");
2032    }
2033
2034    function _paptabTabLink($text, $link, $image, $highlight) {
2035        return '<div class="' . ($highlight ? "papmodex" : "papmode")
2036            . '"><a href="' . $link . '" class="noul">'
2037            . Ht::img($image, "[$text]", "papmodeimg")
2038            . "&nbsp;<u" . ($highlight ? ' class="x"' : "") . ">" . $text
2039            . "</u></a></div>\n";
2040    }
2041
2042    private function _paptabBeginKnown() {
2043        $prow = $this->prow;
2044
2045        // what actions are supported?
2046        $canEdit = $this->user->can_edit_paper($prow);
2047        $canReview = $this->user->can_review($prow, null);
2048        $canAssign = $this->admin;
2049        $canHome = ($canEdit || $canAssign || $this->mode === "contact");
2050
2051        $t = "";
2052
2053        // paper tabs
2054        if ($canEdit || $canReview || $canAssign || $canHome) {
2055            $t .= '<div class="submission_modes">';
2056
2057            // home link
2058            $highlight = ($this->mode !== "assign" && $this->mode !== "edit"
2059                          && $this->mode !== "contact" && $this->mode !== "re");
2060            $a = ""; // ($this->mode === "edit" || $this->mode === "re" ? "&amp;m=p" : "");
2061            $t .= $this->_paptabTabLink("Main", hoturl("paper", "p=$prow->paperId$a"), "view48.png", $highlight);
2062
2063            if ($canEdit)
2064                $t .= $this->_paptabTabLink("Edit", hoturl("paper", "p=$prow->paperId&amp;m=edit"), "edit48.png", $this->mode === "edit");
2065
2066            if ($canReview)
2067                $t .= $this->_paptabTabLink("Review", hoturl("review", "p=$prow->paperId&amp;m=re"), "review48.png", $this->mode === "re" && (!$this->editrrow || $this->editrrow->contactId == $this->user->contactId));
2068
2069            if ($canAssign)
2070                $t .= $this->_paptabTabLink("Assign", hoturl("assign", "p=$prow->paperId"), "assign48.png", $this->mode === "assign");
2071
2072            $t .= "</div>";
2073        }
2074
2075        return $t;
2076    }
2077
2078    static private function _echo_clickthrough($ctype) {
2079        global $Conf, $Now;
2080        $data = $Conf->message_html("clickthrough_$ctype");
2081        echo Ht::form(["class" => "ui"]), '<div>', $data;
2082        $buttons = [Ht::submit("Agree", ["class" => "btn btnbig btn-highlight ui js-clickthrough"])];
2083        echo Ht::hidden("clickthrough_type", $ctype),
2084            Ht::hidden("clickthrough_id", sha1($data)),
2085            Ht::hidden("clickthrough_time", $Now),
2086            Ht::actions($buttons, ["class" => "aab aabig aabr"]), "</div></form>";
2087    }
2088
2089    static function echo_review_clickthrough() {
2090        echo '<div class="revcard js-clickthrough-terms"><div class="revcard_head"><h3>Reviewing terms</h3></div><div class="revcard_body">', Ht::xmsg("error", "You must agree to these terms before you can save reviews.");
2091        self::_echo_clickthrough("review");
2092        echo "</form></div></div>";
2093    }
2094
2095    private function _echo_editable_form() {
2096        $form_js = ["id" => "paperform"];
2097        if ($this->prow && $this->prow->timeSubmitted > 0)
2098            $form_js["data-submitted"] = $this->prow->timeSubmitted;
2099        if ($this->prow && !$this->editable)
2100            $form_js["data-contacts-only"] = 1;
2101        if ($this->useRequest)
2102            $form_js["class"] = "alert";
2103        echo Ht::form(hoturl_post("paper", "p=" . ($this->prow ? $this->prow->paperId : "new") . "&amp;m=edit"), $form_js);
2104        Ht::stash_script('$("#paperform").on("change", ".js-check-submittable", handle_ui)');
2105        if ($this->prow
2106            && $this->prow->paperStorageId > 1
2107            && $this->prow->timeSubmitted > 0
2108            && !$this->conf->setting("sub_freeze"))
2109            Ht::stash_script('$("#paperform").on("submit", edit_paper_ui)');
2110        Ht::stash_script('$(function(){$("#paperform input[name=paperUpload]").trigger("change")})');
2111    }
2112
2113    private function make_echo_editable_option($o) {
2114        return (object) [
2115            "position" => $o->form_position(),
2116            "option" => $o,
2117            "callback" => function () use ($o) {
2118                if ($o->edit_condition()
2119                    && !$o->compile_edit_condition($this->_prow))
2120                    return;
2121                $ov = $this->_prow->option($o->id);
2122                $ov = $ov ? : new PaperOptionValue($this->prow, $o);
2123                $o->echo_editable_html($ov, $this->useRequest ? $this->qreq[$o->formid] : null, $this);
2124            }
2125        ];
2126    }
2127
2128    private function _echo_editable_body() {
2129        global $ConfSitePATH;
2130        $this->_echo_editable_form();
2131        echo '<div>';
2132
2133        if (($m = $this->editMessage()))
2134            echo $m, '<div class="g"></div>';
2135        if ($this->quit) {
2136            echo "</div></form>";
2137            return;
2138        }
2139
2140        $this->echoActions(true);
2141
2142        $ofields = [];
2143        foreach ($this->conf->paper_opts->option_list_type(!$this->canUploadFinal) as $opt)
2144            if ((!$this->prow || get($this->view_options, $opt->id))
2145                && !$opt->internal)
2146                $ofields[] = $this->make_echo_editable_option($opt);
2147        $gxt = new GroupedExtensions($this->user, ["etc/submissioneditgroups.json"], $this->conf->opt("submissionEditGroups"), $ofields);
2148        $this->edit_fields = array_values($gxt->groups());
2149        for ($this->edit_fields_position = 0;
2150             $this->edit_fields_position < count($this->edit_fields);
2151             ++$this->edit_fields_position) {
2152            $uf = $this->edit_fields[$this->edit_fields_position];
2153            $cb = get($uf, "callback");
2154            if ($cb instanceof Closure)
2155                call_user_func($cb, $uf);
2156            else if (is_string($cb) && str_starts_with($cb, "*"))
2157                call_user_func([$this, substr($cb, 1)], $uf);
2158            else if ($cb)
2159                call_user_func($cb, $this, $uf);
2160        }
2161
2162        // Submit button
2163        $this->echo_editable_complete();
2164        $this->echoActions(false);
2165
2166        echo "</div></form>";
2167    }
2168
2169    function paptabBegin() {
2170        $prow = $this->prow;
2171
2172        if ($prow)
2173            $this->_papstrip();
2174        if ($this->npapstrip)
2175            echo "</div></div></div></div>\n<div class=\"papcard\">";
2176        else
2177            echo '<div class="pedcard">';
2178        if ($this->editable)
2179            echo '<div class="pedcard_body">';
2180        else
2181            echo '<div class="papcard_body">';
2182
2183        $this->echoDivEnter();
2184        if ($this->editable) {
2185            if (!$this->user->can_clickthrough("submit")) {
2186                echo '<div class="js-clickthrough-container">',
2187                    '<div class="js-clickthrough-terms">',
2188                    '<h3>Submission terms</h3>',
2189                    Ht::xmsg("error", "You must agree to these terms to register a submission.");
2190                self::_echo_clickthrough("submit");
2191                echo '</div><div class="js-clickthrough-body hidden">';
2192                $this->_echo_editable_body();
2193                echo '</div></div>';
2194            } else
2195                $this->_echo_editable_body();
2196        } else {
2197            if ($this->mode === "edit" && ($m = $this->editMessage()))
2198                echo $m, "<div class='g'></div>\n";
2199            $status_info = $this->user->paper_status_info($this->prow);
2200            echo '<p class="xd"><span class="pstat ', $status_info[0], '">',
2201                htmlspecialchars($status_info[1]), "</span></p>";
2202            $this->paptabDownload();
2203            echo '<div class="paperinfo"><div class="paperinfo-row">';
2204            $has_abstract = $this->paptabAbstract();
2205            echo '<div class="paperinfo-c', ($has_abstract ? "r" : "b"), '">';
2206            $this->paptabAuthors(!$this->editable && $this->mode === "edit"
2207                                 && $prow->timeSubmitted > 0);
2208            $this->paptabTopicsOptions();
2209            echo '</div></div></div>';
2210        }
2211        $this->echoDivExit();
2212
2213        if (!$this->editable && $this->mode === "edit") {
2214            $this->_echo_editable_form();
2215            $this->echo_editable_contact_author();
2216            $this->echoActions(false);
2217            echo "</form>";
2218        } else if (!$this->editable && $this->user->act_author_view($prow)
2219                   && !$this->user->contactId) {
2220            echo '<hr class="papcard_sep" />',
2221                "To edit this submission, <a href=\"", hoturl("index"), "\">sign in using your email and password</a>.";
2222        }
2223
2224        Ht::stash_script("shortcut().add()");
2225        if ($this->editable || $this->mode === "edit")
2226            Ht::stash_script('hiliter_children("#paperform", true)');
2227    }
2228
2229    private function _paptabSepContaining($t) {
2230        if ($t !== "")
2231            echo '<hr class="papcard_sep" />', $t;
2232    }
2233
2234    function _paptabReviewLinks($rtable, $editrrow, $ifempty) {
2235        require_once("reviewtable.php");
2236
2237        $t = "";
2238        if ($rtable)
2239            $t .= reviewTable($this->prow, $this->all_rrows, $this->mycrows,
2240                              $editrrow, $this->mode);
2241        $t .= reviewLinks($this->prow, $this->all_rrows, $this->mycrows,
2242                          $editrrow, $this->mode, $this->allreviewslink);
2243        if (($empty = ($t === "")))
2244            $t = $ifempty;
2245        if ($t)
2246            echo '<hr class="papcard_sep" />';
2247        echo $t, "</div></div>\n";
2248        return $empty;
2249    }
2250
2251    function _privilegeMessage() {
2252        $a = "<a href=\"" . SelfHref::make($this->qreq, ["forceShow" => 0]) . "\">";
2253        return $a . Ht::img("override24.png", "[Override]", "dlimg")
2254            . "</a>&nbsp;You have used administrator privileges to view and edit reviews for this submission. (" . $a . "Unprivileged view</a>)";
2255    }
2256
2257    private function include_comments() {
2258        return !$this->allreviewslink
2259            && (!empty($this->mycrows)
2260                || $this->user->can_comment($this->prow, null)
2261                || $this->conf->any_response_open);
2262    }
2263
2264    function paptabEndWithReviewsAndComments() {
2265        $prow = $this->prow;
2266
2267        if ($this->user->is_admin_force()
2268            && !$this->user->can_view_review($prow, null, false))
2269            $this->_paptabSepContaining($this->_privilegeMessage());
2270        else if ($this->user->contactId == $prow->managerContactId
2271                 && !$this->user->privChair
2272                 && $this->user->contactId > 0)
2273            $this->_paptabSepContaining("You are this submission’s administrator.");
2274
2275        $empty = $this->_paptabReviewLinks(true, null, "<div class='hint'>There are no reviews or comments for you to view.</div>");
2276        if ($empty)
2277            return;
2278
2279        // text format link
2280        $viewable = array();
2281        foreach ($this->viewable_rrows as $rr)
2282            if ($rr->reviewModified > 1) {
2283                $viewable[] = "reviews";
2284                break;
2285            }
2286        foreach ($this->crows as $cr)
2287            if ($this->user->can_view_comment($prow, $cr)) {
2288                $viewable[] = "comments";
2289                break;
2290            }
2291        if (count($viewable))
2292            echo '<div class="notecard"><div class="notecard_body">',
2293                "<a href='", hoturl("review", "p=$prow->paperId&amp;m=r&amp;text=1"), "' class='xx'>",
2294                Ht::img("txt24.png", "[Text]", "dlimg"),
2295                "&nbsp;<u>", ucfirst(join(" and ", $viewable)),
2296                " in plain text</u></a></div></div>\n";
2297
2298        $this->render_rc($this->reviews_and_comments());
2299    }
2300
2301    function reviews_and_comments() {
2302        $a = [];
2303        foreach ($this->viewable_rrows as $rrow)
2304            if ($rrow->reviewSubmitted || $rrow->reviewModified > 1)
2305                $a[] = $rrow;
2306        if ($this->include_comments())
2307            $a = array_merge($a, $this->mycrows ? : []);
2308        usort($a, "PaperInfo::review_or_comment_compare");
2309        return $a;
2310    }
2311
2312    private function has_response($respround) {
2313        foreach ($this->mycrows as $cr)
2314            if (($cr->commentType & COMMENTTYPE_RESPONSE)
2315                && $cr->commentRound == $respround)
2316                return true;
2317        return false;
2318    }
2319
2320    private function render_rc($rcs) {
2321        $s = "";
2322        $ncmt = 0;
2323        $rf = $this->conf->review_form();
2324        foreach ($rcs as $rc)
2325            if (isset($rc->reviewId)) {
2326                $rcj = $rf->unparse_review_json($this->prow, $rc, $this->user);
2327                $s .= "review_form.add_review(" . json_encode_browser($rcj) . ");\n";
2328            } else {
2329                ++$ncmt;
2330                $rcj = $rc->unparse_json($this->user);
2331                $s .= "papercomment.add(" . json_encode_browser($rcj) . ");\n";
2332            }
2333
2334        if ($this->include_comments()) {
2335            $cs = [];
2336            if ($this->user->can_comment($this->prow, null)) {
2337                $ct = $this->prow->has_author($this->user) ? COMMENTTYPE_BYAUTHOR : 0;
2338                $cs[] = new CommentInfo((object) ["commentType" => $ct], $this->prow);
2339            }
2340            if ($this->admin || $this->prow->has_author($this->user)) {
2341                foreach ($this->conf->resp_rounds() as $rrd)
2342                    if (!$this->has_response($rrd->number)) {
2343                        $crow = CommentInfo::make_response_template($rrd->number, $this->prow);
2344                        if ($this->user->can_respond($this->prow, $crow))
2345                            $cs[] = $crow;
2346                    }
2347            }
2348            foreach ($cs as $c) {
2349                ++$ncmt;
2350                $s .= "papercomment.add(" . json_encode_browser($c->unparse_json($this->user)) . ");\n";
2351            }
2352        }
2353
2354        if ($ncmt)
2355            CommentInfo::echo_script($this->prow);
2356        echo Ht::unstash_script($s);
2357    }
2358
2359    function paptabComments() {
2360        if ($this->include_comments())
2361            $this->render_rc($this->mycrows);
2362    }
2363
2364    function paptabEndWithReviewMessage() {
2365        if ($this->editable) {
2366            echo "</div></div>\n";
2367            return;
2368        }
2369
2370        $m = array();
2371        if ($this->all_rrows
2372            && ($whyNot = $this->user->perm_view_review($this->prow, null)))
2373            $m[] = "You can’t see the reviews for this submission. " . whyNotText($whyNot);
2374        if ($this->prow
2375            && !$this->conf->time_review_open()
2376            && $this->prow->review_type($this->user)) {
2377            if ($this->rrow)
2378                $m[] = "You can’t edit your review because the site is not open for reviewing.";
2379            else
2380                $m[] = "You can’t begin your assigned review because the site is not open for reviewing.";
2381        }
2382        if (count($m))
2383            $this->_paptabSepContaining(join("<br />", $m));
2384
2385        $this->_paptabReviewLinks(false, null, "");
2386    }
2387
2388    function paptabEndWithEditableReview() {
2389        $prow = $this->prow;
2390        $act_pc = $this->user->act_pc($prow);
2391
2392        // review messages
2393        $whyNot = $this->user->perm_view_review($prow, null, false);
2394        $msgs = array();
2395        if (!$this->rrow && !$this->prow->review_type($this->user))
2396            $msgs[] = "You haven’t been assigned to review this submission, but you can review it anyway.";
2397        if ($whyNot && $this->user->is_admin_force()) {
2398            $msgs[] = $this->_privilegeMessage();
2399        } else if ($whyNot && isset($whyNot["reviewNotComplete"])
2400                   && ($this->user->isPC || $this->conf->setting("extrev_view"))) {
2401            $nother = 0;
2402            $myrrow = null;
2403            foreach ($this->all_rrows as $rrow)
2404                if ($this->user->is_my_review($rrow))
2405                    $myrrow = $rrow;
2406                else if ($rrow->reviewSubmitted)
2407                    ++$nother;
2408            if ($nother > 0) {
2409                if ($myrrow && $myrrow->timeApprovalRequested > 0)
2410                    $msgs[] = $this->conf->_("You’ll be able to see %d other reviews once yours is approved.", $nother);
2411                else
2412                    $msgs[] = $this->conf->_("You’ll be able to see %d other reviews once you complete your own.", $nother);
2413            }
2414        }
2415        if (count($msgs) > 0)
2416            $this->_paptabSepContaining(join("<br />\n", $msgs));
2417
2418        // links
2419        $this->_paptabReviewLinks(true, $this->editrrow, "");
2420
2421        // review form, possibly with deadline warning
2422        $opt = array("edit" => $this->mode === "re");
2423
2424        if ($this->editrrow
2425            && ($this->user->is_owned_review($this->editrrow) || $this->admin)
2426            && !$this->conf->time_review($this->editrrow, $act_pc, true)) {
2427            if ($this->admin)
2428                $override = " As an administrator, you can override this deadline.";
2429            else {
2430                $override = "";
2431                if ($this->editrrow->reviewSubmitted)
2432                    $opt["edit"] = false;
2433            }
2434            if ($this->conf->time_review_open())
2435                $opt["editmessage"] = "The <a href='" . hoturl("deadlines") . "'>review deadline</a> has passed, so the review can no longer be changed.$override";
2436            else
2437                $opt["editmessage"] = "The site is not open for reviewing, so the review cannot be changed.$override";
2438        } else if (!$this->user->can_review($prow, $this->editrrow))
2439            $opt["edit"] = false;
2440
2441        // maybe clickthrough
2442        $need_clickthrough = $opt["edit"] && !$this->user->can_clickthrough("review");
2443        $rf = $this->conf->review_form();
2444        if ($need_clickthrough) {
2445            echo '<div class="js-clickthrough-container">';
2446            self::echo_review_clickthrough();
2447            echo '<div class="js-clickthrough-body">';
2448        }
2449        $rf->show($prow, $this->editrrow, $opt, $this->review_values);
2450        if ($need_clickthrough)
2451            echo '</div></div>';
2452    }
2453
2454
2455    // Functions for loading papers
2456
2457    static private function _maybeSearchPaperId($qreq) {
2458        global $Conf, $Me, $Now;
2459
2460        // if a number, don't search
2461        if ((string) $qreq->paperId !== "") {
2462            if (ctype_digit($qreq->paperId) && $qreq->paperId[0] !== "0")
2463                return false;
2464            if (preg_match('/^\s*#?([1-9]\d*)\s*$/s', $qreq->paperId, $m)) {
2465                $qreq->paperId = $m[1];
2466                return false;
2467            }
2468        }
2469
2470        // if a complex request, or a form upload, don't search
2471        foreach ($qreq->keys() as $k)
2472            if (!in_array($k, ["p", "paperId", "m", "mode", "forceShow", "go", "actas", "t"]))
2473                return false;
2474
2475        // if no paper ID set, find one
2476        if (!isset($qreq->paperId)) {
2477            $q = "select min(Paper.paperId) from Paper ";
2478            if ($Me->isPC)
2479                $q .= "where timeSubmitted>0";
2480            else if ($Me->has_review())
2481                $q .= "join PaperReview on (PaperReview.paperId=Paper.paperId and PaperReview.contactId=$Me->contactId)";
2482            else
2483                $q .= "join ContactInfo on (ContactInfo.paperId=Paper.paperId and ContactInfo.contactId=$Me->contactId and ContactInfo.conflictType>=" . CONFLICT_AUTHOR . ")";
2484            $result = $Conf->q_raw($q);
2485            if (($paperId = edb_row($result)))
2486                $qreq->paperId = $paperId[0];
2487            return false;
2488        }
2489
2490        // if invalid contact, don't search
2491        if ($Me->is_empty())
2492            return false;
2493
2494        // actually try to search
2495        if ($qreq->paperId === "(All)")
2496            $qreq->paperId = "";
2497        $search = new PaperSearch($Me, ["q" => $qreq->paperId, "t" => $qreq->get("t", 0)]);
2498        $ps = $search->paper_ids();
2499        if (count($ps) == 1) {
2500            $list = $search->session_list_object();
2501            $qreq->paperId = $qreq->p = $list->ids[0];
2502            // DISABLED: check if the paper is in the current list
2503            unset($qreq->ls);
2504            $list->set_cookie();
2505            // ensure URI makes sense ("paper/2" not "paper/searchterm")
2506            SelfHref::redirect($qreq);
2507            return true;
2508        } else {
2509            $t = $qreq->t ? "&t=" . urlencode($qreq->t) : "";
2510            go(hoturl("search", "q=" . urlencode($qreq->paperId) . $t));
2511            exit;
2512        }
2513    }
2514
2515    static function clean_request(Qrequest $qreq) {
2516        if (!isset($qreq->paperId) && isset($qreq->p))
2517            $qreq->paperId = $qreq->p;
2518        if (!isset($qreq->reviewId) && isset($qreq->r))
2519            $qreq->reviewId = $qreq->r;
2520        if (!isset($qreq->commentId) && isset($qreq->c))
2521            $qreq->commentId = $qreq->c;
2522        if (!isset($qreq->reviewId)
2523            && preg_match(',\A/\d+[A-Z]+\z,i', Navigation::path()))
2524            $qreq->reviewId = substr(Navigation::path(), 1);
2525        else if (!isset($qreq->paperId) && ($pc = Navigation::path_component(0)))
2526            $qreq->paperId = $pc;
2527        if (!isset($qreq->paperId) && isset($qreq->reviewId)
2528            && preg_match('/^(\d+)[A-Z]+$/i', $qreq->reviewId, $m))
2529            $qreq->paperId = $m[1];
2530    }
2531
2532    static function paperRow(Qrequest $qreq, &$whyNot) {
2533        global $Conf, $Me;
2534
2535        self::clean_request($qreq);
2536        if (isset($qreq->paperId) && $qreq->paperId === "new")
2537            return null;
2538
2539        $sel = array();
2540        if (isset($qreq->paperId)
2541            || (!isset($qreq->reviewId) && !isset($qreq->commentId))) {
2542            self::_maybeSearchPaperId($qreq);
2543            $sel["paperId"] = $qreq->get("paperId", 1);
2544        } else if (isset($qreq->reviewId))
2545            $sel["reviewId"] = $qreq->reviewId;
2546        else if (isset($qreq->commentId))
2547            $sel["commentId"] = $qreq->commentId;
2548
2549        $sel["topics"] = $sel["options"] = true;
2550        if (($Me->isPC && $Conf->timePCReviewPreferences()) || $Me->privChair)
2551            $sel["reviewerPreference"] = true;
2552        if ($Me->isPC || $Conf->setting("tag_rank"))
2553            $sel["tags"] = true;
2554
2555        if (!($prow = $Conf->paperRow($sel, $Me, $whyNot)))
2556            return null;
2557        $rrow = null;
2558        if (isset($sel["reviewId"]))
2559            $rrow = $prow->review_of_id($sel["reviewId"]);
2560        if (($whyNot = $Me->perm_view_paper($prow))
2561            || (!isset($qreq->paperId)
2562                && !$Me->can_view_review($prow, $rrow)
2563                && !$Me->privChair)) {
2564            // Don't allow querier to probe review/comment<->paper mapping
2565            if (!isset($qreq->paperId))
2566                $whyNot = array("invalidId" => "paper");
2567            return null;
2568        }
2569        if (!isset($qreq->paperId))
2570            $qreq->paperId = $prow->paperId;
2571        return $prow;
2572    }
2573
2574    function resolveReview($want_review) {
2575        $this->prow->ensure_full_reviews();
2576        $this->all_rrows = $this->prow->reviews_by_display();
2577
2578        $this->viewable_rrows = array();
2579        $round_mask = 0;
2580        $min_view_score = VIEWSCORE_MAX;
2581        foreach ($this->all_rrows as $rrow)
2582            if ($this->user->can_view_review($this->prow, $rrow)) {
2583                $this->viewable_rrows[] = $rrow;
2584                if ($rrow->reviewRound !== null)
2585                    $round_mask |= 1 << (int) $rrow->reviewRound;
2586                $min_view_score = min($min_view_score, $this->user->view_score_bound($this->prow, $rrow));
2587            }
2588        $rf = $this->conf->review_form();
2589        Ht::stash_script("review_form.set_form(" . json_encode_browser($rf->unparse_json($round_mask, $min_view_score)) . ")");
2590
2591        $rrid = strtoupper((string) $this->qreq->reviewId);
2592        while ($rrid !== "" && $rrid[0] === "0")
2593            $rrid = substr($rrid, 1);
2594
2595        $this->rrow = $myrrow = $approvable_rrow = null;
2596        foreach ($this->viewable_rrows as $rrow) {
2597            if ($rrid !== ""
2598                && (strcmp($rrow->reviewId, $rrid) == 0
2599                    || ($rrow->reviewOrdinal
2600                        && strcmp($rrow->paperId . unparseReviewOrdinal($rrow->reviewOrdinal), $rrid) == 0)))
2601                $this->rrow = $rrow;
2602            if ($rrow->contactId == $this->user->contactId
2603                || (!$myrrow && $this->user->is_my_review($rrow)))
2604                $myrrow = $rrow;
2605            if ($rrow->requestedBy == $this->user->contactId
2606                && !$rrow->reviewSubmitted
2607                && $rrow->timeApprovalRequested)
2608                $approvable_rrow = $rrow;
2609        }
2610
2611        if ($this->rrow)
2612            $this->editrrow = $this->rrow;
2613        else if (!$approvable_rrow || ($myrrow && $myrrow->reviewModified && !$this->prefer_approvable))
2614            $this->editrrow = $myrrow;
2615        else
2616            $this->editrrow = $approvable_rrow;
2617
2618        if ($want_review && $this->user->can_review($this->prow, $this->editrrow, false))
2619            $this->mode = "re";
2620    }
2621
2622    function resolveComments() {
2623        $this->crows = $this->mycrows = array();
2624        if ($this->prow) {
2625            $this->crows = $this->prow->all_comments();
2626            $this->mycrows = $this->prow->viewable_comments($this->user);
2627        }
2628    }
2629
2630    function all_reviews() {
2631        return $this->all_rrows;
2632    }
2633
2634    function viewable_comments() {
2635        return $this->mycrows;
2636    }
2637
2638    function fixReviewMode() {
2639        $prow = $this->prow;
2640        if ($this->mode === "re" && $this->rrow
2641            && !$this->user->can_review($prow, $this->rrow, false)
2642            && ($this->rrow->contactId != $this->user->contactId
2643                || $this->rrow->reviewSubmitted))
2644            $this->mode = "p";
2645        if ($this->mode === "p" && $this->rrow
2646            && !$this->user->can_view_review($prow, $this->rrow))
2647            $this->rrow = $this->editrrow = null;
2648        if ($this->mode === "p" && !$this->rrow && !$this->editrrow
2649            && $this->user->can_review($prow, null, false)) {
2650            $viewable_rrow = $my_rrow = null;
2651            foreach ($this->all_rrows as $rrow) {
2652                if ($this->user->can_view_review($prow, $rrow))
2653                    $viewable_rrow = $rrow;
2654                if ($rrow->contactId == $this->user->contactId
2655                    || (!$my_rrow && $this->user->is_my_review($rrow)))
2656                    $my_rrow = $rrow;
2657            }
2658            if (!$viewable_rrow) {
2659                $this->mode = "re";
2660                $this->editrrow = $my_rrow;
2661            }
2662        }
2663        if ($this->mode === "p" && $prow && empty($this->viewable_rrows)
2664            && empty($this->mycrows)
2665            && $prow->has_author($this->user)
2666            && !$this->allow_admin
2667            && ($this->conf->timeFinalizePaper($prow) || $prow->timeSubmitted <= 0))
2668            $this->mode = "edit";
2669    }
2670}
2671