1<?php 2// paperinfo.php -- HotCRP paper objects 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class PaperContactInfo { 6 public $paperId; 7 public $contactId; 8 public $conflictType = 0; 9 public $reviewType = 0; 10 public $reviewSubmitted = 0; 11 public $review_status = 0; 12 13 public $rights_forced = null; 14 public $forced_rights_link = null; 15 16 // set by Contact::rights() 17 public $allow_administer; 18 public $can_administer; 19 public $allow_pc_broad; 20 public $allow_pc; 21 public $potential_reviewer; 22 public $allow_review; 23 public $act_author; 24 public $allow_author; 25 public $view_conflict_type; 26 public $act_author_view; 27 public $allow_author_view; 28 public $nonblind; 29 30 public $vsreviews_array; 31 public $vsreviews_version; 32 33 static function make_empty(PaperInfo $prow, $cid) { 34 $ci = new PaperContactInfo; 35 $ci->paperId = $prow->paperId; 36 $ci->contactId = $cid; 37 if ($cid > 0 38 && isset($prow->leadContactId) 39 && $prow->leadContactId == $cid) 40 $ci->review_status = 1; 41 return $ci; 42 } 43 44 static function make_my(PaperInfo $prow, $contact, $object) { 45 $cid = is_object($contact) ? $contact->contactId : $contact; 46 $ci = PaperContactInfo::make_empty($prow, $cid); 47 $ci->conflictType = (int) $object->conflictType; 48 if (property_exists($object, "myReviewPermissions")) { 49 $ci->mark_my_review_permissions($object->myReviewPermissions); 50 } else if ($object instanceof PaperInfo 51 && property_exists($object, "reviewSignatures")) { 52 $rev_tokens = is_object($contact) ? $contact->review_tokens() : null; 53 foreach ($object->reviews_of_user($cid, $rev_tokens) as $rrow) 54 $ci->mark_review($rrow); 55 } 56 return $ci; 57 } 58 59 private function mark_conflict($ct) { 60 $this->conflictType = max($ct, $this->conflictType); 61 } 62 63 private function mark_review_type($rt, $rs, $rns) { 64 $this->reviewType = max($rt, $this->reviewType); 65 $this->reviewSubmitted = max($rs, $this->reviewSubmitted); 66 if ($rt > 0) { 67 if ($rs > 0 || $rns == 0) 68 $this->review_status = 1; 69 else if ($this->review_status == 0) 70 $this->review_status = -1; 71 } 72 } 73 74 function mark_review(ReviewInfo $rrow) { 75 $this->mark_review_type($rrow->reviewType, (int) $rrow->reviewSubmitted, $rrow->reviewNeedsSubmit); 76 } 77 78 private function mark_my_review_permissions($sig) { 79 if ((string) $sig !== "") { 80 foreach (explode(",", $sig) as $r) { 81 list($rt, $rs, $rns) = explode(" ", $r); 82 $this->mark_review_type((int) $rt, (int) $rs, (int) $rns); 83 } 84 } 85 } 86 87 static function load_into(PaperInfo $prow, $cid, $rev_tokens) { 88 global $Me; 89 $conf = $prow->conf; 90 $pid = $prow->paperId; 91 $q = "select conflictType, reviewType, reviewSubmitted, reviewNeedsSubmit"; 92 if ($cid 93 && !$rev_tokens 94 && ($row_set = $prow->_row_set) 95 && $row_set->size() > 1) { 96 $result = $conf->qe("$q, Paper.paperId paperId, ? contactId 97 from Paper 98 left join PaperConflict on (PaperConflict.paperId=Paper.paperId and PaperConflict.contactId=?) 99 left join PaperReview on (PaperReview.paperId=Paper.paperId and PaperReview.contactId=?) 100 where Paper.paperId?a", 101 $cid, $cid, $cid, $row_set->paper_ids()); 102 foreach ($row_set->all() as $row) 103 $row->_clear_contact_info($cid); 104 while ($result && ($local = $result->fetch_row())) { 105 $row = $row_set->get($local[4]); 106 $ci = $row->_get_contact_info($local[5]); 107 $ci->mark_conflict((int) $local[0]); 108 $ci->mark_review_type((int) $local[1], (int) $local[2], (int) $local[3]); 109 } 110 Dbl::free($result); 111 return; 112 } 113 if ($cid 114 && !$rev_tokens 115 && (!$Me || ($Me->contactId != $cid 116 && ($Me->privChair || $Me->contactId == $prow->managerContactId))) 117 && ($pcm = $conf->pc_members()) 118 && isset($pcm[$cid])) { 119 $cids = array_keys($pcm); 120 $result = $conf->qe("$q, ContactInfo.contactId 121 from ContactInfo 122 left join PaperConflict on (PaperConflict.paperId=? and PaperConflict.contactId=ContactInfo.contactId) 123 left join PaperReview on (PaperReview.paperId=? and PaperReview.contactId=ContactInfo.contactId) 124 where roles!=0 and (roles&" . Contact::ROLE_PC . ")!=0", 125 $pid, $pid); 126 } else { 127 $cids = [$cid]; 128 if ($cid) { 129 $q = "$q, ? contactId 130 from (select ? paperId) P 131 left join PaperConflict on (PaperConflict.paperId=? and PaperConflict.contactId=?) 132 left join PaperReview on (PaperReview.paperId=? and (PaperReview.contactId=?"; 133 $qv = [$cid, $pid, $pid, $cid, $pid, $cid]; 134 if ($rev_tokens) { 135 $q .= " or PaperReview.reviewToken?a"; 136 $qv[] = $rev_tokens; 137 } 138 $result = $conf->qe_apply("$q))", $qv); 139 } else 140 $result = null; 141 } 142 foreach ($cids as $cid) 143 $prow->_clear_contact_info($cid); 144 while ($result && ($local = $result->fetch_row())) { 145 $ci = $prow->_get_contact_info($local[4]); 146 $ci->mark_conflict((int) $local[0]); 147 $ci->mark_review_type((int) $local[1], (int) $local[2], (int) $local[3]); 148 } 149 Dbl::free($result); 150 } 151 152 function get_forced_rights() { 153 if (!$this->forced_rights_link) { 154 $ci = $this->forced_rights_link = clone $this; 155 $ci->vsreviews_array = null; 156 } 157 return $this->forced_rights_link; 158 } 159} 160 161class PaperInfo_Conflict { 162 public $contactId; 163 public $conflictType; 164 public $email; 165 166 function __construct($cid, $ctype, $email = null) { 167 $this->contactId = (int) $cid; 168 $this->conflictType = (int) $ctype; 169 $this->email = $email; 170 } 171} 172 173class PaperInfoSet implements IteratorAggregate { 174 private $prows = []; 175 private $by_pid = []; 176 public $loaded_allprefs = 0; 177 function __construct(PaperInfo $prow = null) { 178 if ($prow) 179 $this->add($prow, true); 180 } 181 function add(PaperInfo $prow, $copy = false) { 182 $this->prows[] = $prow; 183 if (!isset($this->by_pid[$prow->paperId])) 184 $this->by_pid[$prow->paperId] = $prow; 185 if (!$copy) { 186 assert(!$prow->_row_set); 187 $prow->_row_set = $this; 188 } 189 } 190 function take_all(PaperInfoSet $set) { 191 foreach ($set->prows as $prow) { 192 $prow->_row_set = null; 193 $this->add($prow); 194 } 195 $set->prows = $set->by_pid = []; 196 } 197 function all() { 198 return $this->prows; 199 } 200 function size() { 201 return count($this->prows); 202 } 203 function is_empty() { 204 return empty($this->prows); 205 } 206 function paper_ids() { 207 return array_keys($this->by_pid); 208 } 209 function get($pid) { 210 return get($this->by_pid, $pid); 211 } 212 function filter($func) { 213 $next_set = new PaperInfoSet; 214 foreach ($this as $prow) 215 if (call_user_func($func, $prow)) 216 $next_set->add($prow, true); 217 return $next_set; 218 } 219 function any($func) { 220 foreach ($this as $prow) 221 if (($x = call_user_func($func, $prow))) 222 return $x; 223 return false; 224 } 225 function getIterator() { 226 return new ArrayIterator($this->prows); 227 } 228} 229 230class PaperInfo { 231 public $paperId; 232 public $conf; 233 public $title; 234 public $authorInformation; 235 public $abstract; 236 public $collaborators; 237 public $timeSubmitted; 238 public $timeWithdrawn; 239 public $paperStorageId; 240 public $finalPaperStorageId; 241 public $managerContactId; 242 public $paperFormat; 243 public $outcome; 244 // $paperTags: DO NOT LIST (property_exists() is meaningful) 245 // $optionIds: DO NOT LIST (property_exists() is meaningful) 246 // $allConflictTypes: DO NOT LIST (property_exists() is meaningful) 247 // $reviewSignatures: DO NOT LIST (property_exists() is meaningful) 248 249 private $_unaccented_title; 250 private $_contact_info = []; 251 private $_rights_version = 0; 252 private $_author_array; 253 private $_collaborator_array; 254 private $_prefs_array; 255 private $_prefs_cid; 256 private $_desirability; 257 private $_topics_array; 258 private $_topic_interest_score_array; 259 private $_option_values; 260 private $_option_data; 261 private $_option_array; 262 private $_all_option_array; 263 private $_document_array; 264 private $_conflict_array; 265 private $_conflict_array_email; 266 private $_review_array; 267 private $_review_array_version = 0; 268 private $_reviews_have = []; 269 private $_full_review; 270 private $_full_review_key; 271 private $_comment_array; 272 private $_comment_skeleton_array; 273 private $_potential_conflicts; 274 public $_row_set; 275 276 const SUBMITTED_AT_FOR_WITHDRAWN = 1000000000; 277 278 function __construct($p = null, $contact = null, Conf $conf = null) { 279 $this->merge($p, $contact, $conf); 280 } 281 282 private function merge($p, $contact, $conf) { 283 assert($contact === null ? $conf !== null : $contact instanceof Contact); 284 $this->conf = $contact ? $contact->conf : $conf; 285 if ($p) 286 foreach ($p as $k => $v) 287 $this->$k = $v; 288 $this->paperId = (int) $this->paperId; 289 $this->managerContactId = (int) $this->managerContactId; 290 if ($contact && (property_exists($this, "myReviewPermissions") 291 || property_exists($this, "reviewSignatures"))) { 292 $this->_rights_version = Contact::$rights_version; 293 $this->load_my_contact_info($contact, $this); 294 } else if ($contact && property_exists($this, "conflictType")) { 295 error_log("conflictType exists but myReviewPermissions does not " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))); 296 } 297 foreach (["paperTags", "optionIds"] as $k) 298 if (property_exists($this, $k) && $this->$k === null) 299 $this->$k = ""; 300 } 301 302 static function fetch($result, $contact, Conf $conf = null) { 303 $prow = $result ? $result->fetch_object("PaperInfo", [null, $contact, $conf]) : null; 304 if ($prow && !is_int($prow->paperId)) 305 $prow->merge(null, $contact, $conf); 306 return $prow; 307 } 308 309 static function table_name() { 310 return "Paper"; 311 } 312 313 static function id_column() { 314 return "paperId"; 315 } 316 317 static function comment_table_name() { 318 return "PaperComment"; 319 } 320 321 static function my_review_permissions_sql($prefix = "") { 322 return "group_concat({$prefix}reviewType, ' ', coalesce({$prefix}reviewSubmitted,0), ' ', reviewNeedsSubmit)"; 323 } 324 325 function make_whynot($rest = []) { 326 return ["fail" => true, "paperId" => $this->paperId, "conf" => $this->conf] + $rest; 327 } 328 329 330 static private function contact_to_cid($contact) { 331 global $Me; 332 assert($contact !== null); 333 if ($contact && is_object($contact)) 334 return $contact->contactId; 335 else 336 return $contact !== null ? $contact : $Me->contactId; 337 } 338 339 function _get_contact_info($cid) { 340 return get($this->_contact_info, $cid); 341 } 342 343 function _clear_contact_info($cid) { 344 $this->_contact_info[$cid] = PaperContactInfo::make_empty($this, $cid); 345 } 346 347 private function update_rights_version() { 348 if ($this->_rights_version !== Contact::$rights_version) { 349 if ($this->_rights_version) { 350 $this->_contact_info = $this->_reviews_have = []; 351 $this->_review_array = $this->_conflict_array = null; 352 ++$this->_review_array_version; 353 unset($this->reviewSignatures, $this->allConflictType); 354 } 355 $this->_rights_version = Contact::$rights_version; 356 } 357 } 358 359 function contact_info($contact) { 360 global $Me; 361 $this->update_rights_version(); 362 $rev_tokens = null; 363 if (!$contact || !is_object($contact)) 364 error_log("PaperInfo::contact_info bad argument: " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))); 365 if (!$contact || is_object($contact)) { 366 $contact = $contact ? : $Me; 367 $rev_tokens = $contact->review_tokens(); 368 } 369 $cid = self::contact_to_cid($contact); 370 if (!array_key_exists($cid, $this->_contact_info)) { 371 if ($this->_review_array 372 || property_exists($this, "reviewSignatures")) { 373 $ci = PaperContactInfo::make_empty($this, $cid); 374 if (($c = get($this->conflicts(), $cid))) 375 $ci->conflictType = $c->conflictType; 376 foreach ($this->reviews_of_user($cid, $rev_tokens) as $rrow) 377 $ci->mark_review($rrow); 378 $this->_contact_info[$cid] = $ci; 379 } else 380 PaperContactInfo::load_into($this, $cid, $rev_tokens); 381 } 382 return $this->_contact_info[$cid]; 383 } 384 385 function replace_contact_info_map($cimap) { 386 $old_cimap = $this->_contact_info; 387 $this->_contact_info = $cimap; 388 $this->_rights_version = Contact::$rights_version; 389 return $old_cimap; 390 } 391 392 function load_my_contact_info($contact, $object) { 393 $ci = PaperContactInfo::make_my($this, $contact, $object); 394 $this->_contact_info[$ci->contactId] = $ci; 395 } 396 397 398 function unaccented_title() { 399 if ($this->_unaccented_title === null) 400 $this->_unaccented_title = UnicodeHelper::deaccent($this->title); 401 return $this->_unaccented_title; 402 } 403 404 function pretty_text_title_indent($width = 75) { 405 $n = "Paper #{$this->paperId}: "; 406 $vistitle = $this->unaccented_title(); 407 $l = (int) (($width + 0.5 - strlen($vistitle) - strlen($n)) / 2); 408 return strlen($n) + max(0, $l); 409 } 410 411 function pretty_text_title($width = 75) { 412 $l = $this->pretty_text_title_indent($width); 413 return prefix_word_wrap("Paper #{$this->paperId}: ", $this->title, $l); 414 } 415 416 function format_of($text, $check_simple = false) { 417 return $this->conf->check_format($this->paperFormat, $check_simple ? $text : null); 418 } 419 420 function title_format() { 421 return $this->format_of($this->title, true); 422 } 423 424 function abstract_format() { 425 return $this->format_of($this->abstract, true); 426 } 427 428 function edit_format() { 429 return $this->conf->format_info($this->paperFormat); 430 } 431 432 function author_list() { 433 if (!isset($this->_author_array)) { 434 $this->_author_array = array(); 435 foreach (explode("\n", $this->authorInformation) as $line) 436 if ($line != "") 437 $this->_author_array[] = Author::make_tabbed($line); 438 } 439 return $this->_author_array; 440 } 441 442 function author_by_email($email) { 443 foreach ($this->author_list() as $a) 444 if (strcasecmp($a->email, $email) == 0 && (string) $email !== "") 445 return $a; 446 return null; 447 } 448 449 function parse_author_list() { 450 $ai = ""; 451 foreach ($this->_author_array as $au) 452 $ai .= $au->firstName . "\t" . $au->lastName . "\t" . $au->email . "\t" . $au->affiliation . "\n"; 453 return ($this->authorInformation = $ai); 454 } 455 456 function pretty_text_author_list() { 457 $info = ""; 458 foreach ($this->author_list() as $au) { 459 $info .= $au->name() ? : $au->email; 460 if ($au->affiliation) 461 $info .= " (" . $au->affiliation . ")"; 462 $info .= "\n"; 463 } 464 return $info; 465 } 466 467 function conflict_type($contact) { 468 $cid = self::contact_to_cid($contact); 469 if (array_key_exists($cid, $this->_contact_info)) 470 return $this->_contact_info[$cid]->conflictType; 471 else if (($ci = get($this->conflicts(), $cid))) 472 return $ci->conflictType; 473 else 474 return 0; 475 } 476 477 function has_conflict($contact) { 478 return $this->conflict_type($contact) > 0; 479 } 480 481 function has_author($contact) { 482 return $this->conflict_type($contact) >= CONFLICT_AUTHOR; 483 } 484 485 function collaborator_list() { 486 if ($this->_collaborator_array === null) { 487 $this->_collaborator_array = []; 488 foreach (explode("\n", (string) $this->collaborators) as $co) 489 if (($m = AuthorMatcher::make_collaborator_line($co))) 490 $this->_collaborator_array[] = $m; 491 } 492 return $this->_collaborator_array; 493 } 494 495 function potential_conflict_callback(Contact $user, $callback) { 496 $nproblems = $auproblems = 0; 497 if ($this->field_match_pregexes($user->aucollab_general_pregexes(), "authorInformation")) { 498 foreach ($this->author_list() as $n => $au) 499 foreach ($user->aucollab_matchers() as $matcher) { 500 if (($why = $matcher->test($au, $matcher->nonauthor))) { 501 if (!$callback) 502 return true; 503 $auproblems |= $why; 504 ++$nproblems; 505 call_user_func($callback, $user, $matcher, $au, $n + 1, $why); 506 } 507 } 508 } 509 if ((string) $this->collaborators !== "") { 510 $aum = $user->full_matcher(); 511 if (Text::match_pregexes($aum->general_pregexes(), $this->collaborators, UnicodeHelper::deaccent($this->collaborators))) { 512 foreach ($this->collaborator_list() as $co) 513 if (($co->lastName !== "" 514 || !($auproblems & AuthorMatcher::MATCH_AFFILIATION)) 515 && ($why = $aum->test($co, true))) { 516 if (!$callback) 517 return true; 518 ++$nproblems; 519 call_user_func($callback, $user, $aum, $co, 0, $why); 520 } 521 } 522 } 523 return $nproblems > 0; 524 } 525 526 function potential_conflict(Contact $user) { 527 return $this->potential_conflict_callback($user, null); 528 } 529 530 function _potential_conflict_html_callback($user, $matcher, $conflict, $aunum, $why) { 531 if ($aunum) { 532 if ($matcher->nonauthor) { 533 $aumatcher = new AuthorMatcher($conflict); 534 $what = "PC collaborator " . $aumatcher->highlight($matcher) . "<br>matches author #$aunum " . $matcher->highlight($conflict); 535 } else if ($why == AuthorMatcher::MATCH_AFFILIATION) 536 $what = "PC affiliation matches author #$aunum affiliation " . $matcher->highlight($conflict->affiliation); 537 else 538 $what = "PC name matches author #$aunum name " . $matcher->highlight($conflict->name()); 539 $this->_potential_conflicts[] = ["#$aunum", '<div class="mmm">' . $what . '</div>']; 540 } else { 541 if ($why == AuthorMatcher::MATCH_AFFILIATION) 542 $what = "PC affiliation matches paper collaborator "; 543 else 544 $what = "PC name matches paper collaborator "; 545 $this->_potential_conflicts[] = ["other conflicts", '<div class="mmm">' . $what . $matcher->highlight($conflict) . '</div>']; 546 } 547 } 548 549 function potential_conflict_html(Contact $user, $highlight = false) { 550 $this->_potential_conflicts = []; 551 if (!$this->potential_conflict_callback($user, [$this, "_potential_conflict_html_callback"])) 552 return false; 553 usort($this->_potential_conflicts, function ($a, $b) { return strnatcmp($a[0], $b[0]); }); 554 $authors = array_unique(array_map(function ($x) { return $x[0]; }, $this->_potential_conflicts)); 555 $authors = array_filter($authors, function ($f) { return $f !== "other conflicts"; }); 556 $messages = join("", array_map(function ($x) { return $x[1]; }, $this->_potential_conflicts)); 557 $this->_potential_conflicts = null; 558 return ['<div class="pcconfmatch' 559 . ($highlight ? " pcconfmatch-highlight" : "") 560 . '">Possible conflict' 561 . (empty($authors) ? "" : " with " . pluralx($authors, "author") . " " . numrangejoin($authors)) 562 . '…</div>', $messages]; 563 } 564 565 function field_match_pregexes($reg, $field) { 566 $data = $this->$field; 567 $field_deaccent = $field . "_deaccent"; 568 if (!isset($this->$field_deaccent)) { 569 if (preg_match('/[\x80-\xFF]/', $data)) 570 $this->$field_deaccent = UnicodeHelper::deaccent($data); 571 else 572 $this->$field_deaccent = false; 573 } 574 return Text::match_pregexes($reg, $data, $this->$field_deaccent); 575 } 576 577 function submitted_at() { 578 if ($this->timeSubmitted > 0) 579 return (int) $this->timeSubmitted; 580 if ($this->timeWithdrawn > 0) { 581 if ($this->timeSubmitted == -100) 582 return self::SUBMITTED_AT_FOR_WITHDRAWN; 583 if ($this->timeSubmitted < -100) 584 return -(int) $this->timeSubmitted; 585 } 586 return 0; 587 } 588 589 function can_author_view_decision() { 590 return $this->conf->can_all_author_view_decision(); 591 } 592 593 function review_type($contact) { 594 $this->update_rights_version(); 595 $cid = self::contact_to_cid($contact); 596 if (array_key_exists($cid, $this->_contact_info)) 597 $rrow = $this->_contact_info[$cid]; 598 else 599 $rrow = $this->review_of_user($cid); 600 return $rrow ? $rrow->reviewType : 0; 601 } 602 603 function has_reviewer($contact) { 604 return $this->review_type($contact) > 0; 605 } 606 607 function review_not_incomplete($contact) { 608 $ci = $this->contact_info($contact); 609 return $ci && $ci->review_status > 0; 610 } 611 612 function review_submitted($contact) { 613 $ci = $this->contact_info($contact); 614 return $ci && $ci->reviewType > 0 && $ci->reviewSubmitted > 0; 615 } 616 617 function pc_can_become_reviewer() { 618 if (!$this->conf->check_track_review_sensitivity()) 619 return $this->conf->pc_members(); 620 else { 621 $pcm = array(); 622 foreach ($this->conf->pc_members() as $cid => $pc) 623 if ($pc->can_become_reviewer_ignore_conflict($this)) 624 $pcm[$cid] = $pc; 625 return $pcm; 626 } 627 } 628 629 function load_tags() { 630 $result = $this->conf->qe("select group_concat(' ', tag, '#', tagIndex order by tag separator '') from PaperTag where paperId=? group by paperId", $this->paperId); 631 $this->paperTags = ""; 632 if (($row = edb_row($result)) && $row[0] !== null) 633 $this->paperTags = $row[0]; 634 Dbl::free($result); 635 } 636 637 function has_tag($tag) { 638 if (!property_exists($this, "paperTags")) 639 $this->load_tags(); 640 return $this->paperTags !== "" 641 && stripos($this->paperTags, " $tag#") !== false; 642 } 643 644 function has_any_tag($tags) { 645 if (!property_exists($this, "paperTags")) 646 $this->load_tags(); 647 foreach ($tags as $tag) 648 if (stripos($this->paperTags, " $tag#") !== false) 649 return true; 650 return false; 651 } 652 653 function has_viewable_tag($tag, Contact $user) { 654 $tags = $this->viewable_tags($user); 655 return $tags !== "" && stripos(" " . $tags, " $tag#") !== false; 656 } 657 658 function tag_value($tag) { 659 if (!property_exists($this, "paperTags")) 660 $this->load_tags(); 661 if ($this->paperTags !== "" 662 && ($pos = stripos($this->paperTags, " $tag#")) !== false) 663 return (float) substr($this->paperTags, $pos + strlen($tag) + 2); 664 else 665 return false; 666 } 667 668 function all_tags_text() { 669 if (!property_exists($this, "paperTags")) 670 $this->load_tags(); 671 return $this->paperTags; 672 } 673 674 function searchable_tags(Contact $user) { 675 if ($user->allow_administer($this)) 676 return $this->all_tags_text(); 677 else 678 return $this->viewable_tags($user); 679 } 680 681 function viewable_tags(Contact $user) { 682 // see also Contact::can_view_tag() 683 $tags = ""; 684 if ($user->isPC) 685 $tags = (string) $this->all_tags_text(); 686 if ($tags !== "") { 687 $dt = $this->conf->tags(); 688 if ($user->can_view_most_tags($this)) 689 $tags = $dt->strip_nonviewable($tags, $user, $this); 690 else if ($dt->has_sitewide && $user->can_view_tags($this)) 691 $tags = Tagger::strip_nonsitewide($tags, $user); 692 else 693 $tags = ""; 694 } 695 return $tags; 696 } 697 698 function editable_tags(Contact $user) { 699 $tags = $this->all_tags_text(); 700 if ($tags !== "") { 701 $old_overrides = $user->add_overrides(Contact::OVERRIDE_CONFLICT); 702 $tags = $this->viewable_tags($user); 703 if ($tags !== "") { 704 $etags = []; 705 foreach (explode(" ", $tags) as $tag) 706 if ($tag !== "" && $user->can_change_tag($this, $tag, 0, 1)) 707 $etags[] = $tag; 708 $tags = join(" ", $etags); 709 } 710 $user->set_overrides($old_overrides); 711 } 712 return $tags; 713 } 714 715 function add_tag_info_json($pj, Contact $user) { 716 $tagger = new Tagger($user); 717 if (($can_override = $user->can_meaningfully_override($this))) 718 $overrides = $user->add_overrides(Contact::OVERRIDE_CONFLICT); 719 $editable = $this->editable_tags($user); 720 $viewable = $this->viewable_tags($user); 721 $pj->tags = TagInfo::split($viewable); 722 $pj->tags_edit_text = $tagger->unparse($editable); 723 $pj->tags_view_html = $tagger->unparse_and_link($viewable); 724 if (($decor = $tagger->unparse_decoration_html($viewable))) 725 $pj->tag_decoration_html = $decor; 726 $tagmap = $this->conf->tags(); 727 $pj->color_classes = $tagmap->color_classes($viewable); 728 if ($can_override && $viewable) { 729 $user->remove_overrides(Contact::OVERRIDE_CONFLICT); 730 $viewable_c = $this->viewable_tags($user); 731 if ($viewable_c !== $viewable) { 732 $pj->tags_conflicted = TagInfo::split($viewable_c); 733 if ($decor 734 && ($decor_c = $tagger->unparse_decoration_html($viewable_c)) !== $decor) 735 $pj->tag_decoration_html_conflicted = $decor_c; 736 if ($pj->color_classes 737 && ($cc_c = $tagmap->color_classes($viewable_c)) !== $pj->color_classes) 738 $pj->color_classes_conflicted = $cc_c; 739 } 740 } 741 if ($can_override) 742 $user->set_overrides($overrides); 743 } 744 745 private function load_topics() { 746 $row_set = $this->_row_set ? : new PaperInfoSet($this); 747 foreach ($row_set as $prow) 748 $prow->topicIds = null; 749 if ($this->conf->has_topics()) { 750 $result = $this->conf->qe("select paperId, group_concat(topicId) from PaperTopic where paperId?a group by paperId", $row_set->paper_ids()); 751 while ($result && ($row = $result->fetch_row())) { 752 $prow = $row_set->get($row[0]); 753 $prow->topicIds = (string) $row[1]; 754 } 755 Dbl::free($result); 756 } 757 } 758 759 function has_topics() { 760 if (!property_exists($this, "topicIds")) 761 $this->load_topics(); 762 return $this->topicIds !== null && $this->topicIds !== ""; 763 } 764 765 function topic_list() { 766 if ($this->_topics_array === null) { 767 if (!property_exists($this, "topicIds")) 768 $this->load_topics(); 769 $this->_topics_array = []; 770 if ($this->topicIds !== null && $this->topicIds !== "") { 771 foreach (explode(",", $this->topicIds) as $t) 772 $this->_topics_array[] = (int) $t; 773 $tomap = $this->conf->topic_order_map(); 774 usort($this->_topics_array, function ($a, $b) use ($tomap) { 775 return $tomap[$a] - $tomap[$b]; 776 }); 777 } 778 } 779 return $this->_topics_array; 780 } 781 782 function topic_map() { 783 return array_fill_keys($this->topic_list(), true); 784 } 785 786 function named_topic_map() { 787 $t = []; 788 foreach ($this->topic_list() as $tid) { 789 if (empty($t)) 790 $tmap = $this->conf->topic_map(); 791 $t[$tid] = $tmap[$tid]; 792 } 793 return $t; 794 } 795 796 function unparse_topics_text() { 797 return join("; ", $this->named_topic_map()); 798 } 799 800 private static function render_topic($tname, $i, &$long) { 801 $s = '<span class="topicsp topic' . ($i ? : 0); 802 if (strlen($tname) <= 50) 803 $s .= ' nw'; 804 else 805 $long = true; 806 return $s . '">' . htmlspecialchars($tname) . '</span>'; 807 } 808 809 static function unparse_topic_list_html(Conf $conf, $ti) { 810 if (!$ti) 811 return ""; 812 $out = array(); 813 $tmap = $conf->topic_map(); 814 $tomap = $conf->topic_order_map(); 815 $long = false; 816 foreach ($ti as $t => $i) 817 $out[$tomap[$t]] = self::render_topic($tmap[$t], $i, $long); 818 ksort($out); 819 return join($conf->topic_separator(), $out); 820 } 821 822 private static $topic_interest_values = [-0.7071, -0.5, 0, 0.7071, 1]; 823 824 function topic_interest_score($contact) { 825 $score = 0; 826 if (is_int($contact)) 827 $contact = get($this->conf->pc_members(), $contact); 828 if ($contact) { 829 if ($this->_topic_interest_score_array === null) 830 $this->_topic_interest_score_array = array(); 831 if (isset($this->_topic_interest_score_array[$contact->contactId])) 832 $score = $this->_topic_interest_score_array[$contact->contactId]; 833 else { 834 $interests = $contact->topic_interest_map(); 835 $topics = $this->topic_list(); 836 foreach ($topics as $t) 837 if (($j = get($interests, $t, 0))) { 838 if ($j >= -2 && $j <= 2) 839 $score += self::$topic_interest_values[$j + 2]; 840 else if ($j > 2) 841 $score += sqrt($j / 2); 842 else 843 $score += -sqrt(-$j / 4); 844 } 845 if ($score) 846 // * Strong interest in the paper's single topic gets 847 // score 10. 848 $score = (int) ($score / sqrt(count($topics)) * 10 + 0.5); 849 $this->_topic_interest_score_array[$contact->contactId] = $score; 850 } 851 } 852 return $score; 853 } 854 855 856 function load_conflicts($email) { 857 if (!$email && isset($this->allConflictType)) { 858 $this->_conflict_array = []; 859 $this->_conflict_array_email = $email; 860 foreach (explode(",", $this->allConflictType) as $x) { 861 list($cid, $ctype) = explode(" ", $x); 862 $cflt = new PaperInfo_Conflict($cid, $ctype); 863 $this->_conflict_array[$cflt->contactId] = $cflt; 864 } 865 } else { 866 $row_set = $this->_row_set ? : new PaperInfoSet($this); 867 foreach ($row_set->all() as $prow) { 868 $prow->_conflict_array = []; 869 $prow->_conflict_array_email = $email; 870 } 871 if ($email) 872 $result = $this->conf->qe("select paperId, PaperConflict.contactId, conflictType, email from PaperConflict join ContactInfo using (contactId) where paperId?a", $row_set->paper_ids()); 873 else 874 $result = $this->conf->qe("select paperId, contactId, conflictType, null from PaperConflict where paperId?a", $row_set->paper_ids()); 875 while ($result && ($row = $result->fetch_row())) { 876 $prow = $row_set->get($row[0]); 877 $cflt = new PaperInfo_Conflict($row[1], $row[2], $row[3]); 878 $prow->_conflict_array[$cflt->contactId] = $cflt; 879 } 880 Dbl::free($result); 881 } 882 } 883 884 function conflicts($email = false) { 885 if ($this->_conflict_array === null 886 || ($email && !$this->_conflict_array_email)) 887 $this->load_conflicts($email); 888 return $this->_conflict_array; 889 } 890 891 function pc_conflicts($email = false) { 892 return array_intersect_key($this->conflicts($email), $this->conf->pc_members()); 893 } 894 895 function contacts($email = false) { 896 $c = array(); 897 foreach ($this->conflicts($email) as $id => $cflt) 898 if ($cflt->conflictType >= CONFLICT_AUTHOR) 899 $c[$id] = $cflt; 900 return $c; 901 } 902 903 function named_contacts() { 904 $vals = Dbl::fetch_objects($this->conf->qe("select ContactInfo.contactId, conflictType, email, firstName, lastName, affiliation from PaperConflict join ContactInfo using (contactId) where paperId=$this->paperId and conflictType>=" . CONFLICT_AUTHOR)); 905 foreach ($vals as $v) { 906 $v->contactId = (int) $v->contactId; 907 $v->conflictType = (int) $v->conflictType; 908 } 909 return $vals; 910 } 911 912 function load_reviewer_preferences() { 913 if ($this->_row_set && ++$this->_row_set->loaded_allprefs >= 10) 914 $row_set = $this->_row_set->filter(function ($prow) { 915 return !property_exists($prow, "allReviewerPreference"); 916 }); 917 else 918 $row_set = new PaperInfoSet($this); 919 foreach ($row_set as $prow) { 920 $prow->allReviewerPreference = null; 921 $prow->_prefs_array = $prow->_prefs_cid = $prow->_desirability = null; 922 } 923 $result = $this->conf->qe("select paperId, " . $this->conf->query_all_reviewer_preference() . " from PaperReviewPreference where paperId?a group by paperId", $row_set->paper_ids()); 924 while ($result && ($row = $result->fetch_row())) { 925 $prow = $row_set->get($row[0]); 926 $prow->allReviewerPreference = $row[1]; 927 } 928 Dbl::free($result); 929 } 930 931 function reviewer_preferences() { 932 if (!property_exists($this, "allReviewerPreference")) 933 $this->load_reviewer_preferences(); 934 if ($this->_prefs_array === null) { 935 $x = array(); 936 if ($this->allReviewerPreference !== null && $this->allReviewerPreference !== "") { 937 $p = preg_split('/[ ,]/', $this->allReviewerPreference); 938 for ($i = 0; $i + 2 < count($p); $i += 3) { 939 if ($p[$i+1] != "0" || $p[$i+2] != ".") 940 $x[(int) $p[$i]] = array((int) $p[$i+1], $p[$i+2] == "." ? null : (int) $p[$i+2]); 941 } 942 } 943 $this->_prefs_array = $x; 944 } 945 return $this->_prefs_array; 946 } 947 948 function reviewer_preference($contact, $include_topic_score = false) { 949 $cid = is_int($contact) ? $contact : $contact->contactId; 950 if ($this->_prefs_cid === null && $this->_prefs_array === null) { 951 $row_set = $this->_row_set ? : new PaperInfoSet($this); 952 foreach ($row_set as $prow) 953 $prow->_prefs_cid = [$cid, null]; 954 $result = $this->conf->qe("select paperId, preference, expertise from PaperReviewPreference where paperId?a and contactId=?", $row_set->paper_ids(), $cid); 955 while ($result && ($row = $result->fetch_row())) { 956 $prow = $row_set->get($row[0]); 957 $prow->_prefs_cid[1] = [(int) $row[1], $row[2] === null ? null : (int) $row[2]]; 958 } 959 Dbl::free($result); 960 } 961 if ($this->_prefs_cid !== null && $this->_prefs_cid[0] == $cid) 962 $pref = $this->_prefs_cid[1]; 963 else 964 $pref = get($this->reviewer_preferences(), $cid); 965 $pref = $pref ? : [0, null]; 966 if ($include_topic_score) 967 $pref[] = $this->topic_interest_score($contact); 968 return $pref; 969 } 970 971 function desirability() { 972 if ($this->_desirability === null) { 973 $prefs = $this->reviewer_preferences(); 974 $this->_desirability = 0; 975 foreach ($prefs as $pf) { 976 if ($pf[0] > 0) 977 $this->_desirability += 1; 978 else if ($pf[0] > -100 && $pf[0] < 0) 979 $this->_desirability -= 1; 980 } 981 } 982 return $this->_desirability; 983 } 984 985 private function load_options($only_me, $need_data) { 986 if ($this->_option_values === null 987 && isset($this->optionIds) 988 && (!$need_data || $this->optionIds === "")) { 989 if ($this->optionIds === "") 990 $this->_option_values = $this->_option_data = []; 991 else { 992 $this->_option_values = []; 993 preg_match_all('/(\d+)#(-?\d+)/', $this->optionIds, $m); 994 for ($i = 0; $i < count($m[1]); ++$i) 995 $this->_option_values[(int) $m[1][$i]][] = (int) $m[2][$i]; 996 } 997 } else if ($this->_option_values === null 998 || ($need_data && $this->_option_data === null)) { 999 $old_row_set = $this->_row_set; 1000 if ($only_me) 1001 $this->_row_set = null; 1002 $row_set = $this->_row_set ? : new PaperInfoSet($this); 1003 foreach ($row_set->all() as $prow) 1004 $prow->_option_values = $prow->_option_data = []; 1005 $result = $this->conf->qe("select paperId, optionId, value, data, dataOverflow from PaperOption where paperId?a order by paperId", $row_set->paper_ids()); 1006 while ($result && ($row = $result->fetch_row())) { 1007 $prow = $row_set->get((int) $row[0]); 1008 $prow->_option_values[(int) $row[1]][] = (int) $row[2]; 1009 $prow->_option_data[(int) $row[1]][] = $row[3] !== null ? $row[3] : $row[4]; 1010 } 1011 Dbl::free($result); 1012 if ($only_me) 1013 $this->_row_set = $old_row_set; 1014 } 1015 } 1016 1017 private function _make_option_array($all) { 1018 $this->load_options(false, false); 1019 $paper_opts = $this->conf->paper_opts; 1020 $option_array = []; 1021 foreach ($this->_option_values as $oid => $ovalues) 1022 if (($o = $paper_opts->get($oid, $all))) 1023 $option_array[$oid] = new PaperOptionValue($this, $o, $ovalues, get($this->_option_data, $oid)); 1024 uasort($option_array, function ($a, $b) { 1025 if ($a->option && $b->option) 1026 return PaperOption::compare($a->option, $b->option); 1027 else if ($a->option || $b->option) 1028 return $a->option ? -1 : 1; 1029 else 1030 return $a->id - $b->id; 1031 }); 1032 return $option_array; 1033 } 1034 1035 function option_value_data($id) { 1036 if ($this->_option_data === null) 1037 $this->load_options(false, true); 1038 return [get($this->_option_values, $id, []), 1039 get($this->_option_data, $id, [])]; 1040 } 1041 1042 function options() { 1043 if ($this->_option_array === null) 1044 $this->_option_array = $this->_make_option_array(false); 1045 return $this->_option_array; 1046 } 1047 1048 function option($id) { 1049 return get($this->options(), $id); 1050 } 1051 1052 function force_option($id) { 1053 if (($oa = get($this->options(), $id))) 1054 return $oa; 1055 else if (($opt = $this->conf->paper_opts->get($id))) 1056 return new PaperOptionValue($this, $opt); 1057 else 1058 return null; 1059 } 1060 1061 function all_options() { 1062 if ($this->_all_option_array === null) 1063 $this->_all_option_array = $this->_make_option_array(true); 1064 return $this->_all_option_array; 1065 } 1066 1067 function all_option($id) { 1068 return get($this->all_options(), $id); 1069 } 1070 1071 function invalidate_options($reload = false) { 1072 unset($this->optionIds); 1073 $this->_option_array = $this->_all_option_array = 1074 $this->_option_values = $this->_option_data = null; 1075 if ($reload) 1076 $this->load_options(true, true); 1077 } 1078 1079 private function _document_sql() { 1080 return "paperId, paperStorageId, timestamp, mimetype, sha1, documentType, filename, infoJson, size, filterType, originalStorageId, inactive"; 1081 } 1082 1083 function document($dtype, $did = 0, $full = false) { 1084 if ($did <= 0) { 1085 if ($dtype == DTYPE_SUBMISSION) 1086 $did = $this->paperStorageId; 1087 else if ($dtype == DTYPE_FINAL) 1088 $did = $this->finalPaperStorageId; 1089 else if (($oa = $this->force_option($dtype)) 1090 && $oa->option->is_document()) 1091 return $oa->document(0); 1092 } 1093 1094 if ($did <= 1) 1095 return null; 1096 1097 if ($this->_document_array !== null 1098 && array_key_exists($did, $this->_document_array)) 1099 return $this->_document_array[$did]; 1100 1101 if ((($dtype == DTYPE_SUBMISSION 1102 && $did == $this->paperStorageId 1103 && $this->finalPaperStorageId <= 0) 1104 || ($dtype == DTYPE_FINAL 1105 && $did == $this->finalPaperStorageId)) 1106 && !$full) { 1107 $infoJson = get($this, $dtype == DTYPE_SUBMISSION ? "paper_infoJson" : "final_infoJson"); 1108 return new DocumentInfo(["paperStorageId" => $did, "paperId" => $this->paperId, "documentType" => $dtype, "timestamp" => get($this, "timestamp"), "mimetype" => $this->mimetype, "sha1" => $this->sha1, "size" => get($this, "size"), "infoJson" => $infoJson, "is_partial" => true], $this->conf, $this); 1109 } 1110 1111 if ($this->_document_array === null) { 1112 $result = $this->conf->qe("select " . $this->_document_sql() . " from PaperStorage where paperId=? and inactive=0", $this->paperId); 1113 $this->_document_array = []; 1114 while (($di = DocumentInfo::fetch($result, $this->conf, $this))) 1115 $this->_document_array[$di->paperStorageId] = $di; 1116 Dbl::free($result); 1117 } 1118 if (!array_key_exists($did, $this->_document_array)) { 1119 $result = $this->conf->qe("select " . $this->_document_sql() . " from PaperStorage where paperStorageId=?", $did); 1120 $this->_document_array[$did] = DocumentInfo::fetch($result, $this->conf, $this); 1121 Dbl::free($result); 1122 } 1123 return $this->_document_array[$did]; 1124 } 1125 function joindoc() { 1126 return $this->document($this->finalPaperStorageId > 0 ? DTYPE_FINAL : DTYPE_SUBMISSION); 1127 } 1128 function is_joindoc(DocumentInfo $doc) { 1129 return $doc->paperStorageId > 1 1130 && (($doc->paperStorageId == $this->paperStorageId 1131 && $this->finalPaperStorageId <= 0 1132 && $doc->documentType == DTYPE_SUBMISSION) 1133 || ($doc->paperStorageId == $this->finalPaperStorageId 1134 && $doc->documentType == DTYPE_FINAL)); 1135 } 1136 function documents($dtype) { 1137 if ($dtype <= 0) { 1138 $doc = $this->document($dtype, 0, true); 1139 return $doc ? [$doc] : []; 1140 } else if (($oa = $this->option($dtype)) && $oa->has_document()) 1141 return $oa->documents(); 1142 else 1143 return []; 1144 } 1145 function mark_inactive_documents() { 1146 $dids = []; 1147 if ($this->paperStorageId > 1) 1148 $dids[] = $this->paperStorageId; 1149 if ($this->finalPaperStorageId > 1) 1150 $dids[] = $this->finalPaperStorageId; 1151 foreach ($this->options() as $oa) 1152 if ($oa->option->has_document()) 1153 $dids = array_merge($dids, $oa->unsorted_values()); 1154 $this->conf->qe("update PaperStorage set inactive=1 where paperId=? and documentType>=? and paperStorageId?A", $this->paperId, DTYPE_FINAL, $dids); 1155 } 1156 1157 function attachment($dtype, $name) { 1158 $oa = $this->option($dtype); 1159 return $oa ? $oa->attachment($name) : null; 1160 } 1161 1162 function npages() { 1163 $doc = $this->document($this->finalPaperStorageId <= 0 ? DTYPE_SUBMISSION : DTYPE_FINAL); 1164 return $doc ? $doc->npages() : 0; 1165 } 1166 1167 private function ratings_query() { 1168 if ($this->conf->setting("rev_ratings") != REV_RATINGS_NONE) 1169 return "(select group_concat(contactId, ' ', rating) from ReviewRating where paperId=PaperReview.paperId and reviewId=PaperReview.reviewId)"; 1170 else 1171 return "''"; 1172 } 1173 1174 function load_reviews($always = false) { 1175 ++$this->_review_array_version; 1176 1177 if (property_exists($this, "reviewSignatures") 1178 && $this->_review_array === null 1179 && !$always) { 1180 $this->_review_array = $this->_reviews_have = []; 1181 if ((string) $this->reviewSignatures !== "") 1182 foreach (explode(",", $this->reviewSignatures) as $rs) { 1183 $rrow = ReviewInfo::make_signature($this, $rs); 1184 $this->_review_array[$rrow->reviewId] = $rrow; 1185 } 1186 return; 1187 } 1188 1189 if ($this->_row_set && ($this->_review_array === null || $always)) 1190 $row_set = $this->_row_set; 1191 else 1192 $row_set = new PaperInfoSet($this); 1193 $had = []; 1194 foreach ($row_set as $prow) { 1195 $prow->_review_array = []; 1196 $had += $prow->_reviews_have; 1197 $prow->_reviews_have = ["full" => true]; 1198 } 1199 1200 $result = $this->conf->qe("select PaperReview.*, " . $this->ratings_query() . " allRatings from PaperReview where paperId?a order by paperId, reviewId", $row_set->paper_ids()); 1201 while (($rrow = ReviewInfo::fetch($result, $this->conf))) { 1202 $prow = $row_set->get($rrow->paperId); 1203 $prow->_review_array[$rrow->reviewId] = $rrow; 1204 } 1205 Dbl::free($result); 1206 1207 $this->ensure_reviewer_names_set($row_set); 1208 if (get($had, "lastLogin")) 1209 $this->ensure_reviewer_last_login_set($row_set); 1210 } 1211 1212 private function parse_textual_id($textid) { 1213 if (ctype_digit($textid)) 1214 return intval($textid); 1215 if (str_starts_with($textid, (string) $this->paperId)) 1216 $textid = (string) substr($textid, strlen($this->paperId)); 1217 if ($textid !== "" && ctype_upper($textid) 1218 && ($n = parseReviewOrdinal($textid)) > 0) 1219 return -$n; 1220 return false; 1221 } 1222 1223 function reviews_by_id() { 1224 if ($this->_review_array === null) 1225 $this->load_reviews(); 1226 return $this->_review_array; 1227 } 1228 1229 function reviews_by_id_order() { 1230 return array_values($this->reviews_by_id()); 1231 } 1232 1233 function reviews_by_display() { 1234 $rrows = $this->reviews_by_id(); 1235 uasort($rrows, "ReviewInfo::compare"); 1236 return $rrows; 1237 } 1238 1239 function review_of_id($id) { 1240 return get($this->reviews_by_id(), $id); 1241 } 1242 1243 function review_of_user($contact) { 1244 $cid = self::contact_to_cid($contact); 1245 foreach ($this->reviews_by_id() as $rrow) 1246 if ($rrow->contactId == $cid) 1247 return $rrow; 1248 return null; 1249 } 1250 1251 function reviews_of_user($contact, $rev_tokens = null) { 1252 $cid = self::contact_to_cid($contact); 1253 $rrows = []; 1254 foreach ($this->reviews_by_id() as $rrow) 1255 if ($rrow->contactId == $cid 1256 || ($rev_tokens 1257 && $rrow->reviewToken 1258 && in_array($rrow->reviewToken, $rev_tokens))) 1259 $rrows[] = $rrow; 1260 return $rrows; 1261 } 1262 1263 function review_of_ordinal($ordinal) { 1264 foreach ($this->reviews_by_id() as $rrow) 1265 if ($rrow->reviewOrdinal == $ordinal) 1266 return $rrow; 1267 return null; 1268 } 1269 1270 function review_of_token($token) { 1271 if (!is_int($token)) 1272 $token = decode_token($token, "V"); 1273 foreach ($this->reviews_by_id() as $rrow) 1274 if ($rrow->reviewToken == $token) 1275 return $rrow; 1276 return null; 1277 } 1278 1279 function review_of_textual_id($textid) { 1280 if (($n = $this->parse_textual_id($textid)) === false) 1281 return false; 1282 else if ($n < 0) 1283 return $this->review_of_ordinal(-$n); 1284 else 1285 return $this->review_of_id($n); 1286 } 1287 1288 private function ensure_full_review_name() { 1289 if (($rrows = $this->_full_review)) { 1290 foreach (is_array($rrows) ? $rrows : [$rrows] as $rrow) 1291 if (($u = $this->conf->cached_user_by_id($rrow->contactId))) 1292 $rrow->assign_name($u); 1293 } 1294 } 1295 1296 function full_review_of_id($id) { 1297 if ($this->_full_review_key === null 1298 && !isset($this->_reviews_have["full"])) { 1299 $this->_full_review_key = "r$id"; 1300 $result = $this->conf->qe("select PaperReview.*, " . $this->ratings_query() . " allRatings from PaperReview where paperId=? and reviewId=?", $this->paperId, $id); 1301 $this->_full_review = ReviewInfo::fetch($result, $this->conf); 1302 Dbl::free($result); 1303 $this->ensure_full_review_name(); 1304 } 1305 if ($this->_full_review_key === "r$id") 1306 return $this->_full_review; 1307 $this->ensure_full_reviews(); 1308 return $this->review_of_id($id); 1309 } 1310 1311 function full_reviews_of_user($contact) { 1312 $cid = self::contact_to_cid($contact); 1313 if ($this->_full_review_key === null 1314 && !isset($this->_reviews_have["full"])) { 1315 $row_set = $this->_row_set ? : new PaperInfoSet($this); 1316 foreach ($row_set as $prow) { 1317 $prow->_full_review = []; 1318 $prow->_full_review_key = "u$cid"; 1319 } 1320 $result = $this->conf->qe("select PaperReview.*, " . $this->ratings_query() . " allRatings from PaperReview where paperId?a and contactId=? order by paperId, reviewId", $row_set->paper_ids(), $cid); 1321 while (($rrow = ReviewInfo::fetch($result, $this->conf))) { 1322 $prow = $row_set->get($rrow->paperId); 1323 $prow->_full_review[] = $rrow; 1324 } 1325 Dbl::free($result); 1326 $this->ensure_full_review_name(); 1327 } 1328 if ($this->_full_review_key === "u$cid") 1329 return $this->_full_review; 1330 $this->ensure_full_reviews(); 1331 return $this->reviews_of_user($contact); 1332 } 1333 1334 function full_review_of_ordinal($ordinal) { 1335 if ($this->_full_review_key === null 1336 && !isset($this->_reviews_have["full"])) { 1337 $this->_full_review_key = "o$ordinal"; 1338 $result = $this->conf->qe("select PaperReview.*, " . $this->ratings_query() . " allRatings from PaperReview where paperId=? and reviewOrdinal=?", $this->paperId, $ordinal); 1339 $this->_full_review = ReviewInfo::fetch($result, $this->conf); 1340 Dbl::free($result); 1341 $this->ensure_full_review_name(); 1342 } 1343 if ($this->_full_review_key === "o$ordinal") 1344 return $this->_full_review; 1345 $this->ensure_full_reviews(); 1346 return $this->review_of_ordinal($ordinal); 1347 } 1348 1349 function full_review_of_textual_id($textid) { 1350 if (($n = $this->parse_textual_id($textid)) === false) 1351 return false; 1352 else if ($n < 0) 1353 return $this->full_review_of_ordinal(-$n); 1354 else 1355 return $this->full_review_of_id($n); 1356 } 1357 1358 private function fresh_review_of($key, $value) { 1359 $result = $this->conf->qe("select PaperReview.*, " . $this->ratings_query() . " allRatings, ContactInfo.firstName, ContactInfo.lastName, ContactInfo.email from PaperReview join ContactInfo using (contactId) where paperId=? and $key=? order by paperId, reviewId", $this->paperId, $value); 1360 $rrow = ReviewInfo::fetch($result, $this->conf); 1361 Dbl::free($result); 1362 return $rrow; 1363 } 1364 1365 function fresh_review_of_id($id) { 1366 return $this->fresh_review_of("reviewId", $id); 1367 } 1368 1369 function fresh_review_of_user($contact) { 1370 return $this->fresh_review_of("contactId", self::contact_to_cid($contact)); 1371 } 1372 1373 function viewable_submitted_reviews_by_display(Contact $contact) { 1374 $cinfo = $contact->__rights($this, null); 1375 if ($cinfo->vsreviews_array === null 1376 || $cinfo->vsreviews_version !== $this->_review_array_version) { 1377 $cinfo->vsreviews_array = []; 1378 foreach ($this->reviews_by_display() as $id => $rrow) { 1379 if ($rrow->reviewSubmitted > 0 1380 && $contact->can_view_review($this, $rrow)) 1381 $cinfo->vsreviews_array[$id] = $rrow; 1382 } 1383 $cinfo->vsreviews_version = $this->_review_array_version; 1384 } 1385 return $cinfo->vsreviews_array; 1386 } 1387 1388 function viewable_submitted_reviews_by_user(Contact $contact) { 1389 $rrows = []; 1390 foreach ($this->viewable_submitted_reviews_by_display($contact) as $rrow) 1391 $rrows[$rrow->contactId] = $rrow; 1392 return $rrows; 1393 } 1394 1395 function can_view_review_identity_of($cid, Contact $contact) { 1396 if ($contact->can_administer_for_track($this, Track::VIEWREVID) 1397 || $cid == $contact->contactId) 1398 return true; 1399 foreach ($this->reviews_of_user($cid) as $rrow) 1400 if ($contact->can_view_review_identity($this, $rrow)) 1401 return true; 1402 return false; 1403 } 1404 1405 function may_have_viewable_scores($field, Contact $contact) { 1406 $field = is_object($field) ? $field : $this->conf->review_field($field); 1407 return $contact->can_view_review($this, $field->view_score) 1408 || $this->review_type($contact); 1409 } 1410 1411 function ensure_reviews() { 1412 if ($this->_review_array === null) 1413 $this->load_reviews(); 1414 } 1415 1416 function ensure_full_reviews() { 1417 if (!isset($this->_reviews_have["full"])) 1418 $this->load_reviews(true); 1419 } 1420 1421 private function ensure_reviewer_names_set($row_set) { 1422 $missing = []; 1423 foreach ($row_set as $prow) { 1424 $prow->_reviews_have["names"] = true; 1425 foreach ($prow->reviews_by_id() as $rrow) 1426 if (($u = $this->conf->cached_user_by_id($rrow->contactId, true))) 1427 $rrow->assign_name($u); 1428 else 1429 $missing[] = $rrow; 1430 } 1431 if ($this->conf->load_missing_cached_users()) { 1432 foreach ($missing as $rrow) 1433 if (($u = $this->conf->cached_user_by_id($rrow->contactId, true))) 1434 $rrow->assign_name($u); 1435 } 1436 } 1437 1438 function ensure_reviewer_names() { 1439 $this->ensure_reviews(); 1440 if (!empty($this->_review_array) 1441 && !isset($this->_reviews_have["names"])) 1442 $this->ensure_reviewer_names_set($this->_row_set ? : new PaperInfoSet($this)); 1443 } 1444 1445 private function ensure_reviewer_last_login_set($row_set) { 1446 $users = []; 1447 foreach ($row_set as $prow) { 1448 $prow->_reviews_have["lastLogin"] = true; 1449 foreach ($prow->reviews_by_id() as $rrow) 1450 $users[$rrow->contactId] = true; 1451 } 1452 if (!empty($users)) { 1453 $result = $this->conf->qe("select contactId, lastLogin from ContactInfo where contactId?a", array_keys($users)); 1454 $users = Dbl::fetch_iimap($result); 1455 foreach ($row_set as $prow) { 1456 foreach ($prow->reviews_by_id() as $rrow) 1457 $rrow->reviewLastLogin = $users[$rrow->contactId]; 1458 } 1459 } 1460 } 1461 1462 function ensure_reviewer_last_login() { 1463 $this->ensure_reviews(); 1464 if (!empty($this->_review_array) 1465 && !isset($this->_reviews_have["lastLogin"])) 1466 $this->ensure_reviewer_last_login_set($this->_row_set ? : new PaperInfoSet($this)); 1467 } 1468 1469 private function load_review_fields($fid, $maybe_null = false) { 1470 $k = $fid . "Signature"; 1471 $row_set = $this->_row_set ? : new PaperInfoSet($this); 1472 foreach ($row_set as $prow) 1473 $prow->$k = ""; 1474 $select = $maybe_null ? "coalesce($fid,'.')" : $fid; 1475 $result = $this->conf->qe("select paperId, group_concat($select order by reviewId) from PaperReview where paperId?a group by paperId", $row_set->paper_ids()); 1476 while ($result && ($row = $result->fetch_row())) { 1477 $prow = $row_set->get($row[0]); 1478 $prow->$k = $row[1]; 1479 } 1480 Dbl::free($result); 1481 } 1482 1483 function ensure_review_score($field) { 1484 $fid = is_object($field) ? $field->id : $field; 1485 if (!isset($this->_reviews_have[$fid]) 1486 && !isset($this->_reviews_have["full"])) { 1487 $rfi = is_object($field) ? $field : ReviewInfo::field_info($fid, $this->conf); 1488 if (!$rfi) 1489 $this->_reviews_have[$fid] = false; 1490 else if (!$rfi->main_storage) 1491 $this->ensure_full_reviews(); 1492 else { 1493 $this->_reviews_have[$fid] = true; 1494 $k = $rfi->main_storage . "Signature"; 1495 if (!property_exists($this, $k)) 1496 $this->load_review_fields($rfi->main_storage); 1497 $x = explode(",", $this->$k); 1498 foreach ($this->reviews_by_id_order() as $i => $rrow) 1499 $rrow->$fid = (int) $x[$i]; 1500 } 1501 } 1502 } 1503 1504 private function _update_review_word_counts($rids) { 1505 $rf = $this->conf->review_form(); 1506 $result = $this->conf->qe("select * from PaperReview where paperId=$this->paperId and reviewId?a", $rids); 1507 $qs = []; 1508 while (($rrow = ReviewInfo::fetch($result, $this->conf))) { 1509 if ($rrow->reviewWordCount === null) { 1510 $rrow->reviewWordCount = $rf->word_count($rrow); 1511 $qs[] = "update PaperReview set reviewWordCount={$rrow->reviewWordCount} where paperId={$this->paperId} and reviewId={$rrow->reviewId}"; 1512 } 1513 $my_rrow = get($this->_review_array, $rrow->reviewId); 1514 $my_rrow->reviewWordCount = (int) $rrow->reviewWordCount; 1515 } 1516 Dbl::free($result); 1517 if (!empty($qs)) { 1518 $mresult = Dbl::multi_qe($this->conf->dblink, join(";", $qs)); 1519 $mresult->free_all(); 1520 } 1521 } 1522 1523 function ensure_review_word_counts() { 1524 if (!isset($this->_reviews_have["reviewWordCount"])) { 1525 $this->_reviews_have["reviewWordCount"] = true; 1526 if (!property_exists($this, "reviewWordCountSignature")) 1527 $this->load_review_fields("reviewWordCount", true); 1528 $x = explode(",", $this->reviewWordCountSignature); 1529 $bad_ids = []; 1530 1531 foreach ($this->reviews_by_id_order() as $i => $rrow) 1532 if ($x[$i] !== ".") 1533 $rrow->reviewWordCount = (int) $x[$i]; 1534 else 1535 $bad_ids[] = $rrow->reviewId; 1536 if (!empty($bad_ids)) 1537 $this->_update_review_word_counts($bad_ids); 1538 } 1539 } 1540 1541 static function fetch_comment_query() { 1542 return "select PaperComment.*, 1543 firstName reviewFirstName, lastName reviewLastName, email reviewEmail 1544 from PaperComment 1545 join ContactInfo on (ContactInfo.contactId=PaperComment.contactId)"; 1546 } 1547 1548 function fetch_comments($extra_where = null) { 1549 $result = $this->conf->qe(self::fetch_comment_query() 1550 . " where paperId={$this->paperId}" . ($extra_where ? " and $extra_where" : "") 1551 . " order by paperId, commentId"); 1552 $comments = array(); 1553 while (($c = CommentInfo::fetch($result, $this, $this->conf))) 1554 $comments[$c->commentId] = $c; 1555 Dbl::free($result); 1556 return $comments; 1557 } 1558 1559 function load_comments() { 1560 $row_set = $this->_row_set ? : new PaperInfoSet($this); 1561 foreach ($row_set as $prow) 1562 $prow->_comment_array = []; 1563 $result = $this->conf->qe(self::fetch_comment_query() 1564 . " where paperId?a order by paperId, commentId", $row_set->paper_ids()); 1565 $comments = []; 1566 while (($c = CommentInfo::fetch($result, null, $this->conf))) { 1567 $prow = $row_set->get($c->paperId); 1568 $c->set_prow($prow); 1569 $prow->_comment_array[$c->commentId] = $c; 1570 } 1571 Dbl::free($result); 1572 } 1573 1574 function all_comments() { 1575 if ($this->_comment_array === null) 1576 $this->load_comments(); 1577 return $this->_comment_array; 1578 } 1579 1580 function viewable_comments(Contact $user) { 1581 $crows = []; 1582 foreach ($this->all_comments() as $cid => $crow) 1583 if ($user->can_view_comment($this, $crow)) 1584 $crows[$cid] = $crow; 1585 return $crows; 1586 } 1587 1588 function all_comment_skeletons() { 1589 if ($this->_comment_skeleton_array !== null) 1590 return $this->_comment_skeleton_array; 1591 if ($this->_comment_array !== null 1592 || !property_exists($this, "commentSkeletonInfo")) 1593 return $this->all_comments(); 1594 $this->_comment_skeleton_array = []; 1595 preg_match_all('/(\d+);(\d+);(\d+);(\d+);([^|]*)/', $this->commentSkeletonInfo, $ms, PREG_SET_ORDER); 1596 foreach ($ms as $m) { 1597 $c = new CommentInfo((object) [ 1598 "commentId" => $m[1], "contactId" => $m[2], 1599 "commentType" => $m[3], "commentRound" => $m[4], 1600 "commentTags" => $m[5] 1601 ], $this, $this->conf); 1602 $this->_comment_skeleton_array[$c->commentId] = $c; 1603 } 1604 return $this->_comment_skeleton_array; 1605 } 1606 1607 function viewable_comment_skeletons(Contact $user) { 1608 $crows = []; 1609 foreach ($this->all_comment_skeletons() as $cid => $crow) 1610 if ($user->can_view_comment($this, $crow)) 1611 $crows[$cid] = $crow; 1612 return $crows; 1613 } 1614 1615 function has_commenter($contact) { 1616 $cid = self::contact_to_cid($contact); 1617 foreach ($this->all_comment_skeletons() as $crow) 1618 if ($crow->contactId == $cid) 1619 return true; 1620 return false; 1621 } 1622 1623 static function analyze_review_or_comment($x) { 1624 if (isset($x->commentId)) 1625 return [!!($x->commentType & COMMENTTYPE_DRAFT), 1626 (int) $x->timeDisplayed, true]; 1627 else 1628 return [$x->reviewSubmitted && !$x->reviewOrdinal, 1629 (int) $x->timeDisplayed, false]; 1630 } 1631 static function review_or_comment_compare($a, $b) { 1632 list($a_draft, $a_displayed_at, $a_iscomment) = self::analyze_review_or_comment($a); 1633 list($b_draft, $b_displayed_at, $b_iscomment) = self::analyze_review_or_comment($b); 1634 // drafts come last 1635 if ($a_draft !== $b_draft 1636 && ($a_draft ? !$a_displayed_at : !$b_displayed_at)) 1637 return $a_draft ? 1 : -1; 1638 // order by displayed_at 1639 if ($a_displayed_at !== $b_displayed_at) 1640 return $a_displayed_at < $b_displayed_at ? -1 : 1; 1641 // reviews before comments 1642 if ($a_iscomment !== $b_iscomment) 1643 return !$a_iscomment ? -1 : 1; 1644 if ($a_iscomment) 1645 // order by commentId (which generally agrees with ordinal) 1646 return $a->commentId < $b->commentId ? -1 : 1; 1647 else { 1648 // order by ordinal or reviewId 1649 if ($a->reviewOrdinal && $b->reviewOrdinal) 1650 return $a->reviewOrdinal < $b->reviewOrdinal ? -1 : 1; 1651 else 1652 return $a->reviewId < $b->reviewId ? -1 : 1; 1653 } 1654 } 1655 function viewable_submitted_reviews_and_comments(Contact $user) { 1656 $this->ensure_full_reviews(); 1657 $rrows = $this->viewable_submitted_reviews_by_display($user); 1658 $crows = $this->viewable_comments($user); 1659 $rcs = array_merge(array_values($rrows), array_values($crows)); 1660 usort($rcs, "PaperInfo::review_or_comment_compare"); 1661 return $rcs; 1662 } 1663 static function review_or_comment_text_separator($a, $b) { 1664 if (!$a || !$b) 1665 return ""; 1666 else if (isset($a->reviewId) || isset($b->reviewId) 1667 || (($a->commentType | $b->commentType) & COMMENTTYPE_RESPONSE)) 1668 return "\n\n\n"; 1669 else 1670 return "\n\n"; 1671 } 1672 1673 1674 static function notify_user_compare($a, $b) { 1675 // group authors together, then reviewers 1676 $act = (int) $a->conflictType; 1677 $bct = (int) $b->conflictType; 1678 if (($act >= CONFLICT_AUTHOR) !== ($bct >= CONFLICT_AUTHOR)) 1679 return $act >= CONFLICT_AUTHOR ? -1 : 1; 1680 $arp = $a->myReviewPermissions; 1681 $brp = $b->myReviewPermissions; 1682 if ((bool) $arp !== (bool) $brp) 1683 return (bool) $arp ? -1 : 1; 1684 return Contact::compare($a, $b); 1685 } 1686 1687 function notify_reviews($callback, $sending_user) { 1688 $result = $this->conf->qe_raw("select ContactInfo.contactId, firstName, lastName, email, 1689 password, contactTags, roles, defaultWatch, 1690 " . self::my_review_permissions_sql() . " myReviewPermissions, 1691 conflictType, watch, preferredEmail, disabled 1692 from ContactInfo 1693 left join PaperConflict on (PaperConflict.paperId=$this->paperId and PaperConflict.contactId=ContactInfo.contactId) 1694 left join PaperWatch on (PaperWatch.paperId=$this->paperId and PaperWatch.contactId=ContactInfo.contactId) 1695 left join PaperReview on (PaperReview.paperId=$this->paperId and PaperReview.contactId=ContactInfo.contactId) 1696 where (watch&" . Contact::WATCH_REVIEW . ")!=0 1697 or (defaultWatch&" . (Contact::WATCH_REVIEW_ALL | Contact::WATCH_REVIEW_MANAGED) . ")!=0 1698 or conflictType>=" . CONFLICT_AUTHOR . " 1699 or reviewType is not null 1700 or exists (select * from PaperComment where paperId=$this->paperId and contactId=ContactInfo.contactId) 1701 group by ContactInfo.contactId"); 1702 1703 $watchers = []; 1704 $lastContactId = 0; 1705 while (($minic = Contact::fetch($result, $this->conf))) { 1706 if ($minic->contactId == $lastContactId 1707 || ($sending_user && $minic->contactId == $sending_user->contactId) 1708 || Contact::is_anonymous_email($minic->email)) 1709 continue; 1710 $lastContactId = $minic->contactId; 1711 if ($minic->following_reviews($this, $minic->watch)) 1712 $watchers[$minic->contactId] = $minic; 1713 } 1714 Dbl::free($result); 1715 usort($watchers, "PaperInfo::notify_user_compare"); 1716 1717 // save my current contact info map -- we are replacing it with another 1718 // map that lacks review token information and so forth 1719 $cimap = $this->replace_contact_info_map(null); 1720 1721 foreach ($watchers as $minic) { 1722 $this->load_my_contact_info($minic->contactId, $minic); 1723 call_user_func($callback, $this, $minic); 1724 } 1725 1726 $this->replace_contact_info_map($cimap); 1727 } 1728 1729 function notify_final_submit($callback, $sending_user) { 1730 $result = $this->conf->qe_raw("select ContactInfo.contactId, firstName, lastName, email, 1731 password, contactTags, roles, defaultWatch, 1732 " . self::my_review_permissions_sql() . " myReviewPermissions, 1733 conflictType, watch, preferredEmail, disabled 1734 from ContactInfo 1735 left join PaperConflict on (PaperConflict.paperId=$this->paperId and PaperConflict.contactId=ContactInfo.contactId) 1736 left join PaperWatch on (PaperWatch.paperId=$this->paperId and PaperWatch.contactId=ContactInfo.contactId) 1737 left join PaperReview on (PaperReview.paperId=$this->paperId and PaperReview.contactId=ContactInfo.contactId) 1738 where (defaultWatch&" . (Contact::WATCH_FINAL_SUBMIT_ALL) . ")!=0 1739 group by ContactInfo.contactId"); 1740 1741 $watchers = []; 1742 $lastContactId = 0; 1743 while (($minic = Contact::fetch($result, $this->conf))) { 1744 if ($minic->contactId == $lastContactId 1745 || ($sending_user && $minic->contactId == $sending_user->contactId) 1746 || Contact::is_anonymous_email($minic->email)) 1747 continue; 1748 $lastContactId = $minic->contactId; 1749 $watchers[$minic->contactId] = $minic; 1750 } 1751 Dbl::free($result); 1752 usort($watchers, "PaperInfo::notify_user_compare"); 1753 1754 // save my current contact info map -- we are replacing it with another 1755 // map that lacks review token information and so forth 1756 $cimap = $this->replace_contact_info_map(null); 1757 1758 foreach ($watchers as $minic) { 1759 $this->load_my_contact_info($minic->contactId, $minic); 1760 call_user_func($callback, $this, $minic); 1761 } 1762 1763 $this->replace_contact_info_map($cimap); 1764 } 1765 1766 function delete_from_database(Contact $user = null) { 1767 // XXX email self? 1768 if ($this->paperId <= 0) 1769 return false; 1770 $rrows = $this->reviews_by_id(); 1771 1772 $qs = []; 1773 foreach (["PaperWatch", "PaperReviewPreference", "PaperReviewRefused", "ReviewRequest", "PaperTag", "PaperComment", "PaperReview", "PaperTopic", "PaperOption", "PaperConflict", "Paper", "PaperStorage", "Capability"] as $table) { 1774 $qs[] = "delete from $table where paperId={$this->paperId}"; 1775 } 1776 $mresult = Dbl::multi_qe($this->conf->dblink, join(";", $qs)); 1777 $mresult->free_all(); 1778 1779 if (!Dbl::$nerrors) { 1780 $this->conf->update_papersub_setting(-1); 1781 if ($this->outcome > 0) 1782 $this->conf->update_paperacc_setting(-1); 1783 if ($this->leadContactId > 0 || $this->shepherdContactId > 0) 1784 $this->conf->update_paperlead_setting(-1); 1785 if ($this->managerContactId > 0) 1786 $this->conf->update_papermanager_setting(-1); 1787 if ($rrows && array_filter($rrows, function ($rrow) { return $rrow->reviewToken > 0; })) 1788 $this->conf->update_rev_tokens_setting(-1); 1789 if ($rrows && array_filter($rrows, function ($rrow) { return $rrow->reviewType == REVIEW_META; })) 1790 $this->conf->update_metareviews_setting(-1); 1791 $this->conf->log_for($user, $user, "Deleted", $this->paperId); 1792 return true; 1793 } else { 1794 return false; 1795 } 1796 } 1797} 1798