1<?php
2// paperapi.php -- HotCRP paper-related API calls
3// Copyright (c) 2008-2018 Eddie Kohler; see LICENSE.
4
5class PaperApi {
6    static function decision_api(Contact $user, Qrequest $qreq, $prow) {
7        if ($qreq->method() !== "GET") {
8            $aset = new AssignmentSet($user, true);
9            $aset->enable_papers($prow);
10            if (is_numeric($qreq->decision))
11                $qreq->decision = get($user->conf->decision_map(), +$qreq->decision);
12            $aset->parse("paper,action,decision\n{$prow->paperId},decision," . CsvGenerator::quote($qreq->decision));
13            if (!$aset->execute())
14                return $aset->json_result();
15            $prow->outcome = $prow->conf->fetch_ivalue("select outcome from Paper where paperId=?", $prow->paperId);
16        }
17        if (!$user->can_view_decision($prow))
18            json_exit(403, "Permission error.");
19        $dname = $prow->conf->decision_name($prow->outcome);
20        $jr = new JsonResult(["ok" => true, "value" => (int) $prow->outcome, "result" => htmlspecialchars($dname ? : "?")]);
21        if ($user->can_set_decision($prow))
22            $jr->content["editable"] = true;
23        return $jr;
24    }
25
26    private static function paper_pc_api(Contact $user, Qrequest $qreq, $prow, $type) {
27        if ($qreq->method() !== "GET") {
28            if (!isset($qreq->$type))
29                return new JsonResult(400, ["ok" => false, "error" => "Missing parameter."]);
30            $aset = new AssignmentSet($user);
31            $aset->enable_papers($prow);
32            $aset->parse("paper,action,user\n{$prow->paperId},$type," . CsvGenerator::quote($qreq->$type));
33            if (!$aset->execute())
34                return $aset->json_result();
35            $cid = $user->conf->fetch_ivalue("select {$type}ContactId from Paper where paperId=?", $prow->paperId);
36        } else {
37            $k = "can_view_$type";
38            if (!$user->$k($prow))
39                return new JsonResult(403, ["ok" => false, "error" => "Permission error."]);
40            $k = "{$type}ContactId";
41            $cid = $prow->$k;
42        }
43        $luser = $cid ? $user->conf->pc_member_by_id($cid) : null;
44        $j = ["ok" => true, "result" => $luser ? $user->name_html_for($luser) : "None"];
45        if ($user->can_view_reviewer_tags($prow))
46            $j["color_classes"] = $cid ? $user->user_color_classes_for($luser) : "";
47        return $j;
48    }
49
50    static function lead_api(Contact $user, Qrequest $qreq, $prow) {
51        return self::paper_pc_api($user, $qreq, $prow, "lead");
52    }
53
54    static function shepherd_api(Contact $user, Qrequest $qreq, $prow) {
55        return self::paper_pc_api($user, $qreq, $prow, "shepherd");
56    }
57
58    static function manager_api(Contact $user, Qrequest $qreq, $prow) {
59        return self::paper_pc_api($user, $qreq, $prow, "manager");
60    }
61
62    static function tagreport(Contact $user, $prow) {
63        $ret = (object) ["ok" => $user->can_view_tags($prow)];
64        if ($prow)
65            $ret->pid = $prow->paperId;
66        $ret->tagreport = [];
67        if (!$ret->ok)
68            return $ret;
69        if (($vt = $user->conf->tags()->filter("vote"))) {
70            $myprefix = $user->contactId . "~";
71            $qv = $myvotes = array();
72            foreach ($vt as $lbase => $t) {
73                $qv[] = $myprefix . $lbase;
74                $myvotes[$lbase] = 0;
75            }
76            $result = $user->conf->qe("select tag, sum(tagIndex) from PaperTag where tag ?a group by tag", $qv);
77            while (($row = edb_row($result))) {
78                $lbase = strtolower(substr($row[0], strlen($myprefix)));
79                $myvotes[$lbase] += +$row[1];
80            }
81            Dbl::free($result);
82            foreach ($vt as $lbase => $t) {
83                if ($myvotes[$lbase] < $t->vote)
84                    $ret->tagreport[] = (object) ["tag" => "~{$t->tag}", "status" => 0, "message" => plural($t->vote - $myvotes[$lbase], "vote") . " remaining", "search" => "editsort:-#~{$t->tag}"];
85                else if ($myvotes[$lbase] > $t->vote)
86                    $ret->tagreport[] = (object) ["tag" => "~{$t->tag}", "status" => 1, "message" => plural($myvotes[$lbase] - $t->vote, "vote") . " over", "search" => "editsort:-#~{$t->tag}"];
87            }
88        }
89        return $ret;
90    }
91
92    static function tagreport_api(Contact $user, $qreq, $prow) {
93        $jr = new JsonResult((array) self::tagreport($user, $prow));
94        $jr->transfer_messages($user->conf, true);
95        return $jr;
96    }
97
98    static function settags_api(Contact $user, $qreq, $prow) {
99        if ($qreq->cancel)
100            return ["ok" => true];
101        if ($prow && !$user->can_view_paper($prow))
102            return ["ok" => false, "error" => "No such paper."];
103
104        // save tags using assigner
105        $pids = [];
106        $x = array("paper,action,tag");
107        if ($prow) {
108            if (isset($qreq->tags)) {
109                $x[] = "$prow->paperId,tag,all#clear";
110                foreach (TagInfo::split($qreq->tags) as $t)
111                    $x[] = "$prow->paperId,tag," . CsvGenerator::quote($t);
112            }
113            foreach (TagInfo::split((string) $qreq->addtags) as $t)
114                $x[] = "$prow->paperId,tag," . CsvGenerator::quote($t);
115            foreach (TagInfo::split((string) $qreq->deltags) as $t)
116                $x[] = "$prow->paperId,tag," . CsvGenerator::quote($t . "#clear");
117        } else if (isset($qreq->tagassignment)) {
118            $pid = -1;
119            foreach (preg_split('/[\s,]+/', $qreq->tagassignment) as $w)
120                if ($w !== "" && ctype_digit($w))
121                    $pid = intval($w);
122                else if ($w !== "" && $pid > 0) {
123                    $x[] = "$pid,tag," . CsvGenerator::quote($w);
124                    $pids[$pid] = true;
125                }
126        }
127        $assigner = new AssignmentSet($user);
128        $assigner->parse(join("\n", $x));
129        $error = join("<br />", $assigner->errors_html());
130        $ok = $assigner->execute();
131        $user->remove_overrides(Contact::OVERRIDE_CONFLICT);
132
133        // exit
134        if ($ok && $prow) {
135            $prow->load_tags();
136            $taginfo = self::tagreport($user, $prow);
137            $prow->add_tag_info_json($taginfo, $user);
138            $jr = new JsonResult($taginfo);
139        } else if ($ok) {
140            $p = [];
141            if ($pids) {
142                foreach ($user->paper_set(array_keys($pids)) as $prow) {
143                    $p[$prow->paperId] = (object) [];
144                    $prow->add_tag_info_json($p[$prow->paperId], $user);
145                }
146            }
147            $jr = new JsonResult(["ok" => true, "p" => (object) $p]);
148        } else
149            $jr = new JsonResult(["ok" => false, "error" => $error]);
150        return $jr;
151    }
152
153    static function votereport_api(Contact $user, Qrequest $qreq, PaperInfo $prow) {
154        $tagger = new Tagger($user);
155        if (!($tag = $tagger->check($qreq->tag, Tagger::NOVALUE)))
156            json_exit(["ok" => false, "error" => $tagger->error_html]);
157        if (!$user->can_view_peruser_tags($prow, $tag))
158            json_exit(["ok" => false, "error" => "Permission error."]);
159        $votemap = [];
160        preg_match_all('/ (\d+)~' . preg_quote($tag) . '#(\S+)/i', $prow->all_tags_text(), $m);
161        $is_approval = $user->conf->tags()->is_approval($tag);
162        $min_vote = $is_approval ? 0 : 0.001;
163        for ($i = 0; $i != count($m[0]); ++$i)
164            if ($m[2][$i] >= $min_vote)
165                $votemap[$m[1][$i]] = $m[2][$i];
166        $user->ksort_cid_array($votemap);
167        $result = [];
168        foreach ($votemap as $k => $v)
169            if ($is_approval)
170                $result[] = $user->reviewer_html_for($k);
171            else
172                $result[] = $user->reviewer_html_for($k) . " ($v)";
173        if (empty($result))
174            json_exit(["ok" => true, "result" => ""]);
175        else
176            json_exit(["ok" => true, "result" => '<span class="nw">' . join(',</span> <span class="nw">', $result) . '</span>']);
177    }
178
179    static function get_user(Contact $user, Qrequest $qreq, $forceShow = null) {
180        $u = $user;
181        if (isset($qreq->u) || isset($qreq->reviewer)) {
182            $x = isset($qreq->u) ? $qreq->u : $qreq->reviewer;
183            if ($x === ""
184                || strcasecmp($x, "me") == 0
185                || ($user->contactId > 0 && $x == $user->contactId)
186                || strcasecmp($x, $user->email) == 0)
187                $u = $user;
188            else if (ctype_digit($x))
189                $u = $user->conf->cached_user_by_id($x);
190            else
191                $u = $user->conf->cached_user_by_email($x);
192            if (!$u) {
193                error_log("PaperApi::get_user: rejecting user {$x}, requested by {$user->email}");
194                json_exit(403, $user->isPC ? "No such user." : "Permission error.");
195            }
196        }
197        return $u;
198    }
199
200    static function get_reviewer(Contact $user, $qreq, $prow, $forceShow = null) {
201        $u = self::get_user($user, $qreq, $forceShow);
202        if ($u->contactId !== $user->contactId
203            && ($prow ? !$user->can_administer($prow, $forceShow) : !$user->privChair)) {
204            error_log("PaperApi::get_reviewer: rejecting user {$u->contactId}/{$u->email}, requested by {$user->contactId}/{$user->email}");
205            json_exit(403, "Permission error.");
206        }
207        return $u;
208    }
209
210    static function pref_api(Contact $user, $qreq, $prow) {
211        $u = self::get_reviewer($user, $qreq, $prow, true);
212        if ($qreq->method() !== "GET") {
213            $aset = new AssignmentSet($user, true);
214            $aset->enable_papers($prow);
215            $aset->parse("paper,user,preference\n{$prow->paperId}," . CsvGenerator::quote($u->email) . "," . CsvGenerator::quote($qreq->pref, true));
216            if (!$aset->execute())
217                return $aset->json_result();
218            $prow->load_reviewer_preferences();
219        }
220        if ($u->contactId !== $user->contactId && !$user->allow_administer($prow)) {
221            error_log("PaperApi::pref_api: rejecting user {$u->contactId}/{$u->email}, requested by {$user->contactId}/{$user->email}");
222            json_exit(403, "Permission error.");
223        }
224        $pref = $prow->reviewer_preference($u, true);
225        $value = unparse_preference($pref[0], $pref[1]);
226        $jr = new JsonResult(["ok" => true, "value" => $value === "0" ? "" : $value, "pref" => $pref[0]]);
227        if ($pref[1] !== null)
228            $jr->content["prefexp"] = unparse_expertise($pref[1]);
229        if ($user->conf->has_topics())
230            $jr->content["topic_score"] = $pref[2];
231        return $jr;
232    }
233
234    static function checkformat_api(Contact $user, $qreq, $prow) {
235        $dtype = cvtint($qreq->dt, 0);
236        $opt = $user->conf->paper_opts->get($dtype);
237        if (!$opt || !$user->can_view_paper_option($prow, $opt))
238            return ["ok" => false, "error" => "Permission error."];
239        $cf = new CheckFormat($prow->conf);
240        $doc = $cf->fetch_document($prow, $dtype, $qreq->docid);
241        $cf->check_document($prow, $doc);
242        return ["ok" => !$cf->failed, "response" => $cf->document_report($prow, $doc)];
243    }
244
245    static function follow_api(Contact $user, $qreq, $prow) {
246        $reviewer = self::get_reviewer($user, $qreq, $prow);
247        $following = friendly_boolean($qreq->following);
248        if ($following === null)
249            return ["ok" => false, "error" => "Bad 'following'."];
250        $bits = Contact::WATCH_REVIEW_EXPLICIT | ($following ? Contact::WATCH_REVIEW : 0);
251        $user->conf->qe("insert into PaperWatch set paperId=?, contactId=?, watch=? on duplicate key update watch=(watch&~?)|?",
252            $prow->paperId, $reviewer->contactId, $bits,
253            Contact::WATCH_REVIEW_EXPLICIT | Contact::WATCH_REVIEW, $bits);
254        return ["ok" => true, "following" => $following];
255    }
256
257    static function mentioncompletion_api(Contact $user, $qreq, $prow) {
258        $result = [];
259        if ($user->can_view_pc()) {
260            $pcmap = $user->conf->pc_completion_map();
261            foreach ($user->conf->pc_members_and_admins() as $pc)
262                if (!$pc->disabled
263                    && (!$prow || $pc->can_view_new_comment_ignore_conflict($prow))) {
264                    $primary = true;
265                    foreach ($pc->completion_items() as $k => $level)
266                        if (get($pcmap, $k) === $pc) {
267                            $skey = $primary ? "s" : "sm1";
268                            $result[$k] = [$skey => $k, "d" => $pc->name_text()];
269                            $primary = false;
270                        }
271                }
272        }
273        ksort($result);
274        return ["ok" => true, "mentioncompletion" => array_values($result)];
275    }
276
277    static function review_api(Contact $user, Qrequest $qreq, PaperInfo $prow) {
278        if (!$user->can_view_review($prow, null))
279            return new JsonResult(403, "Permission error.");
280        $need_id = false;
281        if (isset($qreq->r)) {
282            $rrow = $prow->full_review_of_textual_id($qreq->r);
283            if ($rrow === false)
284                return new JsonResult(400, "Parameter error.");
285            $rrows = $rrow ? [$rrow] : [];
286        } else if (isset($qreq->u)) {
287            $need_id = true;
288            $u = self::get_user($user, $qreq);
289            $rrows = $prow->full_reviews_of_user($u);
290            if (!$rrows
291                && $user->contactId !== $u->contactId
292                && !$user->can_view_review_identity($prow, null))
293                return new JsonResult(403, "Permission error.");
294        } else {
295            $prow->ensure_full_reviews();
296            $rrows = $prow->viewable_submitted_reviews_by_display($user);
297        }
298        $vrrows = [];
299        $rf = $user->conf->review_form();
300        foreach ($rrows as $rrow)
301            if ($user->can_view_review($prow, $rrow)
302                && (!$need_id || $user->can_view_review_identity($prow, $rrow)))
303                $vrrows[] = $rf->unparse_review_json($prow, $rrow, $user);
304        if (!$vrrows && $rrows)
305            return new JsonResult(403, "Permission error.");
306        else
307            return new JsonResult(["ok" => true, "reviews" => $vrrows]);
308    }
309
310    static function reviewrating_api(Contact $user, Qrequest $qreq, PaperInfo $prow) {
311        if (!$qreq->r
312            || ($rrow = $prow->full_review_of_textual_id($qreq->r)) === false)
313            return new JsonResult(400, "Parameter error.");
314        else if (!$user->can_view_review($prow, $rrow))
315            return new JsonResult(403, "Permission error.");
316        else if (!$rrow)
317            return new JsonResult(404, "No such review.");
318        $editable = $user->can_rate_review($prow, $rrow);
319        if ($qreq->method() !== "GET") {
320            if (!isset($qreq->user_rating)
321                || ($rating = ReviewInfo::parse_rating($qreq->user_rating)) === false)
322                return new JsonResult(400, "Parameter error.");
323            else if (!$editable)
324                return new JsonResult(403, "Permission error.");
325            if ($rating === null)
326                $user->conf->qe("delete from ReviewRating where paperId=? and reviewId=? and contactId=?", $prow->paperId, $rrow->reviewId, $user->contactId);
327            else
328                $user->conf->qe("insert into ReviewRating set paperId=?, reviewId=?, contactId=?, rating=? on duplicate key update rating=values(rating)", $prow->paperId, $rrow->reviewId, $user->contactId, $rating);
329            $rrow = $prow->fresh_review_of_id($rrow->reviewId);
330        }
331        $rating = $rrow->rating_of_user($user);
332        $jr = new JsonResult(["ok" => true, "user_rating" => $rating]);
333        if ($editable)
334            $jr->content["editable"] = true;
335        if ($user->can_view_review_ratings($prow, $rrow))
336            $jr->content["ratings"] = array_values($rrow->ratings());
337        return $jr;
338    }
339
340    static function reviewround_api(Contact $user, $qreq, $prow) {
341        if (!$qreq->r
342            || ($rrow = $prow->full_review_of_textual_id($qreq->r)) === false)
343            return new JsonResult(400, "Parameter error.");
344        else if (!$user->can_administer($prow))
345            return new JsonResult(403, "Permission error.");
346        else if (!$rrow)
347            return new JsonResult(404, "No such review.");
348        $rname = trim((string) $qreq->round);
349        $round = $user->conf->sanitize_round_name($rname);
350        if ($round === false)
351            return ["ok" => false, "error" => Conf::round_name_error($rname)];
352        $rnum = (int) $user->conf->round_number($round, true);
353        $user->conf->qe("update PaperReview set reviewRound=? where paperId=? and reviewId=?", $rnum, $prow->paperId, $rrow->reviewId);
354        return ["ok" => true];
355    }
356}
357