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", - '
', - '

', $topicj->title, '

'; -$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 '
', - '
', - Ht::feedback_html_at("q"), - Ht::entry("q", $Qreq->q, ["id" => "q", "size" => 40]), - '
', - Ht::feedback_html_at("p"), - Ht::entry("p", $Qreq->p, ["id" => "p", "class" => "need-suggest papersearch", "autocomplete" => "off", "size" => 40, "spellcheck" => false]), - '
', - Ht::feedback_html_at("u"), - Ht::entry("u", $Qreq->u, ["id" => "u", "size" => 40]), - '
', - Ht::entry("n", $Qreq->n, ["id" => "n", "size" => 4, "placeholder" => 50]), - '  records at a time', - Ht::feedback_html_at("n"), - '
', - 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", - ' ', - '', - '', - '', - '', - "\n \n", - join("", $trs), - " \n
TimeUserAffected userAction
\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}

  • " . join("
  • ", $emsg) . "
"; - } - 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])) - . '
' - . ($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 '
'; - - $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 '
', - 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 '
    ', - '
      ', 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", - ''; -$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 = ''; - $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 '
    '; - -// Search options -$tOpt = PaperSearch::viewable_limits($Me, $Search->limit()); -$qtOpt = ["ti" => "Title", "ab" => "Abstract"]; -if ($Me->privChair - || $Conf->submission_blindness() === Conf::BLIND_NEVER) { - $qtOpt["au"] = "Authors"; - $qtOpt["n"] = "Title, abstract, and authors"; -} else if ($Conf->submission_blindness() === Conf::BLIND_ALWAYS) { - if ($Me->is_reviewer() - && $Conf->time_reviewer_view_accepted_authors()) { - $qtOpt["au"] = "Accepted authors"; - $qtOpt["n"] = "Title, abstract, and accepted authors"; - } else { - $qtOpt["n"] = "Title and abstract"; - } -} else { - $qtOpt["au"] = "Non-blind authors"; - $qtOpt["n"] = "Title, abstract, and non-blind authors"; -} -if ($Me->privChair) { - $qtOpt["ac"] = "Authors and collaborators"; -} -if ($Me->isPC) { - $qtOpt["re"] = "Reviewers"; - $qtOpt["tag"] = "Tags"; -} - -// Basic search -echo Ht::form($Conf->hoturl("search"), ["method" => "get", "class" => "form-basic-search"]), - Ht::entry("q", (string) $Qreq->q, [ - "size" => 40, "tabindex" => 1, - "class" => "papersearch want-focus need-suggest flex-grow-1", - "placeholder" => "(All)", "aria-label" => "Search" - ]), '
    '; -if ($Search->limit_explicit() || count($tOpt) === 1) { - echo " in ", htmlspecialchars(PaperSearch::limit_description($Conf, $Search->limit())), - Ht::hidden("t", $Search->limit()); -} else { - echo " in ", PaperSearch::limit_selector($tOpt, $Search->limit(), ["tabindex" => 1, "class" => "ml-1"]); -} -echo Ht::submit("Search", ["tabindex" => 1, "class" => "ml-3"]), "
    "; - -echo '
    '; - -// Advanced search -echo Ht::form($Conf->hoturl("search"), ["method" => "get"]), - '
    ', - '
    ', - Ht::select("qt", $qtOpt, $Qreq->get("qt", "n"), ["id" => "htctl-advanced-qt"]), '
    ', - '
    ', - Ht::entry("qa", $Qreq->get("qa", $Qreq->get("q", "")), ["id" => "htctl-advanced-qa", "size" => 60, "class" => "papersearch want-focus need-suggest", "spellcheck" => false]), '
    ', - '
    ', - Ht::entry("qo", $Qreq->get("qo", ""), ["id" => "htctl-advanced-qo", "size" => 60, "spellcheck" => false]), '
    ', - '
    ', - Ht::entry("qx", $Qreq->get("qx", ""), ["id" => "htctl-advanced-qx", "size" => 60, "spellcheck" => false]), '
    '; -if (!$Search->limit_explicit()) { - echo '
    ', - PaperSearch::limit_selector($tOpt, $Search->limit(), ["id" => "htctl-advanced-q"]), '
    '; -} -echo '
    ', - Ht::submit("Search"), - '
    ', - Ht::link("Search help", $Conf->hoturl("help", "t=search")), - ' · ', - Ht::link("Search keywords", $Conf->hoturl("help", "t=keywords")), - '
    ', - '
    ', - '
    '; - -echo "
    "; - -function echo_request_as_hidden_inputs($specialscore) { - global $pl, $pl_text, $Qreq; - foreach (array("q", "qa", "qo", "qx", "qt", "t", "sort") as $x) { - if (isset($Qreq[$x]) - && ($x !== "q" || !isset($Qreq->qa)) - && ($x !== "sort" || !$specialscore || !$pl_text)) - echo Ht::hidden($x, $Qreq[$x]); - } - if ($specialscore && $pl_text) { - echo Ht::hidden("sort", $pl->sortdef(true)); - } -} - -// Saved searches -$ss = array(); -if ($Me->isPC || $Me->privChair) { - $ss = $Conf->named_searches(); - if (!empty($ss) || $pl_text) { - 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 '

    '; - $ss = true; - } else { - $ss = false; - } -} - -// Display options -if (!$pl->is_empty()) { - echo '
    '; - - echo Ht::form($Conf->hoturl_post("search", "redisplay=1"), ["id" => "foldredisplay", "class" => "fn3 fold5c"]); - echo_request_as_hidden_inputs(false); - - echo '
    '; - ksort($display_options->items); - foreach ($display_options->items as $column => $items) { - if (empty($items)) { - continue; - } - echo '
    '; - if (($h = $display_options->headers[$column] ?? "") !== "") { - echo '
    ', $h, '
    '; - } - echo join("", $items), '
    '; - } - echo "
    \n"; - - // "Redisplay" row - echo '
    '; - - // Conflict display - if ($Me->privChair) { - echo '"; - } - - echo '
    ', - Ht::checkbox("showforce", 1, $pl->viewing("force"), - ["id" => "showforce", "class" => "uich js-plinfo"]), - " ", Ht::label("Override conflicts", "showforce"), "'; - if ($Me->privChair) - echo Ht::button("Change default view", ["class" => "ui js-edit-view-options"]), "  "; - echo Ht::submit("Redisplay", array("id" => "redisplay")); - - echo "
    ", $display_options_extra, "
    "; - - // Done - echo ""; - - echo "
    "; -} - -echo "
    "; - -// Tab selectors -echo '
    ', - ' - ', "\n"; -if ($ss) { - echo ' ', "\n"; -} -if (!$pl->is_empty()) { - echo ' ', "\n"; -} -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->group_title($Group), '

    '; - -$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", + '
    ', + '

    ', $topicj->title, '

    '; + $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 '
    ', + '
    ', + Ht::feedback_html_at("q"), + Ht::entry("q", $this->qreq->q, ["id" => "q", "size" => 40]), + '
    ', + Ht::feedback_html_at("p"), + Ht::entry("p", $this->qreq->p, ["id" => "p", "class" => "need-suggest papersearch", "autocomplete" => "off", "size" => 40, "spellcheck" => false]), + '
    ', + Ht::feedback_html_at("u"), + Ht::entry("u", $this->qreq->u, ["id" => "u", "size" => 40]), + '
    ', + Ht::entry("n", $this->qreq->n, ["id" => "n", "size" => 4, "placeholder" => 50]), + '  records at a time', + Ht::feedback_html_at("n"), + '
    ', + 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", + ' ', + '', + '', + '', + '', + "\n \n", + join("", $trs), + " \n
    TimeUserAffected userAction
    \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}

    • " . join("
    • ", $emsg) . "
    "; + } + 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])) + . '
    ' + . ($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 '
    '; + + $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 '
    ', + 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 '
    ', + '
      ', 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", + ''; + $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 = ''; + $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 '

    '; + } + 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 '"; + } + + echo '
    ', + Ht::checkbox("showforce", 1, $this->pl->viewing("force"), + ["id" => "showforce", "class" => "uich js-plinfo"]), + " ", Ht::label("Override conflicts", "showforce"), "'; + 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 '
    '; + + $tOpt = PaperSearch::viewable_limits($user, $search->limit()); + $qtOpt = $this->field_search_types(); + + // Basic search tab + echo Ht::form($this->conf->hoturl("search"), ["method" => "get", "class" => "form-basic-search"]), + Ht::entry("q", (string) $qreq->q, [ + "size" => 40, "tabindex" => 1, + "class" => "papersearch want-focus need-suggest flex-grow-1", + "placeholder" => "(All)", "aria-label" => "Search" + ]), '
    '; + if ($search->limit_explicit() || count($tOpt) === 1) { + echo " in ", htmlspecialchars(PaperSearch::limit_description($this->conf, $search->limit())), + Ht::hidden("t", $search->limit()); + } else { + echo " in ", PaperSearch::limit_selector($tOpt, $search->limit(), ["tabindex" => 1, "class" => "ml-1"]); + } + echo Ht::submit("Search", ["tabindex" => 1, "class" => "ml-3"]), "
    "; + + echo '
    '; + + // Advanced search tab + echo '
    ', + Ht::form($this->conf->hoturl("search"), ["method" => "get"]), + '
    ', + '
    ', + Ht::select("qt", $qtOpt, $qreq->get("qt", "n"), ["id" => "htctl-advanced-qt"]), '
    ', + '
    ', + Ht::entry("qa", $qreq->get("qa", $qreq->get("q", "")), ["id" => "htctl-advanced-qa", "size" => 60, "class" => "papersearch want-focus need-suggest", "spellcheck" => false]), '
    ', + '
    ', + Ht::entry("qo", $qreq->get("qo", ""), ["id" => "htctl-advanced-qo", "size" => 60, "spellcheck" => false]), '
    ', + '
    ', + Ht::entry("qx", $qreq->get("qx", ""), ["id" => "htctl-advanced-qx", "size" => 60, "spellcheck" => false]), '
    '; + if (!$search->limit_explicit()) { + echo '
    ', + PaperSearch::limit_selector($tOpt, $search->limit(), ["id" => "htctl-advanced-q"]), '
    '; + } + echo '
    ', + Ht::submit("Search"), + '
    ', + Ht::link("Search help", $this->conf->hoturl("help", "t=search")), + ' · ', + Ht::link("Search keywords", $this->conf->hoturl("help", "t=keywords")), + '
    ', + '
    ', + '
    '; + + // Saved searches tab + $has_ss = $user->isPC && $this->render_saved_searches($pl_text !== null); + + // Display options tab + if (!$this->pl->is_empty()) { + $this->render_display_options($qreq); + } + + echo "
    "; + + // Tab selectors + echo '
    ', + '', + ''; + if ($has_ss) { + echo ''; + } + if (!$this->pl->is_empty()) { + echo ''; + } + 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->group_title($group), '

    '; + + $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;