1<?php
2// api_requestreview.php -- HotCRP user-related API calls
3// Copyright (c) 2008-2018 Eddie Kohler; see LICENSE.
4
5class RequestReview_API {
6    static function requestreview($user, $qreq, $prow) {
7        if (($whyNot = $user->perm_request_review($prow, true)))
8            return new JsonResult(403, ["ok" => false, "error" => whyNotText($whyNot)]);
9        if (!isset($qreq->email))
10            return new JsonResult(400, "Parameter error.");
11        $email = $qreq->email = trim($qreq->email);
12        if ($email === "" || $email === "newanonymous")
13            return self::requestreview_anonymous($user, $qreq, $prow);
14
15        $reviewer = $user->conf->user_by_email($email);
16        if (!$reviewer && ($email === "" || !validate_email($email)))
17            return self::error_result("email", "Invalid email address.");
18
19        $round = null;
20        if ((string) $qreq->round !== ""
21            && ($rname = $user->conf->sanitize_round_name($qreq->round)) !== false)
22            $round = (int) $user->conf->round_number($rname, false);
23
24        $name_args = Text::analyze_name_args([(object) ["firstName" => $qreq->firstName, "lastName" => $qreq->lastName, "name" => $qreq->name, "affiliation" => $qreq->affiliation, "email" => $email]]);
25        $reason = trim($qreq->reason);
26
27        // check proposal:
28        // - check existing review
29        if ($reviewer && $prow->review_of_user($reviewer))
30            return self::error_result("email", htmlspecialchars($email) . " is already a reviewer.");
31        // - check existing request
32        $request = $user->conf->fetch_first_object("select * from ReviewRequest where paperId=? and email=?", $prow->paperId, $email);
33        if ($request && !$user->allow_administer($prow))
34            return self::error_result("email", htmlspecialchars($email) . " is already a requested reviewer.");
35        // - check existing refusal
36        if ($reviewer
37            && ($refusal = $user->conf->fetch_first_object("select * from PaperReviewRefused where paperId=? and contactId=?", $prow->paperId, $reviewer->contactId))
38            && (!$user->can_administer($prow) || !$qreq->override)) {
39            $errf = ["email" => true];
40            $msg = htmlspecialchars($email) . " has already declined to review this paper.";
41            if ((string) $refusal->reason !== "")
42                $msg .= " They offered this reason: <blockquote>" . htmlspecialchars($refusal->reason) . "</blockquote>";
43            if ($user->allow_administer($prow))
44                $errf["override"] = true;
45            return self::error_result($errf, $msg);
46        }
47        // - check conflict
48        if ($reviewer
49            && ($prow->has_conflict($reviewer)
50                || ($reviewer->isPC && !$reviewer->can_accept_review_assignment($prow))))
51            return self::error_result("email", htmlspecialchars($email) . " cannot be asked to review this paper.");
52
53        // check for potential conflict
54        $xreviewer = $reviewer;
55        if (!$xreviewer)
56            $xreviewer = $user->conf->contactdb_user_by_email($email);
57        if (!$xreviewer)
58            $xreviewer = new Contact($name_args, $user->conf);
59        $potconflict = $prow->potential_conflict_html($xreviewer);
60        if ($potconflict
61            && $user->can_administer($prow)
62            && !$qreq->override)
63            return self::error_result("override", "<p>" . Text::user_html($xreviewer) . " has a potential conflict with this paper, so the request has not been made.</p>" . $potconflict[1]);
64
65        // check whether to make a proposal
66        $extrev_chairreq = $user->conf->setting("extrev_chairreq");
67        if (!$user->can_administer($prow)
68            && ($extrev_chairreq === 1
69                || ($extrev_chairreq === 2 && $potconflict))) {
70            $result = Dbl::qe("insert into ReviewRequest set paperId=?, email=?, firstName=?, lastName=?, affiliation=?, requestedBy=?, reason=?, reviewRound=? on duplicate key update paperId=paperId",
71                $prow->paperId, $email, $xreviewer->firstName, $xreviewer->lastName,
72                $xreviewer->affiliation, $user->contactId, $reason, $round);
73            if ($extrev_chairreq === 2) {
74                $msg = "<p>" . Text::user_html($xreviewer) . " has a potential conflict with this submission, so an administrator must approve your proposed external review before it can take effect.</p>";
75                if ($user->can_view_authors($prow))
76                    $msg .= $potconflict[1];
77            } else
78                $msg = "Proposed an external review from " . Text::user_html($xreviewer) . ". An administrator must approve this proposal for it to take effect.";
79            $user->log_activity("Logged proposal for $email to review", $prow);
80            return ["ok" => true, "action" => "propose", "response" => $msg];
81        }
82
83        // if we get here, we will (try to) assign a review
84
85        // create account
86        if (!$reviewer)
87            $reviewer = Contact::create($user->conf, $user, $xreviewer);
88        if (!$reviewer)
89            return new JsonResult(400, "Error while creating account.");
90
91        // check requester
92        $requester = null;
93        if ($request
94            && $user->can_administer($prow))
95            $requester = $user->conf->cached_user_by_id($request->requestedBy);
96        $requester = $requester ? : $user;
97
98        // assign review
99        $user->assign_review($prow->paperId, $reviewer->contactId, REVIEW_EXTERNAL,
100                             ["mark_notify" => true,
101                              "requester_contact" => $requester,
102                              "requested_email" => $reviewer->email,
103                              "round_number" => $round]);
104
105        // send confirmation mail
106        HotCRPMailer::send_to($reviewer, "@requestreview", $prow,
107                              ["requester_contact" => $requester,
108                               "reason" => $reason]);
109
110        return ["ok" => true, "action" => "request", "response" => "Requested an external review from " . Text::user_html($reviewer) . "."];
111    }
112
113    static function requestreview_anonymous($user, $qreq, $prow) {
114        if (trim((string) $qreq->firstName) !== ""
115            || trim((string) $qreq->lastName) !== "")
116            return new JsonResult(400, "An email address is required to request a review.");
117        if (!$user->allow_administer($prow))
118            return new JsonResult(403, "Only administrators can request anonymous reviews.");
119        $aset = new AssignmentSet($user, true);
120        $aset->enable_papers($prow);
121        $aset->parse("paper,action,user\n{$prow->paperId},review,newanonymous\n");
122        if ($aset->execute()) {
123            $aset_csv = $aset->unparse_csv();
124            assert(count($aset_csv->data) === 1);
125            return ["ok" => true, "action" => "token", "review_token" => $aset_csv->data[0]["review_token"]];
126        } else
127            return new JsonResult(400, ["ok" => false, "error" => $aset->errors_div_html()]);
128    }
129
130    static function error_result($errf, $message) {
131        if (is_string($errf))
132            $errf = [$errf => true];
133        return new JsonResult(400, ["ok" => false, "error" => $message, "errf" => $errf]);
134    }
135}
136