1<?php
2// paperlist.php -- HotCRP helper class for producing paper lists
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class PaperListTableRender {
6    public $table_start;
7    public $thead;
8    public $tbody_class;
9    public $body_rows;
10    public $tfoot;
11    public $table_end;
12    public $error;
13
14    public $ncol;
15    public $titlecol;
16
17    public $colorindex = 0;
18    public $hascolors = false;
19    public $skipcallout;
20    public $last_trclass = "";
21    public $groupstart = [0];
22
23    function __construct($ncol, $titlecol, $skipcallout) {
24        $this->ncol = $ncol;
25        $this->titlecol = $titlecol;
26        $this->skipcallout = $skipcallout;
27    }
28    static function make_error($error) {
29        $tr = new PaperListTableRender(0, 0, 0);
30        $tr->error = $error;
31        return $tr;
32    }
33    function tbody_start() {
34        return "  <tbody class=\"{$this->tbody_class}\">\n";
35    }
36    function heading_row($heading, $attr = []) {
37        if (!$heading) {
38            return "  <tr class=\"plheading\"><td class=\"plheading-blank\" colspan=\"{$this->ncol}\"></td></tr>\n";
39        } else {
40            $x = "  <tr class=\"plheading\"";
41            foreach ($attr as $k => $v)
42                if ($k !== "no_titlecol" && $k !== "tdclass")
43                    $x .= " $k=\"" . str_replace("\"", "&quot;", $v) . "\"";
44            $x .= ">";
45            $titlecol = get($attr, "no_titlecol") ? 0 : $this->titlecol;
46            if ($titlecol)
47                $x .= "<td class=\"plheading-spacer\" colspan=\"{$titlecol}\"></td>";
48            $tdclass = get($attr, "tdclass");
49            $x .= "<td class=\"plheading" . ($tdclass ? " $tdclass" : "") . "\" colspan=\"" . ($this->ncol - $titlecol) . "\">";
50            return $x . $heading . "</td></tr>\n";
51        }
52    }
53    function heading_separator_row() {
54        return "  <tr class=\"plheading\"><td class=\"plheading-separator\" colspan=\"{$this->ncol}\"></td></tr>\n";
55    }
56}
57
58class PaperListReviewAnalysis {
59    private $prow;
60    public $rrow = null;
61    public $round = "";
62    function __construct($rrow, PaperInfo $prow) {
63        $this->prow = $prow;
64        if ($rrow->reviewId) {
65            $this->rrow = $rrow;
66            if ($rrow->reviewRound)
67                $this->round = htmlspecialchars($prow->conf->round_name($rrow->reviewRound));
68        }
69    }
70    function icon_html($includeLink) {
71        $rrow = $this->rrow;
72        if (($title = get(ReviewForm::$revtype_names, $rrow->reviewType)))
73            $title .= " review";
74        else
75            $title = "Review";
76        if (!$rrow->reviewSubmitted)
77            $title .= " (" . $this->description_text() . ")";
78        $t = review_type_icon($rrow->reviewType, !$rrow->reviewSubmitted, $title);
79        if ($includeLink)
80            $t = $this->wrap_link($t);
81        if ($this->round)
82            $t .= '<span class="revround" title="Review round">&nbsp;' . $this->round . "</span>";
83        return $t;
84    }
85    function icon_text() {
86        $x = "";
87        if ($this->rrow->reviewType)
88            $x = get_s(ReviewForm::$revtype_names, $this->rrow->reviewType);
89        if ($x !== "" && $this->round)
90            $x .= ":" . $this->round;
91        return $x;
92    }
93    function description_text() {
94        if (!$this->rrow)
95            return "";
96        else if ($this->rrow->reviewSubmitted)
97            return "complete";
98        else if ($this->rrow->reviewType == REVIEW_SECONDARY
99                 && $this->rrow->reviewNeedsSubmit <= 0)
100            return "delegated";
101        else if ($this->rrow->reviewType == REVIEW_EXTERNAL
102                 && $this->rrow->timeApprovalRequested)
103            return "awaiting approval";
104        else if ($this->rrow->reviewModified > 1)
105            return "in progress";
106        else if ($this->rrow->reviewModified > 0)
107            return "accepted";
108        else
109            return "not started";
110    }
111    function wrap_link($html, $klass = null) {
112        if (!$this->rrow)
113            return $html;
114        if (!$this->rrow->reviewSubmitted)
115            $href = $this->prow->conf->hoturl("review", "r=" . unparseReviewOrdinal($this->rrow));
116        else
117            $href = $this->prow->conf->hoturl("paper", "p=" . $this->rrow->paperId . "#r" . unparseReviewOrdinal($this->rrow));
118        $t = $klass ? "<a class=\"$klass\"" : "<a";
119        return $t . ' href="' . $href . '">' . $html . '</a>';
120    }
121}
122
123class PaperList {
124    public $conf;
125    public $user;
126    public $qreq;
127    public $search;
128    private $_reviewer_user;
129
130    private $sortable;
131    private $foldable;
132    private $_unfold_all = false;
133    private $_paper_link_page;
134    private $_paper_link_mode;
135    private $_view_columns = false;
136    private $_view_compact_columns = false;
137    private $_view_row_numbers = false;
138    private $_view_statistics = false;
139    private $_view_force = false;
140    private $_view_fields = [];
141    private $atab;
142
143    private $_table_id;
144    private $_table_class;
145    private $report_id;
146    private $_row_id_pattern;
147    private $_selection;
148
149    private $_rowset;
150    private $_row_filter;
151    private $_columns_by_name;
152    private $_column_errors_by_name = [];
153
154    private $_header_script = "";
155    private $_header_script_map = [];
156
157    // columns access
158    public $qopts; // set by PaperColumn::prepare
159    public $sorters = [];
160    public $tagger;
161    public $need_tag_attr;
162    public $table_attr;
163    public $row_attr;
164    public $row_overridable;
165    public $row_tags;
166    public $row_tags_overridable;
167    public $need_render;
168    public $has_editable_tags = false;
169    public $check_format;
170
171    // collected during render and exported to caller
172    public $count; // also exported to columns access: 1 more than row index
173    public $ids;
174    public $groups;
175    public $any;
176    private $_has;
177    public $error_html = array();
178
179    static public $include_stash = true;
180
181    static private $stats = [ScoreInfo::SUM, ScoreInfo::MEAN, ScoreInfo::MEDIAN, ScoreInfo::STDDEV_P];
182
183    function __construct(PaperSearch $search, $args = array(), $qreq = null) {
184        $this->search = $search;
185        $this->conf = $this->search->conf;
186        $this->user = $this->search->user;
187        if (!$qreq || !($qreq instanceof Qrequest))
188            $qreq = new Qrequest("GET", $qreq);
189        $this->qreq = $qreq;
190        $this->_reviewer_user = $search->reviewer_user();
191
192        $this->sortable = isset($args["sort"]) && $args["sort"];
193        $this->foldable = $this->sortable || !!get($args, "foldable")
194            || $this->user->is_manager() /* “Override conflicts” fold */;
195
196        $this->_paper_link_page = "";
197        if ($qreq->linkto === "paper" || $qreq->linkto === "review" || $qreq->linkto === "assign")
198            $this->_paper_link_page = $qreq->linkto;
199        else if ($qreq->linkto === "paperedit") {
200            $this->_paper_link_page = "paper";
201            $this->_paper_link_mode = "edit";
202        }
203        $this->atab = $qreq->atab;
204
205        $this->tagger = new Tagger($this->user);
206
207        $this->qopts = $this->search->simple_search_options();
208        if ($this->qopts === false)
209            $this->qopts = ["paperId" => $this->search->paper_ids()];
210        $this->qopts["scores"] = [];
211        // NB that actually processed the search, setting PaperSearch::viewmap
212
213        foreach ($this->search->sorters ? : [] as $sorter)
214            ListSorter::push($this->sorters, $sorter);
215        if ($this->sortable && is_string($args["sort"]))
216            array_unshift($this->sorters, PaperSearch::parse_sorter($args["sort"]));
217        else if ($this->sortable && $qreq->sort)
218            array_unshift($this->sorters, PaperSearch::parse_sorter($qreq->sort));
219
220        if (($report = get($args, "report"))) {
221            $display = null;
222            if (!get($args, "no_session_display"))
223                $display = $this->conf->session("{$report}display", null);
224            if ($display === null)
225                $display = $this->conf->setting_data("{$report}display_default", null);
226            if ($display === null && $report === "pl")
227                $display = $this->conf->review_form()->default_display();
228            $this->set_view_display($display);
229        }
230        if (is_string(get($args, "display")))
231            $this->set_view_display($args["display"]);
232        foreach ($this->search->viewmap ? : [] as $k => $v)
233            $this->set_view($k, $v);
234        if ($this->conf->submission_blindness() != Conf::BLIND_OPTIONAL
235            && get($this->_view_fields, "au")
236            && get($this->_view_fields, "anonau") === null)
237            $this->_view_fields["anonau"] = true;
238
239        $this->_columns_by_name = ["anonau" => [], "aufull" => [], "rownum" => [], "statistics" => []];
240
241        $this->_rowset = get($args, "rowset");
242    }
243
244    function __get($name) {
245        error_log("PaperList::$name " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
246        return $name === "contact" ? $this->user : null;
247    }
248
249    function table_id() {
250        return $this->_table_id;
251    }
252    function set_table_id_class($table_id, $table_class, $row_id_pattern = null) {
253        $this->_table_id = $table_id;
254        $this->_table_class = $table_class;
255        $this->_row_id_pattern = $row_id_pattern;
256    }
257
258    function report_id() {
259        return $this->report_id;
260    }
261    function set_report($report) {
262        $this->report_id = $report;
263    }
264
265    function set_row_filter($filter) {
266        $this->_row_filter = $filter;
267    }
268
269    function add_column($name, PaperColumn $col) {
270        $this->_columns_by_name[$name][] = $col;
271    }
272
273    function set_view($k, $v) {
274        if ($k !== "" && $k[0] === "\"" && $k[strlen($k) - 1] === "\"")
275            $k = substr($k, 1, -1);
276        if (in_array($k, ["compact", "cc", "compactcolumn", "ccol", "compactcolumns"]))
277            $this->_view_compact_columns = $this->_view_columns = $v;
278        else if (in_array($k, ["columns", "column", "col"]))
279            $this->_view_columns = $v;
280        else if ($k === "force")
281            $this->_view_force = $v;
282        else if (in_array($k, ["statistics", "stat", "stats", "totals"]))
283            $this->_view_statistics = $v;
284        else if (in_array($k, ["rownum", "rownumbers"]))
285            $this->_view_row_numbers = $v;
286        else {
287            if ($k === "authors")
288                $k = "au";
289            if ($v && in_array($k, ["aufull", "anonau"]) && !isset($this->_view_fields["au"]))
290                $this->_view_fields["au"] = $v;
291            $this->_view_fields[$k] = $v;
292        }
293    }
294    function set_view_display($str) {
295        $has_sorters = !!array_filter($this->sorters, function ($s) {
296            return $s->thenmap === null;
297        });
298        $splitter = new SearchSplitter($str);
299        while (($w = $splitter->shift()) !== "") {
300            if (($colon = strpos($w, ":")) !== false) {
301                $action = substr($w, 0, $colon);
302                $w = substr($w, $colon + 1);
303            } else
304                $action = "show";
305            if ($action === "sort") {
306                if (!$has_sorters && $w !== "sort:id")
307                    ListSorter::push($this->sorters, PaperSearch::parse_sorter($w));
308            } else if ($action === "edit")
309                $this->set_view($w, "edit");
310            else
311                $this->set_view($w, $action !== "hide");
312        }
313    }
314
315    function set_selection(SearchSelection $ssel) {
316        $this->_selection = $ssel;
317    }
318    function is_selected($paperId, $default = false) {
319        return $this->_selection ? $this->_selection->is_selected($paperId) : $default;
320    }
321
322    function unfold_all() {
323        $this->_unfold_all = true;
324    }
325
326    function mark_has($key, $value = true) {
327        if ($value)
328            $this->_has[$key] = true;
329        else if (!isset($this->_has[$key]))
330            $this->_has[$key] = false;
331    }
332    function has($key) {
333        if (!isset($this->_has[$key]))
334            $this->_has[$key] = $this->_compute_has($key);
335        return $this->_has[$key];
336    }
337    private function _compute_has($key) {
338        // paper options
339        if ($key === "paper" || $key === "final") {
340            $opt = $this->conf->paper_opts->find($key);
341            return $this->user->can_view_some_paper_option($opt)
342                && $this->_rowset->any(function ($row) use ($opt) {
343                    return ($opt->id == DTYPE_SUBMISSION ? $row->paperStorageId : $row->finalPaperStorageId) > 1
344                        && $this->user->can_view_paper_option($row, $opt);
345                });
346        }
347        if (str_starts_with($key, "opt")
348            && ($opt = $this->conf->paper_opts->find($key))) {
349            return $this->user->can_view_some_paper_option($opt)
350                && $this->_rowset->any(function ($row) use ($opt) {
351                    return ($ov = $row->option($opt->id))
352                        && (!$opt->has_document() || $ov->value > 1)
353                        && $this->user->can_view_paper_option($row, $opt);
354                });
355        }
356        // other features
357        if ($key === "abstract")
358            return $this->_rowset->any(function ($row) {
359                return (string) $row->abstract !== "";
360            });
361        if ($key === "openau")
362            return $this->has("authors")
363                && (!$this->user->is_manager()
364                    || $this->_rowset->any(function ($row) {
365                           return $this->user->can_view_authors($row);
366                       }));
367        if ($key === "anonau")
368            return $this->has("authors")
369                && $this->user->is_manager()
370                && $this->_rowset->any(function ($row) {
371                       return $this->user->allow_view_authors($row)
372                           && !$this->user->can_view_authors($row);
373                   });
374        if ($key === "need_submit")
375            return $this->_rowset->any(function ($row) {
376                return $row->timeSubmitted <= 0 && $row->timeWithdrawn <= 0;
377            });
378        if ($key === "accepted")
379            return $this->_rowset->any(function ($row) {
380                return $row->outcome > 0 && $this->user->can_view_decision($row);
381            });
382        if ($key === "need_final")
383            return $this->has("accepted")
384                && $this->_rowset->any(function ($row) {
385                       return $row->outcome > 0
386                           && $this->user->can_view_decision($row)
387                           && $row->timeFinalSubmitted <= 0;
388                   });
389        if (!in_array($key, ["collab", "lead", "shepherd", "topics", "sel", "need_review", "authors", "tags"]))
390            error_log("unexpected PaperList::_compute_has({$key})");
391        return false;
392    }
393
394
395    private function find_columns($name) {
396        if (!array_key_exists($name, $this->_columns_by_name)) {
397            $fs = $this->conf->paper_columns($name, $this->user);
398            if (!$fs) {
399                $errors = $this->conf->xt_factory_errors();
400                if (empty($errors)) {
401                    if ($this->conf->paper_columns($name, $this->conf->site_contact()))
402                        $errors[] = "Permission error.";
403                    else
404                        $errors[] = "No such column.";
405                }
406                $this->_column_errors_by_name[$name] = $errors;
407            }
408            $nfs = [];
409            foreach ($fs as $fdef) {
410                if ($fdef->name === $name)
411                    $nfs[] = PaperColumn::make($this->conf, $fdef);
412                else {
413                    if (!array_key_exists($fdef->name, $this->_columns_by_name))
414                        $this->_columns_by_name[$fdef->name][] = PaperColumn::make($this->conf, $fdef);
415                    $nfs = array_merge($nfs, $this->_columns_by_name[$fdef->name]);
416                }
417            }
418            $this->_columns_by_name[$name] = $nfs;
419        }
420        return $this->_columns_by_name[$name];
421    }
422    private function find_column($name) {
423        return get($this->find_columns($name), 0);
424    }
425
426    function _sort_compare($a, $b) {
427        foreach ($this->sorters as $s) {
428            if (($x = $s->field->compare($a, $b, $s))) {
429                return $x < 0 === $s->reverse ? 1 : -1;
430            }
431        }
432        if ($a->paperId != $b->paperId) {
433            return $a->paperId < $b->paperId ? -1 : 1;
434        } else {
435            return 0;
436        }
437    }
438    function _then_sort_compare($a, $b) {
439        if (($x = $a->_then_sort_info - $b->_then_sort_info)) {
440            return $x < 0 ? -1 : 1;
441        }
442        foreach ($this->sorters as $s) {
443            if (($s->thenmap === null || $s->thenmap === $a->_then_sort_info)
444                && ($x = $s->field->compare($a, $b, $s))) {
445                return $x < 0 === $s->reverse ? 1 : -1;
446            }
447        }
448        if ($a->paperId != $b->paperId) {
449            return $a->paperId < $b->paperId ? -1 : 1;
450        } else {
451            return 0;
452        }
453    }
454    private function _sort($rows) {
455        $overrides = $this->user->add_overrides($this->_view_force ? Contact::OVERRIDE_CONFLICT : 0);
456
457        if (($thenmap = $this->search->thenmap)) {
458            foreach ($rows as $row)
459                $row->_then_sort_info = $thenmap[$row->paperId];
460        }
461        foreach ($this->sorters as $s) {
462            $s->assign_uid();
463            $s->list = $this;
464        }
465        foreach ($this->sorters as $s) {
466            $s->field->analyze_sort($this, $rows, $s);
467        }
468
469        usort($rows, [$this, $thenmap ? "_then_sort_compare" : "_sort_compare"]);
470
471        foreach ($this->sorters as $s)
472            $s->list = null; // break circular ref
473        $this->user->set_overrides($overrides);
474        return $rows;
475    }
476
477    function sortdef($always = false) {
478        if (!empty($this->sorters)
479            && $this->sorters[0]->type
480            && $this->sorters[0]->thenmap === null
481            && ($always || (string) $this->qreq->sort != "")
482            && ($this->sorters[0]->type != "id" || $this->sorters[0]->reverse)) {
483            $x = ($this->sorters[0]->reverse ? "r" : "");
484            if (($fdef = $this->find_column($this->sorters[0]->type))
485                && isset($fdef->score))
486                $x .= $this->sorters[0]->score;
487            return ($fdef ? $fdef->name : $this->sorters[0]->type)
488                . ($x ? ",$x" : "");
489        } else
490            return "";
491    }
492
493
494    function _contentDownload($row) {
495        if ($row->size == 0 || !$this->user->can_view_pdf($row))
496            return "";
497        $dtype = $row->finalPaperStorageId <= 0 ? DTYPE_SUBMISSION : DTYPE_FINAL;
498        return "&nbsp;" . $row->document($dtype)->link_html("", DocumentInfo::L_SMALL | DocumentInfo::L_NOSIZE | DocumentInfo::L_FINALTITLE);
499    }
500
501    function _paperLink(PaperInfo $row) {
502        $pt = $this->_paper_link_page ? : "paper";
503        $rrow = null;
504        if ($pt === "review" || $pt === "finishreview") {
505            $rrow = $row->review_of_user($this->user);
506            if (!$rrow || ($pt === "finishreview" && !$rrow->reviewNeedsSubmit))
507                $pt = "paper";
508            else
509                $pt = "review";
510        }
511        $pl = "p=" . $row->paperId;
512        if ($pt === "paper" && $this->_paper_link_mode)
513            $pl .= "&amp;m=" . $this->_paper_link_mode;
514        else if ($pt === "review") {
515            $pl .= "&amp;r=" . unparseReviewOrdinal($rrow);
516            if ($rrow->reviewSubmitted > 0)
517                $pl .= "&amp;m=r";
518        }
519        return $row->conf->hoturl($pt, $pl);
520    }
521
522    // content downloaders
523    static function wrapChairConflict($text) {
524        return '<span class="fn5"><em>Hidden for conflict</em> <span class="barsep">·</span> <a class="fn5" href="#">Override conflicts</a></span><span class="fx5">' . $text . "</span>";
525    }
526
527    function reviewer_user() {
528        return $this->_reviewer_user;
529    }
530    function set_reviewer_user(Contact $user) {
531        $this->_reviewer_user = $user;
532    }
533
534    function _content_pc($contactId) {
535        $pc = $this->conf->pc_member_by_id($contactId);
536        return $pc ? $this->user->reviewer_html_for($pc) : "";
537    }
538
539    function _text_pc($contactId) {
540        $pc = $this->conf->pc_member_by_id($contactId);
541        return $pc ? $this->user->reviewer_text_for($pc) : "";
542    }
543
544    function _compare_pc($contactId1, $contactId2) {
545        $pc1 = $this->conf->pc_member_by_id($contactId1);
546        $pc2 = $this->conf->pc_member_by_id($contactId2);
547        if ($pc1 === $pc2)
548            return 0;
549        else if (!$pc1 || !$pc2)
550            return $pc1 ? -1 : 1;
551        else
552            return Contact::compare($pc1, $pc2);
553    }
554
555    function displayable_list_actions($prefix) {
556        $la = [];
557        foreach ($this->conf->list_action_map() as $name => $fjs)
558            if (str_starts_with($name, $prefix)) {
559                $uf = null;
560                foreach ($fjs as $fj)
561                    if (Conf::xt_priority_compare($fj, $uf) <= 0
562                        && $this->conf->xt_allowed($fj, $this->user)
563                        && $this->action_xt_displayed($fj))
564                        $uf = $fj;
565                if ($uf)
566                    $la[$name] = $uf;
567            }
568        return $la;
569    }
570
571    function action_xt_displayed($fj) {
572        if (isset($fj->display_if_report)
573            && (str_starts_with($fj->display_if_report, "!")
574                ? $this->report_id === substr($fj->display_if_report, 1)
575                : $this->report_id !== $fj->display_if_report))
576            return false;
577        if (isset($fj->display_if)
578            && !$this->conf->xt_check($fj->display_if, $fj, $this->user))
579            return false;
580        if (isset($fj->display_if_list_has)) {
581            $ifl = $fj->display_if_list_has;
582            foreach (is_array($ifl) ? $ifl : [$ifl] as $h) {
583                if (!is_bool($h)) {
584                    if (str_starts_with($h, "!"))
585                        $h = !$this->has(substr($h, 1));
586                    else
587                        $h = $this->has($h);
588                }
589                if (!$h)
590                    return false;
591            }
592        }
593        if (isset($fj->disabled) && $fj->disabled)
594            return false;
595        return true;
596    }
597
598    static function render_footer_row($arrow_ncol, $ncol, $header,
599                            $lllgroups, $activegroup = -1, $extra = null) {
600        $foot = "<tr class=\"pl_footrow\">\n   ";
601        if ($arrow_ncol) {
602            $foot .= '<td class="plf pl_footselector" colspan="' . $arrow_ncol . '">'
603                . Icons::ui_upperleft() . "</td>\n   ";
604        }
605        $foot .= '<td id="plact" class="plf pl_footer linelinks" colspan="' . $ncol . '">';
606
607        if ($header) {
608            $foot .= "<table class=\"pl_footerpart\"><tbody><tr>\n"
609                . '    <td class="pl_footer_desc">' . $header . "</td>\n"
610                . '   </tr></tbody></table>';
611        }
612
613        foreach ($lllgroups as $i => $lllg) {
614            $attr = ["class" => "linelink pl_footerpart"];
615            if ($i === $activegroup)
616                $attr["class"] .= " active";
617            for ($j = 2; $j < count($lllg); ++$j) {
618                if (is_array($lllg[$j])) {
619                    foreach ($lllg[$j] as $k => $v)
620                        if (str_starts_with($k, "linelink-")) {
621                            $k = substr($k, 9);
622                            if ($k === "class")
623                                $attr["class"] .= " " . $v;
624                            else
625                                $attr[$k] = $v;
626                        }
627                }
628            }
629            $foot .= "<table";
630            foreach ($attr as $k => $v)
631                $foot .= " $k=\"" . htmlspecialchars($v) . "\"";
632            $foot .= "><tbody><tr>\n"
633                . "    <td class=\"pl_footer_desc lll\"><a class=\"ui tla\" href=\""
634                . $lllg[0] . "\">" . $lllg[1] . "</a></td>\n";
635            for ($j = 2; $j < count($lllg); ++$j) {
636                $cell = is_array($lllg[$j]) ? $lllg[$j] : ["content" => $lllg[$j]];
637                $attr = [];
638                foreach ($cell as $k => $v) {
639                    if ($k !== "content" && !str_starts_with($k, "linelink-"))
640                        $attr[$k] = $v;
641                }
642                if ($attr || isset($cell["content"])) {
643                    $attr["class"] = rtrim("lld " . get($attr, "class", ""));
644                    $foot .= "    <td";
645                    foreach ($attr as $k => $v)
646                        $foot .= " $k=\"" . htmlspecialchars($v) . "\"";
647                    $foot .= ">";
648                    if ($j === 2 && isset($cell["content"]) && !str_starts_with($cell["content"], "<b>"))
649                        $foot .= "<b>:&nbsp;</b> ";
650                    if (isset($cell["content"]))
651                        $foot .= $cell["content"];
652                    $foot .= "</td>\n";
653                }
654            }
655            if ($i < count($lllgroups) - 1)
656                $foot .= "    <td>&nbsp;<span class='barsep'>·</span>&nbsp;</td>\n";
657            $foot .= "   </tr></tbody></table>";
658        }
659        return $foot . (string) $extra . "<hr class=\"c\" /></td>\n </tr>";
660    }
661
662    private function _footer($ncol, $extra) {
663        if ($this->count == 0)
664            return "";
665
666        $renderers = [];
667        foreach ($this->conf->list_action_renderers() as $name => $fjs) {
668            $rf = null;
669            foreach ($fjs as $fj)
670                if (Conf::xt_priority_compare($fj, $rf) <= 0
671                    && $this->conf->xt_allowed($fj, $this->user)
672                    && $this->action_xt_displayed($fj))
673                    $rf = $fj;
674            if ($rf) {
675                Conf::xt_resolve_require($rf);
676                $renderers[] = $rf;
677            }
678        }
679        usort($renderers, "Conf::xt_position_compare");
680
681        $lllgroups = [];
682        $whichlll = -1;
683        foreach ($renderers as $rf) {
684            if (($lllg = call_user_func($rf->render_callback, $this, $rf))) {
685                if (is_string($lllg))
686                    $lllg = [$lllg];
687                array_unshift($lllg, $rf->name, $rf->title);
688                $lllg[0] = SelfHref::make($this->qreq, ["atab" => $lllg[0], "anchor" => "plact"]);
689                $lllgroups[] = $lllg;
690                if ($this->qreq->fn == $rf->name || $this->atab == $rf->name)
691                    $whichlll = count($lllgroups) - 1;
692            }
693        }
694
695        $footsel_ncol = $this->_view_columns ? 0 : 1;
696        return self::render_footer_row($footsel_ncol, $ncol - $footsel_ncol,
697            "<b>Select papers</b> (or <a class=\"ui js-select-all\" href=\""
698            . SelfHref::make($this->qreq, ["selectall" => 1, "anchor" => "plact"])
699            . '">select all ' . $this->count . "</a>), then&nbsp;",
700            $lllgroups, $whichlll, $extra);
701    }
702
703    private function _default_linkto($page) {
704        if (!$this->_paper_link_page)
705            $this->_paper_link_page = $page;
706    }
707
708    private function _list_columns() {
709        switch ($this->report_id) {
710        case "a":
711            return "id title revstat statusfull authors collab abstract topics reviewers shepherd scores formulas";
712        case "authorHome":
713            return "id title statusfull";
714        case "act":
715        case "all":
716            return "sel id title revtype revstat statusfull authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas";
717        case "reviewerHome":
718            $this->_default_linkto("finishreview");
719            return "id title revtype status";
720        case "ar":
721        case "r":
722        case "rable":
723            $this->_default_linkto("finishreview");
724            /* fallthrough */
725        case "acc":
726        case "lead":
727        case "manager":
728        case "s":
729        case "vis":
730            return "sel id title revtype revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas";
731        case "req":
732        case "rout":
733            $this->_default_linkto("review");
734            return "sel id title revtype revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas";
735        case "reqrevs":
736            $this->_default_linkto("review");
737            return "id title revdelegation revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas";
738        case "reviewAssignment":
739            $this->_default_linkto("assign");
740            return "id title revpref topicscore desirability assrev authors potentialconflict topics allrevtopicpref reviewers tags scores formulas";
741        case "conflictassign":
742            $this->_default_linkto("assign");
743            return "id title abstract authors potentialconflict revtype editconf tags";
744        case "editpref":
745            $this->_default_linkto("paper");
746            return "sel id title topicscore revtype editpref authors abstract topics";
747        case "reviewers":
748            $this->_default_linkto("assign");
749            return "selon id title status";
750        case "reviewersSel":
751            $this->_default_linkto("assign");
752            return "sel id title status reviewers";
753        default:
754            error_log($this->conf->dbname . ": No such report {$this->report_id}");
755            return null;
756        }
757    }
758
759    function _canonicalize_columns($fields) {
760        if (is_string($fields))
761            $fields = explode(" ", $fields);
762        $field_list = array();
763        foreach ($fields as $fid) {
764            foreach ($this->find_columns($fid) as $fdef)
765                $field_list[] = $fdef;
766        }
767        if ($this->qreq->selectall > 0 && $field_list[0]->name == "sel")
768            $field_list[0] = $this->find_column("selon");
769        return $field_list;
770    }
771
772
773    function rowset() {
774        if ($this->_rowset === null) {
775            $this->qopts["scores"] = array_keys($this->qopts["scores"]);
776            if (empty($this->qopts["scores"]))
777                unset($this->qopts["scores"]);
778            $result = $this->conf->paper_result($this->user, $this->qopts);
779            $this->_rowset = new PaperInfoSet;
780            while (($row = PaperInfo::fetch($result, $this->user))) {
781                assert(!$this->_rowset->get($row->paperId));
782                $this->_rowset->add($row);
783            }
784            Dbl::free($result);
785        }
786        return $this->_rowset;
787    }
788
789    private function _rows($field_list) {
790        if (!$field_list)
791            return null;
792
793        // analyze rows (usually noop)
794        $rows = $this->rowset()->all();
795        foreach ($field_list as $fdef)
796            $fdef->analyze($this, $rows, $field_list);
797
798        // sort rows
799        if (!empty($this->sorters))
800            $rows = $this->_sort($rows);
801
802        // set `ids`
803        $this->ids = [];
804        foreach ($rows as $prow)
805            $this->ids[] = $prow->paperId;
806
807        // set `groups`
808        $this->groups = [];
809        if (!empty($this->search->groupmap))
810            $this->_collect_groups($rows);
811
812        return $rows;
813    }
814
815    private function _collect_groups($srows) {
816        $groupmap = $this->search->groupmap ? : [];
817        $thenmap = $this->search->thenmap ? : [];
818        $rowpos = 0;
819        for ($grouppos = 0;
820             $rowpos < count($srows) || $grouppos < count($groupmap);
821             ++$grouppos) {
822            $first_rowpos = $rowpos;
823            while ($rowpos < count($srows)
824                   && get_i($thenmap, $srows[$rowpos]->paperId) === $grouppos)
825                ++$rowpos;
826            $ginfo = get($groupmap, $grouppos);
827            if (($ginfo === null || $ginfo->is_empty()) && $first_rowpos === 0)
828                continue;
829            $ginfo = $ginfo ? clone $ginfo : TagInfo::make_empty();
830            $ginfo->pos = $first_rowpos;
831            $ginfo->count = $rowpos - $first_rowpos;
832            // leave off an empty “Untagged” section unless editing
833            if ($ginfo->count === 0 && $ginfo->tag && !$ginfo->annoId
834                && !$this->has_editable_tags)
835                continue;
836            $this->groups[] = $ginfo;
837        }
838    }
839
840    function is_folded($fdef) {
841        $fname = $fdef;
842        if (is_object($fdef) || ($fdef = $this->find_column($fname)))
843            $fname = $fdef->fold ? $fdef->name : null;
844        else if ($fname === "force")
845            return !$this->_view_force;
846        else if ($fname === "rownum")
847            return !$this->_view_row_numbers;
848        else if ($fname === "statistics")
849            return !$this->_view_statistics;
850        if ($fname === "authors")
851            $fname = "au";
852        if (!$fname || $this->_unfold_all || $this->qreq["show$fname"])
853            return false;
854        return !get($this->_view_fields, $fname);
855    }
856
857    private function _wrap_conflict($main_content, $override_content, PaperColumn $fdef) {
858        if ($main_content === $override_content)
859            return $main_content;
860        $tag = $fdef->viewable_row() ? "div" : "span";
861        if ((string) $main_content !== "")
862            $main_content = "<$tag class=\"fn5\">$main_content</$tag>";
863        if ((string) $override_content !== "")
864            $override_content = "<$tag class=\"fx5\">$override_content</$tag>";
865        return $main_content . $override_content;
866    }
867
868    private function _row_field_content(PaperColumn $fdef, PaperInfo $row) {
869        $content = "";
870        if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_FOLD_IFEMPTY) {
871            $empty = $fdef->content_empty($this, $row);
872            if ($empty) {
873                $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
874                $empty = $fdef->content_empty($this, $row);
875                if (!$empty && $fdef->is_visible)
876                    $content = $this->_wrap_conflict("", $fdef->content($this, $row), $fdef);
877                $this->user->set_overrides($overrides);
878            } else if ($fdef->is_visible)
879                $content = $fdef->content($this, $row);
880        } else if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_FOLD_BOTH) {
881            $content1 = $content2 = "";
882            $empty1 = $fdef->content_empty($this, $row);
883            if (!$empty1 && $fdef->is_visible)
884                $content1 = $fdef->content($this, $row);
885            $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
886            $empty2 = $fdef->content_empty($this, $row);
887            if (!$empty2 && $fdef->is_visible)
888                $content2 = $fdef->content($this, $row);
889            $this->user->set_overrides($overrides);
890            $empty = $empty1 && $empty2;
891            $content = $this->_wrap_conflict($content1, $content2, $fdef);
892        } else if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_ALWAYS) {
893            $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
894            $empty = $fdef->content_empty($this, $row);
895            if (!$empty && $fdef->is_visible)
896                $content = $fdef->content($this, $row);
897            $this->user->set_overrides($overrides);
898        } else {
899            $empty = $fdef->content_empty($this, $row);
900            if (!$empty && $fdef->is_visible)
901                $content = $fdef->content($this, $row);
902        }
903        return [$empty, $content];
904    }
905
906    private function _row_setup(PaperInfo $row) {
907        ++$this->count;
908        $this->row_attr = [];
909        $this->row_overridable = $this->user->can_meaningfully_override($row);
910
911        $this->row_tags = $this->row_tags_overridable = null;
912        if (isset($row->paperTags) && $row->paperTags !== "") {
913            if ($this->row_overridable) {
914                $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
915                $this->row_tags_overridable = $row->viewable_tags($this->user);
916                $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT);
917                $this->row_tags = $row->viewable_tags($this->user);
918                $this->user->set_overrides($overrides);
919            } else
920                $this->row_tags = $row->viewable_tags($this->user);
921        }
922    }
923
924    private function _row_content($rstate, PaperInfo $row, $fieldDef) {
925        // main columns
926        $tm = "";
927        foreach ($fieldDef as $fdef) {
928            if (!$fdef->viewable_column()
929                || (!$fdef->is_visible && $fdef->has_content))
930                continue;
931            list($empty, $content) = $this->_row_field_content($fdef, $row);
932            if ($fdef->is_visible) {
933                if ($content !== "") {
934                    $tm .= "<td class=\"pl " . $fdef->className;
935                    if ($fdef->fold)
936                        $tm .= " fx{$fdef->fold}";
937                    $tm .= "\">" . $content . "</td>";
938                } else {
939                    $tm .= "<td";
940                    if ($fdef->fold)
941                        $tm .= " class=\"fx{$fdef->fold}\"";
942                    $tm .= "></td>";
943                }
944            }
945            if ($fdef->is_visible ? $content !== "" : !$empty)
946                $fdef->has_content = true;
947        }
948
949        // extension columns
950        $tt = "";
951        foreach ($fieldDef as $fdef) {
952            if (!$fdef->viewable_row()
953                || (!$fdef->is_visible && $fdef->has_content))
954                continue;
955            list($empty, $content) = $this->_row_field_content($fdef, $row);
956            if ($fdef->is_visible) {
957                if ($content !== "" && ($ch = $fdef->header($this, false))) {
958                    if ($content[0] !== "<"
959                        || !preg_match('/\A((?:<(?:div|p).*?>)*)([\s\S]*)\z/', $content, $cm))
960                        $cm = [null, "", $content];
961                    $content = $cm[1] . '<em class="plx">' . $ch . ':</em> ' . $cm[2];
962                }
963                $tt .= "<div class=\"" . $fdef->className;
964                if ($fdef->fold)
965                    $tt .= " fx" . $fdef->fold;
966                $tt .= "\">" . $content . "</div>";
967            }
968            if ($fdef->is_visible ? $content !== "" : !$empty)
969                $fdef->has_content = !$empty;
970        }
971
972        // filter
973        if ($this->_row_filter
974            && !call_user_func($this->_row_filter, $this, $row, $fieldDef, $tm, $tt)) {
975            --$this->count;
976            return "";
977        }
978
979        // tags
980        if ($this->need_tag_attr) {
981            if ($this->row_tags_overridable
982                && $this->row_tags_overridable !== $this->row_tags) {
983                $this->row_attr["data-tags"] = trim($this->row_tags_overridable);
984                $this->row_attr["data-tags-conflicted"] = trim($this->row_tags);
985            } else
986                $this->row_attr["data-tags"] = trim($this->row_tags);
987        }
988
989        // row classes
990        $trclass = [];
991        $cc = "";
992        if (get($row, "paperTags")) {
993            if ($this->row_tags_overridable
994                && ($cco = $row->conf->tags()->color_classes($this->row_tags_overridable))) {
995                $ccx = $row->conf->tags()->color_classes($this->row_tags);
996                if ($cco !== $ccx) {
997                    $this->row_attr["data-color-classes"] = $cco;
998                    $this->row_attr["data-color-classes-conflicted"] = $ccx;
999                    $trclass[] = "colorconflict";
1000                }
1001                $cc = $this->_view_force ? $cco : $ccx;
1002                $rstate->hascolors = $rstate->hascolors || str_ends_with($cco, " tagbg");
1003            } else if ($this->row_tags)
1004                $cc = $row->conf->tags()->color_classes($this->row_tags);
1005        }
1006        if ($cc) {
1007            $trclass[] = $cc;
1008            $rstate->hascolors = $rstate->hascolors || str_ends_with($cc, " tagbg");
1009        }
1010        if (!$cc || !$rstate->hascolors)
1011            $trclass[] = "k" . $rstate->colorindex;
1012        if (($highlightclass = get($this->search->highlightmap, $row->paperId)))
1013            $trclass[] = $highlightclass[0] . "highlightmark";
1014        $trclass = join(" ", $trclass);
1015        $rstate->colorindex = 1 - $rstate->colorindex;
1016        $rstate->last_trclass = $trclass;
1017
1018        $t = "  <tr";
1019        if ($this->_row_id_pattern)
1020            $t .= " id=\"" . str_replace("#", $row->paperId, $this->_row_id_pattern) . "\"";
1021        $t .= " class=\"pl $trclass\" data-pid=\"$row->paperId";
1022        foreach ($this->row_attr as $k => $v)
1023            $t .= "\" $k=\"" . htmlspecialchars($v);
1024        $t .= "\">" . $tm . "</tr>\n";
1025
1026        if ($tt !== "" || $this->table_id()) {
1027            $t .= "  <tr class=\"plx $trclass\" data-pid=\"$row->paperId\">";
1028            if ($rstate->skipcallout > 0)
1029                $t .= "<td colspan=\"$rstate->skipcallout\"></td>";
1030            $t .= "<td class=\"plx\" colspan=\"" . ($rstate->ncol - $rstate->skipcallout) . "\">$tt</td></tr>\n";
1031        }
1032
1033        return $t;
1034    }
1035
1036    private function _groups_for($grouppos, $rstate, &$body, $last) {
1037        for ($did_groupstart = false;
1038             $grouppos < count($this->groups)
1039             && ($last || $this->count > $this->groups[$grouppos]->pos);
1040             ++$grouppos) {
1041            if ($this->count !== 1 && $did_groupstart === false)
1042                $rstate->groupstart[] = $did_groupstart = count($body);
1043            $ginfo = $this->groups[$grouppos];
1044            if ($ginfo->is_empty()) {
1045                $body[] = $rstate->heading_row(null);
1046            } else {
1047                $attr = [];
1048                if ($ginfo->tag)
1049                    $attr["data-anno-tag"] = $ginfo->tag;
1050                if ($ginfo->annoId) {
1051                    $attr["data-anno-id"] = $ginfo->annoId;
1052                    $attr["data-tags"] = "{$ginfo->tag}#{$ginfo->tagIndex}";
1053                    if (get($this->table_attr, "data-drag-tag"))
1054                        $attr["tdclass"] = "need-draghandle";
1055                }
1056                $x = "<span class=\"plheading-group";
1057                if ($ginfo->heading !== ""
1058                    && ($format = $this->conf->check_format($ginfo->annoFormat, $ginfo->heading))) {
1059                    $x .= " need-format\" data-format=\"$format";
1060                    $this->need_render = true;
1061                }
1062                $x .= "\" data-title=\"" . htmlspecialchars($ginfo->heading)
1063                    . "\">" . htmlspecialchars($ginfo->heading)
1064                    . ($ginfo->heading !== "" ? " " : "")
1065                    . "</span><span class=\"plheading-count\">"
1066                    . plural($ginfo->count, "paper") . "</span>";
1067                $body[] = $rstate->heading_row($x, $attr);
1068                $rstate->colorindex = 0;
1069            }
1070        }
1071        return $grouppos;
1072    }
1073
1074    private function _field_title($fdef) {
1075        $t = $fdef->header($this, false);
1076        if (!$fdef->viewable_column()
1077            || !$fdef->sort
1078            || !$this->sortable
1079            || !($sort_url = $this->search->url_site_relative_raw()))
1080            return $t;
1081
1082        $default_score_sort = ListSorter::default_score_sort($this->conf);
1083        $sort_name = $fdef->sort_name($default_score_sort);
1084        $sort_url = htmlspecialchars(Navigation::siteurl() . $sort_url)
1085            . (strpos($sort_url, "?") ? "&amp;" : "?") . "sort=" . urlencode($sort_name);
1086        $s0 = get($this->sorters, 0);
1087
1088        $sort_class = "pl_sort";
1089        if ($s0 && $s0->thenmap === null
1090            && $sort_name === $s0->field->sort_name($s0->score ? : $default_score_sort)) {
1091            $sort_class = "pl_sort pl_sorting" . ($s0->reverse ? "_rev" : "_fwd");
1092            $sort_url .= $s0->reverse ? "" : urlencode(" reverse");
1093        }
1094
1095        if ($this->user->overrides() & Contact::OVERRIDE_CONFLICT)
1096            $sort_url .= "&amp;forceShow=1";
1097        return '<a class="' . $sort_class . '" rel="nofollow" href="' . $sort_url . '">' . $t . '</a>';
1098    }
1099
1100    private function _analyze_folds($rstate, $fieldDef) {
1101        $classes = $jscol = array();
1102        $has_sel = false;
1103        $has_statistics = $has_loadable_statistics = false;
1104        $default_score_sort = ListSorter::default_score_sort($this->conf);
1105        foreach ($fieldDef as $fdef) {
1106            $j = ["name" => $fdef->name,
1107                  "title" => $fdef->header($this, false),
1108                  "position" => $fdef->position];
1109            if ($fdef->className != "pl_" . $fdef->name)
1110                $j["className"] = $fdef->className;
1111            if ($fdef->viewable_column()) {
1112                $j["column"] = true;
1113                if ($fdef->has_statistics()) {
1114                    $j["has_statistics"] = true;
1115                    if ($fdef->has_content)
1116                        $has_loadable_statistics = true;
1117                    if ($fdef->has_content && $fdef->is_visible)
1118                        $has_statistics = true;
1119                }
1120                if ($fdef->sort)
1121                    $j["sort_name"] = $fdef->sort_name($default_score_sort);
1122            }
1123            if (!$fdef->is_visible)
1124                $j["missing"] = true;
1125            if ($fdef->has_content && !$fdef->is_visible)
1126                $j["loadable"] = true;
1127            if ($fdef->fold)
1128                $j["foldnum"] = $fdef->fold;
1129            $fdef->annotate_field_js($this, $j);
1130            $jscol[] = $j;
1131            if ($fdef->fold)
1132                $classes[] = "fold" . $fdef->fold . ($fdef->is_visible ? "o" : "c");
1133            if ($fdef instanceof SelectorPaperColumn)
1134                $has_sel = true;
1135        }
1136        // authorship requires special handling
1137        if ($this->has("anonau"))
1138            $classes[] = "fold2" . ($this->is_folded("anonau") ? "c" : "o");
1139        // row number folding
1140        if ($has_sel)
1141            $classes[] = "fold6" . ($this->_view_row_numbers ? "o" : "c");
1142        if ($this->user->is_track_manager())
1143            $classes[] = "fold5" . ($this->_view_force ? "o" : "c");
1144        $classes[] = "fold7" . ($this->_view_statistics ? "o" : "c");
1145        $classes[] = "fold8" . ($has_statistics ? "o" : "c");
1146        if ($this->_table_id)
1147            Ht::stash_script("plinfo.initialize(\"#{$this->_table_id}\"," . json_encode_browser($jscol) . ");");
1148        return $classes;
1149    }
1150
1151    private function _make_title_header_extra($rstate, $fieldDef, $show_links) {
1152        $titleextra = "";
1153        if ($show_links && $this->has("authors")) {
1154            $titleextra .= '<span class="sep"></span>';
1155            if ($this->conf->submission_blindness() == Conf::BLIND_NEVER)
1156                $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au">'
1157                    . '<span class="fn1">Show authors</span><span class="fx1">Hide authors</span></a>';
1158            else if ($this->user->is_manager() && !$this->has("openau"))
1159                $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au anonau">'
1160                    . '<span class="fn1 fn2">Show authors</span><span class="fx1 fx2">Hide authors</span></a>';
1161            else if ($this->user->is_manager() && $this->has("anonau"))
1162                $titleextra .= '<a class="ui js-plinfo fn1" href="#" data-plinfo-field="au">Show authors</a>'
1163                    . '<a class="ui js-plinfo fx1 fn2" href="#" data-plinfo-field="anonau">Show all authors</a>'
1164                    . '<a class="ui js-plinfo fx1 fx2" href="#" data-plinfo-field="au anonau">Hide authors</a>';
1165            else
1166                $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au">'
1167                    . '<span class="fn1">Show authors</span><span class="fx1">Hide authors</span></a>';
1168        }
1169        if ($show_links && $this->has("tags")) {
1170            $tagfold = $this->find_column("tags")->fold;
1171            $titleextra .= '<span class="sep"></span>';
1172            $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="tags">'
1173                . '<span class="fn' . $tagfold . '">Show tags</span><span class="fx' . $tagfold . '">Hide tags</span></a>';
1174        }
1175        if ($titleextra)
1176            $titleextra = '<span class="pl_titleextra">' . $titleextra . '</span>';
1177        return $titleextra;
1178    }
1179
1180    private function _column_split($rstate, $colhead, &$body) {
1181        if (count($rstate->groupstart) <= 1)
1182            return false;
1183        $rstate->groupstart[] = count($body);
1184        $rstate->split_ncol = count($rstate->groupstart) - 1;
1185
1186        $rownum_marker = "<span class=\"pl_rownum fx6\">";
1187        $rownum_len = strlen($rownum_marker);
1188        $nbody = array("<tr>");
1189        $tbody_class = "pltable" . ($rstate->hascolors ? " pltable_colored" : "");
1190        for ($i = 1; $i < count($rstate->groupstart); ++$i) {
1191            $nbody[] = '<td class="plsplit_col top" width="' . (100 / $rstate->split_ncol) . '%"><div class="plsplit_col"><table width="100%">';
1192            $nbody[] = $colhead . "  <tbody class=\"$tbody_class\">\n";
1193            $number = 1;
1194            for ($j = $rstate->groupstart[$i - 1]; $j < $rstate->groupstart[$i]; ++$j) {
1195                $x = $body[$j];
1196                if (($pos = strpos($x, $rownum_marker)) !== false) {
1197                    $pos += strlen($rownum_marker);
1198                    $x = substr($x, 0, $pos) . preg_replace('/\A\d+/', $number, substr($x, $pos));
1199                    ++$number;
1200                } else if (strpos($x, "<td class=\"plheading-blank") !== false)
1201                    $x = "";
1202                $nbody[] = $x;
1203            }
1204            $nbody[] = "  </tbody>\n</table></div></td>\n";
1205        }
1206        $nbody[] = "</tr>";
1207
1208        $body = $nbody;
1209        $rstate->last_trclass = "plsplit_col";
1210        return true;
1211    }
1212
1213    private function _prepare($report_id = null) {
1214        $this->_has = [];
1215        $this->count = 0;
1216        $this->need_render = false;
1217        $this->report_id = $this->report_id ? : $report_id;
1218        return true;
1219    }
1220
1221    private function _expand_view_column($k, $report) {
1222        if (in_array($k, ["anonau", "aufull"]))
1223            return [];
1224        $fs = $this->find_columns($k);
1225        if (!$fs) {
1226            if (!$this->search->viewmap || !isset($this->search->viewmap[$k])) {
1227                if (($rfinfo = ReviewInfo::field_info($k, $this->conf))
1228                    && ($rfield = $this->conf->review_field($rfinfo->id)))
1229                    $fs = $this->find_columns($rfield->name);
1230            } else if ($report && isset($this->_column_errors_by_name[$k])) {
1231                foreach ($this->_column_errors_by_name[$k] as $i => $err)
1232                    $this->error_html[] = ($i ? "" : "Can’t show “" . htmlspecialchars($k) . "”: ") . $err;
1233            }
1234        }
1235        return $fs;
1236    }
1237
1238    private function _view_columns($field_list) {
1239        // add explicitly requested columns
1240        $viewmap_add = [];
1241        foreach ($this->_view_fields as $k => $v) {
1242            $f = $this->_expand_view_column($k, !!$v);
1243            foreach ($f as $fx) {
1244                $viewmap_add[$fx->name] = $v;
1245                foreach ($field_list as $ff)
1246                    if ($fx && $fx->name == $ff->name)
1247                        $fx = null;
1248                if ($fx)
1249                    $field_list[] = $fx;
1250            }
1251        }
1252        foreach ($viewmap_add as $k => $v)
1253            $this->_view_fields[$k] = $v;
1254        foreach ($field_list as $fi => $f) {
1255            if (get($this->_view_fields, $f->name) === "edit")
1256                $f->mark_editable();
1257        }
1258
1259        // remove deselected columns;
1260        // in compactcolumns view, remove non-minimal columns
1261        $minimal = $this->_view_compact_columns;
1262        $field_list2 = array();
1263        foreach ($field_list as $fdef) {
1264            $v = get($this->_view_fields, $fdef->name);
1265            if ($v
1266                || $fdef->fold
1267                || ($v !== false && (!$minimal || $fdef->minimal)))
1268                $field_list2[] = $fdef;
1269        }
1270        return $field_list2;
1271    }
1272
1273    private function _prepare_sort() {
1274        $sorters = [];
1275        foreach ($this->sorters as $sorter) {
1276            if ($sorter->field) {
1277                // already prepared (e.g., NumericOrderPaperColumn)
1278                $sorters[] = $sorter;
1279            } else if ($sorter->type
1280                       && ($field = $this->find_column($sorter->type))) {
1281                if ($field->prepare($this, PaperColumn::PREP_SORT)
1282                    && $field->sort) {
1283                    $sorter->field = $field->realize($this);
1284                    $sorter->name = $field->name;
1285                    $sorters[] = $sorter;
1286                }
1287            } else if ($sorter->type) {
1288                if ($this->user->can_view_tags(null)
1289                    && ($tagger = new Tagger($this->user))
1290                    && ($tag = $tagger->check($sorter->type))
1291                    && $this->conf->fetch_ivalue("select exists (select * from PaperTag where tag=?)", $tag))
1292                    $this->search->warn("Unrecognized sort “" . htmlspecialchars($sorter->type) . "”. Did you mean “sort:#" . htmlspecialchars($sorter->type) . "”?");
1293                else
1294                    $this->search->warn("Unrecognized sort “" . htmlspecialchars($sorter->type) . "”.");
1295            }
1296        }
1297        if (empty($sorters)) {
1298            $sorters[] = PaperSearch::parse_sorter("id");
1299            $sorters[0]->field = $this->find_column("id");
1300        }
1301        $this->sorters = $sorters;
1302
1303        // set defaults
1304        foreach ($this->sorters as $s) {
1305            if ($s->reverse === null)
1306                $s->reverse = false;
1307            if ($s->score === null)
1308                $s->score = ListSorter::default_score_sort($this->conf);
1309        }
1310    }
1311
1312    private function _prepare_columns($field_list) {
1313        $field_list2 = [];
1314        $this->need_tag_attr = false;
1315        $this->table_attr = [];
1316        foreach ($field_list as $fdef) {
1317            if ($fdef) {
1318                $fdef->is_visible = !$this->is_folded($fdef);
1319                $fdef->has_content = false;
1320                if ($fdef->prepare($this, $fdef->is_visible ? 1 : 0)) {
1321                    $field_list2[] = $fdef->realize($this);
1322                }
1323            }
1324        }
1325        assert(empty($this->row_attr));
1326        return $field_list2;
1327    }
1328
1329    function make_review_analysis($xrow, PaperInfo $row) {
1330        return new PaperListReviewAnalysis($xrow, $row);
1331    }
1332
1333    function add_header_script($script, $uniqueid = false) {
1334        if ($uniqueid) {
1335            if (isset($this->_header_script_map[$uniqueid]))
1336                return;
1337            $this->_header_script_map[$uniqueid] = true;
1338        }
1339        if ($this->_header_script !== ""
1340            && ($ch = $this->_header_script[strlen($this->_header_script) - 1]) !== "}"
1341            && $ch !== "{" && $ch !== ";")
1342            $this->_header_script .= ";";
1343        $this->_header_script .= $script;
1344    }
1345
1346    private function _columns($field_list, $table_html) {
1347        $field_list = $this->_canonicalize_columns($field_list);
1348        if ($table_html)
1349            $field_list = $this->_view_columns($field_list);
1350        $this->_prepare_sort(); // NB before prepare_columns so columns see sorter
1351        return $this->_prepare_columns($field_list);
1352    }
1353
1354    private function _statistics_rows($rstate, $fieldDef) {
1355        $any_empty = null;
1356        foreach ($fieldDef as $fdef)
1357            if ($fdef->viewable_column() && $fdef->has_statistics())
1358                $any_empty = $any_empty || $fdef->statistic($this, ScoreInfo::COUNT) != $this->count;
1359        if ($any_empty === null)
1360            return "";
1361        $t = '  <tr class="pl_statheadrow fx8">';
1362        if ($rstate->titlecol)
1363            $t .= "<td colspan=\"{$rstate->titlecol}\" class=\"plstat\"></td>";
1364        $t .= "<td colspan=\"" . ($rstate->ncol - $rstate->titlecol) . "\" class=\"plstat\">" . foldupbutton(7, "Statistics") . "</td></tr>\n";
1365        foreach (self::$stats as $stat) {
1366            $t .= '  <tr';
1367            if ($this->_row_id_pattern)
1368                $t .= " id=\"" . str_replace("#", "stat_" . ScoreInfo::$stat_keys[$stat], $this->_row_id_pattern) . "\"";
1369            $t .= ' class="pl_statrow fx7 fx8" data-statistic="' . ScoreInfo::$stat_keys[$stat] . '">';
1370            $col = 0;
1371            foreach ($fieldDef as $fdef) {
1372                if (!$fdef->viewable_column() || !$fdef->is_visible)
1373                    continue;
1374                $class = "plstat " . $fdef->className;
1375                if ($fdef->has_statistics())
1376                    $content = $fdef->statistic($this, $stat);
1377                else if ($col == $rstate->titlecol) {
1378                    $content = ScoreInfo::$stat_names[$stat];
1379                    $class = "plstat pl_statheader";
1380                } else
1381                    $content = "";
1382                $t .= '<td class="' . $class;
1383                if ($fdef->fold)
1384                    $t .= ' fx' . $fdef->fold;
1385                $t .= '">' . $content . '</td>';
1386                ++$col;
1387            }
1388            $t .= "</tr>\n";
1389        }
1390        return $t;
1391    }
1392
1393    function ids_and_groups() {
1394        if (!$this->_prepare())
1395            return null;
1396        $field_list = $this->_columns("id", false);
1397        $rows = $this->_rows($field_list);
1398        if ($rows === null)
1399            return null;
1400        $this->count = count($this->ids);
1401        return [$this->ids, $this->groups];
1402    }
1403
1404    function paper_ids() {
1405        $idh = $this->ids_and_groups();
1406        return $idh ? $idh[0] : null;
1407    }
1408
1409    private function _listDescription() {
1410        switch ($this->report_id) {
1411        case "reviewAssignment":
1412            return "Review assignments";
1413        case "editpref":
1414            return "Review preferences";
1415        case "reviewers":
1416        case "reviewersSel":
1417            return "Proposed assignments";
1418        default:
1419            return null;
1420        }
1421    }
1422
1423    function session_list_object() {
1424        assert($this->ids !== null);
1425        return $this->search->create_session_list_object($this->ids, $this->_listDescription(), $this->sortdef());
1426    }
1427
1428    function table_render($report_id, $options = array()) {
1429        if (!$this->_prepare($report_id))
1430            return PaperListTableRender::make_error("Internal error");
1431        // need tags for row coloring
1432        if ($this->user->can_view_tags(null))
1433            $this->qopts["tags"] = true;
1434
1435        // get column list
1436        if (isset($options["field_list"]))
1437            $field_list = $options["field_list"];
1438        else
1439            $field_list = $this->_list_columns();
1440        if (!$field_list)
1441            return PaperListTableRender::make_error("No matching report");
1442
1443        // turn off forceShow
1444        $overrides = $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT);
1445
1446        // expand fields, check sort
1447        $field_list = $this->_columns($field_list, true);
1448        $rows = $this->_rows($field_list);
1449
1450        if (empty($rows)) {
1451            $this->user->set_overrides($overrides);
1452            if ($rows === null)
1453                return null;
1454            if (($altq = $this->search->alternate_query())) {
1455                $altqh = htmlspecialchars($altq);
1456                $url = $this->search->url_site_relative_raw($altq);
1457                if (substr($url, 0, 5) == "search")
1458                    $altqh = "<a href=\"" . htmlspecialchars(Navigation::siteurl() . $url) . "\">" . $altqh . "</a>";
1459                return PaperListTableRender::make_error("No matching papers. Did you mean “{$altqh}”?");
1460            }
1461            return PaperListTableRender::make_error("No matching papers");
1462        }
1463
1464        // get field array
1465        $fieldDef = array();
1466        $ncol = $titlecol = 0;
1467        // folds: au:1, anonau:2, fullrow:3, aufull:4, force:5, rownum:6, statistics:7,
1468        // statistics-exist:8, [fields]
1469        $next_fold = 9;
1470        foreach ($field_list as $fdef) {
1471            if ($fdef->viewable()) {
1472                $fieldDef[$fdef->name] = $fdef;
1473                if ($fdef->fold === true) {
1474                    $fdef->fold = $next_fold;
1475                    ++$next_fold;
1476                }
1477            }
1478            if ($fdef->name == "title")
1479                $titlecol = $ncol;
1480            if ($fdef->viewable_column() && $fdef->is_visible)
1481                ++$ncol;
1482        }
1483
1484        // count non-callout columns
1485        $skipcallout = 0;
1486        foreach ($fieldDef as $fdef) {
1487            if ($fdef->position === null || $fdef->position >= 100)
1488                break;
1489            else
1490                ++$skipcallout;
1491        }
1492
1493        // create render state
1494        $rstate = new PaperListTableRender($ncol, $titlecol, $skipcallout);
1495
1496        // collect row data
1497        $body = array();
1498        $grouppos = empty($this->groups) ? -1 : 0;
1499        $need_render = false;
1500        foreach ($rows as $row) {
1501            $this->_row_setup($row);
1502            if ($grouppos >= 0)
1503                $grouppos = $this->_groups_for($grouppos, $rstate, $body, false);
1504            $body[] = $this->_row_content($rstate, $row, $fieldDef);
1505            if ($this->need_render && !$need_render) {
1506                Ht::stash_script('$(plinfo.render_needed)', 'plist_render_needed');
1507                $need_render = true;
1508            }
1509            if ($this->need_render && $this->count % 16 == 15) {
1510                $body[count($body) - 1] .= "  " . Ht::script('plinfo.render_needed()') . "\n";
1511                $this->need_render = false;
1512            }
1513        }
1514        if ($grouppos >= 0 && $grouppos < count($this->groups))
1515            $this->_groups_for($grouppos, $rstate, $body, true);
1516        if ($this->count === 0)
1517            return PaperListTableRender::make_error("No matching papers");
1518
1519        // analyze `has`, including authors
1520        foreach ($fieldDef as $fdef)
1521            $this->mark_has($fdef->name, $fdef->has_content);
1522
1523        // statistics rows
1524        $tfoot = "";
1525        if (!$this->_view_columns)
1526            $tfoot = $this->_statistics_rows($rstate, $fieldDef);
1527
1528        // restore forceShow
1529        $this->user->set_overrides($overrides);
1530
1531        // header cells
1532        $colhead = "";
1533        if (!defval($options, "noheader")) {
1534            $colhead .= " <thead class=\"pltable\">\n  <tr class=\"pl_headrow\">";
1535            $titleextra = $this->_make_title_header_extra($rstate, $fieldDef,
1536                                                          get($options, "header_links"));
1537
1538            foreach ($fieldDef as $fdef) {
1539                if (!$fdef->viewable_column() || !$fdef->is_visible)
1540                    continue;
1541                if ($fdef->has_content) {
1542                    $colhead .= "<th class=\"pl plh " . $fdef->className;
1543                    if ($fdef->fold)
1544                        $colhead .= " fx" . $fdef->fold;
1545                    $colhead .= "\">";
1546                    if ($fdef->has_content)
1547                        $colhead .= $this->_field_title($fdef);
1548                    if ($titleextra && $fdef->className == "pl_title") {
1549                        $colhead .= $titleextra;
1550                        $titleextra = false;
1551                    }
1552                    $colhead .= "</th>";
1553                } else {
1554                    $colhead .= "<th";
1555                    if ($fdef->fold)
1556                        $colhead .= " class=\"fx{$fdef->fold}\"";
1557                    $colhead .= "></th>";
1558                }
1559            }
1560
1561            $colhead .= "</tr>\n";
1562
1563            if ($this->search->is_order_anno
1564                && isset($this->table_attr["data-drag-tag"])) {
1565                $drag_tag = $this->tagger->check($this->table_attr["data-drag-tag"]);
1566                if (strcasecmp($drag_tag, $this->search->is_order_anno) == 0
1567                    && $this->user->can_change_tag_anno($drag_tag)) {
1568                    $colhead .= "  <tr class=\"pl_headrow pl_annorow\" data-anno-tag=\"{$this->search->is_order_anno}\">";
1569                    if ($rstate->titlecol)
1570                        $colhead .= "<td class=\"plh\" colspan=\"$rstate->titlecol\"></td>";
1571                    $colhead .= "<td class=\"plh\" colspan=\"" . ($rstate->ncol - $rstate->titlecol) . "\"><a class=\"ui js-annotate-order\" href=\"\">Annotate order</a></td></tr>\n";
1572                }
1573            }
1574
1575            $colhead .= " </thead>\n";
1576        }
1577
1578        // table skeleton including fold classes
1579        $foldclasses = array();
1580        if ($this->foldable)
1581            $foldclasses = $this->_analyze_folds($rstate, $fieldDef);
1582        $enter = "<table class=\"pltable";
1583        if ($this->_table_class)
1584            $enter .= " " . $this->_table_class;
1585        if (get($options, "list"))
1586            $enter .= " has-hotlist has-fold";
1587        if (!empty($foldclasses))
1588            $enter .= " " . join(" ", $foldclasses);
1589        if ($this->_table_id)
1590            $enter .= "\" id=\"" . $this->_table_id;
1591        if (!empty($options["attributes"]))
1592            foreach ($options["attributes"] as $n => $v)
1593                $enter .= "\" $n=\"" . htmlspecialchars($v);
1594        if (get($options, "fold_session_prefix"))
1595            $enter .= "\" data-fold-session-prefix=\"" . htmlspecialchars($options["fold_session_prefix"]);
1596        if ($this->search->is_order_anno)
1597            $enter .= "\" data-order-tag=\"{$this->search->is_order_anno}";
1598        if ($this->groups)
1599            $enter .= "\" data-groups=\"" . htmlspecialchars(json_encode_browser($this->groups));
1600        foreach ($this->table_attr as $k => $v)
1601            $enter .= "\" $k=\"" . htmlspecialchars($v);
1602        if (get($options, "list"))
1603            $enter .= "\" data-hotlist=\"" . htmlspecialchars($this->session_list_object()->info_string());
1604        if ($this->sortable && ($url = $this->search->url_site_relative_raw())) {
1605            $url = Navigation::siteurl() . $url . (strpos($url, "?") ? "&" : "?") . "sort={sort}";
1606            $enter .= "\" data-sort-url-template=\"" . htmlspecialchars($url);
1607        }
1608        $enter .= "\">\n";
1609        if (self::$include_stash)
1610            $enter .= Ht::unstash();
1611        $rstate->table_start = $enter;
1612        $rstate->table_end = "</table>";
1613
1614        // maybe make columns, maybe not
1615        if ($this->_view_columns && !empty($this->ids)
1616            && $this->_column_split($rstate, $colhead, $body)) {
1617            $rstate->table_start = '<div class="plsplit_col_ctr_ctr"><div class="plsplit_col_ctr">' . $rstate->table_start;
1618            $rstate->table_end .= "</div></div>";
1619            $ncol = $rstate->split_ncol;
1620            $rstate->tbody_class = "pltable_split";
1621        } else {
1622            $rstate->thead = $colhead;
1623            $rstate->tbody_class = "pltable" . ($rstate->hascolors ? " pltable_colored" : "");
1624        }
1625        if ($this->has_editable_tags)
1626            $rstate->tbody_class .= " need-editable-tags";
1627
1628        // footer
1629        reset($fieldDef);
1630        if (current($fieldDef) instanceof SelectorPaperColumn
1631            && !get($options, "nofooter"))
1632            $tfoot .= $this->_footer($ncol, get_s($options, "footer_extra"));
1633        if ($tfoot)
1634            $rstate->tfoot = ' <tfoot class="pltable' . ($rstate->hascolors ? " pltable_colored" : "") . '">' . $tfoot . "</tfoot>\n";
1635
1636        // header scripts to set up delegations
1637        if ($this->_header_script)
1638            $rstate->thead .= '  ' . Ht::script($this->_header_script) . "\n";
1639
1640        $rstate->body_rows = $body;
1641        return $rstate;
1642    }
1643
1644    function table_html($report_id, $options = array()) {
1645        $render = $this->table_render($report_id, $options);
1646        if ($render->error)
1647            return $render->error;
1648        else
1649            return $render->table_start
1650                . ($render->thead ? : "")
1651                . $render->tbody_start()
1652                . join("", $render->body_rows)
1653                . "  </tbody>\n"
1654                . ($render->tfoot ? : "")
1655                . "</table>";
1656    }
1657
1658    function column_json($fieldId) {
1659        if (!$this->_prepare()
1660            || !($fdef = $this->find_column($fieldId)))
1661            return null;
1662
1663        // field is never folded, no sorting
1664        $this->set_view($fdef->name, true);
1665        assert(!$this->is_folded($fdef));
1666        $this->sorters = [];
1667
1668        // get rows
1669        $field_list = $this->_columns([$fdef->name], false);
1670        assert(count($field_list) === 1);
1671        $rows = $this->_rows($field_list);
1672        if ($rows === null)
1673            return null;
1674        $fdef = $field_list[0];
1675
1676        // turn off forceShow
1677        $overrides = $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT);
1678
1679        // output field data
1680        $data = array();
1681        if (($x = $fdef->header($this, false)))
1682            $data["{$fdef->name}.headerhtml"] = $x;
1683        $m = array();
1684        foreach ($rows as $row) {
1685            $this->_row_setup($row);
1686            list($empty, $content) = $this->_row_field_content($fdef, $row);
1687            $m[$row->paperId] = $content;
1688            foreach ($this->row_attr as $k => $v) {
1689                if (!isset($data["attr.$k"]))
1690                    $data["attr.$k"] = [];
1691                $data["attr.$k"][$row->paperId] = $v;
1692            }
1693        }
1694        $data["{$fdef->name}.html"] = $m;
1695
1696        // output statistics
1697        if ($fdef->has_statistics()) {
1698            $m = [];
1699            foreach (self::$stats as $stat)
1700                $m[ScoreInfo::$stat_keys[$stat]] = $fdef->statistic($this, $stat);
1701            $data["{$fdef->name}.stat.html"] = $m;
1702        }
1703        $this->mark_has($fdef->name, $fdef->has_content);
1704
1705        // restore forceShow
1706        $this->user->set_overrides($overrides);
1707        return $data;
1708    }
1709
1710    function text_json($fields) {
1711        if (!$this->_prepare())
1712            return null;
1713
1714        // get column list, check sort
1715        $field_list = $this->_columns($fields, false);
1716        $rows = $this->_rows($field_list);
1717        if ($rows === null)
1718            return null;
1719
1720        $x = array();
1721        foreach ($rows as $row) {
1722            $this->_row_setup($row);
1723            $p = array("id" => $row->paperId);
1724            foreach ($field_list as $fdef) {
1725                if ($fdef->viewable()
1726                    && !$fdef->content_empty($this, $row)
1727                    && ($text = $fdef->text($this, $row)) !== "")
1728                    $p[$fdef->name] = $text;
1729            }
1730            $x[$row->paperId] = (object) $p;
1731        }
1732
1733        return $x;
1734    }
1735
1736    private function _row_text_csv_data(PaperInfo $row, $fieldDef) {
1737        $csv = [];
1738        foreach ($fieldDef as $fdef) {
1739            $empty = $fdef->content_empty($this, $row);
1740            $c = $empty ? "" : $fdef->text($this, $row);
1741            if ($c !== "")
1742                $fdef->has_content = true;
1743            $csv[$fdef->name] = $c;
1744        }
1745        return $csv;
1746    }
1747
1748    private function _groups_for_csv($grouppos, &$csv) {
1749        for (; $grouppos < count($this->groups)
1750               && $this->groups[$grouppos]->pos < $this->count;
1751               ++$grouppos) {
1752            $ginfo = $this->groups[$grouppos];
1753            $csv["__precomment__"] = $ginfo->is_empty() ? "none" : $ginfo->heading;
1754        }
1755        return $grouppos;
1756    }
1757
1758    function text_csv($report_id, $options = array()) {
1759        if (!$this->_prepare($report_id))
1760            return null;
1761
1762        // get column list, check sort
1763        if (isset($options["field_list"]))
1764            $field_list = $options["field_list"];
1765        else
1766            $field_list = $this->_list_columns();
1767        if (!$field_list)
1768            return null;
1769        $field_list = $this->_columns($field_list, true);
1770        $rows = $this->_rows($field_list);
1771        if ($rows === null || empty($rows))
1772            return null;
1773
1774        // get field array
1775        $fieldDef = array();
1776        foreach ($field_list as $fdef)
1777            if ($fdef->viewable() && $fdef->is_visible
1778                && $fdef->header($this, true) != "")
1779                $fieldDef[] = $fdef;
1780
1781        // collect row data
1782        $body = array();
1783        $grouppos = empty($this->groups) ? -1 : 0;
1784        foreach ($rows as $row) {
1785            $this->_row_setup($row);
1786            $csv = $this->_row_text_csv_data($row, $fieldDef);
1787            if ($grouppos >= 0)
1788                $grouppos = $this->_groups_for_csv($grouppos, $csv);
1789            $body[] = $csv;
1790        }
1791
1792        // header cells
1793        $header = [];
1794        foreach ($fieldDef as $fdef)
1795            if ($fdef->has_content)
1796                $header[$fdef->name] = $fdef->header($this, true);
1797
1798        return [$header, $body];
1799    }
1800
1801
1802    function display($report_id) {
1803        if (!($this->_prepare($report_id)
1804              && ($field_list = $this->_list_columns())))
1805            return false;
1806        $field_list = $this->_columns($field_list, false);
1807        $res = [];
1808        if ($this->_view_force)
1809            $res["-3 force"] = "show:force";
1810        if ($this->_view_compact_columns)
1811            $res["-2 ccol"] = "show:ccol";
1812        else if ($this->_view_columns)
1813            $res["-2 col"] = "show:col";
1814        if ($this->_view_row_numbers)
1815            $res["-1 rownum"] = "show:rownum";
1816        if ($this->_view_statistics)
1817            $res["-1 statistics"] = "show:statistics";
1818        $x = [];
1819        foreach ($this->_view_fields as $k => $v) {
1820            $f = $this->_expand_view_column($k, false);
1821            foreach ($f as $col)
1822                if ($v === "edit"
1823                    || ($v && ($col->fold || !$col->is_visible))
1824                    || (!$v && !$col->fold && $col->is_visible)) {
1825                    if ($v !== "edit")
1826                        $v = $v ? "show" : "hide";
1827                    $key = ($col->position ? : 0) . " " . $col->name;
1828                    $res[$key] = $v . ":" . PaperSearch::escape_word($col->name);
1829                }
1830        }
1831        $anonau = get($this->_view_fields, "anonau") && $this->conf->submission_blindness() == Conf::BLIND_OPTIONAL;
1832        $aufull = get($this->_view_fields, "aufull");
1833        if (($anonau || $aufull) && !get($this->_view_fields, "au"))
1834            $res["150 authors"] = "hide:authors";
1835        if ($anonau)
1836            $res["151 anonau"] = "show:anonau";
1837        if ($aufull)
1838            $res["151 aufull"] = "show:aufull";
1839        ksort($res, SORT_NATURAL);
1840        $res = array_values($res);
1841        foreach ($this->sorters as $s) {
1842            $w = "sort:" . ($s->reverse ? "-" : "") . PaperSearch::escape_word($s->field->sort_name($s->score));
1843            if ($w !== "sort:id")
1844                $res[] = $w;
1845        }
1846        return join(" ", $res);
1847    }
1848    static function change_display(Contact $user, $report, $var = null, $val = null) {
1849        $pl = new PaperList(new PaperSearch($user, "NONE"), ["report" => $report, "sort" => true]);
1850        if ($var)
1851            $pl->set_view($var, $val);
1852        $user->conf->save_session("{$report}display", $pl->display("s"));
1853    }
1854}
1855