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