diff --git a/api.php b/api.php
index d6bad6d08..588d6aee0 100644
--- a/api.php
+++ b/api.php
@@ -1,125 +1,5 @@
false, "error" => "API function missing."]);
- exit;
- }
-}
-if ($_GET["fn"] === "deadlines") {
- $_GET["fn"] = "status";
-}
-if (!isset($_GET["p"])
- && ($p = Navigation::path_component(1, true))
- && ctype_digit($p)) {
- $_GET["p"] = $p;
-}
-
-// trackerstatus is a special case: prevent session creation
-if ($_GET["fn"] === "trackerstatus") {
- require_once("src/init.php");
- Contact::$no_main_user = true;
- require_once("src/initweb.php");
- MeetingTracker::trackerstatus_api(new Contact(null, $Conf));
- exit;
-}
-
-// initialization
-require_once("src/initweb.php");
-
-function handle_api(Conf $conf, Contact $me, Qrequest $qreq) {
- if ($qreq->base !== null) {
- $conf->set_siteurl($qreq->base);
- }
- if (!$me->has_account_here()
- && ($key = $me->capability("@kiosk"))) {
- $kiosks = $conf->setting_json("__tracker_kiosk") ? : (object) array();
- if (isset($kiosks->$key) && $kiosks->$key->update_at >= Conf::$now - 172800) {
- if ($kiosks->$key->update_at < Conf::$now - 3600) {
- $kiosks->$key->update_at = Conf::$now;
- $conf->save_setting("__tracker_kiosk", 1, $kiosks);
- }
- $me->tracker_kiosk_state = $kiosks->$key->show_papers ? 2 : 1;
- }
- }
- if ($qreq->p) {
- $conf->set_paper_request($qreq, $me);
- }
-
- // requests
- if ($conf->has_api($qreq->fn) || $me->is_disabled()) {
- $conf->call_api_exit($qreq->fn, $me, $qreq, $conf->paper);
- }
-
- if ($qreq->fn === "events") {
- if (!$me->is_reviewer()) {
- json_exit(403, ["ok" => false]);
- }
- $from = $qreq->from;
- if (!$from || !ctype_digit($from)) {
- $from = Conf::$now;
- }
- $when = $from;
- $rf = $conf->review_form();
- $events = new PaperEvents($me);
- $rows = [];
- $more = false;
- foreach ($events->events($when, 11) as $xr) {
- if (count($rows) == 10) {
- $more = true;
- } else {
- if ($xr->crow) {
- $rows[] = $xr->crow->unparse_flow_entry($me);
- } else {
- $rows[] = $rf->unparse_flow_entry($xr->prow, $xr->rrow, $me);
- }
- $when = $xr->eventTime;
- }
- }
- json_exit(["ok" => true, "from" => (int) $from, "to" => (int) $when - 1,
- "rows" => $rows, "more" => $more]);
- }
-
- // from here on: `status` and `track` requests
- $is_track = $qreq->fn === "track";
- if ($is_track) {
- MeetingTracker::track_api($me, $qreq); // may fall through to act like `status`
- } else if ($qreq->fn !== "status") {
- json_exit(404, "Unknown request “" . $qreq->fn . "”");
- }
-
- $j = $me->my_deadlines($conf->paper ? [$conf->paper] : []);
-
- if ($conf->paper && $me->can_view_tags($conf->paper)) {
- $pj = (object) ["pid" => $conf->paper->paperId];
- $conf->paper->add_tag_info_json($pj, $me);
- if (count((array) $pj) > 1) {
- $j->p = [$conf->paper->paperId => $pj];
- }
- }
-
- if ($is_track && ($new_trackerid = $qreq->annex("new_trackerid"))) {
- $j->new_trackerid = $new_trackerid;
- }
- $j->ok = true;
- json_exit($j);
-}
-
-handle_api(Conf::$main, Contact::$main_user, $Qreq);
+include("index.php");
diff --git a/assign.php b/assign.php
index bde1e36d6..142d57414 100644
--- a/assign.php
+++ b/assign.php
@@ -2,7 +2,8 @@
// assign.php -- HotCRP per-paper assignment/conflict management page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->email) {
$Me->escape();
}
diff --git a/autoassign.php b/autoassign.php
index 58c8cf4a6..afb958e37 100644
--- a/autoassign.php
+++ b/autoassign.php
@@ -2,7 +2,8 @@
// autoassign.php -- HotCRP automatic paper assignment page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->is_manager()) {
$Me->escape();
}
diff --git a/bulkassign.php b/bulkassign.php
index 653c6ede5..d7834f01b 100644
--- a/bulkassign.php
+++ b/bulkassign.php
@@ -2,7 +2,8 @@
// bulkassign.php -- HotCRP bulk paper assignment page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->is_manager()) {
$Me->escape();
}
diff --git a/buzzer.php b/buzzer.php
index 8c7632106..9bfa7f4b3 100644
--- a/buzzer.php
+++ b/buzzer.php
@@ -3,7 +3,8 @@
// Copyright (c) 2006-2020 Eddie Kohler; see LICENSE.
// First buzzer version by Nickolai B. Zeldovich
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
function kiosk_manager(Contact $user, Qrequest $qreq) {
$kiosks = (array) ($user->conf->setting_json("__tracker_kiosk") ? : array());
diff --git a/checkupdates.php b/checkupdates.php
index 67a01aca4..2a1d2470d 100644
--- a/checkupdates.php
+++ b/checkupdates.php
@@ -2,7 +2,8 @@
// checkupdates.php -- HotCRP update checker helper
// Copyright (c) 2006-2020 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
header("Content-Type: " . ($Qreq->text ? "text/plain" : "application/json"));
if ($Me->privChair
diff --git a/conflictassign.php b/conflictassign.php
index 85f755592..fb85916c9 100644
--- a/conflictassign.php
+++ b/conflictassign.php
@@ -2,7 +2,8 @@
// manualassign.php -- HotCRP chair's paper assignment page
// Copyright (c) 2006-2020 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->is_manager()) {
$Me->escape();
}
diff --git a/deadlines.php b/deadlines.php
index c7b37dc7d..8f456ddfa 100644
--- a/deadlines.php
+++ b/deadlines.php
@@ -1,136 +1,5 @@
contactId && $Me->is_disabled()) {
- $Viewer = new Contact(["email" => $Me->email], $Conf);
-}
-
-// *** NB If you change this script, also change the logic in index.php ***
-// *** that hides the link when there are no deadlines to show. ***
-
-// header and script
-$Conf->header("Deadlines", "deadlines");
-
-if ($Viewer->privChair) {
- echo "
As PC chair, you can hoturl("settings"), "\">change the deadlines .
\n";
-}
-
-echo "\n";
-
-
-function printDeadline($time, $phrase, $description) {
- global $Conf;
- echo "", $phrase, " : ", $Conf->unparse_time_long($time),
- $Conf->unparse_usertime_span($time), " \n",
- "", $description, ($description ? " " : ""), " ";
-}
-
-$dl = $Viewer->my_deadlines();
-
-// If you change these, also change Contact::has_reportable_deadline().
-if ($dl->sub->reg ?? false) {
- printDeadline($dl->sub->reg, $Conf->_("Registration deadline"),
- $Conf->_("You can register new submissions until this deadline."));
-}
-
-if ($dl->sub->update ?? false) {
- printDeadline($dl->sub->update, $Conf->_("Update deadline"),
- $Conf->_("You can update submissions and upload new versions until this deadline."));
-}
-
-if ($dl->sub->sub ?? false) {
- printDeadline($dl->sub->sub, $Conf->_("Submission deadline"),
- $Conf->_("Submissions must be ready by this deadline to be reviewed."));
-}
-
-if ($dl->resps ?? false) {
- foreach ($dl->resps as $rname => $dlr) {
- if (($dlr->open ?? false)
- && $dlr->open <= Conf::$now
- && ($dlr->done ?? false)) {
- if ($rname == 1) {
- printDeadline($dlr->done, $Conf->_("Response deadline"),
- $Conf->_("You can submit responses to the reviews until this deadline."));
- } else {
- printDeadline($dlr->done, $Conf->_("%s response deadline", $rname),
- $Conf->_("You can submit %s responses to the reviews until this deadline.", $rname));
- }
- }
- }
-}
-
-if (($dl->rev ?? false) && ($dl->rev->open ?? false)) {
- $dlbyround = [];
- $last_dlbyround = null;
- foreach ($Conf->defined_round_list() as $i => $round_name) {
- $isuf = $i ? "_$i" : "";
- $es = +$Conf->setting("extrev_soft$isuf");
- $eh = +$Conf->setting("extrev_hard$isuf");
- $ps = $ph = -1;
-
- $thisdl = [];
- if ($Viewer->isPC) {
- $ps = +$Conf->setting("pcrev_soft$isuf");
- $ph = +$Conf->setting("pcrev_hard$isuf");
- if ($ph && ($ph < Conf::$now || $ps < Conf::$now)) {
- $thisdl[] = "PH" . $ph;
- } else if ($ps) {
- $thisdl[] = "PS" . $ps;
- }
- }
- if ($es != $ps || $eh != $ph) {
- if ($eh && ($eh < Conf::$now || $es < Conf::$now)) {
- $thisdl[] = "EH" . $eh;
- } else if ($es) {
- $thisdl[] = "ES" . $es;
- }
- }
- if (count($thisdl)) {
- $dlbyround[$round_name] = $last_dlbyround = join(" ", $thisdl);
- }
- }
-
- $dlroundunify = true;
- foreach ($dlbyround as $x) {
- if ($x !== $last_dlbyround)
- $dlroundunify = false;
- }
-
- foreach ($dlbyround as $roundname => $dltext) {
- if ($dltext === "") {
- continue;
- }
- $suffix = $roundname === "" ? "" : "_$roundname";
- if ($dlroundunify) {
- $roundname = "";
- }
- foreach (explode(" ", $dltext) as $dldesc) {
- $dt = substr($dldesc, 0, 2);
- $dv = (int) substr($dldesc, 2);
- if ($dt === "PS") {
- printDeadline($dv, $Conf->_("%s review deadline", $roundname),
- $Conf->_("%s reviews are requested by this deadline.", $roundname));
- } else if ($dt === "PH") {
- printDeadline($dv, $Conf->_("%s review hard deadline", $roundname),
- $Conf->_("%s reviews must be submitted by this deadline.", $roundname));
- } else if ($dt === "ES") {
- printDeadline($dv, $Conf->_("%s external review deadline", $roundname),
- $Conf->_("%s reviews are requested by this deadline.", $roundname));
- } else if ($dt === "EH") {
- printDeadline($dv, $Conf->_("%s external review hard deadline", $roundname),
- $Conf->_("%s reviews must be submitted by this deadline.", $roundname));
- }
- }
- if ($dlroundunify) {
- break;
- }
- }
-}
-
-echo "\n";
-
-$Conf->footer();
+include("index.php");
diff --git a/doc.php b/doc.php
index d6ca7e28d..5e66b5193 100644
--- a/doc.php
+++ b/doc.php
@@ -1,167 +1,5 @@
-is_empty()) {
- $Me->escape();
- exit;
- } else if (str_starts_with($status, "5")) {
- $navpath = $Qreq->path();
- error_log($Conf->dbname . ": bad doc $status $msg " . json_encode($Qreq) . ($navpath ? " @$navpath" : "") . ($Me ? " {$Me->email}" : "") . (empty($_SERVER["HTTP_REFERER"]) ? "" : " R[" . $_SERVER["HTTP_REFERER"] . "]"));
- }
-
- header("HTTP/1.1 $status");
- if (isset($Qreq->fn)) {
- json_exit(MessageItem::make_error_json($msg));
- } else {
- $Conf->header("Download", null);
- $msg && Conf::msg_error($msg);
- $Conf->footer();
- exit;
- }
-}
-
-function document_history_element(DocumentInfo $doc, $active) {
- $pj = ["hash" => $doc->text_hash(), "at" => $doc->timestamp, "mimetype" => $doc->mimetype];
- if ($active ? $doc->size() : $doc->size) {
- $pj["size"] = $doc->size;
- }
- if ($doc->filename) {
- $pj["filename"] = $doc->filename;
- }
- if ($active) {
- $pj["active"] = true;
- }
- $pj["link"] = $doc->url(null, DocumentInfo::DOCURL_INCLUDE_TIME | Conf::HOTURL_RAW | Conf::HOTURL_ABSOLUTE);
- return (object) $pj;
-}
-
-function document_history(Contact $user, PaperInfo $prow, $dtype) {
- $docs = $prow->documents($dtype);
-
- $pjs = $actives = [];
- foreach ($docs as $doc) {
- $pjs[] = document_history_element($doc, true);
- $actives[$doc->paperStorageId] = true;
- }
-
- if ($user->can_view_document_history($prow)
- && $dtype >= DTYPE_FINAL) {
- $result = $prow->conf->qe("select paperId, paperStorageId, timestamp, mimetype, sha1, filename, infoJson, size from PaperStorage where paperId=? and documentType=? and filterType is null order by paperStorageId desc", $prow->paperId, $dtype);
- while (($doc = DocumentInfo::fetch($result, $prow->conf, $prow))) {
- if (!isset($actives[$doc->paperStorageId]))
- $pjs[] = document_history_element($doc, false);
- }
- Dbl::free($result);
- }
-
- return $pjs;
-}
-
-function document_download(Contact $user, $qreq) {
- try {
- $dr = new DocumentRequest($qreq, $qreq->path(), $user);
- } catch (Exception $e) {
- document_error("404 Not Found", htmlspecialchars($e->getMessage()));
- }
-
- if (($whyNot = $dr->perm_view_document($user))) {
- document_error(isset($whyNot["permission"]) ? "403 Forbidden" : "404 Not Found", $whyNot->unparse_html());
- }
- $prow = $dr->prow;
- $want_docid = $request_docid = (int) $dr->docid;
-
- // history
- if ($qreq->fn === "history") {
- json_exit(["ok" => true, "result" => document_history($user, $prow, $dr->dtype)]);
- }
-
- if (!isset($qreq->version) && isset($qreq->hash)) {
- $qreq->version = $qreq->hash;
- }
-
- // time
- if (isset($qreq->at) && !isset($qreq->version) && $dr->dtype >= DTYPE_FINAL) {
- if (ctype_digit($qreq->at)) {
- $time = intval($qreq->at);
- } else if (!($time = $user->conf->parse_time($qreq->at))) {
- $time = Conf::$now;
- }
- $want_pj = null;
- foreach (document_history($user, $prow, $dr->dtype) as $pj) {
- if ($want_pj && $want_pj->at <= $time && $pj->at < $want_pj->at) {
- break;
- } else {
- $want_pj = $pj;
- }
- }
- if ($want_pj) {
- $qreq->version = $want_pj->hash;
- }
- }
-
- // version
- if (isset($qreq->version) && $dr->dtype >= DTYPE_FINAL) {
- $version_hash = Filer::hash_as_binary(trim($qreq->version));
- if (!$version_hash) {
- document_error("404 Not Found", "No such version.");
- }
- $want_docid = $user->conf->fetch_ivalue("select max(paperStorageId) from PaperStorage where paperId=? and documentType=? and sha1=? and filterType is null", $dr->paperId, $dr->dtype, $version_hash);
- if ($want_docid !== null && $user->can_view_document_history($prow)) {
- $request_docid = $want_docid;
- }
- }
-
- if ($dr->attachment && !$request_docid) {
- $doc = $prow->attachment($dr->dtype, $dr->attachment);
- } else {
- $doc = $prow->document($dr->dtype, $request_docid);
- }
- if ($want_docid !== 0 && (!$doc || $doc->paperStorageId !== $want_docid)) {
- document_error("404 Not Found", "No such version.");
- } else if (!$doc || $doc->paperStorageId <= 1) {
- document_error("404 Not Found", "No such " . ($dr->attachment ? "attachment" : "document") . " “" . htmlspecialchars($dr->req_filename) . "”.");
- }
-
- // pass through filters
- foreach ($dr->filters as $filter) {
- $doc = $filter->exec($doc) ?? $doc;
- }
-
- // check for contents request
- if ($qreq->fn === "listing" || $qreq->fn === "consolidatedlisting") {
- if (!$doc->is_archive()) {
- json_exit(MessageItem::make_error_json("That file is not an archive."));
- } else if (($listing = $doc->archive_listing(65536)) === false) {
- json_exit(MessageItem::make_error_json($doc->error ? $doc->error_html : "Internal error."));
- } else {
- $listing = ArchiveInfo::clean_archive_listing($listing);
- if ($qreq->fn === "consolidatedlisting") {
- $listing = join(", ", ArchiveInfo::consolidate_archive_listing($listing));
- }
- json_exit(["ok" => true, "result" => $listing]);
- }
- }
-
- // serve document
- session_write_close(); // to allow concurrent clicks
- $opts = ["attachment" => cvtint($qreq->save) > 0];
- if ($doc->has_hash() && ($x = $qreq->hash) && $doc->check_text_hash($x)) {
- $opts["cacheable"] = true;
- }
- if ($doc->download(DocumentRequest::add_connection_options($opts))) {
- DocumentInfo::log_download_activity([$doc], $user);
- } else {
- document_error("500 Server Error", $doc->error_html);
- }
- exit;
-}
-
-$Me->add_overrides(Contact::OVERRIDE_CONFLICT);
-document_download($Me, $Qreq);
+include("index.php");
diff --git a/etc/apifunctions.json b/etc/apifunctions.json
index e1e43148a..55df482c6 100644
--- a/etc/apifunctions.json
+++ b/etc/apifunctions.json
@@ -37,6 +37,10 @@
"name": "decision", "paper": true, "get": true, "post": true,
"function": "Decision_API::run"
},
+ {
+ "name": "events", "get": true,
+ "function": "Events_API::run"
+ },
{
"name": "fieldtext", "get": true,
"function": "Search_API::fieldtext"
diff --git a/etc/pagepartials.json b/etc/pagepartials.json
deleted file mode 100644
index 378699539..000000000
--- a/etc/pagepartials.json
+++ /dev/null
@@ -1,212 +0,0 @@
-[
- {
- "name": "__footer", "render_function": "*Conf::footer"
- },
-
-
- { "name": "index", "alias": "home" },
-
-
- { "name": "home", "allow_disabled": true },
- {
- "name": "home/disabled", "position": 10,
- "request_function": "Home_Partial::disabled_request"
- },
- {
- "name": "home/profile_redirect", "position": 100,
- "request_function": "Home_Partial::profile_redirect_request"
- },
- {
- "name": "home/admin", "position": 900, "allow_if": "chair",
- "allow_request_if": ["getpost", "req.clearbug req.clearnewpcrev"],
- "request_function": "AdminHome_Partial::check_admin",
- "render_function": "AdminHome_Partial::render"
- },
-
- {
- "name": "home/head", "position": 1000,
- "render_function": "*Home_Partial::render_head"
- },
- {
- "name": "home/message", "position": 1100,
- "render_function": "*Home_Partial::render_message"
- },
- {
- "name": "home/welcome", "position": 1200, "allow_if": "!pc",
- "render_function": "*Home_Partial::render_welcome"
- },
- {
- "name": "home/content", "position": 1500,
- "render_function": "Home_Partial::render_content"
- },
-
- {
- "name": "home/sidebar/admin", "position": 100, "allow_if": "manager",
- "render_function": "Home_Partial::render_admin_sidebar"
- },
- {
- "name": "home/sidebar/admin/settings", "position": 10, "allow_if": "chair",
- "render_function": "Home_Partial::render_admin_settings"
- },
- {
- "name": "home/sidebar/admin/users", "position": 20, "allow_if": "manager",
- "render_function": "Home_Partial::render_admin_users"
- },
- {
- "name": "home/sidebar/admin/assignments", "position": 30, "allow_if": "manager",
- "render_function": "Home_Partial::render_admin_assignments"
- },
- {
- "name": "home/sidebar/admin/mail", "position": 40, "allow_if": "manager",
- "render_function": "Home_Partial::render_admin_mail"
- },
- {
- "name": "home/sidebar/admin/log", "position": 50, "allow_if": "manager",
- "render_function": "Home_Partial::render_admin_log"
- },
- {
- "name": "home/sidebar/info", "position": 200,
- "render_function": "Home_Partial::render_info_sidebar"
- },
- [ "home/sidebar/info/deadline", 10, "Home_Partial::render_info_deadline" ],
- [ "home/sidebar/info/pc", 20, "Home_Partial::render_info_pc" ],
- [ "home/sidebar/info/site", 30, "Home_Partial::render_info_site" ],
- {
- "name": "home/sidebar/info/accepted", "position": 40,
- "allow_if": "conf.time_all_author_view_decision",
- "render_function": "Home_Partial::render_info_accepted"
- },
-
-
- [ "home/main/signin", 3000, "*Home_Partial::render_signin" ],
- {
- "name": "home/main/search", "position": 4000,
- "render_function": "*Home_Partial::render_search"
- },
- {
- "name": "home/main/review_requests", "position": 4500, "allow_if": "reviewer",
- "render_function": "*Home_Partial::render_review_requests"
- },
- {
- "name": "home/main/reviews", "position": 5000, "allow_if": "reviewer",
- "render_function": "*Home_Partial::render_reviews"
- },
- {
- "name": "home/main/submissions", "position": 7000,
- "render_function": "*Home_Partial::render_submissions"
- },
- {
- "name": "home/main/review_tokens", "position": 10000,
- "render_function": "*Home_Partial::render_review_tokens"
- },
-
-
- { "name": "newaccount", "allow_disabled": true },
- {
- "name": "newaccount/request", "position": 100,
- "allow_request_if": "anypost",
- "request_function": "*Signin_Partial::create_request"
- },
- [ "newaccount/head", 1000, "Signin_Partial::render_newaccount_head" ],
- [ "newaccount/message", 2000, "home/message" ],
- [ "newaccount/welcome", 2500, "home/welcome" ],
- [ "newaccount/body", 3000, "Signin_Partial::render_newaccount_body" ],
- [ "newaccount/form/description", 10, "Signin_Partial::render_newaccount_form_description" ],
- [ "newaccount/form/email", 20, "Signin_Partial::render_newaccount_form_email" ],
- [ "newaccount/form/actions", 100, "Signin_Partial::render_newaccount_form_actions" ],
-
-
- { "name": "signin", "allow_disabled": true },
- {
- "name": "signin/request", "position": 100,
- "allow_request_if": "anypost",
- "request_function": "Signin_Partial::signin_request"
- },
- {
- "name": "signin/request/basic", "position": 100,
- "signin_function": "Signin_Partial::signin_request_basic"
- },
- {
- "name": "signin/request/success", "position": 100000,
- "signin_function": "Signin_Partial::signin_request_success"
- },
- [ "signin/head", 1000, "Signin_Partial::render_signin_head" ],
- [ "signin/message", 2000, "home/message" ],
- [ "signin/welcome", 2500, "home/welcome" ],
- [ "signin/body", 3000, "Signin_Partial::render_signin_form" ],
- [ "signin/form/description", 10, "Signin_Partial::render_signin_form_description" ],
- [ "signin/form/email", 20, "Signin_Partial::render_signin_form_email" ],
- [ "signin/form/password", 30, "Signin_Partial::render_signin_form_password" ],
- [ "signin/form/actions", 100, "Signin_Partial::render_signin_form_actions" ],
- [ "signin/form/create", 150, "Signin_Partial::render_signin_form_create" ],
-
-
- { "name": "signout", "allow_disabled": true },
- {
- "name": "signout/request", "position": 100,
- "allow_request_if": "anypost",
- "request_function": "Signin_Partial::signout_request"
- },
- [ "signout/head", 1000, "Signin_Partial::render_signout_head" ],
- [ "signout/body", 3000, "Signin_Partial::render_signout_body" ],
-
-
- { "name": "forgotpassword", "allow_disabled": true },
- {
- "name": "forgotpassword/request", "position": 100,
- "allow_request_if": "anypost",
- "request_function": "Signin_Partial::forgot_request"
- },
- [ "forgotpassword/head", 1000, "Signin_Partial::render_forgot_head" ],
- [ "forgotpassword/body", 3000, "Signin_Partial::render_forgot_body" ],
- [ "forgotpassword/form/description", 10, "Signin_Partial::render_forgot_form_description" ],
- [ "forgotpassword/form/email", 20, "Signin_Partial::render_forgot_form_email" ],
- [ "forgotpassword/form/actions", 100, "*Signin_Partial::render_forgot_form_actions" ],
- {
- "name": "forgotpassword/externallogin", "position": false,
- "render_function": "Signin_Partial::forgot_externallogin_message"
- },
-
-
- { "name": "resetpassword", "allow_disabled": true },
- {
- "name": "resetpassword/request", "position": 100,
- "allow_any_request": true,
- "request_function": "*Signin_Partial::reset_request"
- },
- [ "resetpassword/head", 1000, "Signin_Partial::render_reset_head" ],
- [ "resetpassword/message", 2000, "home/message" ],
- [ "resetpassword/welcome", 2500, "home/welcome" ],
- [ "resetpassword/body", 3000, "*Signin_Partial::render_reset_body" ],
- [ "resetpassword/form/description", 10, "Signin_Partial::render_reset_form_description" ],
- [ "resetpassword/form/email", 20, "*Signin_Partial::render_reset_form_email" ],
- [ "resetpassword/form/autopassword", 29, "Signin_Partial::render_reset_form_autopassword" ],
- [ "resetpassword/form/password", 30, "Signin_Partial::render_reset_form_password" ],
- [ "resetpassword/form/actions", 100, "forgotpassword/form/actions" ],
-
-
- { "name": "api", "allow_disabled": true },
- { "name": "assign", "render_php": "assign.php" },
- { "name": "autoassign", "render_php": "autoassign.php" },
- { "name": "bulkassign", "render_php": "bulkassign.php" },
- { "name": "buzzer", "render_php": "buzzer.php" },
- { "name": "checkupdates", "render_php": "checkupdates.php" },
- { "name": "conflictassign", "render_php": "conflictassign.php" },
- { "name": "deadlines", "render_php": "deadlines.php", "allow_disabled": true },
- { "name": "doc", "render_php": "doc.php" },
- { "name": "graph", "render_php": "graph.php" },
- { "name": "help", "render_php": "help.php" },
- { "name": "log", "render_php": "log.php" },
- { "name": "mail", "render_php": "mail.php" },
- { "name": "manualassign", "render_php": "manualassign.php" },
- { "name": "mergeaccounts", "render_php": "mergeaccounts.php" },
- { "name": "offline", "render_php": "offline.php" },
- { "name": "paper", "render_php": "paper.php" },
- { "name": "profile", "render_php": "profile.php" },
- { "name": "review", "render_php": "review.php" },
- { "name": "reviewprefs", "render_php": "reviewprefs.php" },
- { "name": "scorechart", "render_php": "scorechart.php" },
- { "name": "search", "render_php": "search.php" },
- { "name": "settings", "render_php": "settings.php" },
- { "name": "users", "render_php": "users.php", "allow_disabled": true }
-]
diff --git a/etc/pages.json b/etc/pages.json
new file mode 100644
index 000000000..c2ff11072
--- /dev/null
+++ b/etc/pages.json
@@ -0,0 +1,212 @@
+[
+ {
+ "name": "__footer", "render_function": "*Conf::footer"
+ },
+
+
+ { "name": "index", "alias": "home" },
+
+
+ { "name": "home", "allow_disabled": true },
+ {
+ "name": "home/disabled", "position": 10,
+ "request_function": "Home_Page::disabled_request"
+ },
+ {
+ "name": "home/profile_redirect", "position": 100,
+ "request_function": "Home_Page::profile_redirect_request"
+ },
+ {
+ "name": "home/admin", "position": 900, "allow_if": "chair",
+ "allow_request_if": ["getpost", "req.clearbug req.clearnewpcrev"],
+ "request_function": "AdminHome_Page::check_admin",
+ "render_function": "AdminHome_Page::render"
+ },
+
+ {
+ "name": "home/head", "position": 1000,
+ "render_function": "*Home_Page::render_head"
+ },
+ {
+ "name": "home/message", "position": 1100,
+ "render_function": "*Home_Page::render_message"
+ },
+ {
+ "name": "home/welcome", "position": 1200, "allow_if": "!pc",
+ "render_function": "*Home_Page::render_welcome"
+ },
+ {
+ "name": "home/content", "position": 1500,
+ "render_function": "Home_Page::render_content"
+ },
+
+ {
+ "name": "home/sidebar/admin", "position": 100, "allow_if": "manager",
+ "render_function": "Home_Page::render_admin_sidebar"
+ },
+ {
+ "name": "home/sidebar/admin/settings", "position": 10, "allow_if": "chair",
+ "render_function": "Home_Page::render_admin_settings"
+ },
+ {
+ "name": "home/sidebar/admin/users", "position": 20, "allow_if": "manager",
+ "render_function": "Home_Page::render_admin_users"
+ },
+ {
+ "name": "home/sidebar/admin/assignments", "position": 30, "allow_if": "manager",
+ "render_function": "Home_Page::render_admin_assignments"
+ },
+ {
+ "name": "home/sidebar/admin/mail", "position": 40, "allow_if": "manager",
+ "render_function": "Home_Page::render_admin_mail"
+ },
+ {
+ "name": "home/sidebar/admin/log", "position": 50, "allow_if": "manager",
+ "render_function": "Home_Page::render_admin_log"
+ },
+ {
+ "name": "home/sidebar/info", "position": 200,
+ "render_function": "Home_Page::render_info_sidebar"
+ },
+ [ "home/sidebar/info/deadline", 10, "Home_Page::render_info_deadline" ],
+ [ "home/sidebar/info/pc", 20, "Home_Page::render_info_pc" ],
+ [ "home/sidebar/info/site", 30, "Home_Page::render_info_site" ],
+ {
+ "name": "home/sidebar/info/accepted", "position": 40,
+ "allow_if": "conf.time_all_author_view_decision",
+ "render_function": "Home_Page::render_info_accepted"
+ },
+
+
+ [ "home/main/signin", 3000, "*Home_Page::render_signin" ],
+ {
+ "name": "home/main/search", "position": 4000,
+ "render_function": "*Home_Page::render_search"
+ },
+ {
+ "name": "home/main/review_requests", "position": 4500, "allow_if": "reviewer",
+ "render_function": "*Home_Page::render_review_requests"
+ },
+ {
+ "name": "home/main/reviews", "position": 5000, "allow_if": "reviewer",
+ "render_function": "*Home_Page::render_reviews"
+ },
+ {
+ "name": "home/main/submissions", "position": 7000,
+ "render_function": "*Home_Page::render_submissions"
+ },
+ {
+ "name": "home/main/review_tokens", "position": 10000,
+ "render_function": "*Home_Page::render_review_tokens"
+ },
+
+
+ { "name": "newaccount", "allow_disabled": true },
+ {
+ "name": "newaccount/request", "position": 100,
+ "allow_request_if": "anypost",
+ "request_function": "*Signin_Page::create_request"
+ },
+ [ "newaccount/head", 1000, "Signin_Page::render_newaccount_head" ],
+ [ "newaccount/message", 2000, "home/message" ],
+ [ "newaccount/welcome", 2500, "home/welcome" ],
+ [ "newaccount/body", 3000, "Signin_Page::render_newaccount_body" ],
+ [ "newaccount/form/description", 10, "Signin_Page::render_newaccount_form_description" ],
+ [ "newaccount/form/email", 20, "Signin_Page::render_newaccount_form_email" ],
+ [ "newaccount/form/actions", 100, "Signin_Page::render_newaccount_form_actions" ],
+
+
+ { "name": "signin", "allow_disabled": true },
+ {
+ "name": "signin/request", "position": 100,
+ "allow_request_if": "anypost",
+ "request_function": "Signin_Page::signin_request"
+ },
+ {
+ "name": "signin/request/basic", "position": 100,
+ "signin_function": "Signin_Page::signin_request_basic"
+ },
+ {
+ "name": "signin/request/success", "position": 100000,
+ "signin_function": "Signin_Page::signin_request_success"
+ },
+ [ "signin/head", 1000, "Signin_Page::render_signin_head" ],
+ [ "signin/message", 2000, "home/message" ],
+ [ "signin/welcome", 2500, "home/welcome" ],
+ [ "signin/body", 3000, "Signin_Page::render_signin_form" ],
+ [ "signin/form/description", 10, "Signin_Page::render_signin_form_description" ],
+ [ "signin/form/email", 20, "Signin_Page::render_signin_form_email" ],
+ [ "signin/form/password", 30, "Signin_Page::render_signin_form_password" ],
+ [ "signin/form/actions", 100, "Signin_Page::render_signin_form_actions" ],
+ [ "signin/form/create", 150, "Signin_Page::render_signin_form_create" ],
+
+
+ { "name": "signout", "allow_disabled": true },
+ {
+ "name": "signout/request", "position": 100,
+ "allow_request_if": "anypost",
+ "request_function": "Signin_Page::signout_request"
+ },
+ [ "signout/head", 1000, "Signin_Page::render_signout_head" ],
+ [ "signout/body", 3000, "Signin_Page::render_signout_body" ],
+
+
+ { "name": "forgotpassword", "allow_disabled": true },
+ {
+ "name": "forgotpassword/request", "position": 100,
+ "allow_request_if": "anypost",
+ "request_function": "Signin_Page::forgot_request"
+ },
+ [ "forgotpassword/head", 1000, "Signin_Page::render_forgot_head" ],
+ [ "forgotpassword/body", 3000, "Signin_Page::render_forgot_body" ],
+ [ "forgotpassword/form/description", 10, "Signin_Page::render_forgot_form_description" ],
+ [ "forgotpassword/form/email", 20, "Signin_Page::render_forgot_form_email" ],
+ [ "forgotpassword/form/actions", 100, "*Signin_Page::render_forgot_form_actions" ],
+ {
+ "name": "forgotpassword/externallogin", "position": false,
+ "render_function": "Signin_Page::forgot_externallogin_message"
+ },
+
+
+ { "name": "resetpassword", "allow_disabled": true },
+ {
+ "name": "resetpassword/request", "position": 100,
+ "allow_any_request": true,
+ "request_function": "*Signin_Page::reset_request"
+ },
+ [ "resetpassword/head", 1000, "Signin_Page::render_reset_head" ],
+ [ "resetpassword/message", 2000, "home/message" ],
+ [ "resetpassword/welcome", 2500, "home/welcome" ],
+ [ "resetpassword/body", 3000, "*Signin_Page::render_reset_body" ],
+ [ "resetpassword/form/description", 10, "Signin_Page::render_reset_form_description" ],
+ [ "resetpassword/form/email", 20, "*Signin_Page::render_reset_form_email" ],
+ [ "resetpassword/form/autopassword", 29, "Signin_Page::render_reset_form_autopassword" ],
+ [ "resetpassword/form/password", 30, "Signin_Page::render_reset_form_password" ],
+ [ "resetpassword/form/actions", 100, "forgotpassword/form/actions" ],
+
+
+ { "name": "api", "render_function": "API_Page::go", "allow_disabled": true },
+ { "name": "assign", "render_php": "assign.php" },
+ { "name": "autoassign", "render_php": "autoassign.php" },
+ { "name": "bulkassign", "render_php": "bulkassign.php" },
+ { "name": "buzzer", "render_php": "buzzer.php" },
+ { "name": "checkupdates", "render_php": "checkupdates.php" },
+ { "name": "conflictassign", "render_php": "conflictassign.php" },
+ { "name": "deadlines", "render_function": "Deadlines_Page::go", "allow_disabled": true },
+ { "name": "doc", "render_function": "Doc_Page::go" },
+ { "name": "graph", "render_php": "graph.php" },
+ { "name": "help", "render_function": "Help_Page::go" },
+ { "name": "log", "render_function": "Log_Page::go" },
+ { "name": "mail", "render_php": "mail.php" },
+ { "name": "manualassign", "render_php": "manualassign.php" },
+ { "name": "mergeaccounts", "render_php": "mergeaccounts.php" },
+ { "name": "offline", "render_php": "offline.php" },
+ { "name": "paper", "render_function": "Paper_Page::go" },
+ { "name": "profile", "render_php": "profile.php" },
+ { "name": "review", "render_function": "Review_Page::go" },
+ { "name": "reviewprefs", "render_function": "ReviewPrefs_Page::go" },
+ { "name": "scorechart", "render_php": "scorechart.php" },
+ { "name": "search", "render_function": "Search_Page::go" },
+ { "name": "settings", "render_function": "Settings_Page::go" },
+ { "name": "users", "render_php": "users.php", "allow_disabled": true }
+]
diff --git a/graph.php b/graph.php
index f66c0ac3f..f22ecf7e9 100644
--- a/graph.php
+++ b/graph.php
@@ -2,7 +2,8 @@
// graph.php -- HotCRP review preference graph drawing page
// Copyright (c) 2006-2020 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
$Graph = $Qreq->g;
if (!$Graph
diff --git a/help.php b/help.php
index d72989ef4..e6191738e 100644
--- a/help.php
+++ b/help.php
@@ -1,75 +1,5 @@
opt("helpTopics"));
-
-if (!$Qreq->t && preg_match('/\A\/\w+\/*\z/i', $Qreq->path())) {
- $Qreq->t = $Qreq->path_component(0);
-}
-$topic = $Qreq->t ? : "topics";
-$want_topic = $help_topics->canonical_group($topic);
-if (!$want_topic) {
- $want_topic = "topics";
-}
-if ($want_topic !== $topic) {
- $Conf->redirect_self($Qreq, ["t" => $want_topic]);
-}
-$topicj = $help_topics->get($topic);
-
-$Conf->header("Help", "help", ["title_div" => ' ', "body_class" => "leftmenu"]);
-
-$hth = new HelpRenderer($help_topics, $Me);
-
-
-/** @param HelpRenderer $hth */
-function show_help_topics($hth) {
- echo "\n";
- foreach ($hth->groups() as $ht) {
- if ($ht->name !== "topics" && isset($ht->title)) {
- echo 'name"), '">', $ht->title, ' ';
- if (isset($ht->description)) {
- echo '', $ht->description ?? "", ' ';
- }
- echo "\n";
- }
- }
- echo " \n";
-}
-
-
-echo '\n",
- '',
- '';
-$hth->render_group($topic, true);
-echo " \n";
-
-
-$Conf->footer();
+include("index.php");
diff --git a/index.php b/index.php
index 7618ca3b7..e4e830695 100644
--- a/index.php
+++ b/index.php
@@ -3,23 +3,12 @@
// Copyright (c) 2006-2020 Eddie Kohler; see LICENSE.
require_once("lib/navigation.php");
-$nav = Navigation::get();
-// handle `/u/USERINDEX/`
-if ($nav->page === "u") {
- $unum = $nav->path_component(0);
- if ($unum !== false && ctype_digit($unum)) {
- if (!$nav->shift_path_components(2)) {
- // redirect `/u/USERINDEX` => `/u/USERINDEX/`
- Navigation::redirect_absolute("{$nav->server}{$nav->base_path}u/{$unum}/{$nav->query}");
- }
- } else {
- // redirect `/u/XXXX` => `/`
- Navigation::redirect_absolute("{$nav->server}{$nav->base_path}{$nav->query}");
- }
-}
-
-function gx_call_requests(Conf $conf, Contact $user, Qrequest $qreq, $group, GroupedExtensions $gx) {
+/** @param Contact $user
+ * @param Qrequest $qreq
+ * @param string $group
+ * @param GroupedExtensions $gx */
+function gx_call_requests($user, $qreq, $group, $gx) {
$gx->add_xt_checker([$qreq, "xt_allow"]);
$reqgj = [];
$not_allowed = false;
@@ -31,7 +20,7 @@ function gx_call_requests(Conf $conf, Contact $user, Qrequest $qreq, $group, Gro
}
}
if ($not_allowed && $qreq->is_post() && !$qreq->valid_token()) {
- $conf->msg($conf->_i("badpost"), 2);
+ $user->conf->msg($user->conf->_i("badpost"), 2);
}
foreach ($reqgj as $gj) {
if ($gx->call_function($gj->request_function, $gj) === false) {
@@ -40,25 +29,61 @@ function gx_call_requests(Conf $conf, Contact $user, Qrequest $qreq, $group, Gro
}
}
+/** @param Contact $user
+ * @param Qrequest $qreq
+ * @param NavigationState $nav */
+function gx_go($user, $qreq, $nav) {
+ try {
+ $gx = $user->conf->page_partials($user);
+ $pagej = $gx->get($nav->page);
+ if (!$pagej || str_starts_with($pagej->name, "__")) {
+ header("HTTP/1.0 404 Not Found");
+ } else if ($user->is_disabled() && !($pagej->allow_disabled ?? false)) {
+ header("HTTP/1.0 403 Forbidden");
+ } else if (isset($pagej->render_php)) {
+ return $pagej->render_php;
+ } else {
+ $gx->set_root($pagej->group)->set_context_args([$user, $qreq, $gx]);
+ gx_call_requests($user, $qreq, $pagej->group, $gx);
+ $gx->render_group($pagej->group, true);
+ }
+ } catch (Redirection $redir) {
+ $user->conf->redirect($redir->url);
+ } catch (JsonCompletion $jc) {
+ $jc->result->emit($qreq->valid_token());
+ } catch (PageCompletion $pc) {
+ }
+}
+
+$nav = Navigation::get();
+
+// handle `/u/USERINDEX/`
+if ($nav->page === "u") {
+ $unum = $nav->path_component(0);
+ if ($unum !== false && ctype_digit($unum)) {
+ if (!$nav->shift_path_components(2)) {
+ // redirect `/u/USERINDEX` => `/u/USERINDEX/`
+ Navigation::redirect_absolute("{$nav->server}{$nav->base_path}u/{$unum}/{$nav->query}");
+ }
+ } else {
+ // redirect `/u/XXXX` => `/`
+ Navigation::redirect_absolute("{$nav->server}{$nav->base_path}{$nav->query}");
+ }
+}
+
// handle special pages
-if ($nav->page === "images" || $nav->page === "scripts" || $nav->page === "stylesheets") {
+if ($nav->page === "api") {
+ require_once("src/init.php");
+ API_Page::go_nav($nav, Conf::$main);
+} else if ($nav->page === "images" || $nav->page === "scripts" || $nav->page === "stylesheets") {
$_GET["file"] = $nav->page . $nav->path;
include("cacheable.php");
} else if ($nav->page === "api" || $nav->page === "cacheable" || $nav->page === "scorechart") {
include("{$nav->page}.php");
} else {
- require_once("src/initweb.php");
- $gx = $Conf->page_partials($Me);
- $pagej = $gx->get($nav->page);
- if (!$pagej || str_starts_with($pagej->name, "__")) {
- header("HTTP/1.0 404 Not Found");
- } else if ($Me->is_disabled() && !($pagej->allow_disabled ?? false)) {
- header("HTTP/1.0 403 Forbidden");
- } else if (isset($pagej->render_php)) {
- include($pagej->render_php);
- } else {
- $gx->set_root($pagej->group)->set_context_args([$Me, $Qreq, $gx]);
- gx_call_requests($Conf, $Me, $Qreq, $pagej->group, $gx);
- $gx->render_group($pagej->group, true);
+ require_once("src/init.php");
+ initialize_request();
+ if (($s = gx_go($Me, $Qreq, $nav))) {
+ include($s);
}
}
diff --git a/lib/base.php b/lib/base.php
index 3c065dd9a..9f7cf49ca 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -106,14 +106,14 @@ function cleannl($text) {
function commajoin($what, $joinword = "and") {
$what = array_values($what);
$c = count($what);
- if ($c == 0) {
+ if ($c === 0) {
return "";
- } else if ($c == 1) {
+ } else if ($c === 1) {
return $what[0];
- } else if ($c == 2) {
+ } else if ($c === 2) {
return $what[0] . " " . $joinword . " " . $what[1];
} else {
- return join(", ", array_slice($what, 0, -1)) . ", " . $joinword . " " . $what[count($what) - 1];
+ return join(", ", array_slice($what, 0, -1)) . ", " . $joinword . " " . $what[$c - 1];
}
}
diff --git a/lib/qrequest.php b/lib/qrequest.php
index d2cfa3c46..1ef3241cd 100644
--- a/lib/qrequest.php
+++ b/lib/qrequest.php
@@ -19,6 +19,10 @@ class Qrequest implements ArrayAccess, IteratorAggregate, Countable, JsonSeriali
private $____path;
/** @var ?string */
private $____referrer;
+
+ /** @var Qrequest */
+ static public $main_request;
+
/** @param string $method */
function __construct($method, $data = null) {
$this->____method = $method;
@@ -422,6 +426,7 @@ static function make_global() : Qrequest {
if (!empty($errors)) {
$qreq->set_annex("upload_errors", $errors);
}
+ Qrequest::$main_request = $qreq;
return $qreq;
}
}
diff --git a/log.php b/log.php
index fb674a132..5552a901f 100644
--- a/log.php
+++ b/log.php
@@ -1,895 +1,5 @@
is_manager()) {
- $Me->escape();
-}
-
-unset($Qreq->forceShow, $_GET["forceShow"], $_POST["forceShow"]);
-$nlinks = 6;
-
-$page = $Qreq->page;
-if ($page === "earliest") {
- $page = false;
-} else {
- $page = cvtint($page, -1);
- if ($page <= 0)
- $page = 1;
-}
-
-$count = 50;
-if (isset($Qreq->n) && trim($Qreq->n) !== "") {
- $count = cvtint($Qreq->get("n", 50), -1);
- if ($count <= 0) {
- $count = 50;
- Ht::error_at("n", "Show records: Expected a number greater than 0.");
- }
-}
-$count = min($count, 200);
-
-$Qreq->q = trim((string) $Qreq->q);
-$Qreq->p = trim((string) $Qreq->p);
-if (isset($Qreq->acct) && !isset($Qreq->u)) {
- $Qreq->u = $Qreq->acct;
-}
-$Qreq->u = trim((string) $Qreq->u);
-$Qreq->date = trim($Qreq->get("date", "now"));
-
-$wheres = array();
-
-$include_pids = null;
-if ($Qreq->p !== "") {
- $Search = new PaperSearch($Me, ["t" => "all", "q" => $Qreq->p]);
- $Search->set_allow_deleted(true);
- $include_pids = $Search->paper_ids();
- foreach ($Search->problem_texts() as $w) {
- Ht::warning_at("p", $w);
- }
- if (!empty($include_pids)) {
- $where = array();
- foreach ($include_pids as $p) {
- $where[] = "paperId=$p";
- $where[] = "action like '%(papers% $p,%'";
- $where[] = "action like '%(papers% $p)%'";
- }
- $wheres[] = "(" . join(" or ", $where) . ")";
- $include_pids = array_flip($include_pids);
- } else {
- if (!$Search->has_problem()) {
- Ht::warning_at("p", "No papers match that search.");
- }
- $wheres[] = "false";
- }
-}
-
-if ($Qreq->u !== "") {
- $ids = array();
- $accts = new SearchSplitter($Qreq->u);
- while (($word = $accts->shift()) !== "") {
- $flags = ContactSearch::F_TAG | ContactSearch::F_USER | ContactSearch::F_ALLOW_DELETED;
- if (substr($word, 0, 1) === "\"") {
- $flags |= ContactSearch::F_QUOTED;
- $word = preg_replace('/(?:\A"|"\z)/', "", $word);
- }
- $Search = new ContactSearch($flags, $word, $Me);
- foreach ($Search->user_ids() as $id) {
- $ids[$id] = $id;
- }
- }
- $where = array();
- if (count($ids)) {
- $result = $Conf->qe("select contactId, email from ContactInfo where contactId?a union select contactId, email from DeletedContactInfo where contactId?a", $ids, $ids);
- while (($row = $result->fetch_row())) {
- $where[] = "contactId=$row[0]";
- $where[] = "destContactId=$row[0]";
- $where[] = "action like " . Dbl::utf8ci("'% " . sqlq_for_like($row[1]) . "%'");
- }
- }
- if (count($where)) {
- $wheres[] = "(" . join(" or ", $where) . ")";
- } else {
- Ht::warning_at("u", "No matching users.");
- $wheres[] = "false";
- }
-}
-
-if ($Qreq->q !== "") {
- $where = array();
- $str = $Qreq->q;
- while (($str = ltrim($str)) !== "") {
- if ($str[0] === '"') {
- preg_match('/\A"([^"]*)"?/', $str, $m);
- } else {
- preg_match('/\A([^"\s]+)/', $str, $m);
- }
- $str = (string) substr($str, strlen($m[0]));
- if ($m[1] !== "") {
- $where[] = "action like " . Dbl::utf8ci("'%" . sqlq_for_like($m[1]) . "%'");
- }
- }
- $wheres[] = "(" . join(" or ", $where) . ")";
-}
-
-$first_timestamp = false;
-if ($Qreq->date === "") {
- $Qreq->date = "now";
-}
-if ($Qreq->date !== "now" && isset($Qreq->q)) {
- $first_timestamp = $Conf->parse_time($Qreq->date);
- if ($first_timestamp === false) {
- Ht::error_at("date", "Invalid date. Try format “YYYY-MM-DD HH:MM:SS”.");
- }
-}
-
-class LogRow {
- /** @var non-empty-string */
- public $logId;
- /** @var non-empty-string */
- public $timestamp;
- /** @var non-empty-string */
- public $contactId;
- /** @var ?non-empty-string */
- public $destContactId;
- /** @var ?non-empty-string */
- public $trueContactId;
- /** @var string */
- public $action;
- /** @var ?non-empty-string */
- public $paperId;
- public $data;
-
- public $cleanedAction;
- /** @var ?list */
- public $paperIdArray;
- /** @var ?list */
- public $destContactIdArray;
-}
-
-class LogRowGenerator {
- /** @var Conf */
- private $conf;
- private $wheres;
- private $page_size;
- private $delta = 0;
- private $lower_offset_bound;
- private $upper_offset_bound;
- private $rows_offset;
- private $rows_max_offset;
- /** @var list */
- private $rows = [];
- private $filter;
- private $page_to_offset;
- private $log_url_base;
- private $explode_mail = false;
- private $mail_stash;
- /** @var array */
- private $users;
- /** @var array */
- private $need_users;
-
- function __construct(Conf $conf, $wheres, $page_size) {
- $this->conf = $conf;
- $this->wheres = $wheres;
- $this->page_size = $page_size;
- $this->set_filter(null);
- $this->users = $conf->pc_users();
- $this->need_users = [];
- }
-
- function set_filter($filter) {
- $this->filter = $filter;
- $this->rows = [];
- $this->lower_offset_bound = 0;
- $this->upper_offset_bound = INF;
- $this->page_to_offset = [];
- }
-
- function set_explode_mail($explode_mail) {
- $this->explode_mail = $explode_mail;
- }
-
- function has_filter() {
- return !!$this->filter;
- }
-
- function page_size() {
- return $this->page_size;
- }
-
- function page_delta() {
- return $this->delta;
- }
-
- function set_page_delta($delta) {
- assert(is_int($delta) && $delta >= 0 && $delta < $this->page_size);
- $this->delta = $delta;
- }
-
- private function page_offset($pageno) {
- $offset = ($pageno - 1) * $this->page_size;
- if ($offset > 0 && $this->delta > 0) {
- $offset -= $this->page_size - $this->delta;
- }
- return $offset;
- }
-
- private function load_rows($pageno, $limit, $delta_adjusted = false) {
- $limit = (int) $limit;
- if ($pageno > 1 && $this->delta > 0 && !$delta_adjusted) {
- --$pageno;
- $limit += $this->page_size;
- }
- $offset = ($pageno - 1) * $this->page_size;
- $db_offset = $offset;
- if (($this->filter || !$this->explode_mail) && $db_offset !== 0) {
- if (!isset($this->page_to_offset[$pageno])) {
- $xlimit = min(4 * $this->page_size + $limit, 2000);
- $xpageno = max($pageno - floor($xlimit / $this->page_size), 1);
- $this->load_rows($xpageno, $xlimit, true);
- if ($this->rows_offset <= $offset && $offset + $limit <= $this->rows_max_offset)
- return;
- }
- $xpageno = $pageno;
- while ($xpageno > 1 && !isset($this->page_to_offset[$xpageno])) {
- --$xpageno;
- }
- $db_offset = $xpageno > 1 ? $this->page_to_offset[$xpageno] : 0;
- }
-
- $q = "select logId, timestamp, contactId, destContactId, trueContactId, action, paperId from ActionLog";
- if (!empty($this->wheres)) {
- $q .= " where " . join(" and ", $this->wheres);
- }
- $q .= " order by logId desc";
-
- $this->rows = [];
- $this->rows_offset = $offset;
- $n = 0;
- $exhausted = false;
- while ($n < $limit && !$exhausted) {
- $result = $this->conf->qe_raw($q . " limit $db_offset,$limit");
- $first_db_offset = $db_offset;
- while (($row = $result->fetch_object("LogRow"))) {
- '@phan-var LogRow $row';
- $this->need_users[(int) $row->contactId] = true;
- $destuid = (int) ($row->destContactId ? : $row->contactId);
- $this->need_users[$destuid] = true;
- ++$db_offset;
- if (!$this->explode_mail
- && $this->mail_stash
- && $this->mail_stash->action === $row->action) {
- $this->mail_stash->destContactIdArray[] = $destuid;
- if ($row->paperId) {
- $this->mail_stash->paperIdArray[] = (int) $row->paperId;
- }
- continue;
- }
- if (!$this->filter || call_user_func($this->filter, $row)) {
- $this->rows[] = $row;
- ++$n;
- if ($n % $this->page_size === 0) {
- $this->page_to_offset[$pageno + ($n / $this->page_size)] = $db_offset;
- }
- if (!$this->explode_mail) {
- if (substr($row->action, 0, 11) === "Sent mail #") {
- $this->mail_stash = $row;
- $row->destContactIdArray = [$destuid];
- $row->destContactId = null;
- $row->paperIdArray = [];
- if ($row->paperId) {
- $row->paperIdArray[] = (int) $row->paperId;
- $row->paperId = null;
- }
- } else {
- $this->mail_stash = null;
- }
- }
- }
- }
- Dbl::free($result);
- $exhausted = $first_db_offset + $limit !== $db_offset;
- }
-
- if ($n > 0) {
- $this->lower_offset_bound = max($this->lower_offset_bound, $this->rows_offset + $n);
- }
- if ($exhausted) {
- $this->upper_offset_bound = min($this->upper_offset_bound, $this->rows_offset + $n);
- }
- $this->rows_max_offset = $exhausted ? INF : $this->rows_offset + $n;
- }
-
- /** @param int $pageno
- * @return bool */
- function has_page($pageno, $load_npages = null) {
- global $nlinks;
- assert(is_int($pageno) && $pageno >= 1);
- $offset = $this->page_offset($pageno);
- if ($offset >= $this->lower_offset_bound
- && $offset < $this->upper_offset_bound) {
- if ($load_npages) {
- $limit = $load_npages * $this->page_size;
- } else {
- $limit = ($nlinks + 1) * $this->page_size + 30;
- }
- if ($this->filter) {
- $limit = max($limit, 2000);
- }
- $this->load_rows($pageno, $limit);
- }
- return $offset < $this->lower_offset_bound;
- }
-
- /** @param int $pageno
- * @param int $timestamp
- * @return bool */
- function page_after($pageno, $timestamp, $load_npages = null) {
- $rows = $this->page_rows($pageno, $load_npages);
- return !empty($rows) && $rows[count($rows) - 1]->timestamp > $timestamp;
- }
-
- /** @param int $pageno
- * @return list */
- function page_rows($pageno, $load_npages = null) {
- assert(is_int($pageno) && $pageno >= 1);
- if (!$this->has_page($pageno, $load_npages)) {
- return [];
- }
- $offset = $this->page_offset($pageno);
- if ($offset < $this->rows_offset
- || $offset + $this->page_size > $this->rows_max_offset) {
- $this->load_rows($pageno, $this->page_size);
- }
- return array_slice($this->rows, $offset - $this->rows_offset, $this->page_size);
- }
-
- function set_log_url_base($url) {
- $this->log_url_base = $url;
- }
-
- function page_link_html($pageno, $html) {
- $url = $this->log_url_base;
- if ($pageno !== 1 && $this->delta > 0) {
- $url .= "&offset=" . $this->delta;
- }
- return '' . $html . ' ';
- }
-
- private function _make_users() {
- unset($this->need_users[0]);
- $this->need_users = array_diff_key($this->need_users, $this->users);
- if (!empty($this->need_users)) {
- $result = $this->conf->qe("select contactId, firstName, lastName, affiliation, email, roles, contactTags, disabled, primaryContactId from ContactInfo where contactId?a", array_keys($this->need_users));
- while (($user = Contact::fetch($result, $this->conf))) {
- $this->users[$user->contactId] = $user;
- unset($this->need_users[$user->contactId]);
- }
- Dbl::free($result);
- }
- if (!empty($this->need_users)) {
- foreach ($this->need_users as $cid => $x) {
- $user = $this->users[$cid] = new Contact(["contactId" => $cid, "disabled" => 1], $this->conf);
- $user->disabled = "deleted";
- }
- $result = $this->conf->qe("select contactId, firstName, lastName, '' affiliation, email, 1 disabled from DeletedContactInfo where contactId?a", array_keys($this->need_users));
- while (($user = Contact::fetch($result, $this->conf))) {
- $this->users[$user->contactId] = $user;
- $user->disabled = "deleted";
- }
- Dbl::free($result);
- }
- $this->need_users = [];
- }
-
- /** @param LogRow $row
- * @param 'contactId'|'destContactId'|'trueContactId' $key
- * @return list */
- function users_for($row, $key) {
- if (!empty($this->need_users)) {
- $this->_make_users();
- }
- $uid = $row->$key;
- if (!$uid && $key === "contactId") {
- $uid = $row->destContactId;
- }
- $u = $uid ? [$this->users[$uid]] : [];
- if ($key === "destContactId" && isset($row->destContactIdArray)) {
- foreach ($row->destContactIdArray as $uid) {
- $u[] = $this->users[$uid];
- }
- }
- return $u;
- }
-
- /** @param LogRow $row
- * @return list */
- function paper_ids($row) {
- if (!isset($row->cleanedAction)) {
- if (!isset($row->paperIdArray)) {
- $row->paperIdArray = [];
- }
- if (preg_match('/\A(.* |)\(papers ([\d, ]+)\)?\z/', $row->action, $m)) {
- $row->cleanedAction = rtrim($m[1]);
- foreach (preg_split('/[\s,]+/', $m[2]) as $p) {
- if ($p !== "")
- $row->paperIdArray[] = (int) $p;
- }
- } else {
- $row->cleanedAction = $row->action;
- }
- if ($row->paperId) {
- $row->paperIdArray[] = (int) $row->paperId;
- }
- $row->paperIdArray = array_values(array_unique($row->paperIdArray));
- }
- return $row->paperIdArray;
- }
-
- function cleaned_action($row) {
- if (!isset($row->cleanedAction)) {
- $this->paper_ids($row);
- }
- return $row->cleanedAction;
- }
-}
-
-class LogRowFilter {
- private $user;
- private $pidset;
- private $want;
- private $includes;
-
- function __construct(Contact $user, $pidset, $want, $includes) {
- $this->user = $user;
- $this->pidset = $pidset;
- $this->want = $want;
- $this->includes = $includes;
- }
- private function test_pidset($row, $pidset, $want, $includes) {
- if ($row->paperId) {
- return isset($pidset[$row->paperId]) === $want
- && (!$includes || isset($includes[$row->paperId]));
- } else if (preg_match('/\A(.*) \(papers ([\d, ]+)\)?\z/', $row->action, $m)) {
- preg_match_all('/\d+/', $m[2], $mm);
- $pids = [];
- $included = !$includes;
- foreach ($mm[0] as $pid) {
- if (isset($pidset[$pid]) === $want) {
- $pids[] = $pid;
- $included = $included || isset($includes[$pid]);
- }
- }
- if (empty($pids) || !$included) {
- return false;
- } else if (count($pids) === 1) {
- $row->action = $m[1];
- $row->paperId = $pids[0];
- } else {
- $row->action = $m[1] . " (papers " . join(", ", $pids) . ")";
- }
- return true;
- } else
- return $this->user->privChair;
- }
- function __invoke($row) {
- if ($this->user->hidden_papers !== null
- && !$this->test_pidset($row, $this->user->hidden_papers, false, null)) {
- return false;
- } else if ($row->contactId === $this->user->contactId) {
- return true;
- } else {
- return $this->test_pidset($row, $this->pidset, $this->want, $this->includes);
- }
- }
-}
-
-if ($Qreq->download) {
- $lrg = new LogRowGenerator($Conf, $wheres, 1000000);
-} else {
- $lrg = new LogRowGenerator($Conf, $wheres, $count);
-}
-
-$exclude_pids = $Me->hidden_papers ? : [];
-if ($Me->privChair && $Conf->has_any_manager()) {
- foreach ($Me->paper_set(["myConflicts" => true]) as $prow) {
- if (!$Me->allow_administer($prow))
- $exclude_pids[$prow->paperId] = true;
- }
-}
-
-if (!$Me->privChair) {
- $good_pids = [];
- foreach ($Me->paper_set($Conf->check_any_admin_tracks($Me) ? [] : ["myManaged" => true]) as $prow) {
- if ($Me->allow_administer($prow))
- $good_pids[$prow->paperId] = true;
- }
- $lrg->set_filter(new LogRowFilter($Me, $good_pids, true, $include_pids));
-} else if (!$Qreq->forceShow && !empty($exclude_pids)) {
- $lrg->set_filter(new LogRowFilter($Me, $exclude_pids, false, $include_pids));
-}
-
-if ($Qreq->download) {
- session_commit();
- $csvg = $Conf->make_csvg("log");
- $narrow = true;
- $csvg->select(["date", "email", "affected_email", "via",
- $narrow ? "paper" : "papers", "action"]);
- foreach ($lrg->page_rows(1) as $row) {
- $date = date("Y-m-d H:i:s e", (int) $row->timestamp);
- $xusers = $xdest_users = [];
- foreach ($lrg->users_for($row, "contactId") as $u) {
- $xusers[] = $u->email;
- }
- foreach ($lrg->users_for($row, "destContactId") as $u) {
- $xdest_users[] = $u->email;
- }
- if ($xdest_users == $xusers) {
- $xdest_users = [];
- }
- if ($row->trueContactId) {
- $via = $row->trueContactId < 0 ? "link" : "admin";
- } else {
- $via = "";
- }
- $pids = $lrg->paper_ids($row);
- $action = $lrg->cleaned_action($row);
- if ($narrow) {
- if (empty($xusers)) {
- $xusers = [""];
- }
- if (empty($xdest_users)) {
- $xdest_users = [""];
- }
- if (empty($pids)) {
- $pids = [];
- }
- foreach ($xusers as $u1) {
- foreach ($xdest_users as $u2) {
- foreach ($pids as $p) {
- $csvg->add_row([$date, $u1, $u2, $via, $p, $action]);
- }
- }
- }
- } else {
- $csvg->add_row([
- $date, join(" ", $xusers), join(" ", $xdest_users),
- $via, join(" ", $pids), $action
- ]);
- }
- }
- $csvg->emit();
- exit;
-}
-
-if ($first_timestamp) {
- $page = 1;
- while ($lrg->page_after($page, $first_timestamp, ceil(2000 / $lrg->page_size()))) {
- ++$page;
- }
- $delta = 0;
- foreach ($lrg->page_rows($page) as $row) {
- if ($row->timestamp > $first_timestamp)
- ++$delta;
- }
- if ($delta) {
- $lrg->set_page_delta($delta);
- ++$page;
- }
-} else if ($page === false) { // handle `earliest`
- $page = 1;
- while ($lrg->has_page($page + 1, ceil(2000 / $lrg->page_size()))) {
- ++$page;
- }
-} else if ($Qreq->offset
- && ($delta = cvtint($Qreq->offset)) >= 0
- && $delta < $lrg->page_size()) {
- $lrg->set_page_delta($delta);
-}
-
-
-// render search list
-function searchbar(LogRowGenerator $lrg, $page) {
- global $Conf, $Me, $nlinks, $Qreq, $first_timestamp;
-
- $date = "";
- $dplaceholder = null;
- if (Ht::problem_status_at("date")) {
- $date = $Qreq->date;
- } else if ($page === 1) {
- $dplaceholder = "now";
- } else if (($rows = $lrg->page_rows($page))) {
- $dplaceholder = $Conf->unparse_time_log((int) $rows[0]->timestamp);
- } else if ($first_timestamp) {
- $dplaceholder = $Conf->unparse_time_log((int) $first_timestamp);
- }
-
- echo Ht::form(hoturl("log"), ["method" => "get", "id" => "searchform", "class" => "clearfix"]);
- if ($Qreq->forceShow) {
- echo Ht::hidden("forceShow", 1);
- }
- echo '',
- '
Concerning action(s) ',
- Ht::feedback_html_at("q"),
- Ht::entry("q", $Qreq->q, ["id" => "q", "size" => 40]),
- '
Concerning paper(s) ',
- Ht::feedback_html_at("p"),
- Ht::entry("p", $Qreq->p, ["id" => "p", "class" => "need-suggest papersearch", "autocomplete" => "off", "size" => 40, "spellcheck" => false]),
- '
Concerning user(s) ',
- Ht::feedback_html_at("u"),
- Ht::entry("u", $Qreq->u, ["id" => "u", "size" => 40]),
- '
Show ',
- Ht::entry("n", $Qreq->n, ["id" => "n", "size" => 4, "placeholder" => 50]),
- ' records at a time',
- Ht::feedback_html_at("n"),
- '
Starting at ',
- Ht::feedback_html_at("date"),
- Ht::entry("date", $date, ["id" => "date", "size" => 40, "placeholder" => $dplaceholder]),
- '
',
- Ht::submit("Show"),
- Ht::submit("download", "Download", ["class" => "ml-3"]),
- '';
-
- if ($page > 1 || $lrg->has_page(2)) {
- $urls = ["q=" . urlencode($Qreq->q)];
- foreach (["p", "u", "n", "forceShow"] as $x) {
- if ($Qreq[$x])
- $urls[] = "$x=" . urlencode($Qreq[$x]);
- }
- $lrg->set_log_url_base(hoturl("log", join("&", $urls)));
- echo "";
- if ($page > 1) {
- echo $lrg->page_link_html(1, "Newest "), " | ";
- }
- echo "
";
- if ($page > 1) {
- echo $lrg->page_link_html($page - 1, "" . Icons::ui_linkarrow(3) . "Newer ");
- }
- echo "
";
- if ($page - $nlinks > 1) {
- echo " ...";
- }
- for ($p = max($page - $nlinks, 1); $p < $page; ++$p) {
- echo " ", $lrg->page_link_html($p, $p);
- }
- echo "
", $page, "
";
- for ($p = $page + 1; $p <= $page + $nlinks && $lrg->has_page($p); ++$p) {
- echo $lrg->page_link_html($p, $p), " ";
- }
- if ($lrg->has_page($page + $nlinks + 1)) {
- echo "... ";
- }
- echo "
";
- if ($lrg->has_page($page + 1)) {
- echo $lrg->page_link_html($page + 1, "Older" . Icons::ui_linkarrow(1) . " ");
- }
- echo "
";
- if ($lrg->has_page($page + $nlinks + 1)) {
- echo " | ", $lrg->page_link_html("earliest", "Oldest ");
- }
- echo "
";
- }
- echo " \n";
-}
-
-// render rows
-$user_html = [];
-
-/** @param Contact $user */
-function set_user_html($user, $qreq_n) {
- global $Conf, $Me, $user_html;
- if (($pc = $Conf->pc_member_by_id($user->contactId))) {
- $user = $pc;
- }
- if ($user->disabled === "deleted") {
- $t = '' . $user->name_h(NAME_E) . '';
- } else {
- $t = $user->name_h(NAME_P);
- }
- $dt = null;
- if (($viewable = $user->viewable_tags($Me))) {
- $dt = $Conf->tags();
- if (($colors = $dt->color_classes($viewable))) {
- $t = '' . $t . ' ';
- }
- }
- $t = ' "", "u" => $user->email, "n" => $qreq_n]) . '">' . $t . ' ';
- if ($dt && $dt->has_decoration) {
- $tagger = new Tagger($Me);
- $t .= $tagger->unparse_decoration_html($viewable, Tagger::DECOR_USER);
- }
- $roles = 0;
- if (isset($user->roles) && ($user->roles & Contact::ROLE_PCLIKE)) {
- $roles = $user->viewable_pc_roles($Me);
- }
- if (!($roles & Contact::ROLE_PCLIKE)) {
- $t .= ' <' . htmlspecialchars($user->email) . '>';
- }
- if ($roles !== 0 && ($rolet = Contact::role_html_for($roles))) {
- $t .= " $rolet";
- }
- $user_html[$user->contactId] = $t;
- return $t;
-}
-
-/** @param list $users */
-function render_users($users, $via) {
- global $Conf, $Qreq, $Me, $user_html;
- if (empty($users) && $via < 0) {
- return "via author link ";
- }
- $all_pc = true;
- $ts = [];
- $last_user = null;
- usort($users, $Conf->user_comparator());
- foreach ($users as $user) {
- if ($user === $last_user) {
- continue;
- }
- if ($all_pc
- && (!isset($user->roles) || !($user->roles & Contact::ROLE_PCLIKE))) {
- $all_pc = false;
- }
- if ($user->disabled === "deleted") {
- if ($user->email) {
- $t = '' . $user->name_h(NAME_E) . '';
- } else {
- $t = '[deleted user ' . $user->contactId . ']';
- }
- } else {
- if (isset($user_html[$user->contactId])) {
- $t = $user_html[$user->contactId];
- } else {
- $t = set_user_html($user, $Qreq->n);
- }
- if ($via) {
- $t .= ($via < 0 ? ' via link ' : ' via admin ');
- }
- }
- $ts[] = $t;
- $last_user = $user;
- }
- if (count($ts) <= 3) {
- return join(", ", $ts);
- } else {
- $fmt = $all_pc ? "%d PC users" : "%d users";
- return '';
- }
-}
-
-$Conf->header("Log", "actionlog");
-
-$trs = [];
-$has_dest_user = false;
-foreach ($lrg->page_rows($page) as $row) {
- $t = ['' . $Conf->unparse_time_log((int) $row->timestamp) . ' '];
-
- $xusers = $lrg->users_for($row, "contactId");
- $xdest_users = $lrg->users_for($row, "destContactId");
- $via = $row->trueContactId;
-
- if ($xdest_users && $xusers != $xdest_users) {
- $t[] = '' . render_users($xusers, $via) . ' '
- . '' . render_users($xdest_users, false) . ' ';
- $has_dest_user = true;
- } else {
- $t[] = '' . render_users($xusers, $via) . ' ';
- }
-
- // XXX users that aren't in contactId slot
- // if (preg_match(',\A(.*)<([^>]*@[^>]*)>\s*(.*)\z,', $act, $m)) {
- // $t .= htmlspecialchars($m[2]);
- // $act = $m[1] . $m[3];
- // } else
- // $t .= "[None]";
-
- $act = $lrg->cleaned_action($row);
- $at = "";
- if (strpos($act, "eview ") !== false
- && preg_match('/\A(.* |)([Rr]eview )(\d+)( .*|)\z/', $act, $m)) {
- $at = htmlspecialchars($m[1])
- . Ht::link($m[2] . $m[3], $Conf->hoturl("review", ["p" => $row->paperId, "r" => $m[3]]))
- . "";
- $act = $m[4];
- } else if (substr($act, 0, 7) === "Comment"
- && preg_match('/\AComment (\d+)(.*)\z/s', $act, $m)) {
- $at = "hoturl("paper", "p={$row->paperId}#cid{$m[1]}") . "\">Comment " . $m[1] . " ";
- $act = $m[2];
- } else if (substr($act, 0, 8) === "Response"
- && preg_match('/\AResponse (\d+)(.*)\z/s', $act, $m)) {
- $at = "hoturl("paper", "p={$row->paperId}#cid{$m[1]}") . "\">Response " . $m[1] . " ";
- $act = $m[2];
- } else if (strpos($act, " mail ") !== false
- && preg_match('/\A(Sending|Sent|Account was sent) mail #(\d+)(.*)\z/s', $act, $m)) {
- $at = $m[1] . " hoturl("mail", "fromlog=$m[2]") . "\">mail #$m[2] ";
- $act = $m[3];
- } else if (substr($act, 0, 3) === "Tag"
- && preg_match('{\ATag:? ((?:[-+]#[^\s#]*(?:#[-+\d.]+|)(?: |\z))+)(.*)\z}s', $act, $m)) {
- $at = "Tag";
- $act = $m[2];
- foreach (explode(" ", rtrim($m[1])) as $word) {
- if (($hash = strpos($word, "#", 2)) === false) {
- $hash = strlen($word);
- }
- $at .= " " . $word[0] . ' substr($word, 1, $hash - 1)])
- . '">' . htmlspecialchars(substr($word, 1, $hash - 1))
- . ' ' . substr($word, $hash);
- }
- } else if ($row->paperId > 0
- && (substr($act, 0, 8) === "Updated "
- || substr($act, 0, 10) === "Submitted "
- || substr($act, 0, 11) === "Registered ")
- && preg_match('/\A(\S+(?: final)?)(.*)\z/', $act, $m)
- && preg_match('/\A(.* )(final|submission)((?:,| |\z).*)\z/', $m[2], $mm)) {
- $at = $m[1] . $mm[1] . "paperId}&dt={$mm[2]}&at={$row->timestamp}") . "\">{$mm[2]} ";
- $act = $mm[3];
- }
- $at .= htmlspecialchars($act);
- if (($pids = $lrg->paper_ids($row))) {
- if (count($pids) === 1)
- $at .= ' (paper ' . $pids[0] . " )";
- else {
- $at .= ' (papers ';
- foreach ($pids as $i => $p)
- $at .= ($i ? ', ' : ' ') . '' . $p . ' ';
- $at .= ')';
- }
- }
- $t[] = '' . $at . ' ';
- $trs[] = ' ' . join("", $t) . " \n";
-}
-
-if (!$Me->privChair || !empty($exclude_pids)) {
- echo '';
- if (!$Me->privChair) {
- $Conf->msg("Only showing your actions and entries for papers you administer.", "xinfo");
- } else if (!empty($exclude_pids)
- && (!$include_pids || array_intersect_key($include_pids, $exclude_pids))
- && array_keys($exclude_pids) != array_keys($Me->hidden_papers ? : [])) {
- $req = [];
- foreach (["q", "p", "u", "n"] as $k) {
- if ($Qreq->$k !== "")
- $req[$k] = $Qreq->$k;
- }
- $req["page"] = $page;
- if ($page > 1 && $lrg->page_delta() > 0) {
- $req["offset"] = $lrg->page_delta();
- }
- if ($Qreq->forceShow) {
- $Conf->msg("Showing all entries. (" . Ht::link("Unprivileged view", $Conf->selfurl($Qreq, $req + ["forceShow" => null])) . ")", "xinfo");
- } else {
- $Conf->msg("Not showing entries for " . Ht::link("conflicted administered papers", hoturl("search", "q=" . join("+", array_keys($exclude_pids)))) . ".", "xinfo");
- }
- }
- echo '
';
-}
-
-searchbar($lrg, $page);
-if (!empty($trs)) {
- echo "\n",
- ' ',
- 'Time ',
- 'User ',
- 'Affected user ',
- 'Action ',
- "\n \n",
- join("", $trs),
- " \n
\n";
-} else {
- echo "No records\n";
-}
-
-$Conf->footer();
+include("index.php");
diff --git a/mail.php b/mail.php
index 930a70505..08ab54176 100644
--- a/mail.php
+++ b/mail.php
@@ -2,7 +2,8 @@
// mail.php -- HotCRP mail tool
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
require_once("src/mailclasses.php");
if (!$Me->is_manager() && !$Me->isPC) {
$Me->escape();
diff --git a/manualassign.php b/manualassign.php
index abcbe4073..75783a184 100644
--- a/manualassign.php
+++ b/manualassign.php
@@ -2,7 +2,8 @@
// manualassign.php -- HotCRP chair's paper assignment page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->is_manager()) {
$Me->escape();
}
diff --git a/mergeaccounts.php b/mergeaccounts.php
index c2003e6ec..91bfb27ce 100644
--- a/mergeaccounts.php
+++ b/mergeaccounts.php
@@ -2,7 +2,8 @@
// mergeaccounts.php -- HotCRP account merging page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->email) {
$Me->escape();
}
diff --git a/offline.php b/offline.php
index a8bbcca63..3ac2fd0be 100644
--- a/offline.php
+++ b/offline.php
@@ -2,7 +2,8 @@
// offline.php -- HotCRP offline review management page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
if (!$Me->email) {
$Me->escape();
}
diff --git a/paper.php b/paper.php
index 7781d0232..ccdf66dbd 100644
--- a/paper.php
+++ b/paper.php
@@ -1,525 +1,5 @@
conf = $user->conf;
- $this->user = $user;
- $this->qreq = $qreq;
- }
-
- function echo_header() {
- $m = $this->pt ? $this->pt->mode : ($this->qreq->m ?? "p");
- PaperTable::echo_header($this->pt, "paper-" . ($m === "edit" ? "edit" : "view"), $m, $this->qreq);
- }
-
- function error_exit($msg) {
- $this->echo_header();
- Ht::stash_script("hotcrp.shortcut().add()");
- $msg && Conf::msg_error($msg);
- $this->conf->footer();
- exit;
- }
-
- function load_prow() {
- // determine whether request names a paper
- try {
- $pr = new PaperRequest($this->user, $this->qreq, false);
- $this->prow = $this->conf->paper = $pr->prow;
- } catch (Redirection $redir) {
- assert(PaperRequest::simple_qreq($this->qreq));
- $this->conf->redirect($redir->url);
- } catch (PermissionProblem $perm) {
- $this->error_exit($perm->set("listViewable", true)->unparse_html());
- }
- }
-
- function handle_cancel() {
- if ($this->prow->timeSubmitted && $this->qreq->m === "edit") {
- unset($this->qreq->m);
- }
- $this->conf->redirect_self($this->qreq);
- }
-
- function handle_withdraw() {
- if (($whynot = $this->user->perm_withdraw_paper($this->prow))) {
- Conf::msg_error($whynot->unparse_html() . " The submission has not been withdrawn.");
- return;
- }
-
- $reason = (string) $this->qreq->reason;
- if ($reason === ""
- && $this->user->can_administer($this->prow)
- && $this->qreq->doemail > 0) {
- $reason = (string) $this->qreq->emailNote;
- }
-
- $aset = new AssignmentSet($this->user, true);
- $aset->enable_papers($this->prow);
- $aset->parse("paper,action,withdraw reason\n{$this->prow->paperId},withdraw," . CsvGenerator::quote($reason));
- if (!$aset->execute()) {
- error_log("{$this->conf->dbname}: withdraw #{$this->prow->paperId} failure: " . json_encode($aset->json_result()));
- }
- $this->load_prow();
-
- // email contact authors themselves
- if (!$this->user->can_administer($this->prow) || $this->qreq->doemail) {
- $tmpl = $this->prow->has_author($this->user) ? "@authorwithdraw" : "@adminwithdraw";
- HotCRPMailer::send_contacts($tmpl, $this->prow, ["reason" => $reason, "infoNames" => 1]);
- }
-
- // email reviewers
- if ($this->prow->all_reviews()) {
- $preps = [];
- foreach ($this->prow->review_followers() as $minic) {
- if ($minic->contactId !== $this->user->contactId
- && ($p = HotCRPMailer::prepare_to($minic, "@withdrawreviewer", ["prow" => $this->prow, "reason" => $reason]))) {
- if (!$minic->can_view_review_identity($this->prow, null)) {
- $p->unique_preparation = true;
- }
- $preps[] = $p;
- }
- }
- HotCRPMailer::send_combined_preparations($preps);
- }
-
- $this->conf->redirect_self($this->qreq);
- }
-
- function handle_revive() {
- if (($whynot = $this->user->perm_revive_paper($this->prow))) {
- Conf::msg_error($whynot->unparse_html());
- return;
- }
-
- $aset = new AssignmentSet($this->user, true);
- $aset->enable_papers($this->prow);
- $aset->parse("paper,action\n{$this->prow->paperId},revive");
- if (!$aset->execute()) {
- error_log("{$this->conf->dbname}: revive #{$this->prow->paperId} failure: " . json_encode($aset->json_result()));
- }
- $this->conf->redirect_self($this->qreq);
- }
-
- function handle_delete() {
- if ($this->prow->paperId <= 0) {
- $this->conf->confirmMsg("Submission deleted.");
- } else if (!$this->user->can_administer($this->prow)) {
- Conf::msg_error("Only the program chairs can permanently delete submissions. Authors can withdraw submissions, which is effectively the same.");
- } else {
- // mail first, before contact info goes away
- if ($this->qreq->doemail) {
- HotCRPMailer::send_contacts("@deletepaper", $this->prow, ["reason" => (string) $this->qreq->emailNote, "infoNames" => 1]);
- }
- if ($this->prow->delete_from_database($this->user)) {
- $this->conf->confirmMsg("Submission #{$this->prow->paperId} deleted.");
- }
- $this->error_exit("");
- }
- }
-
- /** @return string */
- private function deadline_note($dl, $future_msg, $past_msg) {
- $deadline = $this->conf->unparse_setting_time_span($dl);
- $strong = false;
- if ($deadline === "N/A") {
- $msg = "";
- } else if ($this->conf->time_after_setting($dl)) {
- $msg = $past_msg;
- $strong = true;
- } else {
- $msg = $future_msg;
- }
- if ($msg !== "") {
- $msg = $this->conf->_($msg, $deadline);
- }
- if ($msg !== "" && $strong) {
- $msg = "{$msg} ";
- }
- return $msg;
- }
-
- /** @return list */
- private function missing_required_fields(PaperInfo $prow) {
- $missing = [];
- foreach ($prow->form_fields() as $o) {
- if ($o->test_required($prow) && !$o->value_present($prow->force_option($o)))
- $missing[] = $o;
- }
- return $missing;
- }
-
- function handle_update($action) {
- $conf = $this->conf;
- // XXX lock tables
- $is_new = $this->prow->paperId <= 0;
- $was_submitted = $this->prow->timeSubmitted > 0;
- $this->useRequest = true;
-
- $this->ps = new PaperStatus($conf, $this->user);
- $prepared = $this->ps->prepare_save_paper_web($this->qreq, $this->prow, $action);
-
- if (!$prepared) {
- if ($is_new && $this->qreq->has_files()) {
- // XXX save uploaded files
- $this->ps->error_at(null, "Your uploaded files were ignored. ");
- }
- $t = $conf->_("Your changes were not saved. Please fix these errors and try again.");
- $emsg = $this->ps->landmarked_problem_texts();
- if (!empty($emsg)) {
- $t = "{$t}
";
- }
- Conf::msg_error($t);
- return;
- }
-
- // check deadlines
- if ($is_new) {
- // we know that can_start_paper implies can_finalize_paper
- $whynot = $this->user->perm_start_paper();
- } else if ($action === "final") {
- $whynot = $this->user->perm_edit_final_paper($this->prow);
- } else {
- $whynot = $this->user->perm_edit_paper($this->prow);
- if ($whynot
- && $action === "update"
- && !count(array_diff($this->ps->diffs, ["contacts", "status"]))) {
- $whynot = $this->user->perm_finalize_paper($this->prow);
- }
- }
- if ($whynot) {
- Conf::msg_error($whynot->unparse_html());
- $this->useRequest = !$is_new; // XXX used to have more complex logic
- return;
- }
-
- // actually update
- $this->ps->execute_save();
-
- $warnmsgs = $this->ps->landmarked_problem_texts();
- $webnotes = $warnmsgs ? " " . join(" ", $warnmsgs) . " " : "";
-
- $new_prow = $conf->paper_by_id($this->ps->paperId, $this->user, ["topics" => true, "options" => true]);
- if (!$new_prow) {
- $conf->msg($conf->_("Your submission was not saved. Please correct these errors and save again.") . $webnotes, "merror");
- return;
- }
- assert($this->user->can_view_paper($new_prow));
-
- // submit paper if no error so far
- $_GET["paperId"] = $_GET["p"] = $this->qreq->paperId = $this->qreq->p = $this->ps->paperId;
-
- if ($action === "final") {
- $submitkey = "timeFinalSubmitted";
- $storekey = "finalPaperStorageId";
- } else {
- $submitkey = "timeSubmitted";
- $storekey = "paperStorageId";
- }
- $newsubmit = $new_prow->timeSubmitted > 0 && !$was_submitted;
-
- // confirmation message
- if ($action === "final") {
- $actiontext = "Updated final";
- $template = "@submitfinalpaper";
- } else if ($newsubmit) {
- $actiontext = "Updated";
- $template = "@submitpaper";
- } else if ($is_new) {
- $actiontext = "Registered";
- $template = "@registerpaper";
- } else {
- $actiontext = "Updated";
- $template = "@updatepaper";
- }
-
- // log message
- $this->ps->log_save_activity($this->user, $action);
-
- // additional information
- $notes = [];
- if ($action == "final") {
- if ($new_prow->timeFinalSubmitted <= 0) {
- $notes[] = $conf->_("The final version has not yet been submitted.");
- }
- $notes[] = $this->deadline_note("final_soft",
- "You have until %s to make further changes.",
- "The deadline for submitting final versions was %s.");
- } else if ($new_prow->timeSubmitted > 0) {
- $notes[] = $conf->_("The submission is ready for review.");
- if ($conf->setting("sub_freeze") <= 0) {
- $notes[] = $this->deadline_note("sub_update",
- "You have until %s to make further changes.", "");
- }
- } else {
- if ($conf->setting("sub_freeze") > 0) {
- $notes[] = $conf->_("The submission has not yet been completed.");
- } else if (($missing = $this->missing_required_fields($new_prow))) {
- $missing_names = array_map(function ($o) { return $o->missing_title(); }, $missing);
- $notes[] = $conf->_("The submission is not ready for review; required fields %#H are missing.", $missing_names);
- } else {
- $notes[] = $conf->_("The submission is marked as not ready for review.");
- }
- $notes[] = $this->deadline_note("sub_update",
- "You have until %s to make further changes.",
- "The deadline for updating submissions was %s.");
- if (($msg = $this->deadline_note("sub_sub", "Submissions incomplete as of %s will not be considered for review.", "")) !== "") {
- $notes[] = "{$msg} ";
- }
- }
- $notes = join(" ", array_filter($notes, function ($n) { return $n !== ""; }));
-
- // HTML confirmation
- if (empty($this->ps->diffs)) {
- $webmsg = $conf->_("No changes to submission #%d.", $new_prow->paperId);
- } else {
- $webmsg = $conf->_("$actiontext submission #%d.", $new_prow->paperId);
- }
- if ($this->ps->has_error()) {
- $webmsg .= " " . $conf->_("Please correct these issues and save again.");
- }
- if ($notes || $webnotes) {
- $webmsg .= " " . $notes . $webnotes;
- }
- $conf->msg($webmsg, $new_prow->$submitkey > 0 ? "confirm" : "warning");
-
- // mail confirmation to all contact authors if changed
- if (!empty($this->ps->diffs)) {
- if (!$this->user->can_administer($new_prow) || $this->qreq->doemail) {
- $options = ["infoNames" => 1];
- if ($this->user->can_administer($new_prow)) {
- if (!$new_prow->has_author($this->user)) {
- $options["adminupdate"] = true;
- }
- if (isset($this->qreq->emailNote)) {
- $options["reason"] = $this->qreq->emailNote;
- }
- }
- if ($notes !== "") {
- $options["notes"] = preg_replace('/<\/?(?:span.*?|strong)>/', "", $notes) . "\n\n";
- }
- HotCRPMailer::send_contacts($template, $new_prow, $options);
- }
-
- // other mail confirmations
- if ($action === "final" && $new_prow->timeFinalSubmitted > 0) {
- $followers = $new_prow->final_update_followers();
- $template = "@finalsubmitnotify";
- } else if ($is_new) {
- $followers = $new_prow->register_followers();
- $template = $newsubmit ? "@newsubmitnotify" : "@registernotify";
- } else if ($newsubmit) {
- $followers = $new_prow->newsubmit_followers();
- $template = "@newsubmitnotify";
- } else {
- $followers = [];
- $template = "@none";
- }
- foreach ($followers as $minic) {
- if ($minic->contactId !== $this->user->contactId)
- HotCRPMailer::send_to($minic, $template, ["prow" => $new_prow]);
- }
- }
-
- $conf->paper = $this->prow = $new_prow;
- if (!$this->ps->has_error() || ($is_new && $new_prow)) {
- $conf->redirect_self($this->qreq, ["p" => $new_prow->paperId, "m" => "edit"]);
- }
- }
-
- function handle_updatecontacts() {
- $conf = $this->conf;
- $this->useRequest = true;
-
- if (!$this->user->can_administer($this->prow)
- && !$this->prow->has_author($this->user)) {
- Conf::msg_error($this->prow->make_whynot(["permission" => "edit_contacts"])->unparse_html());
- return;
- }
-
- $this->ps = new PaperStatus($this->conf, $this->user);
- if (!$this->ps->prepare_save_paper_web($this->qreq, $this->prow, "updatecontacts")) {
- Conf::msg_error("" . join(" ", $this->ps->message_texts()) . " ");
- return;
- }
-
- if (!$this->ps->diffs) {
- Conf::msg_warning($conf->_("No changes to submission #%d.", $this->prow->paperId));
- } else if ($this->ps->execute_save()) {
- Conf::msg_confirm($conf->_("Updated contacts for submission #%d.", $this->prow->paperId));
- $this->user->log_activity("Paper edited: contacts", $this->prow->paperId);
- }
-
- if (!$this->ps->has_error()) {
- $conf->redirect_self($this->qreq);
- }
- }
-
- private function prepare_edit_mode() {
- if (!$this->ps) {
- $this->prow->set_allow_absent($this->prow->paperId === 0);
- $this->ps = PaperStatus::make_prow($this->user, $this->prow);
- $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
- foreach ($this->prow->form_fields() as $o) {
- if ($this->user->can_edit_option($this->prow, $o)) {
- $ov = $this->prow->force_option($o);
- $o->value_check($ov, $this->user);
- $ov->copy_messages_to($this->ps);
- }
- }
- $this->user->set_overrides($old_overrides);
- $this->prow->set_allow_absent(false);
- }
-
- $old_overrides = $this->user->remove_overrides(Contact::OVERRIDE_CHECK_TIME);
- $editable = $this->user->can_edit_paper($this->prow)
- || $this->user->can_edit_final_paper($this->prow);
- $this->user->set_overrides($old_overrides);
- $this->pt->set_edit_status($this->ps, $editable, $editable && $this->useRequest);
- }
-
- function render() {
- // correct modes
- $this->pt = $pt = new PaperTable($this->user, $this->qreq, $this->prow);
- if ($pt->can_view_reviews()
- || $pt->mode === "re"
- || ($this->prow->paperId > 0 && $this->user->can_edit_review($this->prow))) {
- $pt->resolve_review(false);
- }
- $pt->resolve_comments();
- if ($pt->mode === "edit") {
- $this->prepare_edit_mode();
- }
-
- // produce paper table
- $this->echo_header();
- $pt->echo_paper_info();
-
- if ($pt->mode === "edit") {
- $pt->paptabEndWithoutReviews();
- } else {
- if ($pt->mode === "re") {
- $pt->echo_review_form();
- $pt->echo_main_link();
- } else if ($pt->can_view_reviews()) {
- $pt->paptabEndWithReviewsAndComments();
- } else {
- $pt->paptabEndWithReviewMessage();
- $pt->echo_comments();
- }
- // restore comment across logout bounce
- if ($this->qreq->editcomment) {
- $cid = $this->qreq->c;
- $preferred_resp_round = false;
- if (($x = $this->qreq->response)) {
- $preferred_resp_round = $this->conf->resp_round_number($x);
- }
- if ($preferred_resp_round === false) {
- $preferred_resp_round = $this->user->preferred_resp_round_number($this->prow);
- }
- $j = null;
- foreach ($this->prow->viewable_comments($this->user) as $crow) {
- if ($crow->commentId == $cid
- || ($cid === null
- && ($crow->commentType & CommentInfo::CT_RESPONSE) != 0
- && $crow->commentRound === $preferred_resp_round))
- $j = $crow->unparse_json($this->user);
- }
- if (!$j) {
- $j = (object) ["is_new" => true, "editable" => true];
- if ($this->user->act_author_view($this->prow)) {
- $j->by_author = true;
- }
- if ($preferred_resp_round !== false) {
- $j->response = $this->conf->resp_round_name($preferred_resp_round);
- }
- }
- if (($x = $this->qreq->text) !== null) {
- $j->text = $x;
- $j->visibility = $this->qreq->visibility;
- $tags = trim((string) $this->qreq->tags);
- $j->tags = $tags === "" ? [] : preg_split('/\s+/', $tags);
- $j->blind = !!$this->qreq->blind;
- $j->draft = !!$this->qreq->draft;
- }
- Ht::stash_script("hotcrp.edit_comment(" . json_encode_browser($j) . ")");
- }
- }
-
- echo "\n";
- $this->conf->footer();
- }
-
- static function go(Contact $user, Qrequest $qreq) {
- if (!isset($qreq->m) && ($pc = $qreq->path_component(1))) {
- $qreq->m = $pc;
- } else if (!isset($qreq->m) && isset($qreq->mode)) {
- $qreq->m = $qreq->mode;
- }
-
- $pp = new PaperPage($user, $qreq);
- $pp->load_prow();
-
- // fix user
- if ($qreq->is_post() && $qreq->valid_token()) {
- $user->ensure_account_here();
- // XXX escape unless update && can_start_paper???
- }
- $user->add_overrides(Contact::OVERRIDE_CHECK_TIME);
- if ($pp->prow->paperId == 0 && $user->privChair && !$user->conf->time_start_paper()) {
- $user->add_overrides(Contact::OVERRIDE_CONFLICT);
- }
-
- // fix request
- $pp->useRequest = isset($qreq->title) && $qreq->has_annex("after_login");
- if ($qreq->emailNote === "Optional explanation") {
- unset($qreq->emailNote);
- }
- if ($qreq->reason === "Optional explanation") {
- unset($qreq->reason);
- }
- if ($qreq->post && $qreq->post_empty()) {
- $pp->conf->post_missing_msg();
- }
-
- // action
- if ($qreq->cancel) {
- $pp->handle_cancel();
- } else if ($qreq->update && $qreq->valid_post()) {
- $pp->handle_update($qreq->submitfinal ? "final" : "update");
- } else if ($qreq->updatecontacts && $qreq->valid_post()) {
- $pp->handle_updatecontacts();
- } else if ($qreq->withdraw && $qreq->valid_post()) {
- $pp->handle_withdraw();
- } else if ($qreq->revive && $qreq->valid_post()) {
- $pp->handle_revive();
- } else if ($qreq->delete && $qreq->valid_post()) {
- $pp->handle_delete();
- } else if ($qreq->updateoverride && $qreq->valid_token()) {
- $pp->conf->redirect_self($qreq, ["m" => "edit", "forceShow" => 1]);
- }
-
- // render
- $pp->render();
- }
-}
-
-PaperPage::go($Me, $Qreq);
+include("index.php");
diff --git a/profile.php b/profile.php
index 6a5c62cfe..c1fa48154 100644
--- a/profile.php
+++ b/profile.php
@@ -2,7 +2,8 @@
// profile.php -- HotCRP profile management page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
// check for change-email capabilities
diff --git a/review.php b/review.php
index c86e8466a..4c012e532 100644
--- a/review.php
+++ b/review.php
@@ -1,458 +1,5 @@
is_reviewer()) {
- ensure_session();
-}
-
-class ReviewPage {
- /** @var Conf */
- public $conf;
- /** @var Contact */
- public $user;
- /** @var Qrequest */
- public $qreq;
- /** @var PaperInfo */
- public $prow;
- /** @var ?ReviewInfo */
- public $rrow;
- /** @var bool */
- public $rrow_explicit;
- /** @var PaperTable */
- public $pt;
- /** @var ?ReviewValues */
- public $rv;
-
- function __construct(Contact $user, Qrequest $qreq) {
- $this->conf = $user->conf;
- $this->user = $user;
- $this->qreq = $qreq;
- }
-
- /** @return ReviewForm */
- function rf() {
- return $this->conf->review_form();
- }
-
- function echo_header() {
- PaperTable::echo_header($this->pt, "review", $this->qreq->m, $this->qreq);
- }
-
- function error_exit($msg) {
- $this->echo_header();
- Ht::stash_script("hotcrp.shortcut().add()");
- $msg && Conf::msg_error($msg);
- $this->conf->footer();
- exit;
- }
-
- function load_prow() {
- // determine whether request names a paper
- try {
- $pr = new PaperRequest($this->user, $this->qreq, true);
- $this->prow = $this->conf->paper = $pr->prow;
- if ($pr->rrow) {
- $this->rrow = $pr->rrow;
- $this->rrow_explicit = true;
- } else {
- $this->rrow = $this->my_rrow($this->qreq->m === "rea");
- $this->rrow_explicit = false;
- }
- } catch (Redirection $redir) {
- assert(PaperRequest::simple_qreq($this->qreq));
- $this->conf->redirect($redir->url);
- } catch (PermissionProblem $perm) {
- $this->error_exit($perm->set("listViewable", true)->unparse_html());
- }
- }
-
- /** @return ?ReviewInfo */
- function my_rrow($prefer_approvable) {
- $myrrow = $apprrow1 = $apprrow2 = null;
- $admin = $this->user->can_administer($this->prow);
- foreach ($this->prow->reviews_as_display() as $rrow) {
- if ($this->user->can_view_review($this->prow, $rrow)) {
- if ($rrow->contactId === $this->user->contactId
- || (!$myrrow && $this->user->is_my_review($rrow))) {
- $myrrow = $rrow;
- } else if ($rrow->reviewStatus === ReviewInfo::RS_DELIVERED
- && !$apprrow1
- && $rrow->requestedBy === $this->user->contactXid) {
- $apprrow1 = $rrow;
- } else if ($rrow->reviewStatus === ReviewInfo::RS_DELIVERED
- && !$apprrow2
- && $admin) {
- $apprrow2 = $rrow;
- }
- }
- }
- if (($apprrow1 || $apprrow2)
- && ($prefer_approvable || !$myrrow)) {
- return $apprrow1 ?? $apprrow2;
- } else {
- return $myrrow;
- }
- }
-
- function reload_prow() {
- $this->prow->load_reviews(true);
- if ($this->rrow) {
- $this->rrow = $this->prow->review_by_id($this->rrow->reviewId);
- } else {
- $this->rrow = $this->prow->review_by_ordinal_id($this->qreq->reviewId);
- }
- }
-
- function handle_cancel() {
- $this->conf->redirect($this->prow->hoturl([], Conf::HOTURL_RAW));
- }
-
- function handle_update() {
- // do not unsubmit submitted review
- if ($this->rrow && $this->rrow->reviewStatus >= ReviewInfo::RS_COMPLETED) {
- $this->qreq->ready = 1;
- }
-
- $rv = new ReviewValues($this->rf());
- $rv->paperId = $this->prow->paperId;
- if (($whynot = $this->user->perm_submit_review($this->prow, $this->rrow))) {
- $rv->msg_at(null, $whynot->unparse_html(), MessageSet::ERROR);
- } else if ($rv->parse_qreq($this->qreq, $this->qreq->override)) {
- if (isset($this->qreq->approvesubreview)
- && $this->rrow
- && $this->user->can_approve_review($this->prow, $this->rrow)) {
- $rv->set_adopt();
- }
- if ($rv->check_and_save($this->user, $this->prow, $this->rrow)) {
- $this->qreq->r = $this->qreq->reviewId = $rv->review_ordinal_id;
- }
- }
- $rv->report();
- if (!$rv->has_error() && !$rv->has_problem_at("ready")) {
- $this->conf->redirect_self($this->qreq);
- }
- $this->rv = $rv;
- $this->reload_prow();
- }
-
- function handle_upload_form() {
- if (!$this->qreq->has_file("uploadedFile")) {
- Conf::msg_error("Select a review form to upload.");
- return;
- }
- $rv = ReviewValues::make_text($this->rf(),
- $this->qreq->file_contents("uploadedFile"),
- $this->qreq->file_filename("uploadedFile"));
- if ($rv->parse_text($this->qreq->override)
- && $rv->check_and_save($this->user, $this->prow, $this->rrow)) {
- $this->qreq->r = $this->qreq->reviewId = $rv->review_ordinal_id;
- }
- if (!$rv->has_error() && $rv->parse_text($this->qreq->override)) {
- $rv->msg_at(null, "Only the first review form in the file was parsed. " . Ht::link("Upload multiple-review files here.", $this->conf->hoturl("offline")), MessageSet::WARNING);
- }
- $rv->report();
- if (!$rv->has_error()) {
- $this->conf->redirect_self($this->qreq);
- }
- $this->reload_prow();
- }
-
- function handle_download_form() {
- $filename = "review-" . ($this->rrow ? $this->rrow->unparse_ordinal_id() : $this->prow->paperId);
- $rf = $this->rf();
- $this->conf->make_csvg($filename, CsvGenerator::TYPE_STRING)
- ->set_inline(false)
- ->add_string($rf->text_form_header(false)
- . $rf->text_form($this->prow, $this->rrow, $this->user, null))
- ->emit();
- exit;
- }
-
- function handle_download_text() {
- $rf = $this->rf();
- if ($this->rrow && $this->rrow_explicit) {
- $this->conf->make_csvg("review-" . $this->rrow->unparse_ordinal_id(), CsvGenerator::TYPE_STRING)
- ->add_string($rf->unparse_text($this->prow, $this->rrow, $this->user))
- ->emit();
- } else {
- $lastrc = null;
- $texts = [
- "{$this->conf->short_name} Paper #{$this->prow->paperId} Reviews and Comments\n",
- str_repeat("=", 75) . "\n",
- prefix_word_wrap("", "Paper #{$this->prow->paperId} {$this->prow->title}", 0, 75),
- "\n\n"
- ];
- foreach ($this->prow->viewable_submitted_reviews_and_comments($this->user) as $rc) {
- $texts[] = PaperInfo::review_or_comment_text_separator($lastrc, $rc);
- if (isset($rc->reviewId)) {
- $texts[] = $rf->unparse_text($this->prow, $rc, $this->user, ReviewForm::UNPARSE_NO_TITLE);
- } else {
- $texts[] = $rc->unparse_text($this->user, ReviewForm::UNPARSE_NO_TITLE);
- }
- $lastrc = $rc;
- }
- if (!$lastrc) {
- $texts[] = "Nothing to show.\n";
- }
- $this->conf->make_csvg("reviews-{$this->prow->paperId}", CsvGenerator::TYPE_STRING)
- ->append_strings($texts)
- ->emit();
- }
- exit;
- }
-
- function handle_adopt() {
- if (!$this->rrow || !$this->rrow_explicit) {
- Conf::msg_error("Missing review to delete.");
- return;
- } else if (!$this->user->can_approve_review($this->prow, $this->rrow)) {
- return;
- }
-
- $rv = new ReviewValues($this->rf());
- $rv->paperId = $this->prow->paperId;
- $my_rrow = $this->prow->review_by_user($this->user);
- $my_rid = ($my_rrow ?? $this->rrow)->unparse_ordinal_id();
- if (($whynot = $this->user->perm_submit_review($this->prow, $my_rrow))) {
- $rv->msg_at(null, $whynot->unparse_html(), MessageSet::ERROR);
- } else if ($rv->parse_qreq($this->qreq, $this->qreq->override)) {
- $rv->set_ready($this->qreq->adoptsubmit);
- if ($rv->check_and_save($this->user, $this->prow, $my_rrow)) {
- $my_rid = $rv->review_ordinal_id;
- if (!$rv->has_problem_at("ready")) {
- // mark the source review as approved
- $rvx = new ReviewValues($this->rf());
- $rvx->set_adopt();
- $rvx->check_and_save($this->user, $this->prow, $this->rrow);
- }
- }
- }
- $rv->report();
- $this->conf->redirect_self($this->qreq, ["r" => $my_rid]);
- }
-
- function handle_delete() {
- if (!$this->rrow || !$this->rrow_explicit) {
- Conf::msg_error("Missing review to delete.");
- return;
- } else if (!$this->user->can_administer($this->prow)) {
- return;
- }
- $result = $this->conf->qe("delete from PaperReview where paperId=? and reviewId=?", $this->prow->paperId, $this->rrow->reviewId);
- if ($result->affected_rows) {
- $this->user->log_activity_for($this->rrow->contactId, "Review {$this->rrow->reviewId} deleted", $this->prow);
- $this->conf->confirmMsg("Deleted review.");
- $this->conf->qe("delete from ReviewRating where paperId=? and reviewId=?", $this->prow->paperId, $this->rrow->reviewId);
- if ($this->rrow->reviewToken !== 0) {
- $this->conf->update_rev_tokens_setting(-1);
- }
- if ($this->rrow->reviewType == REVIEW_META) {
- $this->conf->update_metareviews_setting(-1);
- }
-
- // perhaps a delegatee needs to redelegate
- if ($this->rrow->reviewType < REVIEW_SECONDARY
- && $this->rrow->requestedBy > 0) {
- $this->user->update_review_delegation($this->prow->paperId, $this->rrow->requestedBy, -1);
- }
- }
- $this->conf->redirect_self($this->qreq, ["r" => null, "reviewId" => null]);
- }
-
- function handle_unsubmit() {
- if ($this->rrow
- && $this->rrow->reviewStatus >= ReviewInfo::RS_DELIVERED
- && $this->user->can_administer($this->prow)) {
- $result = $this->user->unsubmit_review_row($this->rrow);
- if ($result->affected_rows) {
- $this->user->log_activity_for($this->rrow->contactId, "Review {$this->rrow->reviewId} unsubmitted", $this->prow);
- $this->conf->confirmMsg("Unsubmitted review.");
- }
- $this->conf->redirect_self($this->qreq);
- }
- }
-
- /** @return ?int */
- function current_capability_rrid() {
- if (($capuid = $this->user->capability("@ra{$this->prow->paperId}"))) {
- $u = $this->conf->cached_user_by_id($capuid);
- $rrow = $this->prow->review_by_user($capuid);
- $refs = $u ? $this->prow->review_refusals_by_user($u) : [];
- if ($rrow && (!$this->rrow || $this->rrow === $rrow)) {
- return $rrow->reviewId;
- } else if (!$rrow && !empty($refs) && $refs[0]->refusedReviewId > 0) {
- return $refs[0]->refusedReviewId;
- }
- }
- return null;
- }
-
- function handle_accept_decline_redirect($capuid) {
- if (!$this->qreq->is_get()
- || !($rrid = $this->current_capability_rrid())) {
- return;
- }
- $isaccept = $this->qreq->accept;
- echo "
-
-
-
-Redirection
-\n",
- Ht::form($this->conf->hoturl_post("api/" . ($isaccept ? "acceptreview" : "declinereview"), ["p" => $this->prow->paperId, "r" => $rrid, "verbose" => 1, "redirect" => 1]), ["id" => "redirectform"]),
- Ht::submit("Press to continue"),
- "",
- Ht::script("document.getElementById('redirectform').submit()"),
- "";
- exit;
- }
-
- function render_decline_message($capuid) {
- $ref = $this->prow->review_refusals_by_user_id($capuid);
- if ($ref && $ref[0] && $ref[0]->refusedReviewId) {
- $rrid = $ref[0]->refusedReviewId;
- $this->conf->msg(
- "You declined to complete this review. Thank you for informing us.
"
- . Ht::form($this->conf->hoturl_post("api/declinereview", ["p" => $this->prow->paperId, "r" => $rrid, "redirect" => 1]))
- . 'Optional explanation '
- . ($ref[0]->reason ? "" : '
If you’d like, you may enter a brief explanation here.
')
- . Ht::textarea("reason", $ref[0]->reason, ["rows" => 3, "cols" => 40, "spellcheck" => true, "class" => "w-text", "id" => "declinereason"])
- . '
'
- . '
' . Ht::submit("Update explanation", ["class" => "btn-primary"])
- . '
' . Ht::submit("Accept review", ["formaction" => $this->conf->hoturl_post("api/acceptreview", ["p" => $this->prow->paperId, "r" => $rrid, "verbose" => 1, "redirect" => 1])])
- . '
', 1);
- } else {
- $this->conf->msg("You have declined to complete this review. Thank you for informing us.
", 1);
- }
- }
-
- function render_accept_other_message($capuid) {
- if (($u = $this->conf->cached_user_by_id($capuid))) {
- if (PaperRequest::simple_qreq($this->qreq)
- && ($i = $this->user->session_user_index($u->email)) >= 0) {
- $selfurl = $this->conf->selfurl($this->qreq, null, Conf::HOTURL_SITEREL | Conf::HOTURL_RAW);
- $this->conf->redirect(Navigation::base_absolute() . "u/{$i}/{$selfurl}");
- } else if ($this->user->has_email()) {
- $mx = 'This review is assigned to ' . htmlspecialchars($u->email) . ', while you are signed in as ' . htmlspecialchars($this->user->email) . '. You can edit the review anyway since you accessed it using a special link.';
- if ($this->rrow->reviewStatus <= ReviewInfo::RS_DRAFTED) {
- $m = Ht::form($this->conf->hoturl_post("api/claimreview", ["p" => $this->prow->paperId, "r" => $this->rrow->reviewId, "redirect" => 1]), ["class" => "has-fold foldc"])
- . "$mx Alternately, you can reassign it to this account .
"
- . '';
- foreach ($this->user->session_users() as $e) {
- $m .= '
' . Ht::submit("Reassign to " . htmlspecialchars($e), ["name" => "email", "value" => $e]) . '
';
- }
- $m .= '
';
- } else {
- $m = "{$mx}
";
- }
- $this->conf->msg($m, 1);
- } else {
- $this->conf->msg(
- 'This review is assigned to ' . htmlspecialchars($u->email) . '. You can edit the review since you accessed it using a special link.
', 1);
- }
- }
- }
-
- function render() {
- $this->pt = $pt = new PaperTable($this->user, $this->qreq, $this->prow);
- $pt->resolve_review(!!$this->rrow);
- $pt->resolve_comments();
-
- // mode
- if ($this->rv) {
- $pt->set_review_values($this->rv);
- } else if ($this->qreq->has_annex("after_login")) {
- $rv = new ReviewValues($this->rf());
- $rv->parse_qreq($this->qreq, $this->qreq->override);
- $pt->set_review_values($rv);
- }
-
- // paper table
- $this->echo_header();
- $pt->echo_paper_info();
-
- if (!$this->user->can_view_review($this->prow, $this->rrow)
- && !$this->user->can_edit_review($this->prow, $this->rrow)) {
- $pt->paptabEndWithReviewMessage();
- } else {
- if ($pt->mode === "re" || $this->rrow) {
- $pt->echo_review_form();
- $pt->echo_main_link();
- } else if ($this->rrow) {
- $pt->echo_rc([$this->rrow], false);
- $pt->echo_main_link();
- } else {
- $pt->paptabEndWithReviewsAndComments();
- }
- }
-
- echo "\n";
- $this->conf->footer();
- }
-
- static function go(Contact $user, Qrequest $qreq) {
- // fix request
- if (!isset($qreq->m) && isset($qreq->mode)) {
- $qreq->m = $qreq->mode;
- }
- if ($qreq->post && $qreq->default) {
- if ($qreq->has_file("uploadedFile")) {
- $qreq->uploadForm = 1;
- } else {
- $qreq->update = 1;
- }
- } else if ($qreq->submitreview) {
- $qreq->update = $qreq->ready = 1;
- } else if ($qreq->savedraft) {
- $qreq->update = 1;
- unset($qreq->ready);
- }
-
- $pp = new ReviewPage($user, $qreq);
- $pp->load_prow();
-
- // fix user
- $user->add_overrides(Contact::OVERRIDE_CHECK_TIME);
- $capuid = $user->capability("@ra{$pp->prow->paperId}");
-
- // action
- if ($qreq->cancel) {
- $pp->handle_cancel();
- } else if ($qreq->update && $qreq->valid_post()) {
- $pp->handle_update();
- } else if ($qreq->adoptreview && $qreq->valid_post()) {
- $pp->handle_adopt();
- } else if ($qreq->uploadForm && $qreq->valid_post()) {
- $pp->handle_upload_form();
- } else if ($qreq->downloadForm) {
- $pp->handle_download_form();
- } else if ($qreq->text) {
- $pp->handle_download_text();
- } else if ($qreq->unsubmitreview && $qreq->valid_post()) {
- $pp->handle_unsubmit();
- } else if ($qreq->deletereview && $qreq->valid_post()) {
- $pp->handle_delete();
- } else if (($qreq->accept || $qreq->decline) && $capuid) {
- $pp->handle_accept_decline_redirect($capuid);
- }
-
- // capability messages: decline, accept to different user
- if ($capuid) {
- if (!$pp->rrow
- && $pp->prow->review_refusals_by_user_id($capuid)) {
- $pp->render_decline_message($capuid);
- } else if ($pp->rrow
- && $capuid === $pp->rrow->contactId
- && $capuid !== $user->contactXid) {
- $pp->render_accept_other_message($capuid);
- }
- }
-
- $pp->render();
- }
-}
-
-ReviewPage::go($Me, $Qreq);
+include("index.php");
diff --git a/reviewprefs.php b/reviewprefs.php
index 3f02fdabe..871e807e2 100644
--- a/reviewprefs.php
+++ b/reviewprefs.php
@@ -1,229 +1,5 @@
privChair && !$Me->isPC) {
- $Me->escape();
-}
-
-if (isset($Qreq->default) && $Qreq->defaultfn) {
- $Qreq->fn = $Qreq->defaultfn;
-} else if (isset($Qreq->default)) {
- $Qreq->fn = "saveprefs";
-}
-
-
-// set reviewer
-$reviewer = $Me;
-$incorrect_reviewer = false;
-if ($Qreq->reviewer
- && $Me->privChair
- && $Qreq->reviewer !== $Me->email
- && $Qreq->reviewer !== $Me->contactId) {
- $incorrect_reviewer = true;
- foreach ($Conf->full_pc_members() as $pcm) {
- if (strcasecmp($Qreq->reviewer, $pcm->email) == 0
- || $Qreq->reviewer === (string) $pcm->contactId) {
- $reviewer = $pcm;
- $incorrect_reviewer = false;
- $Qreq->reviewer = $pcm->email;
- }
- }
-} else if (!$Qreq->reviewer && !($Me->roles & Contact::ROLE_PC)) {
- foreach ($Conf->pc_members() as $pcm) {
- $Conf->redirect_self($Qreq, ["reviewer" => $pcm->email]);
- // in case redirection fails:
- $reviewer = $pcm;
- break;
- }
-}
-if ($incorrect_reviewer) {
- Conf::msg_error("Reviewer " . htmlspecialchars($Qreq->reviewer) . " is not on the PC.");
-}
-
-
-// cancel action
-if ($Qreq->cancel) {
- $Conf->redirect_self($Qreq);
-}
-
-
-// backwards compat
-if ($Qreq->fn
- && strpos($Qreq->fn, "/") === false
- && isset($Qreq[$Qreq->fn . "fn"])) {
- $Qreq->fn .= "/" . $Qreq[$Qreq->fn . "fn"];
-}
-if (!str_starts_with($Qreq->fn, "get/")
- && !in_array($Qreq->fn, ["uploadpref", "tryuploadpref", "applyuploadpref", "setpref", "saveprefs"])) {
- unset($Qreq->fn);
-}
-
-// Update preferences
-function savePreferences($qreq) {
- global $Conf, $Me, $reviewer, $incorrect_reviewer;
- if ($incorrect_reviewer) {
- Conf::msg_error("Preferences not saved.");
- return;
- }
-
- $csvg = new CsvGenerator;
- $csvg->select(["paper", "email", "preference"]);
- $suffix = "u" . $reviewer->contactId;
- foreach ($qreq as $k => $v) {
- if (strlen($k) > 7 && substr($k, 0, 7) == "revpref") {
- if (str_ends_with($k, $suffix)) {
- $k = substr($k, 0, -strlen($suffix));
- }
- if (($p = cvtint(substr($k, 7))) > 0) {
- $csvg->add_row([$p, $reviewer->email, $v]);
- }
- }
- }
- if ($csvg->is_empty()) {
- Conf::msg_error("No reviewer preferences to update.");
- return;
- }
-
- $aset = new AssignmentSet($Me, true);
- $aset->parse($csvg->unparse());
- if ($aset->execute()) {
- Conf::msg_confirm("Preferences saved.");
- $Conf->redirect_self($qreq);
- } else {
- Conf::msg_error(join(" ", $aset->messages_html()));
- }
-}
-
-// paper selection, search actions
-global $SSel;
-$SSel = SearchSelection::make($Qreq, $Me);
-SearchSelection::clear_request($Qreq);
-$Qreq->q = $Qreq->q ?? "";
-$Qreq->t = "editpref";
-if ($Qreq->fn === "saveprefs") {
- if ($Qreq->valid_post())
- savePreferences($Qreq);
-} else if ($Qreq->fn !== null) {
- ListAction::call($Qreq->fn, $Me, $Qreq, $SSel);
-}
-
-
-// set options to view
-if (isset($Qreq->redisplay)) {
- $pfd = " ";
- foreach ($Qreq as $k => $v) {
- if (substr($k, 0, 4) == "show" && $v)
- $pfd .= substr($k, 4) . " ";
- }
- $Me->save_session("pfdisplay", $pfd);
- $Conf->redirect_self($Qreq);
-}
-
-
-// Header and body
-$Conf->header("Review preferences", "revpref");
-$Conf->infoMsg($Conf->_i("revprefdescription", null, $Conf->has_topics()));
-
-
-// search
-$search = (new PaperSearch($Me, ["t" => $Qreq->t, "q" => $Qreq->q, "reviewer" => $reviewer]))->set_urlbase("reviewprefs");
-$pl = new PaperList("pf", $search, ["sort" => true], $Qreq);
-$pl->apply_view_report_default();
-$pl->apply_view_session();
-$pl->apply_view_qreq();
-$pl->set_table_id_class("foldpl", "pltable-fullw", "p#");
-$pl->set_table_decor(PaperList::DECOR_HEADER | PaperList::DECOR_FOOTER | PaperList::DECOR_LIST);
-$pl->set_table_fold_session("pfdisplay.");
-
-
-// DISPLAY OPTIONS
-echo Ht::form($Conf->hoturl("reviewprefs"), [
- "method" => "get", "id" => "searchform",
- "class" => "has-fold fold10" . ($pl->viewing("authors") ? "o" : "c")
-]);
-
-if ($Me->privChair) {
- echo 'User ';
-
- $prefcount = [];
- $result = $Conf->qe_raw("select contactId, count(*) from PaperReviewPreference where preference!=0 or expertise is not null group by contactId");
- while (($row = $result->fetch_row())) {
- $prefcount[(int) $row[0]] = (int) $row[1];
- }
-
- $sel = [];
- foreach ($Conf->pc_members() as $p) {
- $sel[$p->email] = $p->name_h(NAME_P|NAME_S) . " [" . plural($prefcount[$p->contactId] ?? 0, "pref") . "]";
- }
- if (!isset($sel[$reviewer->email])) {
- $sel[$reviewer->email] = $reviewer->name_h(NAME_P|NAME_S) . " [" . ($prefcount[$reviewer->contactId] ?? 0) . "; not on PC]";
- }
-
- echo Ht::select("reviewer", $sel, $reviewer->email, ["id" => "htctl-prefs-user"]), '
';
- Ht::stash_script('$("#searchform select[name=reviewer]").on("change", function () { $("#searchform")[0].submit() })');
-}
-
-echo 'Search ',
- Ht::entry("q", $Qreq->q, [
- "id" => "htctl-prefs-q", "size" => 32, "placeholder" => "(All)",
- "class" => "papersearch want-focus need-suggest", "spellcheck" => false
- ]), ' ', Ht::submit("redisplay", "Redisplay"), '
';
-
-function show_pref_element($pl, $name, $text, $extra = []) {
- return ''
- . Ht::checkbox("show$name", 1, $pl->viewing($name), [
- "class" => "uich js-plinfo ignore-diff" . (isset($extra["fold_target"]) ? " js-foldup" : ""),
- "data-fold-target" => $extra["fold_target"] ?? null
- ]) . " " . Ht::label($text) . '';
-}
-$show_data = [];
-if ($pl->has("abstract")) {
- $show_data[] = show_pref_element($pl, "abstract", "Abstract");
-}
-if (($vat = $pl->viewable_author_types()) !== 0) {
- $extra = ["fold_target" => 10];
- if ($vat & 2) {
- $show_data[] = show_pref_element($pl, "au", "Authors", $extra);
- $extra = ["item_class" => "fx10"];
- }
- if ($vat & 1) {
- $show_data[] = show_pref_element($pl, "anonau", "Authors (deblinded)", $extra);
- $extra = ["item_class" => "fx10"];
- }
- $show_data[] = show_pref_element($pl, "aufull", "Full author info", $extra);
-}
-if ($Conf->has_topics()) {
- $show_data[] = show_pref_element($pl, "topics", "Topics");
-}
-if (!empty($show_data) && !$pl->is_empty()) {
- echo 'Show ',
- '
', join('', $show_data), ' ';
-}
-echo "";
-Ht::stash_script("$(\"#showau\").on(\"change\", function () { hotcrp.foldup.call(this, null, {n:10}) })");
-
-
-// main form
-$hoturl_args = [];
-if ($reviewer->contactId !== $Me->contactId) {
- $hoturl_args["reviewer"] = $reviewer->email;
-}
-if ($Qreq->q) {
- $hoturl_args["q"] = $Qreq->q;
-}
-if ($Qreq->sort) {
- $hoturl_args["sort"] = $Qreq->sort;
-}
-echo Ht::form($Conf->hoturl_post("reviewprefs", $hoturl_args), ["id" => "sel", "class" => "ui-submit js-submit-paperlist assignpc"]),
- Ht::hidden("defaultfn", ""),
- Ht::entry("____updates____", "", ["class" => "hidden ignore-diff"]),
- Ht::hidden_default_submit("default", 1);
-echo "\n",
- '
', Ht::submit("fn", "Save changes", ["value" => "saveprefs", "class" => "btn-primary"]), '
';
-$pl->echo_table_html();
-echo "
\n";
-
-$Conf->footer();
+include("index.php");
diff --git a/search.php b/search.php
index 1d3f5ae4e..a95844da0 100644
--- a/search.php
+++ b/search.php
@@ -1,489 +1,5 @@
is_empty()) {
- $Me->escape();
-}
-
-if (isset($Qreq->default) && $Qreq->defaultfn) {
- $Qreq->fn = $Qreq->defaultfn;
-}
-assert(!$Qreq->ajax);
-
-
-// search canonicalization
-if ((isset($Qreq->qa) || isset($Qreq->qo) || isset($Qreq->qx)) && !isset($Qreq->q)) {
- $Qreq->q = PaperSearch::canonical_query((string) $Qreq->qa, $Qreq->qo, $Qreq->qx, $Qreq->qt, $Conf);
-} else {
- unset($Qreq->qa, $Qreq->qo, $Qreq->qx);
-}
-if (isset($Qreq->t) && !isset($Qreq->q)) {
- $Qreq->q = "";
-}
-if (isset($Qreq->q)) {
- $Qreq->q = trim($Qreq->q);
- if ($Qreq->q === "(All)") {
- $Qreq->q = "";
- }
-}
-
-
-// paper group
-if (!PaperSearch::viewable_limits($Me, $Qreq->t)) {
- $Conf->header("Search", "search");
- Conf::msg_error("You aren’t allowed to search submissions.");
- exit;
-}
-
-
-// paper selection
-global $SSel;
-if (!$SSel) {
- $SSel = SearchSelection::make($Qreq, $Me);
- SearchSelection::clear_request($Qreq);
-}
-
-// look for search action
-if ($Qreq->fn) {
- $fn = $Qreq->fn;
- if (strpos($fn, "/") === false && isset($Qreq[$Qreq->fn . "fn"])) {
- $fn .= "/" . $Qreq[$Qreq->fn . "fn"];
- }
- ListAction::call($fn, $Me, $Qreq, $SSel);
-}
-
-
-// set fields to view
-if ($Qreq->redisplay) {
- $settings = [];
- foreach ($Qreq as $k => $v) {
- if ($v && substr($k, 0, 4) === "show") {
- $settings[substr($k, 4)] = true;
- }
- }
- Session_API::change_display($Me, "pl", $settings);
-}
-if ($Qreq->scoresort) {
- $Qreq->scoresort = ListSorter::canonical_short_score_sort($Qreq->scoresort);
- Session_API::setsession($Me, "scoresort=" . $Qreq->scoresort);
-}
-if ($Qreq->redisplay) {
- if (isset($Qreq->forceShow) && !$Qreq->forceShow && $Qreq->showforce) {
- $forceShow = 0;
- } else {
- $forceShow = $Qreq->forceShow || $Qreq->showforce ? 1 : null;
- }
- $Conf->redirect_self($Qreq, ["#" => "view", "forceShow" => $forceShow]);
-}
-
-
-// set display options, including forceShow if chair
-$pldisplay = $Me->session("pldisplay");
-if ($Me->privChair && !isset($Qreq->forceShow)
- && preg_match('/\b(show:|)force\b/', $pldisplay)) {
- $Qreq->forceShow = 1;
- $Me->add_overrides(Contact::OVERRIDE_CONFLICT);
-}
-
-
-// search
-$Conf->header("Search", "search");
-echo Ht::unstash(); // need the JS right away
-if (isset($Qreq->q)) {
- $Search = new PaperSearch($Me, $Qreq);
-} else {
- $Search = new PaperSearch($Me, ["t" => $Qreq->t, "q" => "NONE"]);
-}
-assert(!isset($Qreq->display));
-$pl = new PaperList("pl", $Search, ["sort" => true], $Qreq);
-$pl->apply_view_report_default();
-$pl->apply_view_session();
-$pl->apply_view_qreq();
-if (isset($Qreq->q)) {
- $pl->set_table_id_class("foldpl", "pltable-fullw", "p#");
- $pl->set_table_decor(PaperList::DECOR_HEADER | PaperList::DECOR_FOOTER | PaperList::DECOR_STATISTICS | PaperList::DECOR_LIST);
- $pl->set_table_fold_session("pldisplay.");
- if ($SSel->count()) {
- $pl->set_selection($SSel);
- }
- $pl->qopts["options"] = true; // get efficient access to `has(OPTION)`
- $pl_text = $pl->table_html();
- unset($Qreq->atab);
-} else {
- $pl_text = null;
-}
-
-
-// SEARCH FORMS
-
-// Prepare more display options
-$display_options_extra = "";
-
-class Search_DisplayOptions {
- /** @var array */
- public $headers = [];
- /** @var array> */
- public $items = [];
-
- /** @param int $column
- * @param string $header */
- function set_header($column, $header) {
- $this->headers[$column] = $header;
- }
- /** @param int $column
- * @param string $item */
- function item($column, $item) {
- if (!isset($this->headers[$column])) {
- $this->headers[$column] = "";
- }
- $this->items[$column][] = $item;
- }
- /** @param int $column
- * @param string $type
- * @param string $title */
- function checkbox_item($column, $type, $title, $options = []) {
- global $pl;
- $options["class"] = "uich js-plinfo";
- $x = ''
- . Ht::checkbox("show$type", 1, $pl->viewing($type), $options)
- . ' ' . $title . ' ';
- $this->item($column, $x);
- }
-}
-
-$display_options = new Search_DisplayOptions;
-
-// Create checkboxes
-
-if ($pl_text) {
- // Abstract
- if ($pl->has("abstract")) {
- $display_options->checkbox_item(1, "abstract", "Abstracts");
- }
-
- // Authors group
- if (($vat = $pl->viewable_author_types()) !== 0) {
- if ($vat & 2) {
- $display_options->checkbox_item(1, "au", "Authors");
- }
- if ($vat & 1) {
- $display_options->checkbox_item(1, "anonau", "Authors (deblinded)");
- }
- $display_options->checkbox_item(1, "aufull", "Full author info");
- }
- if ($pl->has("collab")) {
- $display_options->checkbox_item(1, "collab", "Collaborators");
- }
-
- // Abstract group
- if ($Conf->has_topics()) {
- $display_options->checkbox_item(1, "topics", "Topics");
- }
-
- // Row numbers
- if ($pl->has("sel")) {
- $display_options->checkbox_item(1, "rownum", "Row numbers");
- }
-
- // Options
- foreach ($Conf->options() as $ox) {
- if ($ox->search_keyword() !== false
- && $ox->can_render(FieldRender::CFSUGGEST)
- && $pl->has("opt$ox->id")) {
- $display_options->checkbox_item(10, $ox->search_keyword(), $ox->name);
- }
- }
-
- // Reviewers group
- if ($Me->privChair) {
- $display_options->checkbox_item(20, "pcconflicts", "PC conflicts");
- $display_options->checkbox_item(20, "allpref", "Review preferences");
- }
- if ($Me->can_view_some_review_identity()) {
- $display_options->checkbox_item(20, "reviewers", "Reviewers");
- }
-
- // Tags group
- if ($Me->isPC && $pl->has("tags")) {
- $opt = [];
- if ($Search->limit() === "a" && !$Me->privChair) {
- $opt["disabled"] = true;
- }
- $display_options->checkbox_item(20, "tags", "Tags", $opt);
- if ($Me->privChair) {
- foreach ($Conf->tags() as $t) {
- if ($t->allotment || $t->approval || $t->rank)
- $display_options->checkbox_item(20, "tagreport:{$t->tag}", "#~{$t->tag} report", $opt);
- }
- }
- }
-
- if ($Me->isPC && $pl->has("lead")) {
- $display_options->checkbox_item(20, "lead", "Discussion leads");
- }
- if ($Me->isPC && $pl->has("shepherd")) {
- $display_options->checkbox_item(20, "shepherd", "Shepherds");
- }
-
- // Scores group
- foreach ($Conf->review_form()->viewable_fields($Me) as $f) {
- if ($f->has_options)
- $display_options->checkbox_item(30, $f->search_keyword(), $f->name_html);
- }
- if (!empty($display_options->items[30])) {
- $display_options->set_header(30, "Scores: ");
- $sortitem = 'Sort by: '
- . Ht::select("scoresort", ListSorter::score_sort_selector_options(),
- ListSorter::canonical_long_score_sort(ListSorter::default_score_sort($Me)),
- ["id" => "scoresort"])
- . '
? ';
- $display_options->item(30, $sortitem);
- }
-
- // Formulas group
- $named_formulas = $Conf->viewable_named_formulas($Me);
- foreach ($named_formulas as $formula) {
- $display_options->checkbox_item(40, "formula:" . $formula->abbreviation(), htmlspecialchars($formula->name));
- }
- if ($named_formulas) {
- $display_options->set_header(40, "Formulas: ");
- }
- if ($Me->isPC && $Search->limit() !== "a") {
- $display_options->item(40, '');
- }
-}
-
-
-echo '\n\n";
-if (!$pl->is_empty()) {
- Ht::stash_script("\$(document.body).addClass(\"want-hash-focus\")");
-}
-echo Ht::unstash();
-
-
-if ($pl_text) {
- if ($Me->has_hidden_papers()
- && !empty($Me->hidden_papers)
- && $Me->is_actas_user()) {
- $pl->message_set()->warning_at(null, $Conf->_("Submissions %#Ns are totally hidden when viewing the site as another user.", array_map(function ($n) { return "#$n"; }, array_keys($Me->hidden_papers))));
- }
- if ($Search->has_problem() || $pl->message_set()->has_messages()) {
- echo '';
- $Conf->warnMsg(array_merge($Search->problem_texts(), $pl->message_set()->message_texts()), true);
- echo '
';
- }
-
- echo "
\n\n";
-
- if ($pl->has("sel")) {
- echo Ht::form($Conf->selfurl($Qreq, ["post" => post_value(), "forceShow" => null]), ["id" => "sel", "class" => "ui-submit js-submit-paperlist"]),
- Ht::hidden("defaultfn", ""),
- Ht::hidden("forceShow", (string) $Qreq->forceShow, ["id" => "forceShow"]),
- Ht::entry("____updates____", "", ["class" => "hidden ignore-diff"]),
- Ht::hidden_default_submit("default", 1);
- }
-
- echo $pl_text;
- if ($pl->is_empty()
- && $Search->limit() !== "s"
- && !$Search->limit_explicit()) {
- $a = [];
- foreach (["q", "qa", "qo", "qx", "qt", "sort", "showtags"] as $xa) {
- if (isset($Qreq[$xa])
- && ($xa != "q" || !isset($Qreq->qa))) {
- $a[] = "$xa=" . urlencode($Qreq[$xa]);
- }
- }
- reset($tOpt);
- if (key($tOpt) != $Search->limit()
- && !in_array($Search->limit(), ["all", "viewable", "act"], true)) {
- echo " (
hoturl("search", join("&", $a)), "\">Repeat search in ", strtolower(current($tOpt)), " )";
- }
- }
-
- if ($pl->has("sel")) {
- echo "";
- }
- echo "
\n";
-} else {
- echo ' ';
-}
-
-$Conf->footer();
+include("index.php");
diff --git a/settings.php b/settings.php
index c32eb8fd4..5790adbbe 100644
--- a/settings.php
+++ b/settings.php
@@ -1,97 +1,5 @@
cancel)) {
- $Conf->redirect_self($Qreq);
-}
-
-$Sv = SettingValues::make_request($Me, $Qreq);
-$Sv->session_highlight();
-if (!$Sv->viewable_by_user()) {
- $Me->escape();
-}
-
-function choose_setting_group($qreq, SettingValues $sv) {
- global $Conf, $Me;
- $req_group = $qreq->group;
- if (!$req_group && preg_match('/\A\/\w+\/*\z/', $qreq->path())) {
- $req_group = $qreq->path_component(0);
- }
- $want_group = $req_group;
- if (!$want_group && isset($_SESSION["sg"])) { // NB not conf-specific session, global
- $want_group = $_SESSION["sg"];
- }
- $want_group = $sv->canonical_group($want_group);
- if (!$want_group || !$sv->group_title($want_group)) {
- if ($sv->conf->time_some_author_view_review()) {
- $want_group = $sv->canonical_group("decisions");
- } else if ($sv->conf->time_after_setting("sub_sub") || $sv->conf->time_review_open()) {
- $want_group = $sv->canonical_group("reviews");
- } else {
- $want_group = $sv->canonical_group("submissions");
- }
- }
- if (!$want_group) {
- $Me->escape();
- }
- if ($want_group !== $req_group && !$qreq->post && $qreq->post_empty()) {
- $Conf->redirect_self($qreq, ["group" => $want_group, "#" => $sv->group_hashid($req_group)]);
- }
- $sv->set_canonical_page($want_group);
- return $want_group;
-}
-$Group = $Qreq->group = choose_setting_group($Qreq, $Sv);
-$_SESSION["sg"] = $Group;
-
-if (isset($Qreq->update) && $Qreq->valid_post()) {
- if ($Sv->execute()) {
- $Me->save_session("settings_highlight", $Sv->message_field_map());
- if (!empty($Sv->updated_fields())) {
- $Sv->conf->confirmMsg("Changes saved.");
- } else {
- $Sv->conf->warnMsg("No changes.");
- }
- $Sv->report();
- $Conf->redirect_self($Qreq);
- }
-}
-
-$Sv->crosscheck();
-
-$Conf->header("Settings", "settings", ["subtitle" => $Sv->group_title($Group), "title_div" => ' ', "body_class" => "leftmenu"]);
-echo Ht::unstash(); // clear out other script references
-echo $Conf->make_script_file("scripts/settings.js"), "\n";
-
-echo Ht::form($Conf->hoturl_post("settings", "group=$Group"),
- ["id" => "settingsform", "class" => "need-unload-protection"]);
-
-echo '\n",
- '',
- '';
-
-$Sv->report(isset($Qreq->update) && $Qreq->valid_post());
-$Sv->render_group(strtolower($Group), true);
-
-
-echo '',
- '
', Ht::submit("update", "Save changes", ["class" => "btn-primary"]), '
',
- '
', Ht::submit("cancel", "Cancel", ["formnovalidate" => true]), '
',
- '
', "\n";
-
-Ht::stash_script('hiliter_children("#settingsform")');
-$Conf->footer();
+include("index.php");
diff --git a/src/api/api_events.php b/src/api/api_events.php
new file mode 100644
index 000000000..c9190b374
--- /dev/null
+++ b/src/api/api_events.php
@@ -0,0 +1,36 @@
+is_reviewer()) {
+ json_exit(403, ["ok" => false]);
+ }
+ $from = $qreq->from;
+ if (!$from || !ctype_digit($from)) {
+ $from = Conf::$now;
+ }
+ $when = $from;
+ $rf = $user->conf->review_form();
+ $events = new PaperEvents($user);
+ $rows = [];
+ $more = false;
+ foreach ($events->events($when, 11) as $xr) {
+ if (count($rows) == 10) {
+ $more = true;
+ } else {
+ if ($xr->crow) {
+ $rows[] = $xr->crow->unparse_flow_entry($user);
+ } else {
+ $rows[] = $rf->unparse_flow_entry($xr->prow, $xr->rrow, $user);
+ }
+ $when = $xr->eventTime;
+ }
+ }
+ json_exit(["ok" => true, "from" => (int) $from, "to" => (int) $when - 1,
+ "rows" => $rows, "more" => $more]);
+ }
+}
diff --git a/src/api/api_requestreview.php b/src/api/api_requestreview.php
index 02f379d11..6fe59625f 100644
--- a/src/api/api_requestreview.php
+++ b/src/api/api_requestreview.php
@@ -5,7 +5,8 @@
class RequestReview_API {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function requestreview($user, $qreq, $prow) {
$round = null;
if ((string) $qreq->round !== ""
@@ -147,7 +148,8 @@ static function requestreview($user, $qreq, $prow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function requestreview_anonymous($user, $qreq, $prow) {
if (trim((string) $qreq->firstName) !== ""
|| trim((string) $qreq->lastName) !== "") {
@@ -172,7 +174,8 @@ static function requestreview_anonymous($user, $qreq, $prow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function denyreview($user, $qreq, $prow) {
if (!$user->allow_administer($prow)) {
return new JsonResult(403, "Permission error.");
@@ -221,7 +224,8 @@ static function denyreview($user, $qreq, $prow) {
/** @param Contact $user
* @param PaperInfo $prow
- * @param ReviewInfo|ReviewRefusalInfo $remrow */
+ * @param ReviewInfo|ReviewRefusalInfo $remrow
+ * @return bool */
static function allow_accept_decline($user, $prow, $remrow) {
if ($user->can_administer($prow)) {
return true;
@@ -236,7 +240,8 @@ static function allow_accept_decline($user, $prow, $remrow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function acceptreview($user, $qreq, $prow) {
if (!ctype_digit($qreq->r)) {
return self::error_result(400, "r", "Bad request.");
@@ -297,7 +302,8 @@ static function acceptreview($user, $qreq, $prow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function declinereview($user, $qreq, $prow) {
if (!ctype_digit($qreq->r)) {
return self::error_result(400, "r", "Bad request.");
@@ -447,7 +453,8 @@ static function claimreview($user, $qreq, $prow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param PaperInfo $prow */
+ * @param PaperInfo $prow
+ * @return JsonResult */
static function retractreview($user, $qreq, $prow) {
$xrrows = $xrequests = [];
$email = trim($qreq->email);
@@ -527,7 +534,8 @@ static function retractreview($user, $qreq, $prow) {
/** @param Contact $user
* @param Qrequest $qreq
- * @param ?PaperInfo $prow */
+ * @param ?PaperInfo $prow
+ * @return JsonResult */
static function undeclinereview($user, $qreq, $prow) {
$refusals = [];
$email = trim($qreq->email);
@@ -582,7 +590,8 @@ static function undeclinereview($user, $qreq, $prow) {
return new JsonResult(["ok" => true, "action" => "undecline"]);
}
- /** @param string $field */
+ /** @param string $field
+ * @return JsonResult */
static function error_result($status, $field, $message) {
return new JsonResult($status, ["ok" => false, "message_list" => [new MessageItem($field, $message, 2)]]);
}
diff --git a/src/conference.php b/src/conference.php
index 6fc60ede6..f69f18c80 100644
--- a/src/conference.php
+++ b/src/conference.php
@@ -4955,7 +4955,8 @@ function api($fn, Contact $user = null, $method = null) {
return self::xt_enabled($uf) ? $uf : null;
}
/** @return JsonResult */
- private function call_api_on($uf, $fn, Contact $user, Qrequest $qreq, $prow) {
+ function call_api_on($uf, $fn, Contact $user, Qrequest $qreq, $prow) {
+ // NOTE: Does not check $user->can_view_paper($prow)
$method = $qreq->method();
if ($method !== "GET"
&& $method !== "HEAD"
@@ -4980,14 +4981,14 @@ private function call_api_on($uf, $fn, Contact $user, Qrequest $qreq, $prow) {
} else if (!is_string($uf->function)) {
return new JsonResult(404, "Function not found.");
} else {
- ++JsonResultException::$capturing;
+ ++JsonCompletion::$capturing;
try {
self::xt_resolve_require($uf);
$j = call_user_func($uf->function, $user, $qreq, $prow, $uf);
- } catch (JsonResultException $ex) {
+ } catch (JsonCompletion $ex) {
$j = $ex->result;
}
- --JsonResultException::$capturing;
+ --JsonCompletion::$capturing;
return JsonResult::make($j);
}
}
@@ -5006,37 +5007,6 @@ static function paper_error_json_result($whynot) {
$result["message_list"][] = new MessageItem(null, $m, 2);
return new JsonResult($status, $result);
}
- function call_api($fn, Contact $user, Qrequest $qreq, PaperInfo $prow = null) {
- // XXX precondition: $user->can_view_paper($prow) || !$prow
- $uf = $this->api($fn, $user, $qreq->method());
- return $this->call_api_on($uf, $fn, $user, $qreq, $prow);
- }
- function call_api_exit($fn, Contact $user, Qrequest $qreq, PaperInfo $prow = null) {
- // XXX precondition: $user->can_view_paper($prow) || !$prow
- $uf = $this->api($fn, $user, $qreq->method());
- $j = $this->call_api_on($uf, $fn, $user, $qreq, $prow);
- if ($uf
- && $qreq->redirect
- && ($uf->redirect ?? false)
- && preg_match('/\A(?![a-z]+:|\/)./', $qreq->redirect)) {
- $a = $j->content;
- if (($x = $a["error"] ?? $a["error_html"] ?? null)) {
- // XXX some instances of `error` are not html!!!!!!
- $this->msg($x, 2);
- } else if (!($a["ok"] ?? false)) {
- $this->msg("Internal error.", 2);
- }
- foreach ($a["message_list"] ?? [] as $mx) {
- $ma = (array) $mx;
- if (($ma["message"] ?? "") !== "") {
- $this->msg($ma["message"], $ma["status"]);
- }
- }
- $this->redirect($this->make_absolute_site($qreq->redirect));
- } else {
- json_exit($j);
- }
- }
// paper columns
@@ -5348,7 +5318,7 @@ function call_hooks($name, Contact $user = null /* ... args */) {
/** @return GroupedExtensions */
function page_partials(Contact $viewer) {
if (!$this->_page_partials || $this->_page_partials->viewer() !== $viewer) {
- $this->_page_partials = new GroupedExtensions($viewer, ["etc/pagepartials.json"], $this->opt("pagePartials"));
+ $this->_page_partials = new GroupedExtensions($viewer, ["etc/pages.json"], $this->opt("pages"));
}
return $this->_page_partials;
}
diff --git a/src/helpers.php b/src/helpers.php
index 20ed42f42..af4ae8055 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -175,7 +175,23 @@ function jsonSerialize() {
}
}
-class JsonResultException extends Exception {
+class Redirection extends Exception {
+ /** @var string */
+ public $url;
+ /** @param string $url */
+ function __construct($url) {
+ parent::__construct("Redirect to $url");
+ $this->url = $url;
+ }
+}
+
+class PageCompletion extends Exception {
+ function __construct() {
+ parent::__construct("Page complete");
+ }
+}
+
+class JsonCompletion extends Exception {
/** @var JsonResult */
public $result;
/** @var int */
@@ -186,21 +202,13 @@ function __construct($j) {
}
}
-class Redirection extends Exception {
- /** @var string */
- public $url;
- /** @param string $url */
- function __construct($url) {
- parent::__construct("Redirect to $url");
- $this->url = $url;
- }
-}
+class_alias("JsonCompletion", "JsonResultException");
function json_exit($json, $arg2 = null) {
global $Qreq;
$json = JsonResult::make($json, $arg2);
- if (JsonResultException::$capturing > 0) {
- throw new JsonResultException($json);
+ if (JsonCompletion::$capturing > 0) {
+ throw new JsonCompletion($json);
} else {
$json->emit($Qreq && $Qreq->valid_token());
exit;
diff --git a/src/init.php b/src/init.php
index 5f2d662fd..06ffa357d 100644
--- a/src/init.php
+++ b/src/init.php
@@ -91,6 +91,7 @@
libxml_disable_entity_loader(true);
}
+
function expand_json_includes_callback($includelist, $callback) {
$includes = [];
foreach (is_array($includelist) ? $includelist : [$includelist] as $k => $str) {
@@ -139,40 +140,242 @@ function expand_json_includes_callback($includelist, $callback) {
}
}
-global $Opt;
-$Opt = $Opt ?? [];
-if (!($Opt["loaded"] ?? null)) {
- SiteLoader::read_main_options();
- if ($Opt["multiconference"] ?? null) {
- Multiconference::init();
+
+function initialize_conf() {
+ global $Opt;
+ $Opt = $Opt ?? [];
+ if (!($Opt["loaded"] ?? null)) {
+ SiteLoader::read_main_options();
+ if ($Opt["multiconference"] ?? null) {
+ Multiconference::init();
+ }
+ if ($Opt["include"] ?? null) {
+ SiteLoader::read_included_options();
+ }
}
- if ($Opt["include"] ?? null) {
- SiteLoader::read_included_options();
+ if (!($Opt["loaded"] ?? null) || ($Opt["missing"] ?? null)) {
+ Multiconference::fail_bad_options();
+ }
+ if ($Opt["dbLogQueries"] ?? null) {
+ Dbl::log_queries($Opt["dbLogQueries"], $Opt["dbLogQueryFile"] ?? null);
}
-}
-if (!($Opt["loaded"] ?? null) || ($Opt["missing"] ?? null)) {
- Multiconference::fail_bad_options();
-}
-if ($Opt["dbLogQueries"] ?? null) {
- Dbl::log_queries($Opt["dbLogQueries"], $Opt["dbLogQueryFile"] ?? null);
-}
-// Allow lots of memory
-if (!($Opt["memoryLimit"] ?? null) && ini_get_bytes("memory_limit") < (128 << 20)) {
- $Opt["memoryLimit"] = "128M";
+ // Allow lots of memory
+ if (!($Opt["memoryLimit"] ?? null) && ini_get_bytes("memory_limit") < (128 << 20)) {
+ $Opt["memoryLimit"] = "128M";
+ }
+ if ($Opt["memoryLimit"] ?? null) {
+ ini_set("memory_limit", $Opt["memoryLimit"]);
+ }
+
+
+ // Create the conference
+ if (!($Opt["__no_main"] ?? false)) {
+ if (!Conf::$main) {
+ Conf::set_main_instance(new Conf($Opt, true));
+ }
+ if (!Conf::$main->dblink) {
+ Multiconference::fail_bad_database();
+ }
+ }
}
-if ($Opt["memoryLimit"] ?? null) {
- ini_set("memory_limit", $Opt["memoryLimit"]);
+
+
+/** @param NavigationState $nav
+ * @param int $uindex
+ * @param int $nusers */
+function initialize_user_redirect($nav, $uindex, $nusers) {
+ if ($nav->page === "api") {
+ if ($nusers === 0) {
+ json_exit(["ok" => false, "error" => "You have been signed out."]);
+ } else {
+ json_exit(["ok" => false, "error" => "Bad user specification."]);
+ }
+ } else if ($_SERVER["REQUEST_METHOD"] === "GET") {
+ $page = $nav->base_absolute();
+ if ($nusers > 0) {
+ $page = "{$page}u/$uindex/";
+ }
+ if ($nav->page !== "index" || $nav->path !== "") {
+ $page = "{$page}{$nav->page}{$nav->php_suffix}{$nav->path}";
+ }
+ Navigation::redirect_absolute($page . $nav->query);
+ } else {
+ Conf::msg_error("You have been signed out from this account.");
+ }
}
-// Create the conference
-if (!($Opt["__no_main"] ?? false)) {
- if (!Conf::$main) {
- Conf::set_main_instance(new Conf($Opt, true));
+function initialize_request() {
+ global $Qreq;
+ $conf = Conf::$main;
+ $nav = Navigation::get();
+
+ // check PHP suffix
+ if (($php_suffix = Conf::$main->opt("phpSuffix")) !== null) {
+ $nav->php_suffix = $php_suffix;
+ }
+
+ // maybe redirect to https
+ if (Conf::$main->opt("redirectToHttps")) {
+ $nav->redirect_http_to_https(Conf::$main->opt("allowLocalHttp"));
+ }
+
+ // collect $qreq
+ $qreq = $Qreq = Qrequest::make_global();
+
+ // check method
+ if ($qreq->method() !== "GET"
+ && $qreq->method() !== "POST"
+ && $qreq->method() !== "HEAD"
+ && ($qreq->method() !== "OPTIONS" || $nav->page !== "api")) {
+ header("HTTP/1.0 405 Method Not Allowed");
+ exit;
+ }
+
+ // mark as already expired to discourage caching, but allow the browser
+ // to cache for history buttons
+ header("Cache-Control: max-age=0,must-revalidate,private");
+
+ // set up Content-Security-Policy if appropriate
+ Conf::$main->prepare_security_headers();
+
+ // skip user initialization if requested
+ if ($conf->opt["__no_main_user"] ?? null) {
+ return;
+ }
+
+ // set up session
+ if (($sh = $conf->opt["sessionHandler"] ?? null)) {
+ /** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName, PhanNonClassMethodCall */
+ $conf->_session_handler = new $sh($conf);
+ session_set_save_handler($conf->_session_handler, true);
}
- if (!Conf::$main->dblink) {
- Multiconference::fail_bad_database();
+ set_session_name($conf);
+ $sn = session_name();
+
+ // check CSRF token, using old value of session ID
+ if ($qreq->post && $sn && isset($_COOKIE[$sn])) {
+ $sid = $_COOKIE[$sn];
+ $l = strlen($qreq->post);
+ if ($l >= 8 && $qreq->post === substr($sid, strlen($sid) > 16 ? 8 : 0, $l)) {
+ $qreq->approve_token();
+ } else if ($_SERVER["REQUEST_METHOD"] === "POST") {
+ error_log("{$conf->dbname}: bad post={$qreq->post}, cookie={$sid}, url=" . $_SERVER["REQUEST_URI"]);
+ }
+ }
+ ensure_session(ENSURE_SESSION_ALLOW_EMPTY);
+
+ // upgrade session format
+ if (!isset($_SESSION["u"]) && isset($_SESSION["trueuser"])) {
+ $_SESSION["u"] = $_SESSION["trueuser"]->email;
+ unset($_SESSION["trueuser"]);
+ }
+
+ // determine user
+ $trueemail = $_SESSION["u"] ?? null;
+ $userset = $_SESSION["us"] ?? ($trueemail ? [$trueemail] : []);
+ $usercount = count($userset);
+ '@phan-var list $userset';
+
+ $uindex = 0;
+ if ($nav->shifted_path === "") {
+ $wantemail = $_GET["i"] ?? $trueemail;
+ while ($wantemail !== null
+ && $uindex < $usercount
+ && strcasecmp($userset[$uindex], $wantemail) !== 0) {
+ ++$uindex;
+ }
+ if ($uindex < $usercount
+ && ($usercount > 1 || isset($_GET["i"]))
+ && $nav->page !== "api"
+ && ($_SERVER["REQUEST_METHOD"] === "GET" || $_SERVER["REQUEST_METHOD"] === "HEAD")) {
+ // redirect to `/u` version
+ $nav->query = preg_replace('/[?&]i=[^&]+(?=&|\z)/', '', $nav->query);
+ if (str_starts_with($nav->query, "&")) {
+ $nav->query = "?" . substr($nav->query, 1);
+ }
+ initialize_user_redirect($nav, $uindex, count($userset));
+ }
+ } else if (str_starts_with($nav->shifted_path, "u/")) {
+ $uindex = $usercount === 0 ? -1 : (int) substr($nav->shifted_path, 2);
+ }
+ if ($uindex >= 0 && $uindex < $usercount) {
+ $trueemail = $userset[$uindex];
+ } else if ($uindex !== 0) {
+ initialize_user_redirect($nav, 0, $usercount);
+ }
+
+ if (isset($_GET["i"])
+ && $trueemail
+ && strcasecmp($_GET["i"], $trueemail) !== 0) {
+ Conf::msg_error("You are signed in as " . htmlspecialchars($trueemail) . ", not " . htmlspecialchars($_GET["i"]) . ". hoturl("signin", ["email" => $_GET["i"]]) . "\">Sign in ");
+ }
+
+ // look up and activate user
+ $guser = $trueemail ? $conf->user_by_email($trueemail) : null;
+ if (!$guser) {
+ $guser = new Contact($trueemail ? (object) ["email" => $trueemail] : null);
+ }
+ $guser = $guser->activate($qreq, true);
+ Contact::set_main_user($guser);
+
+ // author view capability documents should not be indexed
+ if (!$guser->email
+ && $guser->has_author_view_capability()
+ && !$conf->opt("allowIndexPapers")) {
+ header("X-Robots-Tag: noindex, noarchive");
+ }
+
+ // redirect if disabled
+ if ($guser->is_disabled()) {
+ $gj = $conf->page_partials($guser)->get($nav->page);
+ if (!$gj || !($gj->allow_disabled ?? false)) {
+ $conf->redirect_hoturl("index");
+ }
+ }
+
+ // if bounced through login, add post data
+ if (isset($_SESSION["login_bounce"][4])
+ && $_SESSION["login_bounce"][4] <= Conf::$now) {
+ unset($_SESSION["login_bounce"]);
+ }
+
+ if (!$guser->is_empty()
+ && isset($_SESSION["login_bounce"])
+ && !isset($_SESSION["testsession"])) {
+ $lb = $_SESSION["login_bounce"];
+ if ($lb[0] == $conf->dsn
+ && $lb[2] !== "index"
+ && $lb[2] == Navigation::page()) {
+ assert($qreq instanceof Qrequest);
+ foreach ($lb[3] as $k => $v) {
+ if (!isset($qreq[$k]))
+ $qreq[$k] = $v;
+ }
+ $qreq->set_annex("after_login", true);
+ }
+ unset($_SESSION["login_bounce"]);
+ }
+
+ // set $_SESSION["addrs"]
+ if ($_SERVER["REMOTE_ADDR"]
+ && (!$guser->is_empty()
+ || isset($_SESSION["addrs"]))
+ && (!isset($_SESSION["addrs"])
+ || !is_array($_SESSION["addrs"])
+ || $_SESSION["addrs"][0] !== $_SERVER["REMOTE_ADDR"])) {
+ $as = [$_SERVER["REMOTE_ADDR"]];
+ if (isset($_SESSION["addrs"]) && is_array($_SESSION["addrs"])) {
+ foreach ($_SESSION["addrs"] as $a) {
+ if ($a !== $_SERVER["REMOTE_ADDR"] && count($as) < 5)
+ $as[] = $a;
+ }
+ }
+ $_SESSION["addrs"] = $as;
}
}
+
+
+initialize_conf();
diff --git a/src/initweb.php b/src/initweb.php
deleted file mode 100644
index 45f0f80a7..000000000
--- a/src/initweb.php
+++ /dev/null
@@ -1,204 +0,0 @@
-page === "api") {
- if ($nusers === 0) {
- json_exit(["ok" => false, "error" => "You have been signed out."]);
- } else {
- json_exit(["ok" => false, "error" => "Bad user specification."]);
- }
- } else if ($_SERVER["REQUEST_METHOD"] === "GET") {
- $page = $nav->base_absolute();
- if ($nusers > 0) {
- $page = "{$page}u/$uindex/";
- }
- if ($nav->page !== "index" || $nav->path !== "") {
- $page = "{$page}{$nav->page}{$nav->php_suffix}{$nav->path}";
- }
- Navigation::redirect_absolute($page . $nav->query);
- } else {
- Conf::msg_error("You have been signed out from this account.");
- }
-}
-
-/** @return Qrequest */
-function initialize_web() {
- $conf = Conf::$main;
- $nav = Navigation::get();
-
- // check PHP suffix
- if (($php_suffix = Conf::$main->opt("phpSuffix")) !== null) {
- $nav->php_suffix = $php_suffix;
- }
-
- // maybe redirect to https
- if (Conf::$main->opt("redirectToHttps")) {
- $nav->redirect_http_to_https(Conf::$main->opt("allowLocalHttp"));
- }
-
- // collect $qreq
- $qreq = Qrequest::make_global();
-
- // check method
- if ($qreq->method() !== "GET"
- && $qreq->method() !== "POST"
- && $qreq->method() !== "HEAD"
- && ($qreq->method() !== "OPTIONS" || $nav->page !== "api")) {
- header("HTTP/1.0 405 Method Not Allowed");
- exit;
- }
-
- // mark as already expired to discourage caching, but allow the browser
- // to cache for history buttons
- header("Cache-Control: max-age=0,must-revalidate,private");
-
- // set up Content-Security-Policy if appropriate
- Conf::$main->prepare_security_headers();
-
- // skip user initialization if requested
- if (Contact::$no_main_user) {
- return $qreq;
- }
-
- // set up session
- if (($sh = $conf->opt["sessionHandler"] ?? null)) {
- /** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName, PhanNonClassMethodCall */
- $conf->_session_handler = new $sh($conf);
- session_set_save_handler($conf->_session_handler, true);
- }
- set_session_name($conf);
- $sn = session_name();
-
- // check CSRF token, using old value of session ID
- if ($qreq->post && $sn && isset($_COOKIE[$sn])) {
- $sid = $_COOKIE[$sn];
- $l = strlen($qreq->post);
- if ($l >= 8 && $qreq->post === substr($sid, strlen($sid) > 16 ? 8 : 0, $l)) {
- $qreq->approve_token();
- } else if ($_SERVER["REQUEST_METHOD"] === "POST") {
- error_log("{$conf->dbname}: bad post={$qreq->post}, cookie={$sid}, url=" . $_SERVER["REQUEST_URI"]);
- }
- }
- ensure_session(ENSURE_SESSION_ALLOW_EMPTY);
-
- // upgrade session format
- if (!isset($_SESSION["u"]) && isset($_SESSION["trueuser"])) {
- $_SESSION["u"] = $_SESSION["trueuser"]->email;
- unset($_SESSION["trueuser"]);
- }
-
- // determine user
- $trueemail = $_SESSION["u"] ?? null;
- $userset = $_SESSION["us"] ?? ($trueemail ? [$trueemail] : []);
- $usercount = count($userset);
- '@phan-var list $userset';
-
- $uindex = 0;
- if ($nav->shifted_path === "") {
- $wantemail = $_GET["i"] ?? $trueemail;
- while ($wantemail !== null
- && $uindex < $usercount
- && strcasecmp($userset[$uindex], $wantemail) !== 0) {
- ++$uindex;
- }
- if ($uindex < $usercount
- && ($usercount > 1 || isset($_GET["i"]))
- && $nav->page !== "api"
- && ($_SERVER["REQUEST_METHOD"] === "GET" || $_SERVER["REQUEST_METHOD"] === "HEAD")) {
- // redirect to `/u` version
- $nav->query = preg_replace('/[?&]i=[^&]+(?=&|\z)/', '', $nav->query);
- if (str_starts_with($nav->query, "&")) {
- $nav->query = "?" . substr($nav->query, 1);
- }
- initialize_user_redirect($nav, $uindex, count($userset));
- }
- } else if (str_starts_with($nav->shifted_path, "u/")) {
- $uindex = $usercount === 0 ? -1 : (int) substr($nav->shifted_path, 2);
- }
- if ($uindex >= 0 && $uindex < $usercount) {
- $trueemail = $userset[$uindex];
- } else if ($uindex !== 0) {
- initialize_user_redirect($nav, 0, $usercount);
- }
-
- if (isset($_GET["i"])
- && $trueemail
- && strcasecmp($_GET["i"], $trueemail) !== 0) {
- Conf::msg_error("You are signed in as " . htmlspecialchars($trueemail) . ", not " . htmlspecialchars($_GET["i"]) . ". hoturl("signin", ["email" => $_GET["i"]]) . "\">Sign in ");
- }
-
- // look up and activate user
- $guser = $trueemail ? $conf->user_by_email($trueemail) : null;
- if (!$guser) {
- $guser = new Contact($trueemail ? (object) ["email" => $trueemail] : null);
- }
- $guser = $guser->activate($qreq, true);
- Contact::set_main_user($guser);
-
- // author view capability documents should not be indexed
- if (!$guser->email
- && $guser->has_author_view_capability()
- && !$conf->opt("allowIndexPapers")) {
- header("X-Robots-Tag: noindex, noarchive");
- }
-
- // redirect if disabled
- if ($guser->is_disabled()) {
- $gj = $conf->page_partials($guser)->get($nav->page);
- if (!$gj || !($gj->allow_disabled ?? false)) {
- $conf->redirect_hoturl("index");
- }
- }
-
- // if bounced through login, add post data
- if (isset($_SESSION["login_bounce"][4])
- && $_SESSION["login_bounce"][4] <= Conf::$now) {
- unset($_SESSION["login_bounce"]);
- }
-
- if (!$guser->is_empty()
- && isset($_SESSION["login_bounce"])
- && !isset($_SESSION["testsession"])) {
- $lb = $_SESSION["login_bounce"];
- if ($lb[0] == $conf->dsn
- && $lb[2] !== "index"
- && $lb[2] == Navigation::page()) {
- assert($qreq instanceof Qrequest);
- foreach ($lb[3] as $k => $v) {
- if (!isset($qreq[$k]))
- $qreq[$k] = $v;
- }
- $qreq->set_annex("after_login", true);
- }
- unset($_SESSION["login_bounce"]);
- }
-
- // set $_SESSION["addrs"]
- if ($_SERVER["REMOTE_ADDR"]
- && (!$guser->is_empty()
- || isset($_SESSION["addrs"]))
- && (!isset($_SESSION["addrs"])
- || !is_array($_SESSION["addrs"])
- || $_SESSION["addrs"][0] !== $_SERVER["REMOTE_ADDR"])) {
- $as = [$_SERVER["REMOTE_ADDR"]];
- if (isset($_SESSION["addrs"]) && is_array($_SESSION["addrs"])) {
- foreach ($_SESSION["addrs"] as $a) {
- if ($a !== $_SERVER["REMOTE_ADDR"] && count($as) < 5)
- $as[] = $a;
- }
- }
- $_SESSION["addrs"] = $as;
- }
-
- return $qreq;
-}
-
-$Qreq = initialize_web();
diff --git a/src/logentry.php b/src/logentry.php
new file mode 100644
index 000000000..4455e3af8
--- /dev/null
+++ b/src/logentry.php
@@ -0,0 +1,315 @@
+ */
+ public $paperIdArray;
+ /** @var ?list */
+ public $destContactIdArray;
+}
+
+class LogEntryGenerator {
+ /** @var Conf */
+ private $conf;
+ private $wheres;
+ private $page_size;
+ private $delta = 0;
+ private $lower_offset_bound;
+ private $upper_offset_bound;
+ private $rows_offset;
+ private $rows_max_offset;
+ /** @var list */
+ private $rows = [];
+ private $filter;
+ private $page_to_offset;
+ private $log_url_base;
+ private $explode_mail = false;
+ private $mail_stash;
+ /** @var array */
+ private $users;
+ /** @var array */
+ private $need_users;
+
+ function __construct(Conf $conf, $wheres, $page_size) {
+ $this->conf = $conf;
+ $this->wheres = $wheres;
+ $this->page_size = $page_size;
+ $this->set_filter(null);
+ $this->users = $conf->pc_users();
+ $this->need_users = [];
+ }
+
+ function set_filter($filter) {
+ $this->filter = $filter;
+ $this->rows = [];
+ $this->lower_offset_bound = 0;
+ $this->upper_offset_bound = INF;
+ $this->page_to_offset = [];
+ }
+
+ function set_explode_mail($explode_mail) {
+ $this->explode_mail = $explode_mail;
+ }
+
+ function has_filter() {
+ return !!$this->filter;
+ }
+
+ function page_size() {
+ return $this->page_size;
+ }
+
+ function page_delta() {
+ return $this->delta;
+ }
+
+ function set_page_delta($delta) {
+ assert(is_int($delta) && $delta >= 0 && $delta < $this->page_size);
+ $this->delta = $delta;
+ }
+
+ private function page_offset($pageno) {
+ $offset = ($pageno - 1) * $this->page_size;
+ if ($offset > 0 && $this->delta > 0) {
+ $offset -= $this->page_size - $this->delta;
+ }
+ return $offset;
+ }
+
+ private function load_rows($pageno, $limit, $delta_adjusted = false) {
+ $limit = (int) $limit;
+ if ($pageno > 1 && $this->delta > 0 && !$delta_adjusted) {
+ --$pageno;
+ $limit += $this->page_size;
+ }
+ $offset = ($pageno - 1) * $this->page_size;
+ $db_offset = $offset;
+ if (($this->filter || !$this->explode_mail) && $db_offset !== 0) {
+ if (!isset($this->page_to_offset[$pageno])) {
+ $xlimit = min(4 * $this->page_size + $limit, 2000);
+ $xpageno = max($pageno - floor($xlimit / $this->page_size), 1);
+ $this->load_rows($xpageno, $xlimit, true);
+ if ($this->rows_offset <= $offset && $offset + $limit <= $this->rows_max_offset)
+ return;
+ }
+ $xpageno = $pageno;
+ while ($xpageno > 1 && !isset($this->page_to_offset[$xpageno])) {
+ --$xpageno;
+ }
+ $db_offset = $xpageno > 1 ? $this->page_to_offset[$xpageno] : 0;
+ }
+
+ $q = "select logId, timestamp, contactId, destContactId, trueContactId, action, paperId from ActionLog";
+ if (!empty($this->wheres)) {
+ $q .= " where " . join(" and ", $this->wheres);
+ }
+ $q .= " order by logId desc";
+
+ $this->rows = [];
+ $this->rows_offset = $offset;
+ $n = 0;
+ $exhausted = false;
+ while ($n < $limit && !$exhausted) {
+ $result = $this->conf->qe_raw($q . " limit $db_offset,$limit");
+ $first_db_offset = $db_offset;
+ while (($row = $result->fetch_object("LogEntry"))) {
+ '@phan-var LogEntry $row';
+ $this->need_users[(int) $row->contactId] = true;
+ $destuid = (int) ($row->destContactId ? : $row->contactId);
+ $this->need_users[$destuid] = true;
+ ++$db_offset;
+ if (!$this->explode_mail
+ && $this->mail_stash
+ && $this->mail_stash->action === $row->action) {
+ $this->mail_stash->destContactIdArray[] = $destuid;
+ if ($row->paperId) {
+ $this->mail_stash->paperIdArray[] = (int) $row->paperId;
+ }
+ continue;
+ }
+ if (!$this->filter || call_user_func($this->filter, $row)) {
+ $this->rows[] = $row;
+ ++$n;
+ if ($n % $this->page_size === 0) {
+ $this->page_to_offset[$pageno + ($n / $this->page_size)] = $db_offset;
+ }
+ if (!$this->explode_mail) {
+ if (substr($row->action, 0, 11) === "Sent mail #") {
+ $this->mail_stash = $row;
+ $row->destContactIdArray = [$destuid];
+ $row->destContactId = null;
+ $row->paperIdArray = [];
+ if ($row->paperId) {
+ $row->paperIdArray[] = (int) $row->paperId;
+ $row->paperId = null;
+ }
+ } else {
+ $this->mail_stash = null;
+ }
+ }
+ }
+ }
+ Dbl::free($result);
+ $exhausted = $first_db_offset + $limit !== $db_offset;
+ }
+
+ if ($n > 0) {
+ $this->lower_offset_bound = max($this->lower_offset_bound, $this->rows_offset + $n);
+ }
+ if ($exhausted) {
+ $this->upper_offset_bound = min($this->upper_offset_bound, $this->rows_offset + $n);
+ }
+ $this->rows_max_offset = $exhausted ? INF : $this->rows_offset + $n;
+ }
+
+ /** @param int $pageno
+ * @return bool */
+ function has_page($pageno, $load_npages = null) {
+ global $nlinks;
+ assert(is_int($pageno) && $pageno >= 1);
+ $offset = $this->page_offset($pageno);
+ if ($offset >= $this->lower_offset_bound
+ && $offset < $this->upper_offset_bound) {
+ if ($load_npages) {
+ $limit = $load_npages * $this->page_size;
+ } else {
+ $limit = ($nlinks + 1) * $this->page_size + 30;
+ }
+ if ($this->filter) {
+ $limit = max($limit, 2000);
+ }
+ $this->load_rows($pageno, $limit);
+ }
+ return $offset < $this->lower_offset_bound;
+ }
+
+ /** @param int $pageno
+ * @param int $timestamp
+ * @return bool */
+ function page_after($pageno, $timestamp, $load_npages = null) {
+ $rows = $this->page_rows($pageno, $load_npages);
+ return !empty($rows) && $rows[count($rows) - 1]->timestamp > $timestamp;
+ }
+
+ /** @param int $pageno
+ * @return list */
+ function page_rows($pageno, $load_npages = null) {
+ assert(is_int($pageno) && $pageno >= 1);
+ if (!$this->has_page($pageno, $load_npages)) {
+ return [];
+ }
+ $offset = $this->page_offset($pageno);
+ if ($offset < $this->rows_offset
+ || $offset + $this->page_size > $this->rows_max_offset) {
+ $this->load_rows($pageno, $this->page_size);
+ }
+ return array_slice($this->rows, $offset - $this->rows_offset, $this->page_size);
+ }
+
+ function set_log_url_base($url) {
+ $this->log_url_base = $url;
+ }
+
+ function page_link_html($pageno, $html) {
+ $url = $this->log_url_base;
+ if ($pageno !== 1 && $this->delta > 0) {
+ $url .= "&offset=" . $this->delta;
+ }
+ return '' . $html . ' ';
+ }
+
+ private function _make_users() {
+ unset($this->need_users[0]);
+ $this->need_users = array_diff_key($this->need_users, $this->users);
+ if (!empty($this->need_users)) {
+ $result = $this->conf->qe("select contactId, firstName, lastName, affiliation, email, roles, contactTags, disabled, primaryContactId from ContactInfo where contactId?a", array_keys($this->need_users));
+ while (($user = Contact::fetch($result, $this->conf))) {
+ $this->users[$user->contactId] = $user;
+ unset($this->need_users[$user->contactId]);
+ }
+ Dbl::free($result);
+ }
+ if (!empty($this->need_users)) {
+ foreach ($this->need_users as $cid => $x) {
+ $user = $this->users[$cid] = new Contact(["contactId" => $cid, "disabled" => 1], $this->conf);
+ $user->disabled = "deleted";
+ }
+ $result = $this->conf->qe("select contactId, firstName, lastName, '' affiliation, email, 1 disabled from DeletedContactInfo where contactId?a", array_keys($this->need_users));
+ while (($user = Contact::fetch($result, $this->conf))) {
+ $this->users[$user->contactId] = $user;
+ $user->disabled = "deleted";
+ }
+ Dbl::free($result);
+ }
+ $this->need_users = [];
+ }
+
+ /** @param LogEntry $row
+ * @param 'contactId'|'destContactId'|'trueContactId' $key
+ * @return list */
+ function users_for($row, $key) {
+ if (!empty($this->need_users)) {
+ $this->_make_users();
+ }
+ $uid = (int) $row->$key;
+ if (!$uid && $key === "contactId") {
+ $uid = (int) $row->destContactId;
+ }
+ $u = $uid ? [$this->users[$uid]] : [];
+ if ($key === "destContactId" && isset($row->destContactIdArray)) {
+ foreach ($row->destContactIdArray as $uid) {
+ $u[] = $this->users[$uid];
+ }
+ }
+ return $u;
+ }
+
+ /** @param LogEntry $row
+ * @return list */
+ function paper_ids($row) {
+ if (!isset($row->cleanedAction)) {
+ if (!isset($row->paperIdArray)) {
+ $row->paperIdArray = [];
+ }
+ if (preg_match('/\A(.* |)\(papers ([\d, ]+)\)?\z/', $row->action, $m)) {
+ $row->cleanedAction = rtrim($m[1]);
+ foreach (preg_split('/[\s,]+/', $m[2]) as $p) {
+ if ($p !== "")
+ $row->paperIdArray[] = (int) $p;
+ }
+ } else {
+ $row->cleanedAction = $row->action;
+ }
+ if ($row->paperId) {
+ $row->paperIdArray[] = (int) $row->paperId;
+ }
+ $row->paperIdArray = array_values(array_unique($row->paperIdArray));
+ }
+ return $row->paperIdArray;
+ }
+
+ function cleaned_action($row) {
+ if (!isset($row->cleanedAction)) {
+ $this->paper_ids($row);
+ }
+ return $row->cleanedAction;
+ }
+}
diff --git a/src/logentryfilter.php b/src/logentryfilter.php
new file mode 100644
index 000000000..a16816ce1
--- /dev/null
+++ b/src/logentryfilter.php
@@ -0,0 +1,63 @@
+ */
+ private $pidset;
+ /** @var bool */
+ private $want;
+ private $includes;
+
+ /** @param array $pidset
+ * @param bool $want */
+ function __construct(Contact $user, $pidset, $want, $includes) {
+ $this->user = $user;
+ $this->pidset = $pidset;
+ $this->want = $want;
+ $this->includes = $includes;
+ }
+
+ private function test_pidset($row, $pidset, $want, $includes) {
+ if ($row->paperId) {
+ return isset($pidset[$row->paperId]) === $want
+ && (!$includes || isset($includes[$row->paperId]));
+ } else if (preg_match('/\A(.*) \(papers ([\d, ]+)\)?\z/', $row->action, $m)) {
+ preg_match_all('/\d+/', $m[2], $mm);
+ $pids = [];
+ $included = !$includes;
+ foreach ($mm[0] as $pid) {
+ if (isset($pidset[$pid]) === $want) {
+ $pids[] = $pid;
+ $included = $included || isset($includes[$pid]);
+ }
+ }
+ if (empty($pids) || !$included) {
+ return false;
+ } else if (count($pids) === 1) {
+ $row->action = $m[1];
+ $row->paperId = $pids[0];
+ } else {
+ $row->action = $m[1] . " (papers " . join(", ", $pids) . ")";
+ }
+ return true;
+ } else {
+ return $this->user->privChair;
+ }
+ }
+
+ /** @param LogEntry $row
+ * @return bool */
+ function __invoke($row) {
+ if ($this->user->hidden_papers !== null
+ && !$this->test_pidset($row, $this->user->hidden_papers, false, null)) {
+ return false;
+ } else if ($row->contactId === $this->user->contactId) {
+ return true;
+ } else {
+ return $this->test_pidset($row, $this->pidset, $this->want, $this->includes);
+ }
+ }
+}
diff --git a/src/partials/p_adminhome.php b/src/pages/p_adminhome.php
similarity index 98%
rename from src/partials/p_adminhome.php
rename to src/pages/p_adminhome.php
index 314b3fd7d..acf5c53fd 100644
--- a/src/partials/p_adminhome.php
+++ b/src/pages/p_adminhome.php
@@ -1,8 +1,8 @@
privChair && $qreq->valid_token());
if (isset($qreq->clearbug)
diff --git a/src/pages/p_api.php b/src/pages/p_api.php
new file mode 100644
index 000000000..bcd552195
--- /dev/null
+++ b/src/pages/p_api.php
@@ -0,0 +1,117 @@
+conf;
+ if ($qreq->base !== null) {
+ $conf->set_siteurl($qreq->base);
+ }
+ if (!$user->has_account_here()
+ && ($key = $user->capability("@kiosk"))) {
+ $kiosks = $conf->setting_json("__tracker_kiosk") ? : (object) array();
+ if (isset($kiosks->$key) && $kiosks->$key->update_at >= Conf::$now - 172800) {
+ if ($kiosks->$key->update_at < Conf::$now - 3600) {
+ $kiosks->$key->update_at = Conf::$now;
+ $conf->save_setting("__tracker_kiosk", 1, $kiosks);
+ }
+ $user->tracker_kiosk_state = $kiosks->$key->show_papers ? 2 : 1;
+ }
+ }
+ if ($qreq->p) {
+ $conf->set_paper_request($qreq, $user);
+ }
+
+ // requests
+ $fn = $qreq->fn;
+ $is_track = $fn === "track";
+ if (!$user->is_disabled() && ($is_track || $fn === "status")) {
+ if ($is_track) {
+ MeetingTracker::track_api($user, $qreq); // may fall through to act like `status`
+ }
+
+ $j = $user->my_deadlines($conf->paper ? [$conf->paper] : []);
+ $j->ok = true;
+ if ($is_track && ($new_trackerid = $qreq->annex("new_trackerid"))) {
+ $j->new_trackerid = $new_trackerid;
+ }
+
+ if ($conf->paper && $user->can_view_tags($conf->paper)) {
+ $pj = (object) ["pid" => $conf->paper->paperId];
+ $conf->paper->add_tag_info_json($pj, $user);
+ if (count((array) $pj) > 1) {
+ $j->p = [$conf->paper->paperId => $pj];
+ }
+ }
+ } else {
+ $uf = $conf->api($fn, $user, $qreq->method());
+ $j = $conf->call_api_on($uf, $fn, $user, $qreq, $conf->paper);
+ if ($uf
+ && $qreq->redirect
+ && ($uf->redirect ?? false)
+ && preg_match('/\A(?![a-z]+:|\/)./', $qreq->redirect)) {
+ $a = $j->content;
+ if (($x = $a["error"] ?? $a["error_html"] ?? null)) {
+ // XXX some instances of `error` are not html!!!!!!
+ $conf->msg($x, 2);
+ } else if (!($a["ok"] ?? false)) {
+ $conf->msg("Internal error.", 2);
+ }
+ foreach ($a["message_list"] ?? [] as $mx) {
+ $ma = (array) $mx;
+ if (($ma["message"] ?? "") !== "") {
+ $conf->msg($ma["message"], $ma["status"]);
+ }
+ }
+ $conf->redirect($conf->make_absolute_site($qreq->redirect));
+ }
+ }
+
+ json_exit($j);
+ }
+
+ /** @param NavigationState $nav
+ * @param Conf $conf */
+ static function go_nav($nav, $conf) {
+ // argument cleaning
+ if (!isset($_GET["fn"])) {
+ $fn = $nav->path_component(0, true);
+ if ($fn && ctype_digit($fn)) {
+ if (!isset($_GET["p"])) {
+ $_GET["p"] = $fn;
+ }
+ $fn = $nav->path_component(1, true);
+ }
+ if ($fn) {
+ $_GET["fn"] = $fn;
+ } else if (isset($_GET["track"])) {
+ $_GET["fn"] = "track";
+ } else {
+ http_response_code(404);
+ header("Content-Type: text/plain; charset=utf-8");
+ echo json_encode(["ok" => false, "error" => "API function missing."]);
+ exit;
+ }
+ }
+ if ($_GET["fn"] === "deadlines") {
+ $_GET["fn"] = "status";
+ }
+ if (!isset($_GET["p"])
+ && ($p = $nav->path_component(1, true))
+ && ctype_digit($p)) {
+ $_GET["p"] = $p;
+ }
+
+ // trackerstatus is a special case: prevent session creation
+ if ($_GET["fn"] === "trackerstatus") {
+ global $Opt;
+ $Opt["__no_main_user"] = true;
+ initialize_request();
+ MeetingTracker::trackerstatus_api(new Contact(null, Conf::$main));
+ } else {
+ initialize_request();
+ self::go(Contact::$main_user, Qrequest::$main_request);
+ }
+ }
+}
diff --git a/src/pages/p_deadlines.php b/src/pages/p_deadlines.php
new file mode 100644
index 000000000..3e20eeec9
--- /dev/null
+++ b/src/pages/p_deadlines.php
@@ -0,0 +1,133 @@
+", $conf->_($phrase, $arg), " : ",
+ $conf->unparse_time_long($time), $conf->unparse_usertime_span($time),
+ "\n", $conf->_($description, $arg), " ";
+ }
+
+ static function go(Contact $user) {
+ if ($user->contactId && $user->is_disabled()) {
+ $user = new Contact(["email" => $user->email], $user->conf);
+ }
+
+ // header
+ $conf = $user->conf;
+ $dl = $user->my_deadlines();
+
+ $conf->header("Deadlines", "deadlines");
+
+ if ($user->privChair) {
+ echo "As PC chair, you can hoturl("settings"), "\">change the deadlines .
\n";
+ }
+
+ echo "\n";
+
+ // If you change these, also change Contact::has_reportable_deadline().
+ if ($dl->sub->reg ?? false) {
+ self::dl1($conf, $dl->sub->reg, "Registration deadline",
+ "You can register new submissions until this deadline.");
+ }
+
+ if ($dl->sub->update ?? false) {
+ self::dl1($conf, $dl->sub->update, "Update deadline",
+ "You can update submissions and upload new versions until this deadline.");
+ }
+
+ if ($dl->sub->sub ?? false) {
+ self::dl1($conf, $dl->sub->sub, "Submission deadline",
+ "Submissions must be ready by this deadline to be reviewed.");
+ }
+
+ if ($dl->resps ?? false) {
+ foreach ($dl->resps as $rname => $dlr) {
+ if (($dlr->open ?? false)
+ && $dlr->open <= Conf::$now
+ && ($dlr->done ?? false)) {
+ if ($rname == 1) {
+ self::dl1($conf, $dlr->done, "Response deadline",
+ "You can submit responses to the reviews until this deadline.");
+ } else {
+ self::dl1($conf, $dlr->done, "%s response deadline",
+ "You can submit %s responses to the reviews until this deadline.", $rname);
+ }
+ }
+ }
+ }
+
+ if (($dl->rev ?? false) && ($dl->rev->open ?? false)) {
+ $dlbyround = [];
+ $last_dlbyround = null;
+ foreach ($conf->defined_round_list() as $i => $round_name) {
+ $isuf = $i ? "_$i" : "";
+ $es = +$conf->setting("extrev_soft$isuf");
+ $eh = +$conf->setting("extrev_hard$isuf");
+ $ps = $ph = -1;
+
+ $thisdl = [];
+ if ($user->isPC) {
+ $ps = +$conf->setting("pcrev_soft$isuf");
+ $ph = +$conf->setting("pcrev_hard$isuf");
+ if ($ph && ($ph < Conf::$now || $ps < Conf::$now)) {
+ $thisdl[] = "PH" . $ph;
+ } else if ($ps) {
+ $thisdl[] = "PS" . $ps;
+ }
+ }
+ if ($es != $ps || $eh != $ph) {
+ if ($eh && ($eh < Conf::$now || $es < Conf::$now)) {
+ $thisdl[] = "EH" . $eh;
+ } else if ($es) {
+ $thisdl[] = "ES" . $es;
+ }
+ }
+ if (count($thisdl)) {
+ $dlbyround[$round_name] = $last_dlbyround = join(" ", $thisdl);
+ }
+ }
+
+ $dlroundunify = true;
+ foreach ($dlbyround as $x) {
+ if ($x !== $last_dlbyround)
+ $dlroundunify = false;
+ }
+
+ foreach ($dlbyround as $roundname => $dltext) {
+ if ($dltext === "") {
+ continue;
+ }
+ $suffix = $roundname === "" ? "" : "_$roundname";
+ if ($dlroundunify) {
+ $roundname = "";
+ }
+ foreach (explode(" ", $dltext) as $dldesc) {
+ $dt = substr($dldesc, 0, 2);
+ $dv = (int) substr($dldesc, 2);
+ if ($dt === "PS") {
+ self::dl1($conf, $dv, "%s review deadline",
+ "%s reviews are requested by this deadline.", $roundname);
+ } else if ($dt === "PH") {
+ self::dl1($conf, $dv, "%s review hard deadline",
+ "%s reviews must be submitted by this deadline.", $roundname);
+ } else if ($dt === "ES") {
+ self::dl1($conf, $dv, "%s external review deadline",
+ "%s reviews are requested by this deadline.", $roundname);
+ } else if ($dt === "EH") {
+ self::dl1($conf, $dv, "%s external review hard deadline",
+ "%s reviews must be submitted by this deadline.", $roundname);
+ }
+ }
+ if ($dlroundunify) {
+ break;
+ }
+ }
+ }
+
+ echo "\n";
+ $conf->footer();
+ }
+}
diff --git a/src/pages/p_doc.php b/src/pages/p_doc.php
new file mode 100644
index 000000000..4a537f53d
--- /dev/null
+++ b/src/pages/p_doc.php
@@ -0,0 +1,174 @@
+is_empty()) {
+ $user->escape();
+ exit;
+ } else if (str_starts_with($status, "5")) {
+ $navpath = $qreq->path();
+ error_log($user->conf->dbname . ": bad doc $status $msg "
+ . json_encode($qreq) . ($navpath ? " @$navpath" : "")
+ . ($user ? " {$user->email}" : "")
+ . (empty($_SERVER["HTTP_REFERER"]) ? "" : " R[" . $_SERVER["HTTP_REFERER"] . "]"));
+ }
+
+ header("HTTP/1.1 $status");
+ if (isset($qreq->fn)) {
+ json_exit(MessageItem::make_error_json($msg));
+ } else {
+ $user->conf->header("Download", null);
+ $msg && Conf::msg_error($msg);
+ $user->conf->footer();
+ exit;
+ }
+ }
+
+ /** @param bool $active
+ * @return object */
+ static private function history_element(DocumentInfo $doc, $active) {
+ $pj = ["hash" => $doc->text_hash(), "at" => $doc->timestamp, "mimetype" => $doc->mimetype];
+ if ($active ? $doc->size() : $doc->size) {
+ $pj["size"] = $doc->size;
+ }
+ if ($doc->filename) {
+ $pj["filename"] = $doc->filename;
+ }
+ if ($active) {
+ $pj["active"] = true;
+ }
+ $pj["link"] = $doc->url(null, DocumentInfo::DOCURL_INCLUDE_TIME | Conf::HOTURL_RAW | Conf::HOTURL_ABSOLUTE);
+ return (object) $pj;
+ }
+
+ /** @param int $dtype
+ * @return list */
+ static private function history(Contact $user, PaperInfo $prow, $dtype) {
+ $docs = $prow->documents($dtype);
+
+ $pjs = $actives = [];
+ foreach ($docs as $doc) {
+ $pjs[] = self::history_element($doc, true);
+ $actives[$doc->paperStorageId] = true;
+ }
+
+ if ($user->can_view_document_history($prow)
+ && $dtype >= DTYPE_FINAL) {
+ $result = $prow->conf->qe("select paperId, paperStorageId, timestamp, mimetype, sha1, filename, infoJson, size from PaperStorage where paperId=? and documentType=? and filterType is null order by paperStorageId desc", $prow->paperId, $dtype);
+ while (($doc = DocumentInfo::fetch($result, $prow->conf, $prow))) {
+ if (!isset($actives[$doc->paperStorageId]))
+ $pjs[] = self::history_element($doc, false);
+ }
+ Dbl::free($result);
+ }
+
+ return $pjs;
+ }
+
+ /** @param Contact $user
+ * @param Qrequest $qreq */
+ static function go($user, $qreq) {
+ $user->add_overrides(Contact::OVERRIDE_CONFLICT);
+ try {
+ $dr = new DocumentRequest($qreq, $qreq->path(), $user);
+ } catch (Exception $e) {
+ self::error("404 Not Found", htmlspecialchars($e->getMessage()), $user, $qreq);
+ }
+
+ if (($whyNot = $dr->perm_view_document($user))) {
+ self::error(isset($whyNot["permission"]) ? "403 Forbidden" : "404 Not Found", $whyNot->unparse_html(), $user, $qreq);
+ }
+ $prow = $dr->prow;
+ $want_docid = $request_docid = (int) $dr->docid;
+
+ // history
+ if ($qreq->fn === "history") {
+ json_exit(["ok" => true, "result" => self::history($user, $prow, $dr->dtype)]);
+ }
+
+ if (!isset($qreq->version) && isset($qreq->hash)) {
+ $qreq->version = $qreq->hash;
+ }
+
+ // time
+ if (isset($qreq->at) && !isset($qreq->version) && $dr->dtype >= DTYPE_FINAL) {
+ if (ctype_digit($qreq->at)) {
+ $time = intval($qreq->at);
+ } else if (!($time = $user->conf->parse_time($qreq->at))) {
+ $time = Conf::$now;
+ }
+ $want_pj = null;
+ foreach (self::history($user, $prow, $dr->dtype) as $pj) {
+ if ($want_pj && $want_pj->at <= $time && $pj->at < $want_pj->at) {
+ break;
+ } else {
+ $want_pj = $pj;
+ }
+ }
+ if ($want_pj) {
+ $qreq->version = $want_pj->hash;
+ }
+ }
+
+ // version
+ if (isset($qreq->version) && $dr->dtype >= DTYPE_FINAL) {
+ $version_hash = Filer::hash_as_binary(trim($qreq->version));
+ if (!$version_hash) {
+ self::error("404 Not Found", "No such version.", $user, $qreq);
+ }
+ $want_docid = $user->conf->fetch_ivalue("select max(paperStorageId) from PaperStorage where paperId=? and documentType=? and sha1=? and filterType is null", $dr->paperId, $dr->dtype, $version_hash);
+ if ($want_docid !== null && $user->can_view_document_history($prow)) {
+ $request_docid = $want_docid;
+ }
+ }
+
+ if ($dr->attachment && !$request_docid) {
+ $doc = $prow->attachment($dr->dtype, $dr->attachment);
+ } else {
+ $doc = $prow->document($dr->dtype, $request_docid);
+ }
+ if ($want_docid !== 0 && (!$doc || $doc->paperStorageId !== $want_docid)) {
+ self::error("404 Not Found", "No such version.", $user, $qreq);
+ } else if (!$doc || $doc->paperStorageId <= 1) {
+ self::error("404 Not Found", "No such " . ($dr->attachment ? "attachment" : "document") . " “" . htmlspecialchars($dr->req_filename) . "”.", $user, $qreq);
+ }
+
+ // pass through filters
+ foreach ($dr->filters as $filter) {
+ $doc = $filter->exec($doc) ?? $doc;
+ }
+
+ // check for contents request
+ if ($qreq->fn === "listing" || $qreq->fn === "consolidatedlisting") {
+ if (!$doc->is_archive()) {
+ json_exit(MessageItem::make_error_json("That file is not an archive."));
+ } else if (($listing = $doc->archive_listing(65536)) === false) {
+ json_exit(MessageItem::make_error_json($doc->error ? $doc->error_html : "Internal error."));
+ } else {
+ $listing = ArchiveInfo::clean_archive_listing($listing);
+ if ($qreq->fn === "consolidatedlisting") {
+ $listing = join(", ", ArchiveInfo::consolidate_archive_listing($listing));
+ }
+ json_exit(["ok" => true, "result" => $listing]);
+ }
+ }
+
+ // serve document
+ session_write_close(); // to allow concurrent clicks
+ $opts = ["attachment" => cvtint($qreq->save) > 0];
+ if ($doc->has_hash() && ($x = $qreq->hash) && $doc->check_text_hash($x)) {
+ $opts["cacheable"] = true;
+ }
+ if ($doc->download(DocumentRequest::add_connection_options($opts))) {
+ DocumentInfo::log_download_activity([$doc], $user);
+ } else {
+ self::error("500 Server Error", $doc->error_html, $user, $qreq);
+ }
+ }
+}
diff --git a/src/pages/p_help.php b/src/pages/p_help.php
new file mode 100644
index 000000000..ac69b81bd
--- /dev/null
+++ b/src/pages/p_help.php
@@ -0,0 +1,76 @@
+\n";
+ foreach ($hth->groups() as $ht) {
+ if ($ht->name !== "topics" && isset($ht->title)) {
+ echo 'name"), '">', $ht->title, ' ';
+ if (isset($ht->description)) {
+ echo '', $ht->description ?? "", ' ';
+ }
+ echo "\n";
+ }
+ }
+ echo " \n";
+ }
+
+ static function go(Contact $user, Qrequest $qreq) {
+ $conf = $user->conf;
+
+ $help_topics = new GroupedExtensions($user, [
+ '{"name":"topics","title":"Help topics","position":-1000000,"priority":1000000,"render_function":"Help_Page::show_help_topics"}',
+ "etc/helptopics.json"
+ ], $conf->opt("helpTopics"));
+
+ if (!$qreq->t && preg_match('/\A\/\w+\/*\z/i', $qreq->path())) {
+ $qreq->t = $qreq->path_component(0);
+ }
+ $topic = $qreq->t ? : "topics";
+ $want_topic = $help_topics->canonical_group($topic);
+ if (!$want_topic) {
+ $want_topic = "topics";
+ }
+ if ($want_topic !== $topic) {
+ $conf->redirect_self($qreq, ["t" => $want_topic]);
+ }
+ $topicj = $help_topics->get($topic);
+
+ $conf->header("Help", "help", ["title_div" => ' ', "body_class" => "leftmenu"]);
+
+ $hth = new HelpRenderer($help_topics, $user);
+
+ echo '\n",
+ '',
+ '';
+ $hth->render_group($topic, true);
+ echo " \n";
+
+ $conf->footer();
+ }
+}
diff --git a/src/partials/p_home.php b/src/pages/p_home.php
similarity index 99%
rename from src/partials/p_home.php
rename to src/pages/p_home.php
index 3270e113e..977a5e7b7 100644
--- a/src/partials/p_home.php
+++ b/src/pages/p_home.php
@@ -1,8 +1,8 @@
has_email() || $qreq->signin) {
- Signin_Partial::render_signin_form($user, $qreq, $gx);
+ Signin_Page::render_signin_form($user, $qreq, $gx);
}
}
diff --git a/src/pages/p_log.php b/src/pages/p_log.php
new file mode 100644
index 000000000..7d09a5cd1
--- /dev/null
+++ b/src/pages/p_log.php
@@ -0,0 +1,597 @@
+ */
+ public $user_html = [];
+ /** @var list */
+ private $lef_clauses = [];
+ /** @var ?array */
+ private $include_pids;
+ /** @var ?array */
+ private $exclude_pids;
+
+ function __construct(Contact $viewer, Qrequest $qreq) {
+ $this->conf = $viewer->conf;
+ $this->viewer = $viewer;
+ $this->qreq = $qreq;
+ }
+
+
+ /** @param string $query
+ * @param ?string $field */
+ private function add_search_clause($query, $field) {
+ $search = new PaperSearch($this->viewer, ["t" => "all", "q" => $query]);
+ $search->set_allow_deleted(true);
+ $pids = $search->paper_ids();
+ foreach ($search->problem_texts() as $w) {
+ Ht::warning_at($field, $w);
+ }
+ if (!empty($pids)) {
+ $w = [];
+ foreach ($pids as $p) {
+ $w[] = "paperId=$p";
+ $w[] = "action like '%(papers% $p,%'";
+ $w[] = "action like '%(papers% $p)%'";
+ }
+ $this->lef_clauses[] = "(" . join(" or ", $w) . ")";
+ $this->include_pids = array_flip($pids);
+ } else {
+ if (!$search->has_problem()) {
+ Ht::warning_at($field, "No papers match that search.");
+ }
+ $this->lef_clauses[] = "false";
+ }
+ }
+
+ private function add_user_clause() {
+ $ids = [];
+ $accts = new SearchSplitter($this->qreq->u);
+ while (($word = $accts->shift()) !== "") {
+ $flags = ContactSearch::F_TAG | ContactSearch::F_USER | ContactSearch::F_ALLOW_DELETED;
+ if (substr($word, 0, 1) === "\"") {
+ $flags |= ContactSearch::F_QUOTED;
+ $word = preg_replace('/(?:\A"|"\z)/', "", $word);
+ }
+ $search = new ContactSearch($flags, $word, $this->viewer);
+ foreach ($search->user_ids() as $id) {
+ $ids[$id] = $id;
+ }
+ }
+ $w = [];
+ if (!empty($ids)) {
+ $result = $this->conf->qe("select contactId, email from ContactInfo where contactId?a union select contactId, email from DeletedContactInfo where contactId?a", $ids, $ids);
+ while (($row = $result->fetch_row())) {
+ $w[] = "contactId=$row[0]";
+ $w[] = "destContactId=$row[0]";
+ $w[] = "action like " . Dbl::utf8ci("'% " . sqlq_for_like($row[1]) . "%'");
+ }
+ }
+ if (!empty($w)) {
+ $this->lef_clauses[] = "(" . join(" or ", $w) . ")";
+ } else {
+ Ht::warning_at("u", "No matching users.");
+ $this->lef_clauses[] = "false";
+ }
+ }
+
+ private function add_action_clause() {
+ $w = [];
+ $str = $this->qreq->q;
+ while (($str = ltrim($str)) !== "") {
+ if ($str[0] === '"') {
+ preg_match('/\A"([^"]*)"?/', $str, $m);
+ } else {
+ preg_match('/\A([^"\s]+)/', $str, $m);
+ }
+ $str = (string) substr($str, strlen($m[0]));
+ if ($m[1] !== "") {
+ $w[] = "action like " . Dbl::utf8ci("'%" . sqlq_for_like($m[1]) . "%'");
+ }
+ }
+ $this->lef_clauses[] = "(" . join(" or ", $w) . ")";
+ }
+
+ private function set_date() {
+ $this->first_timestamp = $this->conf->parse_time($this->qreq->date);
+ if ($this->first_timestamp === false) {
+ Ht::error_at("date", "Invalid date. Try format “YYYY-MM-DD HH:MM:SS”.");
+ }
+ }
+
+
+ /** @param int $count
+ * @return LogEntryGenerator */
+ private function make_generator($count) {
+ $leg = new LogEntryGenerator($this->conf, $this->lef_clauses, $count);
+
+ $this->exclude_pids = $this->viewer->hidden_papers ? : [];
+ if ($this->viewer->privChair && $this->conf->has_any_manager()) {
+ foreach ($this->viewer->paper_set(["myConflicts" => true]) as $prow) {
+ if (!$this->viewer->allow_administer($prow)) {
+ $this->exclude_pids[$prow->paperId] = true;
+ }
+ }
+ }
+
+ if (!$this->viewer->privChair) {
+ $good_pids = [];
+ foreach ($this->viewer->paper_set($this->conf->check_any_admin_tracks($this->viewer) ? [] : ["myManaged" => true]) as $prow) {
+ if ($this->viewer->allow_administer($prow)) {
+ $good_pids[$prow->paperId] = true;
+ }
+ }
+ $leg->set_filter(new LogEntryFilter($this->viewer, $good_pids, true, $this->include_pids));
+ } else if (!$this->qreq->forceShow && !empty($this->exclude_pids)) {
+ $leg->set_filter(new LogEntryFilter($this->viewer, $this->exclude_pids, false, $this->include_pids));
+ }
+
+ return $leg;
+ }
+
+ /** @param LogEntryGenerator $leg
+ * @param ?int $page
+ * @return int */
+ function choose_page($leg, $page) {
+ if ($this->first_timestamp) {
+ $page = 1;
+ while ($leg->page_after($page, $this->first_timestamp, ceil(2000 / $leg->page_size()))) {
+ ++$page;
+ }
+ $delta = 0;
+ foreach ($leg->page_rows($page) as $row) {
+ if ($row->timestamp > $this->first_timestamp)
+ ++$delta;
+ }
+ if ($delta) {
+ $leg->set_page_delta($delta);
+ ++$page;
+ }
+ } else if (!$page) { // handle `earliest`
+ $page = 1;
+ while ($leg->has_page($page + 1, ceil(2000 / $leg->page_size()))) {
+ ++$page;
+ }
+ } else if ($this->qreq->offset
+ && ($delta = cvtint($this->qreq->offset)) >= 0
+ && $delta < $leg->page_size()) {
+ $leg->set_page_delta($delta);
+ }
+ return $page;
+ }
+
+
+ /** @param LogEntryGenerator $leg */
+ function handle_download($leg) {
+ session_commit();
+ $csvg = $this->conf->make_csvg("log");
+ $narrow = true;
+ $csvg->select(["date", "email", "affected_email", "via",
+ $narrow ? "paper" : "papers", "action"]);
+ foreach ($leg->page_rows(1) as $row) {
+ $date = date("Y-m-d H:i:s e", (int) $row->timestamp);
+ $xusers = $xdest_users = [];
+ foreach ($leg->users_for($row, "contactId") as $u) {
+ $xusers[] = $u->email;
+ }
+ foreach ($leg->users_for($row, "destContactId") as $u) {
+ $xdest_users[] = $u->email;
+ }
+ if ($xdest_users == $xusers) {
+ $xdest_users = [];
+ }
+ if ($row->trueContactId) {
+ $via = $row->trueContactId < 0 ? "link" : "admin";
+ } else {
+ $via = "";
+ }
+ $pids = $leg->paper_ids($row);
+ $action = $leg->cleaned_action($row);
+ if ($narrow) {
+ if (empty($xusers)) {
+ $xusers = [""];
+ }
+ if (empty($xdest_users)) {
+ $xdest_users = [""];
+ }
+ if (empty($pids)) {
+ $pids = [];
+ }
+ foreach ($xusers as $u1) {
+ foreach ($xdest_users as $u2) {
+ foreach ($pids as $p) {
+ $csvg->add_row([$date, $u1, $u2, $via, $p, $action]);
+ }
+ }
+ }
+ } else {
+ $csvg->add_row([
+ $date, join(" ", $xusers), join(" ", $xdest_users),
+ $via, join(" ", $pids), $action
+ ]);
+ }
+ }
+ $csvg->emit();
+ exit;
+ }
+
+
+ // render search list
+ /** @param int $page */
+ function render_searchbar(LogEntryGenerator $leg, $page) {
+ $date = "";
+ $dplaceholder = null;
+ if (Ht::problem_status_at("date")) {
+ $date = $this->qreq->date;
+ } else if ($page === 1) {
+ $dplaceholder = "now";
+ } else if (($rows = $leg->page_rows($page))) {
+ $dplaceholder = $this->conf->unparse_time_log((int) $rows[0]->timestamp);
+ } else if ($this->first_timestamp) {
+ $dplaceholder = $this->conf->unparse_time_log((int) $this->first_timestamp);
+ }
+
+ echo Ht::form(hoturl("log"), ["method" => "get", "id" => "searchform", "class" => "clearfix"]);
+ if ($this->qreq->forceShow) {
+ echo Ht::hidden("forceShow", 1);
+ }
+ echo '',
+ '
Concerning action(s) ',
+ Ht::feedback_html_at("q"),
+ Ht::entry("q", $this->qreq->q, ["id" => "q", "size" => 40]),
+ '
Concerning paper(s) ',
+ Ht::feedback_html_at("p"),
+ Ht::entry("p", $this->qreq->p, ["id" => "p", "class" => "need-suggest papersearch", "autocomplete" => "off", "size" => 40, "spellcheck" => false]),
+ '
Concerning user(s) ',
+ Ht::feedback_html_at("u"),
+ Ht::entry("u", $this->qreq->u, ["id" => "u", "size" => 40]),
+ '
Show ',
+ Ht::entry("n", $this->qreq->n, ["id" => "n", "size" => 4, "placeholder" => 50]),
+ ' records at a time',
+ Ht::feedback_html_at("n"),
+ '
Starting at ',
+ Ht::feedback_html_at("date"),
+ Ht::entry("date", $date, ["id" => "date", "size" => 40, "placeholder" => $dplaceholder]),
+ '
',
+ Ht::submit("Show"),
+ Ht::submit("download", "Download", ["class" => "ml-3"]),
+ '';
+
+ if ($page > 1 || $leg->has_page(2)) {
+ $urls = ["q=" . urlencode($this->qreq->q)];
+ foreach (["p", "u", "n", "forceShow"] as $x) {
+ if ($this->qreq[$x])
+ $urls[] = "$x=" . urlencode($this->qreq[$x]);
+ }
+ $leg->set_log_url_base(hoturl("log", join("&", $urls)));
+ echo "";
+ if ($page > 1) {
+ echo $leg->page_link_html(1, "Newest "), " | ";
+ }
+ echo "
";
+ if ($page > 1) {
+ echo $leg->page_link_html($page - 1, "" . Icons::ui_linkarrow(3) . "Newer ");
+ }
+ echo "
";
+ if ($page - $this->nlinks > 1) {
+ echo " ...";
+ }
+ for ($p = max($page - $this->nlinks, 1); $p < $page; ++$p) {
+ echo " ", $leg->page_link_html($p, $p);
+ }
+ echo "
", $page, "
";
+ for ($p = $page + 1; $p <= $page + $this->nlinks && $leg->has_page($p); ++$p) {
+ echo $leg->page_link_html($p, $p), " ";
+ }
+ if ($leg->has_page($page + $this->nlinks + 1)) {
+ echo "... ";
+ }
+ echo "
";
+ if ($leg->has_page($page + 1)) {
+ echo $leg->page_link_html($page + 1, "Older" . Icons::ui_linkarrow(1) . " ");
+ }
+ echo "
";
+ if ($leg->has_page($page + $this->nlinks + 1)) {
+ echo " | ", $leg->page_link_html("earliest", "Oldest ");
+ }
+ echo "
";
+ }
+ echo " \n";
+ }
+
+ /** @param Contact $user */
+ function user_html($user) {
+ if (($pc = $this->conf->pc_member_by_id($user->contactId))) {
+ $user = $pc;
+ }
+ if ($user->disabled === "deleted") {
+ $t = '' . $user->name_h(NAME_E) . '';
+ } else {
+ $t = $user->name_h(NAME_P);
+ }
+ $dt = null;
+ if (($viewable = $user->viewable_tags($this->viewer))) {
+ $dt = $this->conf->tags();
+ if (($colors = $dt->color_classes($viewable))) {
+ $t = '' . $t . ' ';
+ }
+ }
+ $url = $this->conf->hoturl("log", ["q" => "", "u" => $user->email, "n" => $this->qreq->n]);
+ $t = "{$t} ";
+ if ($dt && $dt->has_decoration) {
+ $tagger = new Tagger($this->viewer);
+ $t .= $tagger->unparse_decoration_html($viewable, Tagger::DECOR_USER);
+ }
+ $roles = 0;
+ if (isset($user->roles) && ($user->roles & Contact::ROLE_PCLIKE)) {
+ $roles = $user->viewable_pc_roles($this->viewer);
+ }
+ if (!($roles & Contact::ROLE_PCLIKE)) {
+ $t .= ' <' . htmlspecialchars($user->email) . '>';
+ }
+ if ($roles !== 0 && ($rolet = Contact::role_html_for($roles))) {
+ $t .= " $rolet";
+ }
+ return $t;
+ }
+
+ /** @param list $users
+ * @return string */
+ function users_html($users, $via) {
+ if (empty($users) && $via < 0) {
+ return "via author link ";
+ }
+ $all_pc = true;
+ $ts = [];
+ $last_user = null;
+ usort($users, $this->conf->user_comparator());
+ foreach ($users as $user) {
+ if ($user === $last_user) {
+ continue;
+ }
+ if ($all_pc
+ && (!isset($user->roles) || !($user->roles & Contact::ROLE_PCLIKE))) {
+ $all_pc = false;
+ }
+ if ($user->disabled === "deleted") {
+ if ($user->email) {
+ $t = '' . $user->name_h(NAME_E) . '';
+ } else {
+ $t = '[deleted user ' . $user->contactId . ']';
+ }
+ } else {
+ $t = $this->user_html[$user->contactId] ?? null;
+ if ($t === null) {
+ $t = $this->user_html[$user->contactId] = $this->user_html($user);
+ }
+ if ($via) {
+ $t .= ($via < 0 ? ' via link ' : ' via admin ');
+ }
+ }
+ $ts[] = $t;
+ $last_user = $user;
+ }
+ if (count($ts) <= 3) {
+ return join(", ", $ts);
+ } else {
+ $fmt = $all_pc ? "%d PC users" : "%d users";
+ return '';
+ }
+ }
+
+ /** @param LogEntryGenerator $leg
+ * @param int $page */
+ function render_page($leg, $page) {
+ $conf = $this->conf;
+ $conf->header("Log", "actionlog");
+
+ $trs = [];
+ $has_dest_user = false;
+ foreach ($leg->page_rows($page) as $row) {
+ $time = $conf->unparse_time_log((int) $row->timestamp);
+ $t = ["{$time} "];
+
+ $via = $row->trueContactId;
+ $xusers = $leg->users_for($row, "contactId");
+ $xusers_html = $this->users_html($xusers, $via);
+ $xdest_users = $leg->users_for($row, "destContactId");
+
+ if ($xdest_users && $xusers != $xdest_users) {
+ $xdestusers_html = $this->users_html($xdest_users, false);
+ $t[] = "{$xusers_html} {$xdestusers_html} ";
+ $has_dest_user = true;
+ } else {
+ $t[] = "{$xusers_html} ";
+ }
+
+ // XXX users that aren't in contactId slot
+ // if (preg_match(',\A(.*)<([^>]*@[^>]*)>\s*(.*)\z,', $act, $m)) {
+ // $t .= htmlspecialchars($m[2]);
+ // $act = $m[1] . $m[3];
+ // } else
+ // $t .= "[None]";
+
+ $act = $leg->cleaned_action($row);
+ $at = "";
+ if (strpos($act, "eview ") !== false
+ && preg_match('/\A(.* |)([Rr]eview )(\d+)( .*|)\z/', $act, $m)) {
+ $at = htmlspecialchars($m[1])
+ . Ht::link($m[2] . $m[3], $conf->hoturl("review", ["p" => $row->paperId, "r" => $m[3]]))
+ . "";
+ $act = $m[4];
+ } else if (substr($act, 0, 7) === "Comment"
+ && preg_match('/\AComment (\d+)(.*)\z/s', $act, $m)) {
+ $at = "hoturl("paper", "p={$row->paperId}#cid{$m[1]}") . "\">Comment " . $m[1] . " ";
+ $act = $m[2];
+ } else if (substr($act, 0, 8) === "Response"
+ && preg_match('/\AResponse (\d+)(.*)\z/s', $act, $m)) {
+ $at = "hoturl("paper", "p={$row->paperId}#cid{$m[1]}") . "\">Response " . $m[1] . " ";
+ $act = $m[2];
+ } else if (strpos($act, " mail ") !== false
+ && preg_match('/\A(Sending|Sent|Account was sent) mail #(\d+)(.*)\z/s', $act, $m)) {
+ $at = $m[1] . " hoturl("mail", "fromlog=$m[2]") . "\">mail #$m[2] ";
+ $act = $m[3];
+ } else if (substr($act, 0, 3) === "Tag"
+ && preg_match('{\ATag:? ((?:[-+]#[^\s#]*(?:#[-+\d.]+|)(?: |\z))+)(.*)\z}s', $act, $m)) {
+ $at = "Tag";
+ $act = $m[2];
+ foreach (explode(" ", rtrim($m[1])) as $word) {
+ if (($hash = strpos($word, "#", 2)) === false) {
+ $hash = strlen($word);
+ }
+ $at .= " " . $word[0] . ' substr($word, 1, $hash - 1)])
+ . '">' . htmlspecialchars(substr($word, 1, $hash - 1))
+ . ' ' . substr($word, $hash);
+ }
+ } else if ($row->paperId > 0
+ && (substr($act, 0, 8) === "Updated "
+ || substr($act, 0, 10) === "Submitted "
+ || substr($act, 0, 11) === "Registered ")
+ && preg_match('/\A(\S+(?: final)?)(.*)\z/', $act, $m)
+ && preg_match('/\A(.* )(final|submission)((?:,| |\z).*)\z/', $m[2], $mm)) {
+ $at = $m[1] . $mm[1] . "hoturl("doc", "p={$row->paperId}&dt={$mm[2]}&at={$row->timestamp}") . "\">{$mm[2]} ";
+ $act = $mm[3];
+ }
+ $at .= htmlspecialchars($act);
+ if (($pids = $leg->paper_ids($row))) {
+ if (count($pids) === 1)
+ $at .= ' (paper ' . $pids[0] . " )";
+ else {
+ $at .= ' (papers ';
+ foreach ($pids as $i => $p) {
+ $at .= ($i ? ', ' : ' ') . '' . $p . ' ';
+ }
+ $at .= ')';
+ }
+ }
+ $t[] = "{$at} ";
+ $trs[] = ' ' . join("", $t) . " \n";
+ }
+
+ if (!$this->viewer->privChair || !empty($this->exclude_pids)) {
+ echo '';
+ if (!$this->viewer->privChair) {
+ $conf->msg("Only showing your actions and entries for papers you administer.", "xinfo");
+ } else if (!empty($this->exclude_pids)
+ && (!$this->include_pids || array_intersect_key($this->include_pids, $this->exclude_pids))
+ && array_keys($this->exclude_pids) != array_keys($this->viewer->hidden_papers ? : [])) {
+ $req = [];
+ foreach (["q", "p", "u", "n"] as $k) {
+ if ($this->qreq->$k !== "")
+ $req[$k] = $this->qreq->$k;
+ }
+ $req["page"] = $page;
+ if ($page > 1 && $leg->page_delta() > 0) {
+ $req["offset"] = $leg->page_delta();
+ }
+ if ($this->qreq->forceShow) { // XXX never true
+ $conf->msg("Showing all entries. (" . Ht::link("Unprivileged view", $conf->selfurl($this->qreq, $req + ["forceShow" => null])) . ")", "xinfo");
+ } else {
+ $conf->msg("Not showing entries for " . Ht::link("conflicted administered papers", $conf->hoturl("search", "q=" . join("+", array_keys($this->exclude_pids)))) . ".", "xinfo");
+ }
+ }
+ echo '
';
+ }
+
+ $this->render_searchbar($leg, $page);
+ if (!empty($trs)) {
+ echo "\n",
+ ' ',
+ 'Time ',
+ 'User ',
+ 'Affected user ',
+ 'Action ',
+ "\n \n",
+ join("", $trs),
+ " \n
\n";
+ } else {
+ echo "No records\n";
+ }
+
+ $conf->footer();
+ }
+
+ static function go(Contact $viewer, Qrequest $qreq) {
+ if (!$viewer->is_manager()) {
+ $viewer->escape();
+ }
+
+ // clean request
+ unset($qreq->forceShow, $_GET["forceShow"], $_POST["forceShow"]);
+
+ if ($qreq->page === "earliest") {
+ $page = null;
+ } else {
+ $page = max(cvtint($qreq->page, -1), 1);
+ }
+
+ $count = 50;
+ if (isset($qreq->n) && trim($qreq->n) !== "") {
+ $count = cvtint($qreq->n, -1);
+ if ($count <= 0) {
+ $count = 50;
+ Ht::error_at("n", "Show records: Expected a number greater than 0.");
+ }
+ }
+ $count = min($count, 200);
+
+ $qreq->q = trim((string) $qreq->q);
+ $qreq->p = trim((string) $qreq->p);
+ if (isset($qreq->acct) && !isset($qreq->u)) {
+ $qreq->u = $qreq->acct;
+ }
+ $qreq->u = trim((string) $qreq->u);
+ $qreq->date = trim((string) $qreq->date);
+ if (trim($qreq->date) === "") {
+ $qreq->date = "now";
+ }
+
+ // parse filter parts
+ $lp = new Log_Page($viewer, $qreq);
+ if ($qreq->p !== "") {
+ $lp->add_search_clause($qreq->p, "p");
+ }
+ if ($qreq->u !== "") {
+ $lp->add_user_clause();
+ }
+ if ($qreq->q !== "") {
+ $lp->add_action_clause();
+ }
+
+ // create entry generator
+ $leg = $lp->make_generator($qreq->download ? 10000000 : $count);
+
+ if ($qreq->download) {
+ $lp->handle_download($leg);
+ }
+
+ if ($qreq->date !== "now") {
+ $lp->set_date();
+ }
+ $page = $lp->choose_page($leg, $page);
+ $lp->render_page($leg, $page);
+ }
+}
diff --git a/src/pages/p_paper.php b/src/pages/p_paper.php
new file mode 100644
index 000000000..e0161c48b
--- /dev/null
+++ b/src/pages/p_paper.php
@@ -0,0 +1,521 @@
+conf = $user->conf;
+ $this->user = $user;
+ $this->qreq = $qreq;
+ }
+
+ function echo_header() {
+ $m = $this->pt ? $this->pt->mode : ($this->qreq->m ?? "p");
+ PaperTable::echo_header($this->pt, "paper-" . ($m === "edit" ? "edit" : "view"), $m, $this->qreq);
+ }
+
+ function error_exit($msg) {
+ $this->echo_header();
+ Ht::stash_script("hotcrp.shortcut().add()");
+ $msg && Conf::msg_error($msg);
+ $this->conf->footer();
+ throw new PageCompletion;
+ }
+
+ function load_prow() {
+ // determine whether request names a paper
+ try {
+ $pr = new PaperRequest($this->user, $this->qreq, false);
+ $this->prow = $this->conf->paper = $pr->prow;
+ } catch (Redirection $redir) {
+ assert(PaperRequest::simple_qreq($this->qreq));
+ throw $redir;
+ } catch (PermissionProblem $perm) {
+ $this->error_exit($perm->set("listViewable", true)->unparse_html());
+ }
+ }
+
+ function handle_cancel() {
+ if ($this->prow->timeSubmitted && $this->qreq->m === "edit") {
+ unset($this->qreq->m);
+ }
+ $this->conf->redirect_self($this->qreq);
+ }
+
+ function handle_withdraw() {
+ if (($whynot = $this->user->perm_withdraw_paper($this->prow))) {
+ Conf::msg_error($whynot->unparse_html() . " The submission has not been withdrawn.");
+ return;
+ }
+
+ $reason = (string) $this->qreq->reason;
+ if ($reason === ""
+ && $this->user->can_administer($this->prow)
+ && $this->qreq->doemail > 0) {
+ $reason = (string) $this->qreq->emailNote;
+ }
+
+ $aset = new AssignmentSet($this->user, true);
+ $aset->enable_papers($this->prow);
+ $aset->parse("paper,action,withdraw reason\n{$this->prow->paperId},withdraw," . CsvGenerator::quote($reason));
+ if (!$aset->execute()) {
+ error_log("{$this->conf->dbname}: withdraw #{$this->prow->paperId} failure: " . json_encode($aset->json_result()));
+ }
+ $this->load_prow();
+
+ // email contact authors themselves
+ if (!$this->user->can_administer($this->prow) || $this->qreq->doemail) {
+ $tmpl = $this->prow->has_author($this->user) ? "@authorwithdraw" : "@adminwithdraw";
+ HotCRPMailer::send_contacts($tmpl, $this->prow, ["reason" => $reason, "infoNames" => 1]);
+ }
+
+ // email reviewers
+ if ($this->prow->all_reviews()) {
+ $preps = [];
+ foreach ($this->prow->review_followers() as $minic) {
+ if ($minic->contactId !== $this->user->contactId
+ && ($p = HotCRPMailer::prepare_to($minic, "@withdrawreviewer", ["prow" => $this->prow, "reason" => $reason]))) {
+ if (!$minic->can_view_review_identity($this->prow, null)) {
+ $p->unique_preparation = true;
+ }
+ $preps[] = $p;
+ }
+ }
+ HotCRPMailer::send_combined_preparations($preps);
+ }
+
+ $this->conf->redirect_self($this->qreq);
+ }
+
+ function handle_revive() {
+ if (($whynot = $this->user->perm_revive_paper($this->prow))) {
+ Conf::msg_error($whynot->unparse_html());
+ return;
+ }
+
+ $aset = new AssignmentSet($this->user, true);
+ $aset->enable_papers($this->prow);
+ $aset->parse("paper,action\n{$this->prow->paperId},revive");
+ if (!$aset->execute()) {
+ error_log("{$this->conf->dbname}: revive #{$this->prow->paperId} failure: " . json_encode($aset->json_result()));
+ }
+ $this->conf->redirect_self($this->qreq);
+ }
+
+ function handle_delete() {
+ if ($this->prow->paperId <= 0) {
+ $this->conf->confirmMsg("Submission deleted.");
+ } else if (!$this->user->can_administer($this->prow)) {
+ Conf::msg_error("Only the program chairs can permanently delete submissions. Authors can withdraw submissions, which is effectively the same.");
+ } else {
+ // mail first, before contact info goes away
+ if ($this->qreq->doemail) {
+ HotCRPMailer::send_contacts("@deletepaper", $this->prow, ["reason" => (string) $this->qreq->emailNote, "infoNames" => 1]);
+ }
+ if ($this->prow->delete_from_database($this->user)) {
+ $this->conf->confirmMsg("Submission #{$this->prow->paperId} deleted.");
+ }
+ $this->error_exit("");
+ }
+ }
+
+ /** @return string */
+ private function deadline_note($dl, $future_msg, $past_msg) {
+ $deadline = $this->conf->unparse_setting_time_span($dl);
+ $strong = false;
+ if ($deadline === "N/A") {
+ $msg = "";
+ } else if ($this->conf->time_after_setting($dl)) {
+ $msg = $past_msg;
+ $strong = true;
+ } else {
+ $msg = $future_msg;
+ }
+ if ($msg !== "") {
+ $msg = $this->conf->_($msg, $deadline);
+ }
+ if ($msg !== "" && $strong) {
+ $msg = "{$msg} ";
+ }
+ return $msg;
+ }
+
+ /** @return list */
+ private function missing_required_fields(PaperInfo $prow) {
+ $missing = [];
+ foreach ($prow->form_fields() as $o) {
+ if ($o->test_required($prow) && !$o->value_present($prow->force_option($o)))
+ $missing[] = $o;
+ }
+ return $missing;
+ }
+
+ function handle_update($action) {
+ $conf = $this->conf;
+ // XXX lock tables
+ $is_new = $this->prow->paperId <= 0;
+ $was_submitted = $this->prow->timeSubmitted > 0;
+ $this->useRequest = true;
+
+ $this->ps = new PaperStatus($conf, $this->user);
+ $prepared = $this->ps->prepare_save_paper_web($this->qreq, $this->prow, $action);
+
+ if (!$prepared) {
+ if ($is_new && $this->qreq->has_files()) {
+ // XXX save uploaded files
+ $this->ps->error_at(null, "Your uploaded files were ignored. ");
+ }
+ $t = $conf->_("Your changes were not saved. Please fix these errors and try again.");
+ $emsg = $this->ps->landmarked_problem_texts();
+ if (!empty($emsg)) {
+ $t = "{$t}
";
+ }
+ Conf::msg_error($t);
+ return;
+ }
+
+ // check deadlines
+ if ($is_new) {
+ // we know that can_start_paper implies can_finalize_paper
+ $whynot = $this->user->perm_start_paper();
+ } else if ($action === "final") {
+ $whynot = $this->user->perm_edit_final_paper($this->prow);
+ } else {
+ $whynot = $this->user->perm_edit_paper($this->prow);
+ if ($whynot
+ && $action === "update"
+ && !count(array_diff($this->ps->diffs, ["contacts", "status"]))) {
+ $whynot = $this->user->perm_finalize_paper($this->prow);
+ }
+ }
+ if ($whynot) {
+ Conf::msg_error($whynot->unparse_html());
+ $this->useRequest = !$is_new; // XXX used to have more complex logic
+ return;
+ }
+
+ // actually update
+ $this->ps->execute_save();
+
+ $warnmsgs = $this->ps->landmarked_problem_texts();
+ $webnotes = $warnmsgs ? " " . join(" ", $warnmsgs) . " " : "";
+
+ $new_prow = $conf->paper_by_id($this->ps->paperId, $this->user, ["topics" => true, "options" => true]);
+ if (!$new_prow) {
+ $conf->msg($conf->_("Your submission was not saved. Please correct these errors and save again.") . $webnotes, "merror");
+ return;
+ }
+ assert($this->user->can_view_paper($new_prow));
+
+ // submit paper if no error so far
+ $_GET["paperId"] = $_GET["p"] = $this->qreq->paperId = $this->qreq->p = $this->ps->paperId;
+
+ if ($action === "final") {
+ $submitkey = "timeFinalSubmitted";
+ $storekey = "finalPaperStorageId";
+ } else {
+ $submitkey = "timeSubmitted";
+ $storekey = "paperStorageId";
+ }
+ $newsubmit = $new_prow->timeSubmitted > 0 && !$was_submitted;
+
+ // confirmation message
+ if ($action === "final") {
+ $actiontext = "Updated final";
+ $template = "@submitfinalpaper";
+ } else if ($newsubmit) {
+ $actiontext = "Updated";
+ $template = "@submitpaper";
+ } else if ($is_new) {
+ $actiontext = "Registered";
+ $template = "@registerpaper";
+ } else {
+ $actiontext = "Updated";
+ $template = "@updatepaper";
+ }
+
+ // log message
+ $this->ps->log_save_activity($this->user, $action);
+
+ // additional information
+ $notes = [];
+ if ($action == "final") {
+ if ($new_prow->timeFinalSubmitted <= 0) {
+ $notes[] = $conf->_("The final version has not yet been submitted.");
+ }
+ $notes[] = $this->deadline_note("final_soft",
+ "You have until %s to make further changes.",
+ "The deadline for submitting final versions was %s.");
+ } else if ($new_prow->timeSubmitted > 0) {
+ $notes[] = $conf->_("The submission is ready for review.");
+ if ($conf->setting("sub_freeze") <= 0) {
+ $notes[] = $this->deadline_note("sub_update",
+ "You have until %s to make further changes.", "");
+ }
+ } else {
+ if ($conf->setting("sub_freeze") > 0) {
+ $notes[] = $conf->_("The submission has not yet been completed.");
+ } else if (($missing = $this->missing_required_fields($new_prow))) {
+ $missing_names = array_map(function ($o) { return $o->missing_title(); }, $missing);
+ $notes[] = $conf->_("The submission is not ready for review; required fields %#H are missing.", $missing_names);
+ } else {
+ $notes[] = $conf->_("The submission is marked as not ready for review.");
+ }
+ $notes[] = $this->deadline_note("sub_update",
+ "You have until %s to make further changes.",
+ "The deadline for updating submissions was %s.");
+ if (($msg = $this->deadline_note("sub_sub", "Submissions incomplete as of %s will not be considered for review.", "")) !== "") {
+ $notes[] = "{$msg} ";
+ }
+ }
+ $notes = join(" ", array_filter($notes, function ($n) { return $n !== ""; }));
+
+ // HTML confirmation
+ if (empty($this->ps->diffs)) {
+ $webmsg = $conf->_("No changes to submission #%d.", $new_prow->paperId);
+ } else {
+ $webmsg = $conf->_("$actiontext submission #%d.", $new_prow->paperId);
+ }
+ if ($this->ps->has_error()) {
+ $webmsg .= " " . $conf->_("Please correct these issues and save again.");
+ }
+ if ($notes || $webnotes) {
+ $webmsg .= " " . $notes . $webnotes;
+ }
+ $conf->msg($webmsg, $new_prow->$submitkey > 0 ? "confirm" : "warning");
+
+ // mail confirmation to all contact authors if changed
+ if (!empty($this->ps->diffs)) {
+ if (!$this->user->can_administer($new_prow) || $this->qreq->doemail) {
+ $options = ["infoNames" => 1];
+ if ($this->user->can_administer($new_prow)) {
+ if (!$new_prow->has_author($this->user)) {
+ $options["adminupdate"] = true;
+ }
+ if (isset($this->qreq->emailNote)) {
+ $options["reason"] = $this->qreq->emailNote;
+ }
+ }
+ if ($notes !== "") {
+ $options["notes"] = preg_replace('/<\/?(?:span.*?|strong)>/', "", $notes) . "\n\n";
+ }
+ HotCRPMailer::send_contacts($template, $new_prow, $options);
+ }
+
+ // other mail confirmations
+ if ($action === "final" && $new_prow->timeFinalSubmitted > 0) {
+ $followers = $new_prow->final_update_followers();
+ $template = "@finalsubmitnotify";
+ } else if ($is_new) {
+ $followers = $new_prow->register_followers();
+ $template = $newsubmit ? "@newsubmitnotify" : "@registernotify";
+ } else if ($newsubmit) {
+ $followers = $new_prow->newsubmit_followers();
+ $template = "@newsubmitnotify";
+ } else {
+ $followers = [];
+ $template = "@none";
+ }
+ foreach ($followers as $minic) {
+ if ($minic->contactId !== $this->user->contactId)
+ HotCRPMailer::send_to($minic, $template, ["prow" => $new_prow]);
+ }
+ }
+
+ $conf->paper = $this->prow = $new_prow;
+ if (!$this->ps->has_error() || ($is_new && $new_prow)) {
+ $conf->redirect_self($this->qreq, ["p" => $new_prow->paperId, "m" => "edit"]);
+ }
+ }
+
+ function handle_updatecontacts() {
+ $conf = $this->conf;
+ $this->useRequest = true;
+
+ if (!$this->user->can_administer($this->prow)
+ && !$this->prow->has_author($this->user)) {
+ Conf::msg_error($this->prow->make_whynot(["permission" => "edit_contacts"])->unparse_html());
+ return;
+ }
+
+ $this->ps = new PaperStatus($this->conf, $this->user);
+ if (!$this->ps->prepare_save_paper_web($this->qreq, $this->prow, "updatecontacts")) {
+ Conf::msg_error("" . join(" ", $this->ps->message_texts()) . " ");
+ return;
+ }
+
+ if (!$this->ps->diffs) {
+ Conf::msg_warning($conf->_("No changes to submission #%d.", $this->prow->paperId));
+ } else if ($this->ps->execute_save()) {
+ Conf::msg_confirm($conf->_("Updated contacts for submission #%d.", $this->prow->paperId));
+ $this->user->log_activity("Paper edited: contacts", $this->prow->paperId);
+ }
+
+ if (!$this->ps->has_error()) {
+ $conf->redirect_self($this->qreq);
+ }
+ }
+
+ private function prepare_edit_mode() {
+ if (!$this->ps) {
+ $this->prow->set_allow_absent($this->prow->paperId === 0);
+ $this->ps = PaperStatus::make_prow($this->user, $this->prow);
+ $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT);
+ foreach ($this->prow->form_fields() as $o) {
+ if ($this->user->can_edit_option($this->prow, $o)) {
+ $ov = $this->prow->force_option($o);
+ $o->value_check($ov, $this->user);
+ $ov->copy_messages_to($this->ps);
+ }
+ }
+ $this->user->set_overrides($old_overrides);
+ $this->prow->set_allow_absent(false);
+ }
+
+ $old_overrides = $this->user->remove_overrides(Contact::OVERRIDE_CHECK_TIME);
+ $editable = $this->user->can_edit_paper($this->prow)
+ || $this->user->can_edit_final_paper($this->prow);
+ $this->user->set_overrides($old_overrides);
+ $this->pt->set_edit_status($this->ps, $editable, $editable && $this->useRequest);
+ }
+
+ function render() {
+ // correct modes
+ $this->pt = $pt = new PaperTable($this->user, $this->qreq, $this->prow);
+ if ($pt->can_view_reviews()
+ || $pt->mode === "re"
+ || ($this->prow->paperId > 0 && $this->user->can_edit_review($this->prow))) {
+ $pt->resolve_review(false);
+ }
+ $pt->resolve_comments();
+ if ($pt->mode === "edit") {
+ $this->prepare_edit_mode();
+ }
+
+ // produce paper table
+ $this->echo_header();
+ $pt->echo_paper_info();
+
+ if ($pt->mode === "edit") {
+ $pt->paptabEndWithoutReviews();
+ } else {
+ if ($pt->mode === "re") {
+ $pt->echo_review_form();
+ $pt->echo_main_link();
+ } else if ($pt->can_view_reviews()) {
+ $pt->paptabEndWithReviewsAndComments();
+ } else {
+ $pt->paptabEndWithReviewMessage();
+ $pt->echo_comments();
+ }
+ // restore comment across logout bounce
+ if ($this->qreq->editcomment) {
+ $cid = $this->qreq->c;
+ $preferred_resp_round = false;
+ if (($x = $this->qreq->response)) {
+ $preferred_resp_round = $this->conf->resp_round_number($x);
+ }
+ if ($preferred_resp_round === false) {
+ $preferred_resp_round = $this->user->preferred_resp_round_number($this->prow);
+ }
+ $j = null;
+ foreach ($this->prow->viewable_comments($this->user) as $crow) {
+ if ($crow->commentId == $cid
+ || ($cid === null
+ && ($crow->commentType & CommentInfo::CT_RESPONSE) != 0
+ && $crow->commentRound === $preferred_resp_round))
+ $j = $crow->unparse_json($this->user);
+ }
+ if (!$j) {
+ $j = (object) ["is_new" => true, "editable" => true];
+ if ($this->user->act_author_view($this->prow)) {
+ $j->by_author = true;
+ }
+ if ($preferred_resp_round !== false) {
+ $j->response = $this->conf->resp_round_name($preferred_resp_round);
+ }
+ }
+ if (($x = $this->qreq->text) !== null) {
+ $j->text = $x;
+ $j->visibility = $this->qreq->visibility;
+ $tags = trim((string) $this->qreq->tags);
+ $j->tags = $tags === "" ? [] : preg_split('/\s+/', $tags);
+ $j->blind = !!$this->qreq->blind;
+ $j->draft = !!$this->qreq->draft;
+ }
+ Ht::stash_script("hotcrp.edit_comment(" . json_encode_browser($j) . ")");
+ }
+ }
+
+ echo "\n";
+ $this->conf->footer();
+ }
+
+ static function go(Contact $user, Qrequest $qreq) {
+ if (!isset($qreq->m) && ($pc = $qreq->path_component(1))) {
+ $qreq->m = $pc;
+ } else if (!isset($qreq->m) && isset($qreq->mode)) {
+ $qreq->m = $qreq->mode;
+ }
+
+ $pp = new Paper_Page($user, $qreq);
+ $pp->load_prow();
+
+ // fix user
+ if ($qreq->is_post() && $qreq->valid_token()) {
+ $user->ensure_account_here();
+ // XXX escape unless update && can_start_paper???
+ }
+ $user->add_overrides(Contact::OVERRIDE_CHECK_TIME);
+ if ($pp->prow->paperId == 0 && $user->privChair && !$user->conf->time_start_paper()) {
+ $user->add_overrides(Contact::OVERRIDE_CONFLICT);
+ }
+
+ // fix request
+ $pp->useRequest = isset($qreq->title) && $qreq->has_annex("after_login");
+ if ($qreq->emailNote === "Optional explanation") {
+ unset($qreq->emailNote);
+ }
+ if ($qreq->reason === "Optional explanation") {
+ unset($qreq->reason);
+ }
+ if ($qreq->post && $qreq->post_empty()) {
+ $pp->conf->post_missing_msg();
+ }
+
+ // action
+ if ($qreq->cancel) {
+ $pp->handle_cancel();
+ } else if ($qreq->update && $qreq->valid_post()) {
+ $pp->handle_update($qreq->submitfinal ? "final" : "update");
+ } else if ($qreq->updatecontacts && $qreq->valid_post()) {
+ $pp->handle_updatecontacts();
+ } else if ($qreq->withdraw && $qreq->valid_post()) {
+ $pp->handle_withdraw();
+ } else if ($qreq->revive && $qreq->valid_post()) {
+ $pp->handle_revive();
+ } else if ($qreq->delete && $qreq->valid_post()) {
+ $pp->handle_delete();
+ } else if ($qreq->updateoverride && $qreq->valid_token()) {
+ $pp->conf->redirect_self($qreq, ["m" => "edit", "forceShow" => 1]);
+ }
+
+ // render
+ $pp->render();
+ }
+}
diff --git a/src/pages/p_review.php b/src/pages/p_review.php
new file mode 100644
index 000000000..eed13b0f3
--- /dev/null
+++ b/src/pages/p_review.php
@@ -0,0 +1,453 @@
+conf = $user->conf;
+ $this->user = $user;
+ $this->qreq = $qreq;
+ }
+
+ /** @return ReviewForm */
+ function rf() {
+ return $this->conf->review_form();
+ }
+
+ function echo_header() {
+ PaperTable::echo_header($this->pt, "review", $this->qreq->m, $this->qreq);
+ }
+
+ function error_exit($msg) {
+ $this->echo_header();
+ Ht::stash_script("hotcrp.shortcut().add()");
+ $msg && Conf::msg_error($msg);
+ $this->conf->footer();
+ throw new PageCompletion;
+ }
+
+ function load_prow() {
+ // determine whether request names a paper
+ try {
+ $pr = new PaperRequest($this->user, $this->qreq, true);
+ $this->prow = $this->conf->paper = $pr->prow;
+ if ($pr->rrow) {
+ $this->rrow = $pr->rrow;
+ $this->rrow_explicit = true;
+ } else {
+ $this->rrow = $this->my_rrow($this->qreq->m === "rea");
+ $this->rrow_explicit = false;
+ }
+ } catch (Redirection $redir) {
+ assert(PaperRequest::simple_qreq($this->qreq));
+ throw $redir;
+ } catch (PermissionProblem $perm) {
+ $this->error_exit($perm->set("listViewable", true)->unparse_html());
+ }
+ }
+
+ /** @return ?ReviewInfo */
+ function my_rrow($prefer_approvable) {
+ $myrrow = $apprrow1 = $apprrow2 = null;
+ $admin = $this->user->can_administer($this->prow);
+ foreach ($this->prow->reviews_as_display() as $rrow) {
+ if ($this->user->can_view_review($this->prow, $rrow)) {
+ if ($rrow->contactId === $this->user->contactId
+ || (!$myrrow && $this->user->is_my_review($rrow))) {
+ $myrrow = $rrow;
+ } else if ($rrow->reviewStatus === ReviewInfo::RS_DELIVERED
+ && !$apprrow1
+ && $rrow->requestedBy === $this->user->contactXid) {
+ $apprrow1 = $rrow;
+ } else if ($rrow->reviewStatus === ReviewInfo::RS_DELIVERED
+ && !$apprrow2
+ && $admin) {
+ $apprrow2 = $rrow;
+ }
+ }
+ }
+ if (($apprrow1 || $apprrow2)
+ && ($prefer_approvable || !$myrrow)) {
+ return $apprrow1 ?? $apprrow2;
+ } else {
+ return $myrrow;
+ }
+ }
+
+ function reload_prow() {
+ $this->prow->load_reviews(true);
+ if ($this->rrow) {
+ $this->rrow = $this->prow->review_by_id($this->rrow->reviewId);
+ } else {
+ $this->rrow = $this->prow->review_by_ordinal_id($this->qreq->reviewId);
+ }
+ }
+
+ function handle_cancel() {
+ $this->conf->redirect($this->prow->hoturl([], Conf::HOTURL_RAW));
+ }
+
+ function handle_update() {
+ // do not unsubmit submitted review
+ if ($this->rrow && $this->rrow->reviewStatus >= ReviewInfo::RS_COMPLETED) {
+ $this->qreq->ready = 1;
+ }
+
+ $rv = new ReviewValues($this->rf());
+ $rv->paperId = $this->prow->paperId;
+ if (($whynot = $this->user->perm_submit_review($this->prow, $this->rrow))) {
+ $rv->msg_at(null, $whynot->unparse_html(), MessageSet::ERROR);
+ } else if ($rv->parse_qreq($this->qreq, $this->qreq->override)) {
+ if (isset($this->qreq->approvesubreview)
+ && $this->rrow
+ && $this->user->can_approve_review($this->prow, $this->rrow)) {
+ $rv->set_adopt();
+ }
+ if ($rv->check_and_save($this->user, $this->prow, $this->rrow)) {
+ $this->qreq->r = $this->qreq->reviewId = $rv->review_ordinal_id;
+ }
+ }
+ $rv->report();
+ if (!$rv->has_error() && !$rv->has_problem_at("ready")) {
+ $this->conf->redirect_self($this->qreq);
+ }
+ $this->rv = $rv;
+ $this->reload_prow();
+ }
+
+ function handle_upload_form() {
+ if (!$this->qreq->has_file("uploadedFile")) {
+ Conf::msg_error("Select a review form to upload.");
+ return;
+ }
+ $rv = ReviewValues::make_text($this->rf(),
+ $this->qreq->file_contents("uploadedFile"),
+ $this->qreq->file_filename("uploadedFile"));
+ if ($rv->parse_text($this->qreq->override)
+ && $rv->check_and_save($this->user, $this->prow, $this->rrow)) {
+ $this->qreq->r = $this->qreq->reviewId = $rv->review_ordinal_id;
+ }
+ if (!$rv->has_error() && $rv->parse_text($this->qreq->override)) {
+ $rv->msg_at(null, "Only the first review form in the file was parsed. " . Ht::link("Upload multiple-review files here.", $this->conf->hoturl("offline")), MessageSet::WARNING);
+ }
+ $rv->report();
+ if (!$rv->has_error()) {
+ $this->conf->redirect_self($this->qreq);
+ }
+ $this->reload_prow();
+ }
+
+ function handle_download_form() {
+ $filename = "review-" . ($this->rrow ? $this->rrow->unparse_ordinal_id() : $this->prow->paperId);
+ $rf = $this->rf();
+ $this->conf->make_csvg($filename, CsvGenerator::TYPE_STRING)
+ ->set_inline(false)
+ ->add_string($rf->text_form_header(false)
+ . $rf->text_form($this->prow, $this->rrow, $this->user, null))
+ ->emit();
+ throw new PageCompletion;
+ }
+
+ function handle_download_text() {
+ $rf = $this->rf();
+ if ($this->rrow && $this->rrow_explicit) {
+ $this->conf->make_csvg("review-" . $this->rrow->unparse_ordinal_id(), CsvGenerator::TYPE_STRING)
+ ->add_string($rf->unparse_text($this->prow, $this->rrow, $this->user))
+ ->emit();
+ } else {
+ $lastrc = null;
+ $texts = [
+ "{$this->conf->short_name} Paper #{$this->prow->paperId} Reviews and Comments\n",
+ str_repeat("=", 75) . "\n",
+ prefix_word_wrap("", "Paper #{$this->prow->paperId} {$this->prow->title}", 0, 75),
+ "\n\n"
+ ];
+ foreach ($this->prow->viewable_submitted_reviews_and_comments($this->user) as $rc) {
+ $texts[] = PaperInfo::review_or_comment_text_separator($lastrc, $rc);
+ if (isset($rc->reviewId)) {
+ $texts[] = $rf->unparse_text($this->prow, $rc, $this->user, ReviewForm::UNPARSE_NO_TITLE);
+ } else {
+ $texts[] = $rc->unparse_text($this->user, ReviewForm::UNPARSE_NO_TITLE);
+ }
+ $lastrc = $rc;
+ }
+ if (!$lastrc) {
+ $texts[] = "Nothing to show.\n";
+ }
+ $this->conf->make_csvg("reviews-{$this->prow->paperId}", CsvGenerator::TYPE_STRING)
+ ->append_strings($texts)
+ ->emit();
+ }
+ throw new PageCompletion;
+ }
+
+ function handle_adopt() {
+ if (!$this->rrow || !$this->rrow_explicit) {
+ Conf::msg_error("Missing review to delete.");
+ return;
+ } else if (!$this->user->can_approve_review($this->prow, $this->rrow)) {
+ return;
+ }
+
+ $rv = new ReviewValues($this->rf());
+ $rv->paperId = $this->prow->paperId;
+ $my_rrow = $this->prow->review_by_user($this->user);
+ $my_rid = ($my_rrow ?? $this->rrow)->unparse_ordinal_id();
+ if (($whynot = $this->user->perm_submit_review($this->prow, $my_rrow))) {
+ $rv->msg_at(null, $whynot->unparse_html(), MessageSet::ERROR);
+ } else if ($rv->parse_qreq($this->qreq, $this->qreq->override)) {
+ $rv->set_ready($this->qreq->adoptsubmit);
+ if ($rv->check_and_save($this->user, $this->prow, $my_rrow)) {
+ $my_rid = $rv->review_ordinal_id;
+ if (!$rv->has_problem_at("ready")) {
+ // mark the source review as approved
+ $rvx = new ReviewValues($this->rf());
+ $rvx->set_adopt();
+ $rvx->check_and_save($this->user, $this->prow, $this->rrow);
+ }
+ }
+ }
+ $rv->report();
+ $this->conf->redirect_self($this->qreq, ["r" => $my_rid]);
+ }
+
+ function handle_delete() {
+ if (!$this->rrow || !$this->rrow_explicit) {
+ Conf::msg_error("Missing review to delete.");
+ return;
+ } else if (!$this->user->can_administer($this->prow)) {
+ return;
+ }
+ $result = $this->conf->qe("delete from PaperReview where paperId=? and reviewId=?", $this->prow->paperId, $this->rrow->reviewId);
+ if ($result->affected_rows) {
+ $this->user->log_activity_for($this->rrow->contactId, "Review {$this->rrow->reviewId} deleted", $this->prow);
+ $this->conf->confirmMsg("Deleted review.");
+ $this->conf->qe("delete from ReviewRating where paperId=? and reviewId=?", $this->prow->paperId, $this->rrow->reviewId);
+ if ($this->rrow->reviewToken !== 0) {
+ $this->conf->update_rev_tokens_setting(-1);
+ }
+ if ($this->rrow->reviewType == REVIEW_META) {
+ $this->conf->update_metareviews_setting(-1);
+ }
+
+ // perhaps a delegatee needs to redelegate
+ if ($this->rrow->reviewType < REVIEW_SECONDARY
+ && $this->rrow->requestedBy > 0) {
+ $this->user->update_review_delegation($this->prow->paperId, $this->rrow->requestedBy, -1);
+ }
+ }
+ $this->conf->redirect_self($this->qreq, ["r" => null, "reviewId" => null]);
+ }
+
+ function handle_unsubmit() {
+ if ($this->rrow
+ && $this->rrow->reviewStatus >= ReviewInfo::RS_DELIVERED
+ && $this->user->can_administer($this->prow)) {
+ $result = $this->user->unsubmit_review_row($this->rrow);
+ if ($result->affected_rows) {
+ $this->user->log_activity_for($this->rrow->contactId, "Review {$this->rrow->reviewId} unsubmitted", $this->prow);
+ $this->conf->confirmMsg("Unsubmitted review.");
+ }
+ $this->conf->redirect_self($this->qreq);
+ }
+ }
+
+ /** @return ?int */
+ function current_capability_rrid() {
+ if (($capuid = $this->user->capability("@ra{$this->prow->paperId}"))) {
+ $u = $this->conf->cached_user_by_id($capuid);
+ $rrow = $this->prow->review_by_user($capuid);
+ $refs = $u ? $this->prow->review_refusals_by_user($u) : [];
+ if ($rrow && (!$this->rrow || $this->rrow === $rrow)) {
+ return $rrow->reviewId;
+ } else if (!$rrow && !empty($refs) && $refs[0]->refusedReviewId > 0) {
+ return $refs[0]->refusedReviewId;
+ }
+ }
+ return null;
+ }
+
+ function handle_accept_decline_redirect($capuid) {
+ if (!$this->qreq->is_get()
+ || !($rrid = $this->current_capability_rrid())) {
+ return;
+ }
+ $isaccept = $this->qreq->accept;
+ echo "
+
+
+
+Redirection
+\n",
+ Ht::form($this->conf->hoturl_post("api/" . ($isaccept ? "acceptreview" : "declinereview"), ["p" => $this->prow->paperId, "r" => $rrid, "verbose" => 1, "redirect" => 1]), ["id" => "redirectform"]),
+ Ht::submit("Press to continue"),
+ "",
+ Ht::script("document.getElementById(\"redirectform\").submit()"),
+ "\n";
+ throw new PageCompletion;
+ }
+
+ function render_decline_message($capuid) {
+ $ref = $this->prow->review_refusals_by_user_id($capuid);
+ if ($ref && $ref[0] && $ref[0]->refusedReviewId) {
+ $rrid = $ref[0]->refusedReviewId;
+ $this->conf->msg(
+ "You declined to complete this review. Thank you for informing us.
"
+ . Ht::form($this->conf->hoturl_post("api/declinereview", ["p" => $this->prow->paperId, "r" => $rrid, "redirect" => 1]))
+ . 'Optional explanation '
+ . ($ref[0]->reason ? "" : '
If you’d like, you may enter a brief explanation here.
')
+ . Ht::textarea("reason", $ref[0]->reason, ["rows" => 3, "cols" => 40, "spellcheck" => true, "class" => "w-text", "id" => "declinereason"])
+ . '
'
+ . '
' . Ht::submit("Update explanation", ["class" => "btn-primary"])
+ . '
' . Ht::submit("Accept review", ["formaction" => $this->conf->hoturl_post("api/acceptreview", ["p" => $this->prow->paperId, "r" => $rrid, "verbose" => 1, "redirect" => 1])])
+ . '
', 1);
+ } else {
+ $this->conf->msg("You have declined to complete this review. Thank you for informing us.
", 1);
+ }
+ }
+
+ function render_accept_other_message($capuid) {
+ if (($u = $this->conf->cached_user_by_id($capuid))) {
+ if (PaperRequest::simple_qreq($this->qreq)
+ && ($i = $this->user->session_user_index($u->email)) >= 0) {
+ $selfurl = $this->conf->selfurl($this->qreq, null, Conf::HOTURL_SITEREL | Conf::HOTURL_RAW);
+ $this->conf->redirect(Navigation::base_absolute() . "u/{$i}/{$selfurl}");
+ } else if ($this->user->has_email()) {
+ $mx = 'This review is assigned to ' . htmlspecialchars($u->email) . ', while you are signed in as ' . htmlspecialchars($this->user->email) . '. You can edit the review anyway since you accessed it using a special link.';
+ if ($this->rrow->reviewStatus <= ReviewInfo::RS_DRAFTED) {
+ $m = Ht::form($this->conf->hoturl_post("api/claimreview", ["p" => $this->prow->paperId, "r" => $this->rrow->reviewId, "redirect" => 1]), ["class" => "has-fold foldc"])
+ . "$mx Alternately, you can reassign it to this account .
"
+ . '';
+ foreach ($this->user->session_users() as $e) {
+ $m .= '
' . Ht::submit("Reassign to " . htmlspecialchars($e), ["name" => "email", "value" => $e]) . '
';
+ }
+ $m .= '
';
+ } else {
+ $m = "{$mx}
";
+ }
+ $this->conf->msg($m, 1);
+ } else {
+ $this->conf->msg(
+ 'This review is assigned to ' . htmlspecialchars($u->email) . '. You can edit the review since you accessed it using a special link.
', 1);
+ }
+ }
+ }
+
+ function render() {
+ $this->pt = $pt = new PaperTable($this->user, $this->qreq, $this->prow);
+ $pt->resolve_review(!!$this->rrow);
+ $pt->resolve_comments();
+
+ // mode
+ if ($this->rv) {
+ $pt->set_review_values($this->rv);
+ } else if ($this->qreq->has_annex("after_login")) {
+ $rv = new ReviewValues($this->rf());
+ $rv->parse_qreq($this->qreq, $this->qreq->override);
+ $pt->set_review_values($rv);
+ }
+
+ // paper table
+ $this->echo_header();
+ $pt->echo_paper_info();
+
+ if (!$this->user->can_view_review($this->prow, $this->rrow)
+ && !$this->user->can_edit_review($this->prow, $this->rrow)) {
+ $pt->paptabEndWithReviewMessage();
+ } else {
+ if ($pt->mode === "re" || $this->rrow) {
+ $pt->echo_review_form();
+ $pt->echo_main_link();
+ } else if ($this->rrow) {
+ $pt->echo_rc([$this->rrow], false);
+ $pt->echo_main_link();
+ } else {
+ $pt->paptabEndWithReviewsAndComments();
+ }
+ }
+
+ echo "\n";
+ $this->conf->footer();
+ }
+
+ static function go(Contact $user, Qrequest $qreq) {
+ // fix request
+ if (!isset($qreq->m) && isset($qreq->mode)) {
+ $qreq->m = $qreq->mode;
+ }
+ if ($qreq->post && $qreq->default) {
+ if ($qreq->has_file("uploadedFile")) {
+ $qreq->uploadForm = 1;
+ } else {
+ $qreq->update = 1;
+ }
+ } else if ($qreq->submitreview) {
+ $qreq->update = $qreq->ready = 1;
+ } else if ($qreq->savedraft) {
+ $qreq->update = 1;
+ unset($qreq->ready);
+ }
+ if (session_id() === "" && $user->is_reviewer()) {
+ ensure_session();
+ }
+
+ $pp = new Review_Page($user, $qreq);
+ $pp->load_prow();
+
+ // fix user
+ $user->add_overrides(Contact::OVERRIDE_CHECK_TIME);
+ $capuid = $user->capability("@ra{$pp->prow->paperId}");
+
+ // action
+ if ($qreq->cancel) {
+ $pp->handle_cancel();
+ } else if ($qreq->update && $qreq->valid_post()) {
+ $pp->handle_update();
+ } else if ($qreq->adoptreview && $qreq->valid_post()) {
+ $pp->handle_adopt();
+ } else if ($qreq->uploadForm && $qreq->valid_post()) {
+ $pp->handle_upload_form();
+ } else if ($qreq->downloadForm) {
+ $pp->handle_download_form();
+ } else if ($qreq->text) {
+ $pp->handle_download_text();
+ } else if ($qreq->unsubmitreview && $qreq->valid_post()) {
+ $pp->handle_unsubmit();
+ } else if ($qreq->deletereview && $qreq->valid_post()) {
+ $pp->handle_delete();
+ } else if (($qreq->accept || $qreq->decline) && $capuid) {
+ $pp->handle_accept_decline_redirect($capuid);
+ }
+
+ // capability messages: decline, accept to different user
+ if ($capuid) {
+ if (!$pp->rrow
+ && $pp->prow->review_refusals_by_user_id($capuid)) {
+ $pp->render_decline_message($capuid);
+ } else if ($pp->rrow
+ && $capuid === $pp->rrow->contactId
+ && $capuid !== $user->contactXid) {
+ $pp->render_accept_other_message($capuid);
+ }
+ }
+
+ $pp->render();
+ }
+}
diff --git a/src/pages/p_reviewprefs.php b/src/pages/p_reviewprefs.php
new file mode 100644
index 000000000..5e68c126d
--- /dev/null
+++ b/src/pages/p_reviewprefs.php
@@ -0,0 +1,243 @@
+select(["paper", "email", "preference"]);
+ $suffix = "u" . $reviewer->contactId;
+ foreach ($qreq as $k => $v) {
+ if (strlen($k) > 7 && substr($k, 0, 7) == "revpref") {
+ if (str_ends_with($k, $suffix)) {
+ $k = substr($k, 0, -strlen($suffix));
+ }
+ if (($p = cvtint(substr($k, 7))) > 0) {
+ $csvg->add_row([$p, $reviewer->email, $v]);
+ }
+ }
+ }
+ if ($csvg->is_empty()) {
+ Conf::msg_error("No reviewer preferences to update.");
+ return;
+ }
+
+ $aset = new AssignmentSet($user, true);
+ $aset->parse($csvg->unparse());
+ if ($aset->execute()) {
+ Conf::msg_confirm("Preferences saved.");
+ $user->conf->redirect_self($qreq);
+ } else {
+ Conf::msg_error(join(" ", $aset->messages_html()));
+ }
+ }
+
+ /** @param PaperList $pl
+ * @return string */
+ static private function pref_element($pl, $name, $text, $extra = []) {
+ return ''
+ . Ht::checkbox("show$name", 1, $pl->viewing($name), [
+ "class" => "uich js-plinfo ignore-diff" . (isset($extra["fold_target"]) ? " js-foldup" : ""),
+ "data-fold-target" => $extra["fold_target"] ?? null
+ ]) . " " . Ht::label($text) . '';
+ }
+
+ /** @param Contact $user
+ * @param Contact $reviewer
+ * @param Qrequest $qreq */
+ static function render($user, $reviewer, $qreq) {
+ $conf = $user->conf;
+
+ $conf->header("Review preferences", "revpref");
+ $conf->infoMsg($conf->_i("revprefdescription", null, $conf->has_topics()));
+
+ $search = (new PaperSearch($user, [
+ "t" => $qreq->t, "q" => $qreq->q, "reviewer" => $reviewer
+ ]))->set_urlbase("reviewprefs");
+ $pl = new PaperList("pf", $search, ["sort" => true], $qreq);
+ $pl->apply_view_report_default();
+ $pl->apply_view_session();
+ $pl->apply_view_qreq();
+ $pl->set_table_id_class("foldpl", "pltable-fullw", "p#");
+ $pl->set_table_decor(PaperList::DECOR_HEADER | PaperList::DECOR_FOOTER | PaperList::DECOR_LIST);
+ $pl->set_table_fold_session("pfdisplay.");
+
+ // display options
+ echo Ht::form($conf->hoturl("reviewprefs"), [
+ "method" => "get", "id" => "searchform",
+ "class" => "has-fold fold10" . ($pl->viewing("authors") ? "o" : "c")
+ ]);
+
+ if ($user->privChair) {
+ echo 'User ';
+
+ $prefcount = [];
+ $result = $conf->qe_raw("select contactId, count(*) from PaperReviewPreference where preference!=0 or expertise is not null group by contactId");
+ while (($row = $result->fetch_row())) {
+ $prefcount[(int) $row[0]] = (int) $row[1];
+ }
+ Dbl::free($result);
+
+ $sel = [];
+ foreach ($conf->pc_members() as $p) {
+ $sel[$p->email] = $p->name_h(NAME_P|NAME_S) . " [" . plural($prefcount[$p->contactId] ?? 0, "pref") . "]";
+ }
+ if (!isset($sel[$reviewer->email])) {
+ $sel[$reviewer->email] = $reviewer->name_h(NAME_P|NAME_S) . " [" . ($prefcount[$reviewer->contactId] ?? 0) . "; not on PC]";
+ }
+
+ echo Ht::select("reviewer", $sel, $reviewer->email, ["id" => "htctl-prefs-user"]), '
';
+ Ht::stash_script('$("#searchform select[name=reviewer]").on("change", function () { $("#searchform")[0].submit() })');
+ }
+
+ echo 'Search ',
+ Ht::entry("q", $qreq->q, [
+ "id" => "htctl-prefs-q", "size" => 32, "placeholder" => "(All)",
+ "class" => "papersearch want-focus need-suggest", "spellcheck" => false
+ ]), ' ', Ht::submit("redisplay", "Redisplay"), '
';
+
+ $show_data = [];
+ if ($pl->has("abstract")) {
+ $show_data[] = self::pref_element($pl, "abstract", "Abstract");
+ }
+ if (($vat = $pl->viewable_author_types()) !== 0) {
+ $extra = ["fold_target" => 10];
+ if ($vat & 2) {
+ $show_data[] = self::pref_element($pl, "au", "Authors", $extra);
+ $extra = ["item_class" => "fx10"];
+ }
+ if ($vat & 1) {
+ $show_data[] = self::pref_element($pl, "anonau", "Authors (deblinded)", $extra);
+ $extra = ["item_class" => "fx10"];
+ }
+ $show_data[] = self::pref_element($pl, "aufull", "Full author info", $extra);
+ }
+ if ($conf->has_topics()) {
+ $show_data[] = self::pref_element($pl, "topics", "Topics");
+ }
+ if (!empty($show_data) && !$pl->is_empty()) {
+ echo 'Show ',
+ '
', join('', $show_data), ' ';
+ }
+ echo "";
+ Ht::stash_script("$(\"#showau\").on(\"change\", function () { hotcrp.foldup.call(this, null, {n:10}) })");
+
+ // main form
+ $hoturl_args = [];
+ if ($reviewer->contactId !== $user->contactId) {
+ $hoturl_args["reviewer"] = $reviewer->email;
+ }
+ if ($qreq->q) {
+ $hoturl_args["q"] = $qreq->q;
+ }
+ if ($qreq->sort) {
+ $hoturl_args["sort"] = $qreq->sort;
+ }
+ echo Ht::form($conf->hoturl_post("reviewprefs", $hoturl_args), ["id" => "sel", "class" => "ui-submit js-submit-paperlist assignpc"]),
+ Ht::hidden("defaultfn", ""),
+ Ht::entry("____updates____", "", ["class" => "hidden ignore-diff"]),
+ Ht::hidden_default_submit("default", 1);
+ echo "\n",
+ '
',
+ Ht::submit("fn", "Save changes", ["value" => "saveprefs", "class" => "btn-primary"]),
+ '
';
+ $pl->echo_table_html();
+ echo "
\n";
+
+ $conf->footer();
+ }
+
+ /** @param Contact $user
+ * @param Qrequest $qreq */
+ static function go($user, $qreq) {
+ $conf = $user->conf;
+ if (!$user->privChair && !$user->isPC) {
+ $user->escape();
+ }
+
+ if (isset($qreq->default) && $qreq->defaultfn) {
+ $qreq->fn = $qreq->defaultfn;
+ } else if (isset($qreq->default)) {
+ $qreq->fn = "saveprefs";
+ }
+
+ // set reviewer
+ $reviewer = $user;
+ $correct_reviewer = true;
+ if ($qreq->reviewer
+ && $user->privChair
+ && $qreq->reviewer !== $user->email
+ && $qreq->reviewer !== $user->contactId) {
+ $correct_reviewer = false;
+ foreach ($conf->full_pc_members() as $pcm) {
+ if (strcasecmp($qreq->reviewer, $pcm->email) == 0
+ || $qreq->reviewer === (string) $pcm->contactId) {
+ $reviewer = $pcm;
+ $correct_reviewer = true;
+ $qreq->reviewer = $pcm->email;
+ }
+ }
+ } else if (!$qreq->reviewer && !($user->roles & Contact::ROLE_PC)) {
+ foreach ($conf->pc_members() as $pcm) {
+ $conf->redirect_self($qreq, ["reviewer" => $pcm->email]);
+ // in case redirection fails:
+ $reviewer = $pcm;
+ break;
+ }
+ }
+ if (!$correct_reviewer) {
+ Conf::msg_error("Reviewer " . htmlspecialchars($qreq->reviewer) . " is not on the PC.");
+ }
+
+ // cancel action
+ if ($qreq->cancel) {
+ $conf->redirect_self($qreq);
+ }
+
+ // backwards compat
+ if ($qreq->fn
+ && strpos($qreq->fn, "/") === false
+ && isset($qreq[$qreq->fn . "fn"])) {
+ $qreq->fn .= "/" . $qreq[$qreq->fn . "fn"];
+ }
+ if (!str_starts_with($qreq->fn, "get/")
+ && !in_array($qreq->fn, ["uploadpref", "tryuploadpref", "applyuploadpref", "setpref", "saveprefs"])) {
+ unset($qreq->fn);
+ }
+
+ // paper selection, search actions
+ $ssel = SearchSelection::make($qreq, $user);
+ SearchSelection::clear_request($qreq);
+ $qreq->q = $qreq->q ?? "";
+ $qreq->t = "editpref";
+ if ($qreq->fn === "saveprefs") {
+ if ($qreq->valid_post()) {
+ if ($correct_reviewer) {
+ self::save_preferences($user, $reviewer, $qreq);
+ } else {
+ Conf::msg_error("Preferences not saved.");
+ }
+ }
+ } else if ($qreq->fn !== null) {
+ ListAction::call($qreq->fn, $user, $qreq, $ssel);
+ }
+
+ // set options to view
+ if (isset($qreq->redisplay)) {
+ $pfd = " ";
+ foreach ($qreq as $k => $v) {
+ if (substr($k, 0, 4) == "show" && $v)
+ $pfd .= substr($k, 4) . " ";
+ }
+ $user->save_session("pfdisplay", $pfd);
+ $conf->redirect_self($qreq);
+ }
+
+ self::render($user, $reviewer, $qreq);
+ }
+}
diff --git a/src/pages/p_search.php b/src/pages/p_search.php
new file mode 100644
index 000000000..39ade2a3f
--- /dev/null
+++ b/src/pages/p_search.php
@@ -0,0 +1,503 @@
+ */
+ public $headers = [];
+ /** @var array> */
+ public $items = [];
+
+ /** @param Contact $user
+ * @param SearchSelection $ssel */
+ function __construct($user, $ssel) {
+ $this->conf = $user->conf;
+ $this->user = $user;
+ $this->ssel = $ssel;
+ }
+
+
+ /** @param int $column
+ * @param string $header */
+ private function set_header($column, $header) {
+ $this->headers[$column] = $header;
+ }
+ /** @param int $column
+ * @param string $item */
+ private function item($column, $item) {
+ if (!isset($this->headers[$column])) {
+ $this->headers[$column] = "";
+ }
+ $this->items[$column][] = $item;
+ }
+ /** @param int $column
+ * @param string $type
+ * @param string $title */
+ private function checkbox_item($column, $type, $title, $options = []) {
+ $options["class"] = "uich js-plinfo";
+ $x = ''
+ . Ht::checkbox("show$type", 1, $this->pl->viewing($type), $options)
+ . ' ' . $title . ' ';
+ $this->item($column, $x);
+ }
+
+ private function prepare_display_options() {
+ $pl = $this->pl;
+ $user = $this->user;
+
+ // Abstract
+ if ($pl->has("abstract")) {
+ $this->checkbox_item(1, "abstract", "Abstracts");
+ }
+
+ // Authors group
+ if (($vat = $pl->viewable_author_types()) !== 0) {
+ if ($vat & 2) {
+ $this->checkbox_item(1, "au", "Authors");
+ }
+ if ($vat & 1) {
+ $this->checkbox_item(1, "anonau", "Authors (deblinded)");
+ }
+ $this->checkbox_item(1, "aufull", "Full author info");
+ }
+ if ($pl->has("collab")) {
+ $this->checkbox_item(1, "collab", "Collaborators");
+ }
+
+ // Abstract group
+ if ($this->conf->has_topics()) {
+ $this->checkbox_item(1, "topics", "Topics");
+ }
+
+ // Row numbers
+ if ($pl->has("sel")) {
+ $this->checkbox_item(1, "rownum", "Row numbers");
+ }
+
+ // Options
+ foreach ($this->conf->options() as $ox) {
+ if ($ox->search_keyword() !== false
+ && $ox->can_render(FieldRender::CFSUGGEST)
+ && $pl->has("opt$ox->id")) {
+ $this->checkbox_item(10, $ox->search_keyword(), $ox->name);
+ }
+ }
+
+ // Reviewers group
+ if ($user->privChair) {
+ $this->checkbox_item(20, "pcconflicts", "PC conflicts");
+ $this->checkbox_item(20, "allpref", "Review preferences");
+ }
+ if ($user->can_view_some_review_identity()) {
+ $this->checkbox_item(20, "reviewers", "Reviewers");
+ }
+
+ // Tags group
+ if ($user->isPC && $pl->has("tags")) {
+ $opt = [];
+ if ($pl->search->limit() === "a" && !$user->privChair) {
+ $opt["disabled"] = true;
+ }
+ $this->checkbox_item(20, "tags", "Tags", $opt);
+ if ($user->privChair) {
+ foreach ($this->conf->tags() as $t) {
+ if ($t->allotment || $t->approval || $t->rank)
+ $this->checkbox_item(20, "tagreport:{$t->tag}", "#~{$t->tag} report", $opt);
+ }
+ }
+ }
+
+ if ($user->isPC && $pl->has("lead")) {
+ $this->checkbox_item(20, "lead", "Discussion leads");
+ }
+ if ($user->isPC && $pl->has("shepherd")) {
+ $this->checkbox_item(20, "shepherd", "Shepherds");
+ }
+
+ // Scores group
+ foreach ($this->conf->review_form()->viewable_fields($user) as $f) {
+ if ($f->has_options)
+ $this->checkbox_item(30, $f->search_keyword(), $f->name_html);
+ }
+ if (!empty($this->items[30])) {
+ $this->set_header(30, "Scores: ");
+ $sortitem = 'Sort by: '
+ . Ht::select("scoresort", ListSorter::score_sort_selector_options(),
+ ListSorter::canonical_long_score_sort(ListSorter::default_score_sort($user)),
+ ["id" => "scoresort"])
+ . '
? ';
+ $this->item(30, $sortitem);
+ }
+
+ // Formulas group
+ $named_formulas = $this->conf->viewable_named_formulas($user);
+ foreach ($named_formulas as $formula) {
+ $this->checkbox_item(40, "formula:" . $formula->abbreviation(), htmlspecialchars($formula->name));
+ }
+ if ($named_formulas) {
+ $this->set_header(40, "Formulas: ");
+ }
+ if ($user->isPC && $pl->search->limit() !== "a") {
+ $this->item(40, '');
+ }
+ }
+
+ /** @return array */
+ function field_search_types() {
+ $qt = ["ti" => "Title", "ab" => "Abstract"];
+ if ($this->user->privChair
+ || $this->conf->submission_blindness() === Conf::BLIND_NEVER) {
+ $qt["au"] = "Authors";
+ $qt["n"] = "Title, abstract, and authors";
+ } else if ($this->conf->submission_blindness() === Conf::BLIND_ALWAYS) {
+ if ($this->user->is_reviewer()
+ && $this->conf->time_reviewer_view_accepted_authors()) {
+ $qt["au"] = "Accepted authors";
+ $qt["n"] = "Title, abstract, and accepted authors";
+ } else {
+ $qt["n"] = "Title and abstract";
+ }
+ } else {
+ $qt["au"] = "Non-blind authors";
+ $qt["n"] = "Title, abstract, and non-blind authors";
+ }
+ if ($this->user->privChair) {
+ $qt["ac"] = "Authors and collaborators";
+ }
+ if ($this->user->isPC) {
+ $qt["re"] = "Reviewers";
+ $qt["tag"] = "Tags";
+ }
+ return $qt;
+ }
+
+ /** @param bool $always
+ * @return bool */
+ private function render_saved_searches($always) {
+ $ss = $this->conf->named_searches();
+ if (($show = !empty($ss) || $always)) {
+ echo '';
+ if (!empty($ss)) {
+ echo '
';
+ ksort($ss, SORT_NATURAL | SORT_FLAG_CASE);
+ foreach ($ss as $sn => $sv) {
+ $q = $sv->q ?? "";
+ if (isset($sv->t) && $sv->t !== "s") {
+ $q = "({$q}) in:{$sv->t}";
+ }
+ echo '
';
+ }
+ echo '
';
+ }
+ echo '
Edit saved searches
';
+ }
+ return $show;
+ }
+
+ /** @param Qrequest $qreq */
+ private function render_display_options($qreq) {
+ echo '',
+ Ht::form($this->conf->hoturl_post("search", "redisplay=1"), ["id" => "foldredisplay", "class" => "fn3 fold5c"]);
+ foreach (["q", "qa", "qo", "qx", "qt", "t", "sort"] as $x) {
+ if (isset($qreq[$x]) && ($x !== "q" || !isset($qreq->qa)))
+ echo Ht::hidden($x, $qreq[$x]);
+ }
+
+ echo '
';
+ ksort($this->items);
+ foreach ($this->items as $column => $items) {
+ if (!empty($items)) {
+ echo '
';
+ if (($h = $this->headers[$column] ?? "") !== "") {
+ echo '
', $h, '
';
+ }
+ echo join("", $items), '
';
+ }
+ }
+ echo "
\n";
+
+ // "Redisplay" row
+ echo '
';
+
+ // Conflict display
+ if ($this->user->privChair) {
+ echo '',
+ Ht::checkbox("showforce", 1, $this->pl->viewing("force"),
+ ["id" => "showforce", "class" => "uich js-plinfo"]),
+ " ", Ht::label("Override conflicts", "showforce"), " ";
+ }
+
+ echo '';
+ if ($this->user->privChair) {
+ echo Ht::button("Change default view", ["class" => "ui js-edit-view-options"]), " ";
+ }
+ echo Ht::submit("Redisplay", ["id" => "redisplay"]),
+ "
";
+ }
+
+ /** @param Qrequest $qreq */
+ private function render_list($pl_text, $qreq, $tOpt) {
+ $search = $this->pl->search;
+
+ if ($this->user->has_hidden_papers()
+ && !empty($this->user->hidden_papers)
+ && $this->user->is_actas_user()) {
+ $this->pl->message_set()->warning_at(null, $this->conf->_("Submissions %#Ns are totally hidden when viewing the site as another user.", array_map(function ($n) { return "#$n"; }, array_keys($this->user->hidden_papers))));
+ }
+ if ($search->has_problem()
+ || $this->pl->message_set()->has_messages()) {
+ echo '';
+ $this->conf->warnMsg(array_merge($search->problem_texts(), $this->pl->message_set()->message_texts()), true);
+ echo '
';
+ }
+
+ echo "
\n\n";
+
+ if ($this->pl->has("sel")) {
+ echo Ht::form($this->conf->selfurl($qreq, ["post" => post_value(), "forceShow" => null]), ["id" => "sel", "class" => "ui-submit js-submit-paperlist"]),
+ Ht::hidden("defaultfn", ""),
+ Ht::hidden("forceShow", (string) $qreq->forceShow, ["id" => "forceShow"]),
+ Ht::entry("____updates____", "", ["class" => "hidden ignore-diff"]),
+ Ht::hidden_default_submit("default", 1);
+ }
+
+ echo $pl_text;
+
+ if ($this->pl->is_empty()
+ && $search->limit() !== "s"
+ && !$search->limit_explicit()) {
+ $a = [];
+ foreach (["q", "qa", "qo", "qx", "qt", "sort", "showtags"] as $xa) {
+ if (isset($qreq[$xa]) && ($xa !== "q" || !isset($qreq->qa))) {
+ $a[] = "$xa=" . urlencode($qreq[$xa]);
+ }
+ }
+ reset($tOpt);
+ if (key($tOpt) !== $search->limit()
+ && !in_array($search->limit(), ["all", "viewable", "act"], true)) {
+ echo " (
conf->hoturl("search", join("&", $a)), "\">Repeat search in ", strtolower(current($tOpt)), " )";
+ }
+ }
+
+ if ($this->pl->has("sel")) {
+ echo "";
+ }
+ echo "
\n";
+ }
+
+ /** @param Qrequest $qreq */
+ function render($qreq) {
+ $user = $this->user;
+ $this->conf->header("Search", "search");
+ echo Ht::unstash(); // need the JS right away
+
+ // create PaperList
+ if (isset($qreq->q)) {
+ $search = new PaperSearch($user, $qreq);
+ } else {
+ $search = new PaperSearch($user, ["t" => $qreq->t, "q" => "NONE"]);
+ }
+ assert(!isset($qreq->display));
+ $this->pl = new PaperList("pl", $search, ["sort" => true], $qreq);
+ $this->pl->apply_view_report_default();
+ $this->pl->apply_view_session();
+ $this->pl->apply_view_qreq();
+ if (isset($qreq->q)) {
+ $this->pl->set_table_id_class("foldpl", "pltable-fullw", "p#");
+ $this->pl->set_table_decor(PaperList::DECOR_HEADER | PaperList::DECOR_FOOTER | PaperList::DECOR_STATISTICS | PaperList::DECOR_LIST);
+ $this->pl->set_table_fold_session("pldisplay.");
+ if ($this->ssel->count()) {
+ $this->pl->set_selection($this->ssel);
+ }
+ $this->pl->qopts["options"] = true; // get efficient access to `has(OPTION)`
+ $this->prepare_display_options();
+ $pl_text = $this->pl->table_html();
+ unset($qreq->atab);
+ } else {
+ $pl_text = null;
+ }
+
+ // echo form
+ echo '\n\n";
+ if (!$this->pl->is_empty()) {
+ Ht::stash_script("\$(document.body).addClass(\"want-hash-focus\")");
+ }
+ echo Ht::unstash();
+
+ // Paper body
+ if ($pl_text !== null) {
+ $this->render_list($pl_text, $qreq, $tOpt);
+ } else {
+ echo ' ';
+ }
+
+ $this->conf->footer();
+ }
+
+
+ /** @param Contact $user
+ * @param Qrequest $qreq */
+ static function go($user, $qreq) {
+ $conf = $user->conf;
+ if ($user->is_empty()) {
+ $user->escape();
+ }
+
+ // canonicalize request
+ assert(!$qreq->ajax);
+ if (isset($qreq->default) && $qreq->defaultfn) {
+ $qreq->fn = $qreq->defaultfn;
+ }
+ if ((isset($qreq->qa) || isset($qreq->qo) || isset($qreq->qx))
+ && !isset($qreq->q)) {
+ $qreq->q = PaperSearch::canonical_query((string) $qreq->qa, $qreq->qo, $qreq->qx, $qreq->qt, $conf);
+ } else {
+ unset($qreq->qa, $qreq->qo, $qreq->qx);
+ }
+ if (isset($qreq->t) && !isset($qreq->q)) {
+ $qreq->q = "";
+ }
+ if (isset($qreq->q)) {
+ $qreq->q = trim($qreq->q);
+ if ($qreq->q === "(All)") {
+ $qreq->q = "";
+ }
+ }
+
+ // paper group
+ if (!PaperSearch::viewable_limits($user, $qreq->t)) {
+ $conf->header("Search", "search");
+ Conf::msg_error("You aren’t allowed to search submissions.");
+ exit;
+ }
+
+ // paper selection
+ $ssel = SearchSelection::make($qreq, $user);
+ SearchSelection::clear_request($qreq);
+
+ // look for search action
+ if ($qreq->fn) {
+ $fn = $qreq->fn;
+ if (strpos($fn, "/") === false && isset($qreq[$qreq->fn . "fn"])) {
+ $fn .= "/" . $qreq[$qreq->fn . "fn"];
+ }
+ ListAction::call($fn, $user, $qreq, $ssel);
+ }
+
+ // request and session parsing
+ if ($qreq->redisplay) {
+ $settings = [];
+ foreach ($qreq as $k => $v) {
+ if ($v && substr($k, 0, 4) === "show") {
+ $settings[substr($k, 4)] = true;
+ }
+ }
+ Session_API::change_display($user, "pl", $settings);
+ }
+ if ($qreq->scoresort) {
+ $qreq->scoresort = ListSorter::canonical_short_score_sort($qreq->scoresort);
+ Session_API::setsession($user, "scoresort=" . $qreq->scoresort);
+ }
+ if ($qreq->redisplay) {
+ if (isset($qreq->forceShow) && !$qreq->forceShow && $qreq->showforce) {
+ $forceShow = 0;
+ } else {
+ $forceShow = $qreq->forceShow || $qreq->showforce ? 1 : null;
+ }
+ $conf->redirect_self($qreq, ["#" => "view", "forceShow" => $forceShow]);
+ }
+ if ($user->privChair
+ && !isset($qreq->forceShow)
+ && preg_match('/\b(show:|)force\b/', $user->session("pldisplay"))) {
+ $qreq->forceShow = 1;
+ $user->add_overrides(Contact::OVERRIDE_CONFLICT);
+ }
+
+ // display
+ $sp = new Search_Page($user, $ssel);
+ $sp->render($qreq);
+ }
+}
diff --git a/src/pages/p_settings.php b/src/pages/p_settings.php
new file mode 100644
index 000000000..a22e952d3
--- /dev/null
+++ b/src/pages/p_settings.php
@@ -0,0 +1,133 @@
+conf === $user->conf);
+ $this->conf = $user->conf;
+ $this->user = $user;
+ $this->sv = $sv;
+ }
+
+ /** @param Qrequest $qreq
+ * @return string */
+ function choose_setting_group($qreq) {
+ $req_group = $qreq->group;
+ if (!$req_group && preg_match('/\A\/\w+\/*\z/', $qreq->path())) {
+ $req_group = $qreq->path_component(0);
+ }
+ $want_group = $req_group;
+ if (!$want_group && isset($_SESSION["sg"])) { // NB not conf-specific session, global
+ $want_group = $_SESSION["sg"];
+ }
+ $want_group = $this->sv->canonical_group($want_group);
+ if (!$want_group || !$this->sv->group_title($want_group)) {
+ if ($this->conf->time_some_author_view_review()) {
+ $want_group = $this->sv->canonical_group("decisions");
+ } else if ($this->conf->time_after_setting("sub_sub")
+ || $this->conf->time_review_open()) {
+ $want_group = $this->sv->canonical_group("reviews");
+ } else {
+ $want_group = $this->sv->canonical_group("submissions");
+ }
+ }
+ if (!$want_group) {
+ $this->user->escape();
+ }
+ if ($want_group !== $req_group && !$qreq->post && $qreq->post_empty()) {
+ $this->conf->redirect_self($qreq, [
+ "group" => $want_group, "#" => $this->sv->group_hashid($req_group)
+ ]);
+ }
+ $this->sv->set_canonical_page($want_group);
+ return $want_group;
+ }
+
+ /** @param Qrequest $qreq */
+ function handle_update($qreq) {
+ if ($this->sv->execute()) {
+ $this->user->save_session("settings_highlight", $this->sv->message_field_map());
+ if (!empty($this->sv->updated_fields())) {
+ $this->conf->confirmMsg("Changes saved.");
+ } else {
+ $this->conf->warnMsg("No changes.");
+ }
+ $this->sv->report();
+ $this->conf->redirect_self($qreq);
+ }
+ }
+
+ /** @param string $group
+ * @param Qrequest $qreq */
+ function render($group, $qreq) {
+ $sv = $this->sv;
+ $conf = $this->conf;
+ $sv->crosscheck();
+
+ $conf->header("Settings", "settings", ["subtitle" => $sv->group_title($group), "title_div" => ' ', "body_class" => "leftmenu"]);
+ echo Ht::unstash(), // clear out other script references
+ $conf->make_script_file("scripts/settings.js"), "\n",
+
+ Ht::form($conf->hoturl_post("settings", "group={$group}"),
+ ["id" => "settingsform", "class" => "need-unload-protection"]),
+
+ '\n",
+ '',
+ '';
+
+ $sv->report(isset($qreq->update) && $qreq->valid_post());
+ $sv->render_group(strtolower($group), true);
+
+ echo '',
+ '
', Ht::submit("update", "Save changes", ["class" => "btn-primary"]), '
',
+ '
', Ht::submit("cancel", "Cancel", ["formnovalidate" => true]), '
',
+ '
', "\n";
+
+ Ht::stash_script('hiliter_children("#settingsform")');
+ $conf->footer();
+ }
+
+ static function go(Contact $user, Qrequest $qreq) {
+ if (isset($qreq->cancel)) {
+ $user->conf->redirect_self($qreq);
+ }
+
+ $sv = SettingValues::make_request($user, $qreq);
+ $sv->session_highlight();
+ if (!$sv->viewable_by_user()) {
+ $user->escape();
+ }
+
+ $sp = new Settings_Page($sv, $user);
+ $_SESSION["sg"] = $group = $qreq->group = $sp->choose_setting_group($qreq);
+
+ if (isset($qreq->update) && $qreq->valid_post()) {
+ $sp->handle_update($qreq);
+ }
+
+ $sp->render($group, $qreq);
+ }
+}
diff --git a/src/partials/p_signin.php b/src/pages/p_signin.php
similarity index 99%
rename from src/partials/p_signin.php
rename to src/pages/p_signin.php
index c57e5223e..de3e43280 100644
--- a/src/partials/p_signin.php
+++ b/src/pages/p_signin.php
@@ -1,8 +1,8 @@
conf;
$pid = $prow->paperId;
$q = "select conflictType, reviewType, reviewSubmitted, reviewNeedsSubmit";
@@ -158,8 +158,9 @@ static function load_into(PaperInfo $prow, $user) {
}
if ($cid > 0
&& !$rev_tokens
- && (!$Me || ($Me->contactId != $cid
- && ($Me->privChair || $Me->contactXid === $prow->managerContactId)))
+ && (!($viewer = Contact::$main_user)
+ || ($viewer->contactId != $cid
+ && ($viewer->privChair || $viewer->contactXid === $prow->managerContactId)))
&& ($pcm = $conf->pc_members())
&& isset($pcm[$cid])) {
foreach ($pcm as $u) {
diff --git a/src/papertable.php b/src/papertable.php
index 50d660bb4..65fdc12dc 100644
--- a/src/papertable.php
+++ b/src/papertable.php
@@ -158,7 +158,6 @@ function paper_page_prefers_edit_mode() {
/** @param ?PaperTable $paperTable
* @param Qrequest $qreq */
static function echo_header($paperTable, $id, $action_mode, $qreq) {
- global $Me;
$conf = $paperTable ? $paperTable->conf : Conf::$main;
$prow = $paperTable ? $paperTable->prow : null;
$format = 0;
@@ -178,8 +177,8 @@ static function echo_header($paperTable, $id, $action_mode, $qreq) {
} else {
$paperTable->initialize_list();
$title = "#" . $prow->paperId;
- $viewable_tags = $prow->viewable_tags($Me);
- if ($viewable_tags || $Me->can_view_tags($prow)) {
+ $viewable_tags = $prow->viewable_tags($paperTable->user);
+ if ($viewable_tags || $paperTable->user->can_view_tags($prow)) {
$t .= ' has-tag-classes';
if (($color = $prow->conf->tags()->color_classes($viewable_tags)))
$t .= ' ' . $color;
@@ -210,7 +209,7 @@ static function echo_header($paperTable, $id, $action_mode, $qreq) {
$t .= '';
if ($viewable_tags && $conf->tags()->has_decoration) {
- $tagger = new Tagger($Me);
+ $tagger = new Tagger($paperTable->user);
$t .= $tagger->unparse_decoration_html($viewable_tags);
}
}
@@ -223,8 +222,8 @@ static function echo_header($paperTable, $id, $action_mode, $qreq) {
$body_class = "paper";
if ($paperTable
&& $prow->paperId
- && $Me->has_overridable_conflict($prow)
- && ($Me->overrides() & Contact::OVERRIDE_CONFLICT)) {
+ && $paperTable->user->has_overridable_conflict($prow)
+ && ($paperTable->user->overrides() & Contact::OVERRIDE_CONFLICT)) {
$body_class .= " fold5o";
} else {
$body_class .= " fold5c";
diff --git a/src/siteloader.php b/src/siteloader.php
index e0e82b1e6..952b58979 100644
--- a/src/siteloader.php
+++ b/src/siteloader.php
@@ -16,6 +16,7 @@ class SiteLoader {
"FormatChecker" => "src/formatspec.php",
"HashAnalysis" => "lib/filer.php",
"JsonSerializable" => "lib/json.php",
+ "LogEntryGenerator" => "src/logentry.php",
"LoginHelper" => "lib/login.php",
"MailPreparation" => "lib/mailer.php",
"MessageItem" => "lib/messageset.php",
@@ -53,6 +54,7 @@ class SiteLoader {
"_papercolumn.php" => ["pc_", "papercolumns"],
"_papercolumnfactory.php" => ["pc_", "papercolumns"],
"_paperoption.php" => ["o_", "options"],
+ "_page.php" => ["p_", "pages"],
"_partial.php" => ["p_", "partials"],
"_searchterm.php" => ["st_", "search"],
"_settingrenderer.php" => ["s_", "settings"],
diff --git a/src/tarball.sh b/src/tarball.sh
index ecba81ae9..b22b4a7fd 100755
--- a/src/tarball.sh
+++ b/src/tarball.sh
@@ -110,7 +110,7 @@ etc/mailkeywords.json
etc/mailtemplates.json
etc/msgs.json
etc/optiontypes.json
-etc/pagepartials.json
+etc/pages.json
etc/papercolumns.json
etc/profilegroups.json
etc/reviewformlibrary.json
@@ -168,6 +168,7 @@ src/api/api_comment.php
src/api/api_completion.php
src/api/api_decision.php
src/api/api_error.php
+src/api/api_events.php
src/api/api_formatcheck.php
src/api/api_graphdata.php
src/api/api_mail.php
@@ -253,7 +254,6 @@ src/helpers.php
src/helprenderer.php
src/hotcrpmailer.php
src/init.php
-src/initweb.php
src/listaction.php
src/listactions/la_assign.php
src/listactions/la_decide.php
@@ -277,6 +277,8 @@ src/listactions/la_mail.php
src/listactions/la_revpref.php
src/listactions/la_tag.php
src/listsorter.php
+src/logentry.php
+src/logentryfilter.php
src/mailclasses.php
src/meetingtracker.php
src/mergecontacts.php
@@ -290,6 +292,19 @@ src/options/o_pcconflicts.php
src/options/o_submissionversion.php
src/options/o_title.php
src/options/o_topics.php
+src/pages/p_adminhome.php
+src/pages/p_api.php
+src/pages/p_deadlines.php
+src/pages/p_doc.php
+src/pages/p_help.php
+src/pages/p_home.php
+src/pages/p_log.php
+src/pages/p_paper.php
+src/pages/p_review.php
+src/pages/p_reviewprefs.php
+src/pages/p_search.php
+src/pages/p_settings.php
+src/pages/p_signin.php
src/paperapi.php
src/papercolumn.php
src/papercolumns/pc_administrator.php
@@ -324,9 +339,6 @@ src/papersearch.php
src/paperstatus.php
src/papertable.php
src/paperrank.php
-src/partials/p_adminhome.php
-src/partials/p_home.php
-src/partials/p_signin.php
src/permissionproblem.php
src/review.php
src/reviewdiffinfo.php
diff --git a/test/setup.php b/test/setup.php
index 7ccf1948b..7a02aa512 100644
--- a/test/setup.php
+++ b/test/setup.php
@@ -542,7 +542,8 @@ function call_api($fn, $user, $qreq, $prow) {
$qreq = new Qrequest("POST", $qreq);
$qreq->approve_token();
}
- $jr = $user->conf->call_api($fn, $user, $qreq, $prow);
+ $uf = $user->conf->api($fn, $user, $qreq->method());
+ $jr = $user->conf->call_api_on($uf, $fn, $user, $qreq, $prow);
return $jr->content;
}
diff --git a/users.php b/users.php
index 6df4bf02d..893f8e0b2 100644
--- a/users.php
+++ b/users.php
@@ -2,7 +2,8 @@
// users.php -- HotCRP people listing/editing page
// Copyright (c) 2006-2021 Eddie Kohler; see LICENSE.
-require_once("src/initweb.php");
+require_once("src/init.php");
+$Qreq || initialize_request();
require_once("src/contactlist.php");
$Viewer = $Me;