1<?php 2// papersearch.php -- HotCRP helper class for searching for papers 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class SearchWord { 6 public $qword; 7 public $word; 8 public $quoted; 9 public $keyword; 10 public $kwexplicit; 11 public $kwdef; 12 function __construct($qword) { 13 $this->qword = $this->word = $qword; 14 $this->quoted = $qword !== "" && $qword[0] === "\"" 15 && strpos($qword, "\"", 1) === strlen($qword) - 1; 16 if ($this->quoted) 17 $this->word = substr($qword, 1, -1); 18 } 19} 20 21class SearchSplitter { 22 private $str; 23 private $utf8q; 24 public $pos; 25 public $strspan; 26 function __construct($str) { 27 $this->str = ltrim($str); 28 $this->utf8q = strpos($str, chr(0xE2)) !== false; 29 $this->pos = strlen($str) - strlen($this->str); 30 } 31 function is_empty() { 32 return $this->str === ""; 33 } 34 function shift() { 35 if ($this->utf8q 36 && preg_match('/\A([-_.a-zA-Z0-9]+:|["“”][^"“”]+["“”]:|)\s*((?:["“”][^"“”]*(?:["“”]|\z)|[^"“”\s()]*)*)/su', $this->str, $m)) { 37 $result = preg_replace('/[“”]/u', "\"", $m[1] . $m[2]); 38 } else if (preg_match('/\A([-_.a-zA-Z0-9]+:|"[^"]+":|)\s*((?:"[^"]*(?:"|\z)|[^"\s()]*)*)/s', $this->str, $m)) { 39 $result = $m[1] . $m[2]; 40 } else { 41 $this->pos += strlen($this->str); 42 $this->str = ""; 43 $this->strspan = [$this->pos, $this->pos]; 44 return ""; 45 } 46 $this->set_span_and_pos($m[0]); 47 return $result; 48 } 49 function shift_past($str) { 50 assert(str_starts_with($this->str, $str)); 51 $this->set_span_and_pos($str); 52 } 53 function shift_balanced_parens() { 54 $result = substr($this->str, 0, self::span_balanced_parens($this->str)); 55 $this->set_span_and_pos($result); 56 return $result; 57 } 58 function match($re, &$m = null) { 59 return preg_match($re, $this->str, $m); 60 } 61 function starts_with($substr) { 62 return str_starts_with($this->str, $substr); 63 } 64 private function set_span_and_pos($prefix) { 65 $this->strspan = [$this->pos, $this->pos + strlen($prefix)]; 66 $next = ltrim(substr($this->str, strlen($prefix))); 67 $this->pos += strlen($this->str) - strlen($next); 68 $this->str = $next; 69 } 70 static function span_balanced_parens($str) { 71 $pcount = $quote = 0; 72 $len = strlen($str); 73 for ($pos = 0; $pos < $len 74 && (!ctype_space($str[$pos]) || $pcount || $quote); ++$pos) { 75 $ch = $str[$pos]; 76 // translate “” -> " 77 if (ord($ch) === 0xE2 && $pos + 2 < $len && ord($str[$pos + 1]) === 0x80 78 && (ord($str[$pos + 2]) & 0xFE) === 0x9C) 79 $ch = "\""; 80 if ($quote) { 81 if ($ch === "\\" && $pos + 1 < strlen($str)) 82 ++$pos; 83 else if ($ch === "\"") 84 $quote = 0; 85 } else if ($ch === "\"") 86 $quote = 1; 87 else if ($ch === "(" || $ch === "[" || $ch === "{") 88 ++$pcount; 89 else if ($ch === ")" || $ch === "]" || $ch === "}") { 90 if (!$pcount) 91 break; 92 --$pcount; 93 } 94 } 95 return $pos; 96 } 97} 98 99class SearchOperator { 100 public $op; 101 public $unary; 102 public $precedence; 103 public $opinfo; 104 105 static private $list = null; 106 107 function __construct($what, $unary, $precedence, $opinfo = null) { 108 $this->op = $what; 109 $this->unary = $unary; 110 $this->precedence = $precedence; 111 $this->opinfo = $opinfo; 112 } 113 function unparse() { 114 $x = strtoupper($this->op); 115 return $this->opinfo === null ? $x : $x . ":" . $this->opinfo; 116 } 117 118 static function get($name) { 119 if (!self::$list) { 120 self::$list["("] = new SearchOperator("(", true, null); 121 self::$list[")"] = new SearchOperator(")", true, null); 122 self::$list["NOT"] = new SearchOperator("not", true, 7); 123 self::$list["-"] = new SearchOperator("not", true, 7); 124 self::$list["!"] = new SearchOperator("not", true, 7); 125 self::$list["+"] = new SearchOperator("+", true, 7); 126 self::$list["SPACE"] = new SearchOperator("space", false, 6); 127 self::$list["AND"] = new SearchOperator("and", false, 5); 128 self::$list["OR"] = new SearchOperator("or", false, 4); 129 self::$list["XOR"] = new SearchOperator("or", false, 3); 130 self::$list["THEN"] = new SearchOperator("then", false, 2); 131 self::$list["HIGHLIGHT"] = new SearchOperator("highlight", false, 1, ""); 132 } 133 return get(self::$list, $name); 134 } 135} 136 137class SearchTerm { 138 public $type; 139 public $float = []; 140 141 function __construct($type) { 142 $this->type = $type; 143 } 144 static function make_op($op, $terms) { 145 $opstr = is_object($op) ? $op->op : $op; 146 if ($opstr === "not") 147 $qr = new Not_SearchTerm; 148 else if ($opstr === "and" || $opstr === "space") 149 $qr = new And_SearchTerm($opstr); 150 else if ($opstr === "or") 151 $qr = new Or_SearchTerm; 152 else 153 $qr = new Then_SearchTerm($op); 154 foreach (is_array($terms) ? $terms : [$terms] as $qt) 155 $qr->append($qt); 156 return $qr->finish(); 157 } 158 static function make_not(SearchTerm $term) { 159 $qr = new Not_SearchTerm; 160 return $qr->append($term)->finish(); 161 } 162 function negate_if($negate) { 163 return $negate ? self::make_not($this) : $this; 164 } 165 static function make_float($float) { 166 $qe = new True_SearchTerm; 167 $qe->float = $float; 168 return $qe; 169 } 170 171 function is_false() { 172 return false; 173 } 174 function is_true() { 175 return false; 176 } 177 function is_uninteresting() { 178 return false; 179 } 180 function set_float($k, $v) { 181 $this->float[$k] = $v; 182 } 183 function get_float($k, $defval = null) { 184 return get($this->float, $k, $defval); 185 } 186 function apply_strspan($span) { 187 $span1 = get($this->float, "strspan"); 188 if ($span && $span1) 189 $span = [min($span[0], $span1[0]), max($span[1], $span1[1])]; 190 $this->set_float("strspan", $span ? : $span1); 191 } 192 function set_strspan_owner($str) { 193 if (!isset($this->float["strspan_owner"])) 194 $this->set_float("strspan_owner", $str); 195 } 196 197 198 function debug_json() { 199 return $this->type; 200 } 201 202 203 // apply rounds to reviewer searches 204 function adjust_reviews(ReviewAdjustment_SearchTerm $revadj = null, PaperSearch $srch) { 205 if ($this->get_float("used_revadj") && $revadj) 206 $revadj->used_revadj = true; 207 return $this; 208 } 209 210 211 function trivial_rights(Contact $user, PaperSearch $srch) { 212 return false; 213 } 214 215 216 static function andjoin_sqlexpr($q, $default = "false") { 217 if (empty($q)) 218 return $default; 219 else if (in_array("false", $q)) 220 return "false"; 221 else 222 return "(" . join(" and ", $q) . ")"; 223 } 224 static function orjoin_sqlexpr($q, $default = "false") { 225 if (empty($q)) 226 return $default; 227 else if (in_array("true", $q)) 228 return "true"; 229 else 230 return "(" . join(" or ", $q) . ")"; 231 } 232 233 function sqlexpr(SearchQueryInfo $sqi) { 234 assert(false); 235 return "false"; 236 } 237 238 function exec(PaperInfo $row, PaperSearch $srch) { 239 assert(false); 240 return false; 241 } 242 243 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 244 return null; 245 } 246 247 248 function extract_metadata($top, PaperSearch $srch) { 249 if ($top && ($x = $this->get_float("contradiction_warning"))) 250 $srch->contradictions[$x] = true; 251 } 252 function default_sorter($top, $thenmap, PaperSearch $srch) { 253 return false; 254 } 255} 256 257class False_SearchTerm extends SearchTerm { 258 function __construct() { 259 parent::__construct("f"); 260 } 261 function is_false() { 262 return true; 263 } 264 function trivial_rights(Contact $user, PaperSearch $srch) { 265 return true; 266 } 267 function sqlexpr(SearchQueryInfo $sqi) { 268 return "false"; 269 } 270 function exec(PaperInfo $row, PaperSearch $srch) { 271 return false; 272 } 273 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 274 return false; 275 } 276} 277 278class True_SearchTerm extends SearchTerm { 279 function __construct() { 280 parent::__construct("t"); 281 } 282 function is_true() { 283 return true; 284 } 285 function is_uninteresting() { 286 return count($this->float) === 1 && isset($this->float["view"]); 287 } 288 function trivial_rights(Contact $user, PaperSearch $srch) { 289 return true; 290 } 291 function sqlexpr(SearchQueryInfo $sqi) { 292 return "true"; 293 } 294 function exec(PaperInfo $row, PaperSearch $srch) { 295 return true; 296 } 297 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 298 return true; 299 } 300} 301 302class Op_SearchTerm extends SearchTerm { 303 public $child = []; 304 305 function __construct($type) { 306 parent::__construct($type); 307 } 308 protected function append($term) { 309 if ($term) { 310 foreach ($term->float as $k => $v) { 311 $v1 = get($this->float, $k); 312 if ($k === "sort" && $v1) 313 array_splice($this->float[$k], count($v1), 0, $v); 314 else if ($k === "strspan" && $v1) 315 $this->apply_strspan($v); 316 else if (is_array($v1) && is_array($v)) 317 $this->float[$k] = array_replace_recursive($v1, $v); 318 else if ($k !== "opinfo" || $v1 === null) 319 $this->float[$k] = $v; 320 } 321 $this->child[] = $term; 322 } 323 return $this; 324 } 325 protected function finish() { 326 assert(false); 327 } 328 protected function _flatten_children() { 329 $qvs = array(); 330 foreach ($this->child ? : array() as $qv) 331 if ($qv->type === $this->type) 332 $qvs = array_merge($qvs, $qv->child); 333 else 334 $qvs[] = $qv; 335 return $qvs; 336 } 337 protected function _finish_combine($newchild, $any) { 338 $qr = null; 339 if (!$newchild) 340 $qr = $any ? new True_SearchTerm : new False_SearchTerm; 341 else if (count($newchild) == 1) 342 $qr = clone $newchild[0]; 343 if ($qr) { 344 $qr->float = $this->float; 345 return $qr; 346 } else { 347 $this->child = $newchild; 348 return $this; 349 } 350 } 351 352 function set_strspan_owner($str) { 353 if (!isset($this->float["strspan_owner"])) { 354 parent::set_strspan_owner($str); 355 foreach ($this->child as $qv) 356 $qv->set_strspan_owner($str); 357 } 358 } 359 function debug_json() { 360 $a = [$this->type]; 361 foreach ($this->child as $qv) 362 $a[] = $qv->debug_json(); 363 return $a; 364 } 365 function adjust_reviews(ReviewAdjustment_SearchTerm $revadj = null, PaperSearch $srch) { 366 foreach ($this->child as &$qv) 367 $qv = $qv->adjust_reviews($revadj, $srch); 368 return $this; 369 } 370 function trivial_rights(Contact $user, PaperSearch $srch) { 371 foreach ($this->child as $ch) 372 if (!$ch->trivial_rights($user, $srch)) 373 return false; 374 return true; 375 } 376} 377 378class Not_SearchTerm extends Op_SearchTerm { 379 function __construct() { 380 parent::__construct("not"); 381 } 382 protected function finish() { 383 $qv = $this->child ? $this->child[0] : null; 384 $qr = null; 385 if (!$qv || $qv->is_false()) 386 $qr = new True_SearchTerm; 387 else if ($qv->is_true()) 388 $qr = new False_SearchTerm; 389 else if ($qv->type === "not") 390 $qr = clone $qv->child[0]; 391 else if ($qv->type === "revadj") { 392 $qr = clone $qv; 393 $qr->negated = !$qr->negated; 394 } 395 if ($qr) { 396 $qr->float = $this->float; 397 return $qr; 398 } else 399 return $this; 400 } 401 402 function sqlexpr(SearchQueryInfo $sqi) { 403 $sqi->negated = !$sqi->negated; 404 $ff = $this->child[0]->sqlexpr($sqi); 405 if ($sqi->negated 406 && !$this->child[0]->trivial_rights($sqi->user, $sqi->srch)) 407 $ff = "false"; 408 $sqi->negated = !$sqi->negated; 409 if ($ff === "false") 410 return "true"; 411 else if ($ff === "true") 412 return "false"; 413 else 414 return "not ($ff)"; 415 } 416 function exec(PaperInfo $row, PaperSearch $srch) { 417 return !$this->child[0]->exec($row, $srch); 418 } 419 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 420 $x = $this->child[0]->compile_edit_condition($row, $srch); 421 if ($x === null) 422 return null; 423 else if ($x === false || $x === true) 424 return !$x; 425 else 426 return (object) ["type" => "not", "child" => [$x]]; 427 } 428} 429 430class And_SearchTerm extends Op_SearchTerm { 431 function __construct($type) { 432 parent::__construct($type); 433 } 434 protected function finish() { 435 $pn = $revadj = null; 436 $newchild = []; 437 $any = false; 438 foreach ($this->_flatten_children() as $qv) { 439 if ($qv->is_false()) { 440 $qr = new False_SearchTerm; 441 $qr->float = $this->float; 442 return $qr; 443 } else if ($qv->is_true()) 444 $any = true; 445 else if ($qv->type === "revadj") 446 $revadj = $qv->apply($revadj, false); 447 else if ($qv->type === "pn" && $this->type === "space") { 448 if (!$pn) 449 $newchild[] = $pn = $qv; 450 else 451 $pn->pids = array_merge($pn->pids, $qv->pids); 452 } else 453 $newchild[] = $qv; 454 } 455 if ($revadj) // must come first 456 array_unshift($newchild, $revadj); 457 return $this->_finish_combine($newchild, $any); 458 } 459 460 function adjust_reviews(ReviewAdjustment_SearchTerm $revadj = null, PaperSearch $srch) { 461 $myrevadj = null; 462 if ($this->child[0] instanceof ReviewAdjustment_SearchTerm) { 463 $myrevadj = $this->child[0]; 464 $used_revadj = $myrevadj->merge($revadj); 465 } 466 foreach ($this->child as &$qv) 467 if (!($qv instanceof ReviewAdjustment_SearchTerm)) 468 $qv = $qv->adjust_reviews($myrevadj ? : $revadj, $srch); 469 if ($myrevadj && !$myrevadj->used_revadj) { 470 $this->child[0] = $myrevadj->promote($srch); 471 if ($used_revadj) 472 $revadj->used_revadj = true; 473 } 474 return $this; 475 } 476 function sqlexpr(SearchQueryInfo $sqi) { 477 $ff = array(); 478 foreach ($this->child as $subt) 479 $ff[] = $subt->sqlexpr($sqi); 480 return self::andjoin_sqlexpr($ff); 481 } 482 function exec(PaperInfo $row, PaperSearch $srch) { 483 foreach ($this->child as $subt) 484 if (!$subt->exec($row, $srch)) 485 return false; 486 return true; 487 } 488 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 489 $ch = []; 490 $ok = true; 491 foreach ($this->child as $subt) { 492 $x = $subt->compile_edit_condition($row, $srch); 493 if ($x === null) 494 return null; 495 else if ($x === false) 496 $ok = false; 497 else if ($x !== true) 498 $ch[] = $x; 499 } 500 if (!$ok || empty($ch)) 501 return $ok; 502 else 503 return (object) ["type" => "and", "child" => $ch]; 504 } 505 function extract_metadata($top, PaperSearch $srch) { 506 parent::extract_metadata($top, $srch); 507 foreach ($this->child as $qv) 508 $qv->extract_metadata($top, $srch); 509 } 510 function default_sorter($top, $thenmap, PaperSearch $srch) { 511 $s = false; 512 foreach ($this->child as $qv) { 513 $s1 = $qv->default_sorter($top, $thenmap, $srch); 514 if ($s && $s1) 515 return false; 516 $s = $s ? : $s1; 517 } 518 return $s; 519 } 520} 521 522class Or_SearchTerm extends Op_SearchTerm { 523 function __construct() { 524 parent::__construct("or"); 525 } 526 protected function finish() { 527 $pn = $revadj = null; 528 $newchild = []; 529 foreach ($this->_flatten_children() as $qv) { 530 if ($qv->is_true()) 531 return self::make_float($this->float); 532 else if ($qv->is_false()) 533 /* skip */; 534 else if ($qv->type === "revadj") 535 $revadj = $qv->apply($revadj, true); 536 else if ($qv->type === "pn") { 537 if (!$pn) 538 $newchild[] = $pn = $qv; 539 else 540 $pn->pids = array_merge($pn->pids, $qv->pids); 541 } else 542 $newchild[] = $qv; 543 } 544 if ($revadj) 545 array_unshift($newchild, $revadj); 546 return $this->_finish_combine($newchild, false); 547 } 548 549 function sqlexpr(SearchQueryInfo $sqi) { 550 $ff = array(); 551 foreach ($this->child as $subt) 552 $ff[] = $subt->sqlexpr($sqi); 553 return self::orjoin_sqlexpr($ff); 554 } 555 function exec(PaperInfo $row, PaperSearch $srch) { 556 foreach ($this->child as $subt) 557 if ($subt->exec($row, $srch)) 558 return true; 559 return false; 560 } 561 static function compile_or_edit_condition($child, PaperInfo $row, PaperSearch $srch) { 562 $ch = []; 563 $ok = false; 564 foreach ($child as $subt) { 565 $x = $subt->compile_edit_condition($row, $srch); 566 if ($x === null) 567 return null; 568 else if ($x === true) 569 $ok = true; 570 else if ($x !== false) 571 $ch[] = $x; 572 } 573 if ($ok || empty($ch)) 574 return $ok; 575 else 576 return (object) ["type" => "or", "child" => $ch]; 577 } 578 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 579 return self::compile_or_edit_condition($this->child, $row, $srch); 580 } 581 function extract_metadata($top, PaperSearch $srch) { 582 parent::extract_metadata($top, $srch); 583 foreach ($this->child as $qv) 584 $qv->extract_metadata(false, $srch); 585 } 586} 587 588class Then_SearchTerm extends Op_SearchTerm { 589 private $is_highlight; 590 public $nthen; 591 public $highlights; 592 public $highlight_types; 593 594 function __construct(SearchOperator $op) { 595 assert($op->op === "then" || $op->op === "highlight"); 596 parent::__construct("then"); 597 $this->is_highlight = $op->op === "highlight"; 598 if (isset($op->opinfo)) 599 $this->set_float("opinfo", $op->opinfo); 600 } 601 protected function finish() { 602 $opinfo = strtolower($this->get_float("opinfo", "")); 603 $newvalues = $newhvalues = $newhmasks = $newhtypes = []; 604 605 foreach ($this->child as $qvidx => $qv) { 606 if ($qv && $qvidx && $this->is_highlight) { 607 if ($qv->type === "then") { 608 for ($i = 0; $i < $qv->nthen; ++$i) { 609 $newhvalues[] = $qv->child[$i]; 610 $newhmasks[] = (1 << count($newvalues)) - 1; 611 $newhtypes[] = $opinfo; 612 } 613 } else { 614 $newhvalues[] = $qv; 615 $newhmasks[] = (1 << count($newvalues)) - 1; 616 $newhtypes[] = $opinfo; 617 } 618 } else if ($qv && $qv->type === "then") { 619 $pos = count($newvalues); 620 for ($i = 0; $i < $qv->nthen; ++$i) 621 $newvalues[] = $qv->child[$i]; 622 for ($i = $qv->nthen; $i < count($qv->child); ++$i) 623 $newhvalues[] = $qv->child[$i]; 624 foreach ($qv->highlights ? : array() as $hlmask) 625 $newhmasks[] = $hlmask << $pos; 626 foreach ($qv->highlight_types ? : array() as $hltype) 627 $newhtypes[] = $hltype; 628 } else if ($qv) 629 $newvalues[] = $qv; 630 } 631 632 $this->nthen = count($newvalues); 633 $this->highlights = $newhmasks; 634 $this->highlight_types = $newhtypes; 635 array_splice($newvalues, $this->nthen, 0, $newhvalues); 636 $this->child = $newvalues; 637 $this->set_float("sort", []); 638 return $this; 639 } 640 641 function sqlexpr(SearchQueryInfo $sqi) { 642 $ff = array(); 643 foreach ($this->child as $subt) 644 $ff[] = $subt->sqlexpr($sqi); 645 return self::orjoin_sqlexpr(array_slice($ff, 0, $this->nthen), "true"); 646 } 647 function exec(PaperInfo $row, PaperSearch $srch) { 648 for ($i = 0; $i < $this->nthen; ++$i) 649 if ($this->child[$i]->exec($row, $srch)) 650 return true; 651 return false; 652 } 653 function compile_edit_condition(PaperInfo $row, PaperSearch $srch) { 654 return Or_SearchTerm::compile_or_edit_condition(array_slice($this->child, 0, $this->nthen), $row, $srch); 655 } 656 function extract_metadata($top, PaperSearch $srch) { 657 parent::extract_metadata($top, $srch); 658 foreach ($this->child as $qv) 659 $qv->extract_metadata(false, $srch); 660 } 661} 662 663class TextMatch_SearchTerm extends SearchTerm { 664 private $field; 665 private $authorish; 666 private $trivial = null; 667 public $regex; 668 static public $map = [ // NB see field_highlighters() 669 "ti" => "title", "ab" => "abstract", 670 "au" => "authorInformation", "co" => "collaborators" 671 ]; 672 673 function __construct($t, $text, $quoted) { 674 parent::__construct($t); 675 $this->field = self::$map[$t]; 676 $this->authorish = $t === "au" || $t === "co"; 677 if (is_bool($text)) 678 $this->trivial = $text; 679 else 680 $this->regex = Text::star_text_pregexes($text, $quoted); 681 } 682 static function parse($word, SearchWord $sword) { 683 if ($sword->kwexplicit && !$sword->quoted) { 684 if ($word === "any") 685 $word = true; 686 else if ($word === "none") 687 $word = false; 688 } 689 return new TextMatch_SearchTerm($sword->kwdef->name, $word, $sword->quoted); 690 } 691 692 function trivial_rights(Contact $user, PaperSearch $srch) { 693 return $this->trivial && !$this->authorish; 694 } 695 function sqlexpr(SearchQueryInfo $sqi) { 696 $sqi->add_column($this->field, "Paper.{$this->field}"); 697 if ($this->trivial && !$this->authorish) 698 return "Paper.{$this->field}!=''"; 699 else 700 return "true"; 701 } 702 function exec(PaperInfo $row, PaperSearch $srch) { 703 $data = $row->{$this->field}; 704 if ($this->authorish && !$srch->user->allow_view_authors($row)) 705 $data = ""; 706 if ($data === "") 707 return $this->trivial === false; 708 else if ($this->trivial !== null) 709 return $this->trivial; 710 else 711 return $row->field_match_pregexes($this->regex, $this->field); 712 } 713 function extract_metadata($top, PaperSearch $srch) { 714 parent::extract_metadata($top, $srch); 715 if ($this->regex) 716 $srch->regex[$this->type][] = $this->regex; 717 } 718} 719 720class ReviewRating_SearchAdjustment { 721 private $type; 722 private $arg; 723 724 function __construct($type, $arg) { 725 $this->type = $type; 726 $this->arg = $arg; 727 } 728 function must_exist() { 729 if ($this->type === "and") 730 return $this->arg[0]->must_exist() || $this->arg[1]->must_exist(); 731 else if ($this->type === "or") 732 return $this->arg[0]->must_exist() && $this->arg[1]->must_exist(); 733 else if ($this->type === "not") 734 return false; 735 else 736 return !$this->arg->test(0); 737 } 738 private function _test($ratings) { 739 if ($this->type === "and") 740 return $this->arg[0]->_test($ratings) && $this->arg[1]->_test($ratings); 741 else if ($this->type === "or") 742 return $this->arg[0]->_test($ratings) || $this->arg[1]->_test($ratings); 743 else if ($this->type === "not") 744 return !$this->arg->_test($ratings); 745 else { 746 $n = count(array_filter($ratings, function ($r) { return ($r & $this->type) !== 0; })); 747 return $this->arg->test($n); 748 } 749 } 750 function test(Contact $user, PaperInfo $prow, ReviewInfo $rrow) { 751 if ($user->can_view_review_ratings($prow, $rrow, $user->privChair)) 752 $ratings = $rrow->ratings(); 753 else 754 $ratings = []; 755 return $this->_test($ratings); 756 } 757} 758 759class ReviewAdjustment_SearchTerm extends SearchTerm { 760 private $conf; 761 private $round; 762 private $ratings; 763 public $negated = false; 764 public $used_revadj = false; 765 766 function __construct(Conf $conf) { 767 parent::__construct("revadj"); 768 $this->conf = $conf; 769 } 770 static function parse_round($word, SearchWord $sword, PaperSearch $srch) { 771 $srch->_has_review_adjustment = true; 772 if (!$srch->user->isPC) 773 $rounds = null; 774 else if (strcasecmp($word, "none") == 0 || strcasecmp($word, "unnamed") == 0) 775 $rounds = [0]; 776 else if (strcasecmp($word, "any") == 0) 777 $rounds = range(1, count($srch->conf->round_list()) - 1); 778 else { 779 $x = simplify_whitespace($word); 780 $rounds = array_keys(Text::simple_search($x, $srch->conf->round_list())); 781 if (empty($rounds)) { 782 $srch->warn("“" . htmlspecialchars($x) . "” doesn’t match a review round."); 783 return new False_SearchTerm; 784 } 785 } 786 $qv = new ReviewAdjustment_SearchTerm($srch->conf); 787 $qv->round = $rounds; 788 return $qv; 789 } 790 static function parse_rate($word, SearchWord $sword, PaperSearch $srch) { 791 if (!$srch->user->can_view_some_review_ratings()) { 792 if ($srch->user->isPC && $srch->conf->setting("rev_ratings") == REV_RATINGS_NONE) 793 $srch->warn("Review ratings are disabled."); 794 return new False_SearchTerm; 795 } 796 $rate = null; 797 if (strcasecmp($word, "none") == 0) { 798 $rate = "any"; 799 $compar = "=0"; 800 } else if (preg_match('/\A(.+?)\s*(:?|[=!<>]=?|≠|≤|≥)\s*(\d*)\z/', $word, $m) 801 && ($m[3] !== "" || $m[2] === "")) { 802 if ($m[3] === "") 803 $compar = ">0"; 804 else if ($m[2] === "" || $m[2] === ":") 805 $compar = ($m[3] == 0 ? "=0" : ">=" . $m[3]); 806 else 807 $compar = $m[2] . $m[3]; 808 $rate = self::parse_rate_name($m[1]); 809 } 810 if ($rate === null) { 811 $srch->warn("Bad review rating query “" . htmlspecialchars($word) . "”."); 812 return new False_SearchTerm; 813 } else { 814 $srch->_has_review_adjustment = true; 815 $qv = new ReviewAdjustment_SearchTerm($srch->conf); 816 $qv->ratings = new ReviewRating_SearchAdjustment($rate, new CountMatcher($compar)); 817 return $qv; 818 } 819 } 820 static private function parse_rate_name($s) { 821 if (strcasecmp($s, "any") == 0) 822 return ReviewInfo::RATING_GOODMASK | ReviewInfo::RATING_BADMASK; 823 if ($s === "+" || strcasecmp($s, "good") == 0 || strcasecmp($s, "yes") == 0) 824 return ReviewInfo::RATING_GOODMASK; 825 if ($s === "-" || strcasecmp($s, "bad") == 0 || strcasecmp($s, "no") == 0 826 || $s === "\xE2\x88\x92" /* unicode MINUS */) 827 return ReviewInfo::RATING_BADMASK; 828 foreach (ReviewInfo::$rating_bits as $bit => $name) { 829 if (strcasecmp($s, $name) === 0) 830 return $bit; 831 } 832 $x = Text::simple_search($s, ReviewInfo::$rating_options); 833 unset($x[0]); // can't search for “average” 834 if (count($x) == 1) { 835 reset($x); 836 return key($x); 837 } else 838 return null; 839 } 840 841 function merge(ReviewAdjustment_SearchTerm $x = null) { 842 $changed = null; 843 if ($x && $this->round === null && $x->round !== null) 844 $changed = $this->round = $x->round; 845 if ($x && $this->ratings === null && $x->ratings !== null) 846 $changed = $this->ratings = $x->ratings; 847 return $changed !== null; 848 } 849 function promote(PaperSearch $srch) { 850 $rsm = new ReviewSearchMatcher(">0"); 851 if (in_array($srch->limit(), ["r", "rout", "rable"])) 852 $rsm->add_contact($srch->cid); 853 else if ($srch->limit() === "req") { 854 $rsm->apply_requester($srch->cid); 855 $rsm->apply_review_type("external"); // XXX optional PC reviews? 856 } 857 $this->promote_matcher($rsm); 858 $term = new Review_SearchTerm($rsm); 859 return $term->negate_if($this->negated); 860 } 861 function promote_matcher(ReviewSearchMatcher $rsm) { 862 if ($this->round !== null) 863 $rsm->adjust_round_list($this->round); 864 if ($this->ratings !== null) 865 $rsm->adjust_ratings($this->ratings); 866 $this->used_revadj = true; 867 } 868 function adjust_reviews(ReviewAdjustment_SearchTerm $revadj = null, PaperSearch $srch) { 869 if ($revadj || $this->get_float("used_revadj")) 870 return $this; 871 else 872 return $this->promote($srch); 873 } 874 function apply_negation() { 875 if ($this->negated) { 876 if ($this->round !== null) 877 $this->round = array_diff(array_keys($this->conf->round_list()), $this->round); 878 if ($this->ratings !== null) 879 $this->ratings = new ReviewRating_SearchAdjustment("not", $this->ratings); 880 $this->negated = false; 881 } 882 } 883 function apply(ReviewAdjustment_SearchTerm $revadj = null, $is_or = false) { 884 // XXX this is probably not right in fully general cases 885 if (!$revadj) 886 return $this; 887 if ($revadj->negated !== $this->negated || ($revadj->negated && $is_or)) { 888 $revadj->apply_negation(); 889 $this->apply_negation(); 890 } 891 if ($is_or || $revadj->negated) { 892 if ($this->round !== null) 893 $revadj->round = array_unique(array_merge($revadj->round, $this->round)); 894 if ($this->ratings !== null && $revadj->ratings !== null) 895 $revadj->ratings = new ReviewRating_SearchAdjustment("or", [$this->ratings, $revadj->ratings]); 896 else if ($this->ratings !== null) 897 $revadj->ratings = $this->ratings; 898 } else { 899 if ($revadj->round !== null && $this->round !== null) 900 $revadj->round = array_intersect($revadj->round, $this->round); 901 else if ($this->round !== null) 902 $revadj->round = $this->round; 903 if ($this->ratings !== null && $revadj->ratings !== null) 904 $revadj->ratings = new ReviewRating_SearchAdjustment("and", [$this->ratings, $revadj->ratings]); 905 else 906 $revadj->ratings = $this->ratings; 907 } 908 return $revadj; 909 } 910 911 function sqlexpr(SearchQueryInfo $sqi) { 912 return "true"; 913 } 914 function exec(PaperInfo $prow, PaperSearch $srch) { 915 return true; 916 } 917} 918 919class Show_SearchTerm { 920 static function parse($word, SearchWord $sword, PaperSearch $srch) { 921 $word = simplify_whitespace($word); 922 $action = $sword->kwdef->showtype; 923 if (str_starts_with($word, "-") && !$sword->kwdef->sorting) { 924 $action = false; 925 $word = substr($word, 1); 926 } 927 $f = []; 928 $viewfield = $word; 929 if ($word !== "" && $sword->kwdef->sorting) { 930 $f["sort"] = [$word]; 931 $sort = PaperSearch::parse_sorter($viewfield); 932 $viewfield = $sort->type; 933 } 934 if ($viewfield !== "" && $action !== null) 935 $f["view"] = [$viewfield => $action]; 936 return SearchTerm::make_float($f); 937 } 938 static function parse_heading($word, SearchWord $sword) { 939 return SearchTerm::make_float(["heading" => simplify_whitespace($word)]); 940 } 941} 942 943class PaperID_SearchTerm extends SearchTerm { 944 public $pids; 945 946 function __construct($pns) { 947 parent::__construct("pn"); 948 $this->pids = $pns; 949 } 950 function trivial_rights(Contact $user, PaperSearch $srch) { 951 return true; 952 } 953 function sqlexpr(SearchQueryInfo $sqi) { 954 if (empty($this->pids)) 955 return "false"; 956 else 957 return "Paper.paperId in (" . join(",", $this->pids) . ")"; 958 } 959 function exec(PaperInfo $row, PaperSearch $srch) { 960 return in_array($row->paperId, $this->pids); 961 } 962 function in_order() { 963 $pods = $this->pids; 964 sort($pods, SORT_NUMERIC); 965 return $pods == $this->pids; 966 } 967 function default_sorter($top, $thenmap, PaperSearch $srch) { 968 if ($top && !$this->in_order()) { 969 $s = ListSorter::make_field(new NumericOrderPaperColumn($srch->conf, array_flip($this->pids))); 970 $s->thenmap = $thenmap; 971 return $s; 972 } else 973 return false; 974 } 975} 976 977 978class ContactCountMatcher extends CountMatcher { 979 private $_contacts = null; 980 981 function __construct($countexpr, $contacts) { 982 parent::__construct($countexpr); 983 $this->set_contacts($contacts); 984 } 985 function contact_set() { 986 return $this->_contacts; 987 } 988 function has_contacts() { 989 return $this->_contacts !== null; 990 } 991 function has_sole_contact($cid) { 992 return $this->_contacts !== null 993 && count($this->_contacts) == 1 994 && $this->_contacts[0] == $cid; 995 } 996 function contact_match_sql($fieldname) { 997 if ($this->_contacts === null) 998 return "true"; 999 else 1000 return $fieldname . sql_in_numeric_set($this->_contacts); 1001 } 1002 function test_contact($cid) { 1003 return $this->_contacts === null || in_array($cid, $this->_contacts); 1004 } 1005 function add_contact($cid) { 1006 if ($this->_contacts === null) 1007 $this->_contacts = array(); 1008 if (!in_array($cid, $this->_contacts)) 1009 $this->_contacts[] = $cid; 1010 } 1011 function set_contacts($contacts) { 1012 assert($contacts === null || is_array($contacts) || is_int($contacts)); 1013 $this->_contacts = is_int($contacts) ? array($contacts) : $contacts; 1014 } 1015} 1016 1017class SearchQueryInfo { 1018 public $conf; 1019 public $srch; 1020 public $user; 1021 public $tables = array(); 1022 public $columns = array(); 1023 public $negated = false; 1024 private $_has_my_review = false; 1025 1026 function __construct(PaperSearch $srch) { 1027 $this->conf = $srch->conf; 1028 $this->srch = $srch; 1029 $this->user = $srch->user; 1030 } 1031 function add_table($table, $joiner = false) { 1032 // * All added tables must match at most one Paper row each, 1033 // except MyReviews and Limiter. 1034 assert($joiner || !count($this->tables)); 1035 $this->tables[$table] = $joiner; 1036 } 1037 function add_column($name, $expr) { 1038 assert(!isset($this->columns[$name]) || $this->columns[$name] === $expr); 1039 $this->columns[$name] = $expr; 1040 } 1041 function add_conflict_table() { 1042 if (!isset($this->tables["PaperConflict"])) 1043 $this->add_table("PaperConflict", ["left join", "PaperConflict", "PaperConflict.contactId=" . ($this->user->contactId ? : -100)]); 1044 } 1045 function add_conflict_columns() { 1046 $this->add_conflict_table(); 1047 $this->columns["conflictType"] = "PaperConflict.conflictType"; 1048 } 1049 function add_reviewer_columns() { 1050 $this->_has_my_review = true; 1051 } 1052 function finish_reviewer_columns() { 1053 if ($this->_has_my_review) { 1054 $this->add_conflict_columns(); 1055 if (isset($this->columns["reviewSignatures"])) { 1056 /* use that */ 1057 } else if (isset($this->tables["MyReviews"])) { 1058 $this->add_column("myReviewPermissions", PaperInfo::my_review_permissions_sql("MyReviews.")); 1059 } else if (!isset($this->tables["Limiter"])) { 1060 $this->add_table("MyReviews", ["left join", "PaperReview", $this->user->act_reviewer_sql("MyReviews")]); 1061 $this->add_column("myReviewPermissions", PaperInfo::my_review_permissions_sql("MyReviews.")); 1062 } else { 1063 $this->add_column("myReviewPermissions", "(select " . PaperInfo::my_review_permissions_sql() . " from PaperReview where PaperReview.paperId=Paper.paperId and " . $this->user->act_reviewer_sql("PaperReview") . " group by paperId)"); 1064 } 1065 } 1066 } 1067 function add_review_signature_columns() { 1068 if (!isset($this->columns["reviewSignatures"])) 1069 $this->add_column("reviewSignatures", "(select " . ReviewInfo::review_signature_sql() . " from PaperReview r where r.paperId=Paper.paperId)"); 1070 } 1071 function add_score_columns($fid) { 1072 $this->add_review_signature_columns(); 1073 if (!isset($this->columns["{$fid}Signature"]) 1074 && ($f = $this->conf->review_field($fid)) 1075 && $f->main_storage) 1076 $this->add_column("{$fid}Signature", "(select group_concat({$f->main_storage} order by reviewId) from PaperReview where PaperReview.paperId=Paper.paperId)"); 1077 } 1078 function add_review_word_count_columns() { 1079 $this->add_review_signature_columns(); 1080 if (!isset($this->columns["reviewWordCountSignature"])) 1081 $this->add_column("reviewWordCountSignature", "(select group_concat(coalesce(reviewWordCount,'.') order by reviewId) from PaperReview where PaperReview.paperId=Paper.paperId)"); 1082 } 1083 function add_rights_columns() { 1084 if (!isset($this->columns["managerContactId"])) 1085 $this->columns["managerContactId"] = "Paper.managerContactId"; 1086 if (!isset($this->columns["leadContactId"])) 1087 $this->columns["leadContactId"] = "Paper.leadContactId"; 1088 // XXX could avoid the following if user is privChair for everything: 1089 $this->add_conflict_columns(); 1090 $this->add_reviewer_columns(); 1091 } 1092 function add_allConflictType_column() { 1093 if (!isset($this->columns["allConflictType"])) 1094 $this->add_column("allConflictType", "(select group_concat(contactId, ' ', conflictType) from PaperConflict where PaperConflict.paperId=Paper.paperId)"); 1095 } 1096} 1097 1098class PaperSearch { 1099 public $conf; 1100 public $user; 1101 private $contact; 1102 public $cid; 1103 public $privChair; 1104 private $amPC; 1105 1106 private $limitName; 1107 private $fields; 1108 private $_reviewer_user = false; 1109 private $_active_limit; 1110 private $urlbase; 1111 public $warnings = array(); 1112 private $_quiet_count = 0; 1113 1114 public $q; 1115 private $_qe; 1116 public $test_review; 1117 1118 public $regex = []; 1119 public $contradictions = []; 1120 private $_match_preg; 1121 private $_match_preg_query; 1122 1123 private $contact_match = array(); 1124 public $_query_options = array(); 1125 public $_has_review_adjustment = false; 1126 private $_ssRecursion = array(); 1127 private $_allow_deleted = false; 1128 public $thenmap; 1129 public $groupmap; 1130 public $is_order_anno = false; 1131 public $highlightmap; 1132 public $viewmap; 1133 public $sorters = []; 1134 private $_default_sort; // XXX should be used more often 1135 private $_highlight_tags; 1136 1137 private $_matches; // list of ints 1138 1139 static private $_sort_keywords = ["by" => "by", "up" => "up", "down" => "down", 1140 "reverse" => "down", "reversed" => "down", "score" => ""]; 1141 1142 static public $search_type_names = [ 1143 "a" => "Your submissions", 1144 "acc" => "Accepted papers", 1145 "act" => "Active papers", 1146 "all" => "All papers", 1147 "editpref" => "Reviewable papers", 1148 "lead" => "Your discussion leads", 1149 "manager" => "Papers you administer", 1150 "r" => "Your reviews", 1151 "rable" => "Reviewable papers", 1152 "req" => "Your review requests", 1153 "reqrevs" => "Your review requests", 1154 "rout" => "Your incomplete reviews", 1155 "s" => "Submitted papers", 1156 "und" => "Undecided papers", 1157 "unm" => "Unmanaged submissions" 1158 ]; 1159 1160 1161 // NB: `$options` can come from an unsanitized user request. 1162 function __construct(Contact $user, $options) { 1163 if (is_string($options)) 1164 $options = array("q" => $options); 1165 1166 // contact facts 1167 $this->conf = $user->conf; 1168 $this->user = $user; 1169 $this->privChair = $user->privChair; 1170 $this->amPC = $user->isPC; 1171 $this->cid = $user->contactId; 1172 1173 // paper selection 1174 $ptype = (string) get($options, "t"); 1175 if ($ptype === "0") 1176 $ptype = ""; 1177 if ($ptype === "vis") 1178 $this->limitName = "vis"; 1179 else if ($this->privChair && !$ptype && $this->conf->timeUpdatePaper()) 1180 $this->limitName = "all"; 1181 else if (($user->privChair && $ptype === "act") 1182 || ($user->isPC 1183 && (!$ptype || $ptype === "act" || $ptype === "all") 1184 && $this->conf->can_pc_see_all_submissions())) 1185 $this->limitName = "act"; 1186 else if ($user->privChair && $ptype === "unm") 1187 $this->limitName = "unm"; 1188 else if ($user->isPC && (!$ptype || $ptype === "s" || $ptype === "unm")) 1189 $this->limitName = "s"; 1190 else if ($user->isPC && ($ptype === "und" || $ptype === "undec")) 1191 $this->limitName = "und"; 1192 else if ($user->isPC && ($ptype === "acc" 1193 || $ptype === "reqrevs" || $ptype === "req" 1194 || $ptype === "lead" || $ptype === "rable" 1195 || $ptype === "manager" || $ptype === "editpref")) 1196 $this->limitName = $ptype; 1197 else if ($this->privChair && ($ptype === "all" || $ptype === "unsub")) 1198 $this->limitName = $ptype; 1199 else if ($ptype === "r" || $ptype === "rout" || $ptype === "a") 1200 $this->limitName = $ptype; 1201 else if ($ptype === "rable") 1202 $this->limitName = "r"; 1203 else if (!$user->is_reviewer()) 1204 $this->limitName = "a"; 1205 else if (!$user->is_author()) 1206 $this->limitName = "r"; 1207 else 1208 $this->limitName = "ar"; 1209 1210 // default query fields 1211 // NB: If a complex query field, e.g., "re", "tag", or "option", is 1212 // default, then it must be the only default or query construction 1213 // will break. 1214 $this->fields = array(); 1215 $qtype = get($options, "qt", "n"); 1216 if ($qtype === "n" || $qtype === "ti") 1217 $this->fields["ti"] = 1; 1218 if ($qtype === "n" || $qtype === "ab") 1219 $this->fields["ab"] = 1; 1220 if ($user->can_view_some_authors() 1221 && ($qtype === "n" || $qtype === "au" || $qtype === "ac")) 1222 $this->fields["au"] = 1; 1223 if ($this->privChair && $qtype === "ac") 1224 $this->fields["co"] = 1; 1225 if ($this->amPC && $qtype === "re") 1226 $this->fields["re"] = 1; 1227 if ($this->amPC && $qtype === "tag") 1228 $this->fields["tag"] = 1; 1229 1230 // the query itself 1231 $this->q = trim(get_s($options, "q")); 1232 $this->_default_sort = get($options, "sort"); 1233 1234 // reviewer 1235 if (($reviewer = get($options, "reviewer"))) { 1236 if (is_string($reviewer)) { 1237 if (strcasecmp($reviewer, $user->email) == 0) 1238 $reviewer = $user; 1239 else if ($user->can_view_pc()) 1240 $reviewer = $this->conf->pc_member_by_email($reviewer); 1241 else 1242 $reviewer = null; 1243 } else if (!is_object($reviewer) || !($reviewer instanceof Contact)) 1244 $reviewer = null; 1245 if ($reviewer) 1246 $this->_reviewer_user = $reviewer; 1247 } 1248 1249 // URL base 1250 if (isset($options["urlbase"])) 1251 $this->urlbase = $options["urlbase"]; 1252 else 1253 $this->urlbase = $this->conf->hoturl_site_relative_raw("search", "t=" . urlencode($this->limitName)); 1254 if ($qtype !== "n") 1255 $this->urlbase = hoturl_add_raw($this->urlbase, "qt=" . urlencode($qtype)); 1256 if ($this->_reviewer_user 1257 && $this->_reviewer_user->contactId !== $user->contactId 1258 && strpos($this->urlbase, "reviewer=") === false) 1259 $this->urlbase = hoturl_add_raw($this->urlbase, "reviewer=" . urlencode($this->_reviewer_user->email)); 1260 if (strpos($this->urlbase, "&") !== false) 1261 trigger_error(caller_landmark() . " PaperSearch::urlbase should be a raw URL", E_USER_NOTICE); 1262 1263 $this->set_limit($this->limitName); 1264 } 1265 1266 private function set_limit($limit) { 1267 assert($this->_qe === null); 1268 $this->_active_limit = $limit; 1269 if ($this->_active_limit === "editpref") 1270 $this->_active_limit = "rable"; 1271 else if ($this->_active_limit === "reqrevs") 1272 $this->_active_limit = "req"; 1273 if ($this->_active_limit === "rable") { 1274 $u = $this->reviewer_user(); 1275 if ($this->privChair || $this->user === $u) { 1276 if ($u->can_accept_review_assignment_ignore_conflict(null)) { 1277 if ($this->conf->can_pc_see_all_submissions()) 1278 $this->_active_limit = "act"; 1279 else 1280 $this->_active_limit = "s"; 1281 } else if (!$u->isPC) 1282 $this->_active_limit = "r"; 1283 } 1284 } 1285 } 1286 1287 function set_allow_deleted($x) { 1288 assert($this->_qe === null); 1289 $this->_allow_deleted = $x; 1290 } 1291 1292 function __get($name) { 1293 error_log("PaperSearch::$name " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))); 1294 return $name === "contact" ? $this->user : null; 1295 } 1296 1297 function limit() { 1298 return $this->_active_limit; 1299 } 1300 function limit_submitted() { 1301 return !in_array($this->_active_limit, ["a", "ar", "act", "all", "unsub"]); 1302 } 1303 function limit_author() { 1304 return $this->_active_limit === "a"; 1305 } 1306 1307 function reviewer_user() { 1308 return $this->_reviewer_user ? : $this->user; 1309 } 1310 1311 function warn($text) { 1312 if (!$this->_quiet_count) 1313 $this->warnings[] = $text; 1314 } 1315 1316 1317 // PARSING 1318 // Transforms a search string into an expression object, possibly 1319 // including "and", "or", and "not" expressions (which point at other 1320 // expressions). 1321 1322 static function unpack_comparison($text, $quoted) { 1323 $text = trim($text); 1324 $compar = null; 1325 if (preg_match('/\A(.*?)([=!<>]=?|≠|≤|≥)\s*(\d+)\z/s', $text, $m)) { 1326 $text = $m[1]; 1327 $compar = $m[2] . $m[3]; 1328 } 1329 if (($text === "any" || $text === "" || $text === "yes") && !$quoted) 1330 return array("", $compar ? : ">0"); 1331 else if (($text === "none" || $text === "no") && !$quoted) 1332 return array("", "=0"); 1333 else if (!$compar && ctype_digit($text)) 1334 return array("", "=" . $text); 1335 else 1336 return array($text, $compar ? : ">0"); 1337 } 1338 1339 static function check_tautology($compar) { 1340 if ($compar === "<0") 1341 return new False_SearchTerm; 1342 else if ($compar === ">=0") 1343 return new True_SearchTerm; 1344 else 1345 return null; 1346 } 1347 1348 private function make_contact_match($type, $text) { 1349 foreach ($this->contact_match as $i => $cm) 1350 if ($cm->type === $type && $cm->text === $text) 1351 return $cm; 1352 return $this->contact_match[] = new ContactSearch($type, $text, $this->user); 1353 } 1354 1355 private function matching_contacts_base($type, $word, $quoted, $pc_only) { 1356 if ($pc_only) 1357 $type |= ContactSearch::F_PC; 1358 if ($quoted) 1359 $type |= ContactSearch::F_QUOTED; 1360 if (!$quoted && $this->amPC) 1361 $type |= ContactSearch::F_TAG; 1362 $scm = $this->make_contact_match($type, $word); 1363 if ($scm->warn_html) 1364 $this->warn($scm->warn_html); 1365 return $scm->ids; 1366 } 1367 function matching_users($word, $quoted, $pc_only) { 1368 $cids = $this->matching_contacts_base(ContactSearch::F_USER, $word, $quoted, $pc_only); 1369 return empty($cids) ? [] : $cids; 1370 } 1371 function matching_special_contacts($word, $quoted, $pc_only) { 1372 $cids = $this->matching_contacts_base(0, $word, $quoted, $pc_only); 1373 return $cids === false ? null : (empty($cids) ? [] : $cids); 1374 } 1375 1376 static function decision_matchexpr(Conf $conf, $word, $flag) { 1377 $lword = strtolower($word); 1378 if (!($flag & Text::SEARCH_NO_SPECIAL)) { 1379 if ($lword === "yes") 1380 return ">0"; 1381 else if ($lword === "no") 1382 return "<0"; 1383 else if ($lword === "?" || $lword === "none" 1384 || $lword === "unknown" || $lword === "unspecified" 1385 || $lword === "undecided") 1386 return [0]; 1387 else if ($lword === "any") 1388 return "!=0"; 1389 } 1390 return array_keys(Text::simple_search($word, $conf->decision_map(), $flag)); 1391 } 1392 1393 static function status_field_matcher(Conf $conf, $word, $quoted = null) { 1394 if (strlen($word) >= 3 1395 && ($k = Text::simple_search($word, ["w0" => "withdrawn", "s0" => "submitted", "s1" => "ready", "u0" => "in progress", "u1" => "unsubmitted", "u2" => "not ready", "a0" => "active", "x0" => "no submission"]))) { 1396 $k = array_map(function ($x) { return $x[0]; }, array_keys($k)); 1397 $k = array_unique($k); 1398 if (count($k) === 1) { 1399 if ($k[0] === "w") 1400 return ["timeWithdrawn", ">0"]; 1401 else if ($k[0] === "s") 1402 return ["timeSubmitted", ">0"]; 1403 else if ($k[0] === "u") 1404 return ["timeSubmitted", "<=0", "timeWithdrawn", "<=0"]; 1405 else if ($k[0] === "x") 1406 return ["timeSubmitted", "<=0", "timeWithdrawn", "<=0", "paperStorageId", "<=1"]; 1407 else 1408 return ["timeWithdrawn", "<=0"]; 1409 } 1410 } 1411 $flag = $quoted ? Text::SEARCH_NO_SPECIAL : Text::SEARCH_UNPRIVILEGE_EXACT; 1412 return ["outcome", self::decision_matchexpr($conf, $word, $flag)]; 1413 } 1414 1415 static function parse_reconflict($word, SearchWord $sword, PaperSearch $srch) { 1416 // `reconf:` keyword, defined in `etc/searchkeywords.json` 1417 $args = array(); 1418 while (preg_match('/\A\s*#?(\d+)(?:-#?(\d+))?\s*,?\s*(.*)\z/s', $word, $m)) { 1419 $m[2] = (isset($m[2]) && $m[2] ? $m[2] : $m[1]); 1420 foreach (range($m[1], $m[2]) as $p) 1421 $args[$p] = true; 1422 $word = $m[3]; 1423 } 1424 if ($word !== "" || empty($args)) { 1425 $srch->warn("The <code>reconflict</code> keyword expects a list of paper numbers."); 1426 return new False_SearchTerm; 1427 } else if (!$srch->user->privChair) 1428 return new False_SearchTerm; 1429 else { 1430 $result = $srch->conf->qe("select distinct contactId from PaperReview where paperId in (" . join(", ", array_keys($args)) . ")"); 1431 $contacts = array_map("intval", Dbl::fetch_first_columns($result)); 1432 return new Conflict_SearchTerm(">0", $contacts, $srch->user); 1433 } 1434 } 1435 1436 static function parse_has($word, SearchWord $sword, PaperSearch $srch) { 1437 $lword = strtolower($word); 1438 if (($kwdef = $srch->conf->search_keyword($lword, $srch->user))) { 1439 if (get($kwdef, "parse_has_callback")) 1440 $qe = call_user_func($kwdef->parse_has_callback, $word, $sword, $srch); 1441 else if (get($kwdef, "has")) { 1442 $sword2 = new SearchWord($kwdef->has); 1443 $sword2->kwexplicit = true; 1444 $sword2->keyword = $lword; 1445 $sword2->kwdef = $kwdef; 1446 $qe = call_user_func($kwdef->parse_callback, $kwdef->has, $sword2, $srch); 1447 } else 1448 $qe = null; 1449 if ($qe && $sword->keyword === "no") { 1450 if (is_array($qe)) 1451 $qe = SearchTerm::make_op("or", $qe); 1452 $qe = SearchTerm::make_not($qe); 1453 } 1454 if ($qe) 1455 return $qe; 1456 } 1457 $srch->warn("Unknown search “" . $sword->keyword . ":" . htmlspecialchars($word) . "”."); 1458 return new False_SearchTerm; 1459 } 1460 1461 static function parse_sorter($text) { 1462 $text = simplify_whitespace($text); 1463 $sort = ListSorter::make_empty($text === ""); 1464 if (($ch1 = substr($text, 0, 1)) === "-" || $ch1 === "+") { 1465 $sort->reverse = $ch1 === "-"; 1466 $text = ltrim(substr($text, 1)); 1467 } 1468 1469 // separate text into words 1470 $words = array(); 1471 $bypos = false; 1472 while (true) { 1473 preg_match('{\A[,\s]*([^\s\(,]*)(.*)\z}s', $text, $m); 1474 if ($m[1] === "" && $m[2] === "") 1475 break; 1476 if (substr($m[2], 0, 1) === "(") { 1477 $pos = SearchSplitter::span_balanced_parens($m[2]); 1478 $m[1] .= substr($m[2], 0, $pos); 1479 $m[2] = substr($m[2], $pos); 1480 } 1481 $words[] = $m[1]; 1482 $text = ltrim($m[2]); 1483 if ($m[1] === "by" && $bypos === false) 1484 $bypos = count($words) - 1; 1485 } 1486 1487 // go over words 1488 $next_words = array(); 1489 for ($i = 0; $i != count($words); ++$i) { 1490 $w = $words[$i]; 1491 if ($bypos === false || $i > $bypos) { 1492 if (($x = get(self::$_sort_keywords, $w)) !== null) { 1493 if ($x === "up") 1494 $sort->reverse = false; 1495 else if ($x === "down") 1496 $sort->reverse = true; 1497 continue; 1498 } else if (($x = ListSorter::canonical_short_score_sort($w))) { 1499 $sort->score = $x; 1500 continue; 1501 } 1502 } 1503 if ($bypos === false || $i < $bypos) 1504 $next_words[] = $w; 1505 } 1506 1507 if (!empty($next_words)) 1508 $sort->type = join(" ", $next_words); 1509 return $sort; 1510 } 1511 1512 private function _expand_saved_search($word, $recursion) { 1513 if (isset($recursion[$word])) 1514 return false; 1515 $t = $this->conf->setting_data("ss:" . $word, ""); 1516 $search = json_decode($t); 1517 if ($search && is_object($search) && isset($search->q)) 1518 return $search->q; 1519 else 1520 return null; 1521 } 1522 1523 static function parse_saved_search($word, SearchWord $sword, PaperSearch $srch) { 1524 if (!$srch->user->isPC) 1525 return null; 1526 if (($nextq = $srch->_expand_saved_search($word, $srch->_ssRecursion))) { 1527 $srch->_ssRecursion[$word] = true; 1528 $qe = $srch->_search_expression($nextq); 1529 unset($srch->_ssRecursion[$word]); 1530 } else 1531 $qe = null; 1532 if (!$qe && $nextq === false) 1533 $srch->warn("Saved search “" . htmlspecialchars($word) . "” is defined in terms of itself."); 1534 else if (!$qe && !$srch->conf->setting_data("ss:$word")) 1535 $srch->warn("There is no “" . htmlspecialchars($word) . "” saved search."); 1536 else if (!$qe) 1537 $srch->warn("The “" . htmlspecialchars($word) . "” saved search is defined incorrectly."); 1538 $qe = $qe ? : new False_SearchTerm; 1539 if ($nextq) 1540 $qe->set_strspan_owner($nextq); 1541 return $qe; 1542 } 1543 1544 private function _search_keyword(&$qt, SearchWord $sword, $keyword, $kwexplicit) { 1545 $word = $sword->word; 1546 $sword->keyword = $keyword; 1547 $sword->kwexplicit = $kwexplicit; 1548 $sword->kwdef = $this->conf->search_keyword($keyword, $this->user); 1549 if ($sword->kwdef && get($sword->kwdef, "parse_callback")) { 1550 $qx = call_user_func($sword->kwdef->parse_callback, $word, $sword, $this); 1551 if ($qx && !is_array($qx)) 1552 $qt[] = $qx; 1553 else if ($qx) 1554 $qt = array_merge($qt, $qx); 1555 } else 1556 $this->warn("Unrecognized keyword “" . htmlspecialchars($keyword) . "”."); 1557 } 1558 1559 private function _search_word($word, $defkw) { 1560 // check for paper numbers 1561 if (preg_match('/\A(?:#?\d+(?:(?:-|–|—)#?\d+)?(?:\s*,\s*|\z))+\z/', $word)) { 1562 $range = []; 1563 while (preg_match('/\A#?(\d+)(?:(?:-|–|—)#?(\d+))?\s*,?\s*(.*)\z/', $word, $m)) { 1564 $m[2] = (isset($m[2]) && $m[2] ? $m[2] : $m[1]); 1565 $range = array_merge($range, range(intval($m[1]), intval($m[2]))); 1566 $word = $m[3]; 1567 } 1568 return new PaperID_SearchTerm($range); 1569 } 1570 1571 // check for `#TAG` 1572 if (substr($word, 0, 1) === "#") { 1573 ++$this->_quiet_count; 1574 $qe = $this->_search_word("hashtag:" . substr($word, 1), $defkw); 1575 --$this->_quiet_count; 1576 if (!$qe->is_false()) 1577 return $qe; 1578 } 1579 1580 $keyword = $defkw; 1581 if (preg_match('/\A([-_.a-zA-Z0-9]+|"[^"]+")((?:[=!<>]=?|≠|≤|≥)[^:]+|:.*)\z/', $word, $m)) { 1582 if ($m[2][0] === ":") { 1583 $keyword = $m[1]; 1584 $word = ltrim((string) substr($m[2], 1)); 1585 } else { 1586 // Allow searches like "ovemer>2"; parse as "ovemer:>2". 1587 ++$this->_quiet_count; 1588 $qe = $this->_search_word($m[1] . ":" . $m[2], $defkw); 1589 --$this->_quiet_count; 1590 if (!$qe->is_false()) 1591 return $qe; 1592 } 1593 } 1594 if ($keyword && $keyword[0] === '"') 1595 $keyword = trim(substr($keyword, 1, strlen($keyword) - 2)); 1596 1597 $qt = []; 1598 $sword = new SearchWord($word); 1599 if ($keyword) 1600 $this->_search_keyword($qt, $sword, $keyword, true); 1601 else { 1602 // Special-case unquoted "*", "ANY", "ALL", "NONE", "". 1603 if ($word === "*" || $word === "ANY" || $word === "ALL" 1604 || $word === "") 1605 return new True_SearchTerm; 1606 else if ($word === "NONE") 1607 return new False_SearchTerm; 1608 // Otherwise check known keywords. 1609 foreach ($this->fields as $kw => $x) 1610 $this->_search_keyword($qt, $sword, $kw, false); 1611 } 1612 return SearchTerm::make_op("or", $qt); 1613 } 1614 1615 static function escape_word($str) { 1616 $pos = SearchSplitter::span_balanced_parens($str); 1617 if ($pos === strlen($str)) 1618 return $str; 1619 else 1620 return "\"" . str_replace("\"", "\\\"", $str) . "\""; 1621 } 1622 1623 static private function _shift_keyword($splitter, $curqe) { 1624 if (!$splitter->match('/\A(?:[-+!()]|(?:AND|and|OR|or|NOT|not|THEN|then|HIGHLIGHT(?::\w+)?)(?=[\s\(]))/s', $m)) 1625 return null; 1626 $op = SearchOperator::get(strtoupper($m[0])); 1627 if (!$op) { 1628 $colon = strpos($m[0], ":"); 1629 $op = clone SearchOperator::get(strtoupper(substr($m[0], 0, $colon))); 1630 $op->opinfo = substr($m[0], $colon + 1); 1631 } 1632 if ($curqe && $op->unary) 1633 return null; 1634 $splitter->shift_past($m[0]); 1635 return $op; 1636 } 1637 1638 static private function _shift_word($splitter, Conf $conf) { 1639 if (($x = $splitter->shift()) === "") 1640 return $x; 1641 // `HEADING x` parsed as `HEADING:x` 1642 if ($x === "HEADING") { 1643 $lspan = $splitter->strspan[0]; 1644 $x .= ":" . $splitter->shift(); 1645 $splitter->strspan[0] = $lspan; 1646 return $x; 1647 } 1648 // some keywords may be followed by parentheses 1649 if (strpos($x, ":") 1650 && preg_match('/\A([-_.a-zA-Z0-9]+:|"[^"]+":)(?=[^"]|\z)/', $x, $m)) { 1651 if ($m[1][0] === "\"") 1652 $kw = substr($m[1], 1, strlen($m[1]) - 2); 1653 else 1654 $kw = substr($m[1], 0, strlen($m[1]) - 1); 1655 if (($kwdef = $conf->search_keyword($kw)) 1656 && $splitter->starts_with("(") 1657 && get($kwdef, "allow_parens")) { 1658 $lspan = $splitter->strspan[0]; 1659 $x .= $splitter->shift_balanced_parens(); 1660 $splitter->strspan[0] = $lspan; 1661 } 1662 } 1663 return $x; 1664 } 1665 1666 static private function _pop_expression_stack($curqe, &$stack) { 1667 $x = array_pop($stack); 1668 if (!$curqe) 1669 return $x->leftqe; 1670 if ($x->leftqe) 1671 $curqe = SearchTerm::make_op($x->op, [$x->leftqe, $curqe]); 1672 else if ($x->op->op !== "+" && $x->op->op !== "(") 1673 $curqe = SearchTerm::make_op($x->op, [$curqe]); 1674 $curqe->apply_strspan($x->strspan); 1675 return $curqe; 1676 } 1677 1678 private function _search_expression($str) { 1679 $stack = array(); 1680 $defkwstack = array(); 1681 $defkw = $next_defkw = null; 1682 $parens = 0; 1683 $curqe = null; 1684 $splitter = new SearchSplitter($str); 1685 1686 while (!$splitter->is_empty()) { 1687 $op = self::_shift_keyword($splitter, $curqe); 1688 if ($curqe && !$op) 1689 $op = SearchOperator::get("SPACE"); 1690 if (!$curqe && $op && $op->op === "highlight") { 1691 $curqe = new True_SearchTerm; 1692 $curqe->set_float("strspan", [$splitter->strspan[0], $splitter->strspan[0]]); 1693 } 1694 1695 if (!$op) { 1696 $word = self::_shift_word($splitter, $this->conf); 1697 // Bare any-case "all", "any", "none" are treated as keywords. 1698 if (!$curqe 1699 && (empty($stack) || $stack[count($stack) - 1]->op->precedence <= 2) 1700 && ($uword = strtoupper($word)) 1701 && ($uword === "ALL" || $uword === "ANY" || $uword === "NONE") 1702 && $splitter->match('/\A(?:|(?:THEN|then|HIGHLIGHT(?::\w+)?)(?:\s|\().*)\z/')) 1703 $word = $uword; 1704 // Search like "ti:(foo OR bar)" adds a default keyword. 1705 if ($word[strlen($word) - 1] === ":" 1706 && preg_match('/\A(?:[-_.a-zA-Z0-9]+:|"[^"]+":)\z/', $word) 1707 && $splitter->starts_with("(")) 1708 $next_defkw = [substr($word, 0, strlen($word) - 1), $splitter->strspan[0]]; 1709 else { 1710 // The heart of the matter. 1711 $curqe = $this->_search_word($word, $defkw); 1712 if (!$curqe->is_uninteresting()) 1713 $curqe->set_float("strspan", $splitter->strspan); 1714 } 1715 } else if ($op->op === ")") { 1716 while (!empty($stack) 1717 && $stack[count($stack) - 1]->op->op !== "(") 1718 $curqe = self::_pop_expression_stack($curqe, $stack); 1719 if (!empty($stack)) { 1720 $stack[count($stack) - 1]->strspan[1] = $splitter->strspan[1]; 1721 $curqe = self::_pop_expression_stack($curqe, $stack); 1722 --$parens; 1723 $defkw = array_pop($defkwstack); 1724 } 1725 } else if ($op->op === "(") { 1726 assert(!$curqe); 1727 $stkelem = (object) ["op" => $op, "leftqe" => null, "strspan" => $splitter->strspan]; 1728 $defkwstack[] = $defkw; 1729 if ($next_defkw) { 1730 $defkw = $next_defkw[0]; 1731 $stkelem->strspan[0] = $next_defkw[1]; 1732 $next_defkw = null; 1733 } 1734 $stack[] = $stkelem; 1735 ++$parens; 1736 } else if ($op->unary || $curqe) { 1737 $end_precedence = $op->precedence - ($op->precedence <= 1); 1738 while (!empty($stack) 1739 && $stack[count($stack) - 1]->op->precedence > $end_precedence) 1740 $curqe = self::_pop_expression_stack($curqe, $stack); 1741 $stack[] = (object) ["op" => $op, "leftqe" => $curqe, "strspan" => $splitter->strspan]; 1742 $curqe = null; 1743 } 1744 } 1745 1746 while (!empty($stack)) 1747 $curqe = self::_pop_expression_stack($curqe, $stack); 1748 return $curqe; 1749 } 1750 1751 1752 static private function _pop_canonicalize_stack($curqe, &$stack) { 1753 $x = array_pop($stack); 1754 if ($curqe) 1755 $x->qe[] = $curqe; 1756 if (!count($x->qe)) 1757 return null; 1758 if ($x->op->unary) { 1759 $qe = $x->qe[0]; 1760 if ($x->op->op === "not") { 1761 if (preg_match('/\A(?:[(-]|NOT )/i', $qe)) 1762 $qe = "NOT $qe"; 1763 else 1764 $qe = "-$qe"; 1765 } 1766 return $qe; 1767 } else if (count($x->qe) == 1) 1768 return $x->qe[0]; 1769 else if ($x->op->op === "space") 1770 return "(" . join(" ", $x->qe) . ")"; 1771 else 1772 return "(" . join(" " . $x->op->unparse() . " ", $x->qe) . ")"; 1773 } 1774 1775 static private function _canonical_expression($str, $type, Conf $conf) { 1776 $str = trim((string) $str); 1777 if ($str === "") 1778 return ""; 1779 1780 $stack = array(); 1781 $parens = 0; 1782 $defaultop = $type === "all" ? "SPACE" : "XOR"; 1783 $curqe = null; 1784 $t = ""; 1785 $splitter = new SearchSplitter($str); 1786 1787 while (!$splitter->is_empty()) { 1788 $op = self::_shift_keyword($splitter, $curqe); 1789 if ($curqe && !$op) 1790 $op = SearchOperator::get($parens ? "SPACE" : $defaultop); 1791 if (!$op) { 1792 $curqe = self::_shift_word($splitter, $conf); 1793 } else if ($op->op === ")") { 1794 while (count($stack) 1795 && $stack[count($stack) - 1]->op->op !== "(") 1796 $curqe = self::_pop_canonicalize_stack($curqe, $stack); 1797 if (count($stack)) { 1798 array_pop($stack); 1799 --$parens; 1800 } 1801 } else if ($op->op === "(") { 1802 assert(!$curqe); 1803 $stack[] = (object) array("op" => $op, "qe" => array()); 1804 ++$parens; 1805 } else { 1806 $end_precedence = $op->precedence - ($op->precedence <= 1); 1807 while (count($stack) 1808 && $stack[count($stack) - 1]->op->precedence > $end_precedence) 1809 $curqe = self::_pop_canonicalize_stack($curqe, $stack); 1810 $top = count($stack) ? $stack[count($stack) - 1] : null; 1811 if ($top && !$op->unary && $top->op->op === $op->op) 1812 $top->qe[] = $curqe; 1813 else 1814 $stack[] = (object) array("op" => $op, "qe" => array($curqe)); 1815 $curqe = null; 1816 } 1817 } 1818 1819 if ($type === "none") 1820 array_unshift($stack, (object) array("op" => SearchOperator::get("NOT"), "qe" => array())); 1821 while (count($stack)) 1822 $curqe = self::_pop_canonicalize_stack($curqe, $stack); 1823 return $curqe; 1824 } 1825 1826 static function canonical_query($qa, $qo, $qx, Conf $conf) { 1827 $x = array(); 1828 if (($qa = self::_canonical_expression($qa, "all", $conf)) !== "") 1829 $x[] = $qa; 1830 if (($qo = self::_canonical_expression($qo, "any", $conf)) !== "") 1831 $x[] = $qo; 1832 if (($qx = self::_canonical_expression($qx, "none", $conf)) !== "") 1833 $x[] = $qx; 1834 if (count($x) == 1) 1835 return preg_replace('/\A\((.*)\)\z/', '$1', $x[0]); 1836 else 1837 return join(" AND ", $x); 1838 } 1839 1840 1841 // CLEANING 1842 // Clean an input expression series into clauses. The basic purpose of 1843 // this step is to combine all paper numbers into a single group, and to 1844 // assign review adjustments (rates & rounds). 1845 1846 1847 // QUERY CONSTRUCTION 1848 // Build a database query corresponding to an expression. 1849 // The query may be liberal (returning more papers than actually match); 1850 // QUERY EVALUATION makes it precise. 1851 1852 static function unusableRatings(Contact $user) { 1853 if ($user->privChair || $user->conf->setting("pc_seeallrev")) 1854 return array(); 1855 $noratings = array(); 1856 $rateset = $user->conf->setting("rev_rating"); 1857 if ($rateset == REV_RATINGS_PC) 1858 $npr_constraint = "reviewType>" . REVIEW_EXTERNAL; 1859 else 1860 $npr_constraint = "true"; 1861 // This query supposedly returns those reviewIds whose ratings 1862 // are not visible to the current querier 1863 $result = $user->conf->qe("select MPR.reviewId 1864 from PaperReview as MPR 1865 left join (select paperId, count(reviewId) as numReviews from PaperReview where $npr_constraint and reviewNeedsSubmit=0 group by paperId) as NPR on (NPR.paperId=MPR.paperId) 1866 left join (select paperId, count(rating) as numRatings from PaperReview join ReviewRating using (paperId,reviewId) group by paperId) as NRR on (NRR.paperId=MPR.paperId) 1867 where MPR.contactId={$user->contactId} 1868 and numReviews<=2 1869 and numRatings<=2"); 1870 return Dbl::fetch_first_columns($result); 1871 } 1872 1873 1874 // QUERY EVALUATION 1875 // Check the results of the query, reducing the possibly conservative 1876 // overestimate produced by the database to a precise result. 1877 1878 private function _add_deleted_papers($qe) { 1879 if ($qe->type === "or" || $qe->type === "then") { 1880 foreach ($qe->child as $subt) 1881 $this->_add_deleted_papers($subt); 1882 } else if ($qe->type === "pn") { 1883 foreach ($qe->pids as $p) 1884 if (array_search($p, $this->_matches) === false) 1885 $this->_matches[] = (int) $p; 1886 } 1887 } 1888 1889 1890 // BASIC QUERY FUNCTION 1891 1892 private function _add_sorters($qe, $thenmap) { 1893 if (($sorters = $qe->get_float("sort"))) { 1894 foreach ($sorters as $s) 1895 if (($s = self::parse_sorter($s))) { 1896 $s->thenmap = $thenmap; 1897 $this->sorters[] = $s; 1898 } 1899 } else if (($s = $qe->default_sorter(true, $thenmap, $this))) { 1900 $this->sorters[] = $s; 1901 } 1902 } 1903 1904 private function _assign_order_anno_group($g, $dt, $anno_index) { 1905 if (($ta = $dt->order_anno_entry($anno_index))) 1906 $this->groupmap[$g] = $ta; 1907 else if (!isset($this->groupmap[$g])) { 1908 $ta = new TagAnno; 1909 $ta->tag = $dt->tag; 1910 $ta->heading = ""; 1911 $this->groupmap[$g] = $ta; 1912 } 1913 } 1914 1915 private function _find_order_anno_tag($qe) { 1916 $thetag = null; 1917 foreach ($this->sorters as $sorter) { 1918 $tag = $sorter->type ? Tagger::check_tag_keyword($sorter->type, $this->user, Tagger::NOVALUE | Tagger::ALLOWCONTACTID) : false; 1919 $ok = $tag && ($thetag === null || $thetag === $tag); 1920 $thetag = $ok ? $tag : false; 1921 } 1922 if (!$thetag) 1923 return false; 1924 $dt = $this->conf->tags()->add(TagInfo::base($tag)); 1925 if ($dt->has_order_anno()) 1926 return $dt; 1927 foreach ($qe->get_float("view", []) as $vk => $action) { 1928 if ($action === "edit" 1929 && ($t = Tagger::check_tag_keyword($vk, $this->user, Tagger::NOVALUE | Tagger::ALLOWCONTACTID | Tagger::NOTAGKEYWORD)) 1930 && strcasecmp($t, $dt->tag) == 0) 1931 return $dt; 1932 } 1933 return false; 1934 } 1935 1936 private function _check_order_anno($qe, $rowset) { 1937 if (!($dt = $this->_find_order_anno_tag($qe))) 1938 return false; 1939 $this->is_order_anno = $dt->tag; 1940 1941 $tag_order = []; 1942 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 1943 foreach ($this->_matches as $pid) { 1944 $row = $rowset->get($pid); 1945 if ($row->has_viewable_tag($dt->tag, $this->user)) 1946 $tag_order[] = [$row->paperId, $row->tag_value($dt->tag)]; 1947 else 1948 $tag_order[] = [$row->paperId, TAG_INDEXBOUND]; 1949 } 1950 $this->user->set_overrides($old_overrides); 1951 usort($tag_order, "TagInfo::id_index_compar"); 1952 1953 $this->thenmap = []; 1954 $this->_assign_order_anno_group(0, $dt, -1); 1955 $this->groupmap[0]->heading = "none"; 1956 $cur_then = $aidx = $tidx = 0; 1957 $alist = $dt->order_anno_list(); 1958 while ($aidx < count($alist) || $tidx < count($tag_order)) { 1959 if ($tidx == count($tag_order) 1960 || ($aidx < count($alist) && $alist[$aidx]->tagIndex <= $tag_order[$tidx][1])) { 1961 if ($cur_then != 0 || $tidx != 0 || $aidx != 0) 1962 ++$cur_then; 1963 $this->_assign_order_anno_group($cur_then, $dt, $aidx); 1964 ++$aidx; 1965 } else { 1966 $this->thenmap[$tag_order[$tidx][0]] = $cur_then; 1967 ++$tidx; 1968 } 1969 } 1970 } 1971 1972 function term() { 1973 if ($this->_qe !== null) 1974 return $this->_qe; 1975 1976 // parse and clean the query 1977 $qe = $this->_search_expression($this->q); 1978 //Conf::msg_debugt(json_encode($qe->debug_json())); 1979 if (!$qe) 1980 $qe = new True_SearchTerm; 1981 1982 // apply review rounds (top down, needs separate step) 1983 if ($this->_has_review_adjustment) 1984 $qe = $qe->adjust_reviews(null, $this); 1985 1986 // extract regular expressions and set _reviewer if the query is 1987 // about exactly one reviewer, and warn about contradictions 1988 $qe->extract_metadata(true, $this); 1989 foreach ($this->contradictions as $contradiction => $garbage) 1990 $this->warn($contradiction); 1991 1992 return ($this->_qe = $qe); 1993 } 1994 1995 private function _prepare_result($qe) { 1996 $sqi = new SearchQueryInfo($this); 1997 $sqi->add_table("Paper"); 1998 $sqi->add_column("paperId", "Paper.paperId"); 1999 // always include columns needed by rights machinery 2000 $sqi->add_column("timeSubmitted", "Paper.timeSubmitted"); 2001 $sqi->add_column("timeWithdrawn", "Paper.timeWithdrawn"); 2002 $sqi->add_column("outcome", "Paper.outcome"); 2003 if ($this->conf->has_any_lead_or_shepherd()) 2004 $sqi->add_column("leadContactId", "Paper.leadContactId"); 2005 2006 $filters = [$qe->sqlexpr($sqi)]; 2007 //Conf::msg_debugt(var_export($filters, true)); 2008 if ($filters[0] === "false") 2009 return [null, false]; 2010 2011 // status limitation parts 2012 $limit = $this->limit(); 2013 if ($limit === "s" 2014 || $limit === "req" 2015 || $limit === "acc" 2016 || $limit === "und" 2017 || $limit === "unm" 2018 || ($limit === "rable" && !$this->conf->can_pc_see_all_submissions())) 2019 $filters[] = "Paper.timeSubmitted>0"; 2020 else if ($limit === "act" 2021 || $limit === "r" 2022 || $limit === "rable") 2023 $filters[] = "Paper.timeWithdrawn<=0"; 2024 else if ($limit === "unsub") 2025 $filters[] = "(Paper.timeSubmitted<=0 and Paper.timeWithdrawn<=0)"; 2026 else if ($limit === "lead") 2027 $filters[] = "Paper.leadContactId=" . $this->cid; 2028 else if ($limit === "manager") { 2029 if ($this->user->is_track_manager()) 2030 $filters[] = "(Paper.managerContactId=" . $this->cid . " or Paper.managerContactId=0)"; 2031 else 2032 $filters[] = "Paper.managerContactId=" . $this->cid; 2033 $filters[] = "Paper.timeSubmitted>0"; 2034 $sqi->add_conflict_table(); 2035 } 2036 2037 // decision limitation parts 2038 if ($limit === "acc") 2039 $filters[] = "Paper.outcome>0"; 2040 else if ($limit === "und") 2041 $filters[] = "Paper.outcome=0"; 2042 2043 // other search limiters 2044 if ($limit === "a") 2045 $filters[] = $this->user->act_author_view_sql("PaperConflict"); 2046 else if ($limit === "r") 2047 $sqi->add_table("MyReviews", ["join", "PaperReview", $this->user->act_reviewer_sql("MyReviews")]); 2048 else if ($limit === "ar") { 2049 $sqi->add_table("MyReviews", ["left join", "PaperReview", $this->user->act_reviewer_sql("MyReviews")]); 2050 $filters[] = "(" . $this->user->act_author_view_sql("PaperConflict") . " or (Paper.timeWithdrawn<=0 and MyReviews.reviewType is not null))"; 2051 } else if ($limit === "rout") 2052 $sqi->add_table("Limiter", ["join", "PaperReview", $this->user->act_reviewer_sql("Limiter") . " and reviewNeedsSubmit!=0"]); 2053 else if ($limit === "req") 2054 $sqi->add_table("Limiter", ["join", "PaperReview", "Limiter.requestedBy=$this->cid and Limiter.reviewType=" . REVIEW_EXTERNAL]); 2055 else if ($limit === "unm") 2056 $filters[] = "Paper.managerContactId=0"; 2057 else if ($this->q === "re:me") 2058 $sqi->add_table("MyReviews", ["join", "PaperReview", $this->user->act_reviewer_sql("MyReviews")]); 2059 2060 if ($limit === "a" || $limit === "ar") 2061 $sqi->add_conflict_table(); 2062 if ($limit === "r" || $limit === "ar" || $limit === "rout" || $this->q === "re:me") 2063 $sqi->add_reviewer_columns(); 2064 2065 // add permissions tables if we will filter the results 2066 $need_filter = !$qe->trivial_rights($this->user, $this) 2067 || !$this->trivial_limit() 2068 || $this->conf->has_tracks() /* XXX probably only need check_track_view_sensitivity */ 2069 || $qe->type === "then" 2070 || $qe->get_float("heading"); 2071 2072 if ($need_filter) 2073 $sqi->add_rights_columns(); 2074 // XXX some of this should be shared with paperQuery 2075 if (($need_filter && $this->conf->has_track_tags()) 2076 || get($this->_query_options, "tags") 2077 || ($this->user->privChair 2078 && $this->conf->has_any_manager() 2079 && $this->conf->tags()->has_sitewide)) 2080 $sqi->add_column("paperTags", "(select group_concat(' ', tag, '#', tagIndex separator '') from PaperTag where PaperTag.paperId=Paper.paperId)"); 2081 if (get($this->_query_options, "reviewSignatures")) 2082 $sqi->add_review_signature_columns(); 2083 foreach (get($this->_query_options, "scores", []) as $f) 2084 $sqi->add_score_columns($f); 2085 if (get($this->_query_options, "reviewWordCounts")) 2086 $sqi->add_review_word_count_columns(); 2087 if ($this->conf->submission_blindness() == Conf::BLIND_OPTIONAL) 2088 $sqi->add_column("blind", "Paper.blind"); 2089 2090 // create query 2091 $sqi->finish_reviewer_columns(); 2092 $q = "select "; 2093 foreach ($sqi->columns as $colname => $value) 2094 $q .= $value . " " . $colname . ", "; 2095 $q = substr($q, 0, strlen($q) - 2) . "\n from "; 2096 foreach ($sqi->tables as $tabname => $value) 2097 if (!$value) 2098 $q .= $tabname; 2099 else { 2100 $joiners = array("$tabname.paperId=Paper.paperId"); 2101 for ($i = 2; $i < count($value); ++$i) 2102 if ($value[$i]) 2103 $joiners[] = "(" . $value[$i] . ")"; 2104 $q .= "\n " . $value[0] . " " . $value[1] . " as " . $tabname 2105 . " on (" . join("\n and ", $joiners) . ")"; 2106 } 2107 if (!empty($filters)) 2108 $q .= "\n where " . join("\n and ", $filters); 2109 $q .= "\n group by Paper.paperId"; 2110 2111 //Conf::msg_debugt($q); 2112 //error_log($q); 2113 2114 // actually perform query 2115 return [$this->conf->qe_raw($q), $need_filter]; 2116 } 2117 2118 private function _prepare() { 2119 if ($this->_matches !== null) 2120 return; 2121 2122 if ($this->limit() === "x") { 2123 $this->_matches = []; 2124 return true; 2125 } 2126 2127 $qe = $this->term(); 2128 //Conf::msg_debugt(json_encode($qe->debug_json())); 2129 2130 // collect papers 2131 list($result, $need_filter) = $this->_prepare_result($qe); 2132 $rowset = new PaperInfoSet; 2133 while (($row = PaperInfo::fetch($result, $this->user))) 2134 $rowset->add($row); 2135 Dbl::free($result); 2136 2137 // correct query, create thenmap, groupmap, highlightmap 2138 $need_then = $qe->type === "then"; 2139 $this->thenmap = null; 2140 if ($need_then && $qe->nthen > 1) 2141 $this->thenmap = array(); 2142 $this->highlightmap = array(); 2143 $this->_matches = array(); 2144 if ($need_filter) { 2145 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 2146 foreach ($rowset->all() as $row) { 2147 if (!$this->test_limit($row)) 2148 $x = false; 2149 else if ($need_then) { 2150 $x = false; 2151 for ($i = 0; $i < $qe->nthen && $x === false; ++$i) 2152 if ($qe->child[$i]->exec($row, $this)) 2153 $x = $i; 2154 } else 2155 $x = !!$qe->exec($row, $this); 2156 if ($x === false) 2157 continue; 2158 $this->_matches[] = $row->paperId; 2159 if ($this->thenmap !== null) 2160 $this->thenmap[$row->paperId] = $x; 2161 if ($need_then) { 2162 for ($j = $qe->nthen; $j < count($qe->child); ++$j) 2163 if ($qe->child[$j]->exec($row, $this) 2164 && ($qe->highlights[$j - $qe->nthen] & (1 << $x))) 2165 $this->highlightmap[$row->paperId][] = $qe->highlight_types[$j - $qe->nthen]; 2166 } 2167 } 2168 $this->user->set_overrides($old_overrides); 2169 } else { 2170 $this->_matches = $rowset->paper_ids(); 2171 } 2172 2173 // add deleted papers explicitly listed by number (e.g. action log) 2174 if ($this->_allow_deleted) 2175 $this->_add_deleted_papers($qe); 2176 2177 // view and sort information 2178 $this->viewmap = $qe->get_float("view", array()); 2179 $this->_add_sorters($qe, null); 2180 if ($qe->type === "then") 2181 for ($i = 0; $i < $qe->nthen; ++$i) 2182 $this->_add_sorters($qe->child[$i], $this->thenmap ? $i : null); 2183 2184 // group information 2185 $this->groupmap = []; 2186 $sole_qe = $qe; 2187 if ($qe->type === "then") 2188 $sole_qe = $qe->nthen == 1 ? $qe->child[0] : null; 2189 if (!$sole_qe) { 2190 for ($i = 0; $i < $qe->nthen; ++$i) { 2191 $h = $qe->child[$i]->get_float("heading"); 2192 if ($h === null) { 2193 $span = $qe->child[$i]->get_float("strspan"); 2194 $spanstr = $qe->child[$i]->get_float("strspan_owner", $this->q); 2195 $h = rtrim(substr($spanstr, $span[0], $span[1] - $span[0])); 2196 } 2197 $this->groupmap[$i] = TagAnno::make_heading($h); 2198 } 2199 } else if (($h = $sole_qe->get_float("heading"))) 2200 $this->groupmap[0] = TagAnno::make_heading($h); 2201 else 2202 $this->_check_order_anno($sole_qe, $rowset); 2203 } 2204 2205 function paper_ids() { 2206 $this->_prepare(); 2207 return $this->_matches ? : []; 2208 } 2209 2210 function sorted_paper_ids() { 2211 $this->_prepare(); 2212 if ($this->_default_sort || $this->sorters) { 2213 $pl = new PaperList($this, ["sort" => $this->_default_sort]); 2214 return $pl->paper_ids(); 2215 } else 2216 return $this->paper_ids(); 2217 } 2218 2219 function restrict_match($callback) { 2220 $m = []; 2221 foreach ($this->paper_ids() as $pid) 2222 if (call_user_func($callback, $pid)) 2223 $m[] = $pid; 2224 if ($this->_matches !== false) 2225 $this->_matches = $m; 2226 } 2227 2228 private function trivial_limit() { 2229 $limit = $this->limit(); 2230 if ($this->user->has_hidden_papers()) 2231 return false; 2232 else if ($limit === "und" || $limit === "acc" || $limit === "vis") 2233 return $this->privChair; 2234 else if ($limit === "rable" || $limit === "manager") 2235 return false; 2236 else 2237 return true; 2238 } 2239 2240 function test_limit(PaperInfo $prow) { 2241 if (!$this->user->can_view_paper($prow)) 2242 return false; 2243 switch ($this->limit()) { 2244 case "s": 2245 return $prow->timeSubmitted > 0; 2246 case "acc": 2247 return $prow->timeSubmitted > 0 2248 && $this->user->can_view_decision($prow) 2249 && $prow->outcome > 0; 2250 case "und": 2251 return $prow->timeSubmitted > 0 2252 && ($prow->outcome == 0 2253 || !$this->user->can_view_decision($prow)); 2254 case "unm": 2255 return $prow->timeSubmitted > 0 && $prow->managerContactId == 0; 2256 case "rable": 2257 $user = $this->reviewer_user(); 2258 return $user->can_accept_review_assignment_ignore_conflict($prow) 2259 && ($this->conf->can_pc_see_all_submissions() 2260 ? $prow->timeWithdrawn <= 0 2261 : $prow->timeSubmitted > 0) 2262 && ($this->privChair 2263 || $this->user === $user 2264 || $this->user->can_administer($prow)); 2265 case "act": 2266 return $prow->timeWithdrawn <= 0; 2267 case "r": 2268 return $prow->timeWithdrawn <= 0 && $prow->has_reviewer($this->user); 2269 case "unsub": 2270 return $prow->timeSubmitted <= 0 && $prow->timeWithdrawn <= 0; 2271 case "lead": 2272 return $prow->leadContactId == $this->cid; 2273 case "manager": 2274 return $prow->timeSubmitted > 0 && $this->user->allow_administer($prow); 2275 case "a": 2276 return $this->user->act_author_view($prow); 2277 case "ar": 2278 return $this->user->act_author_view($prow) 2279 || ($prow->timeWithdrawn <= 0 && $prow->has_reviewer($this->user)); 2280 case "rout": 2281 foreach ($prow->reviews_of_user($this->user, $this->user->review_tokens()) as $rrow) 2282 if ($rrow->reviewNeedsSubmit != 0) 2283 return true; 2284 return false; 2285 case "req": 2286 if ($prow->timeSubmitted <= 0) 2287 return false; 2288 foreach ($prow->reviews_by_id() as $rrow) 2289 if ($rrow->reviewType == REVIEW_EXTERNAL && $rrow->requestedBy == $this->cid) 2290 return true; 2291 return false; 2292 case "all": 2293 case "vis": 2294 return true; 2295 default: 2296 return false; 2297 } 2298 } 2299 2300 function test(PaperInfo $prow) { 2301 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 2302 $qe = $this->term(); 2303 $x = $this->test_limit($prow) && $qe->exec($prow, $this); 2304 $this->user->set_overrides($old_overrides); 2305 return $x; 2306 } 2307 2308 function filter($prows) { 2309 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 2310 $qe = $this->term(); 2311 $results = []; 2312 foreach ($prows as $prow) 2313 if ($this->test_limit($prow) && $qe->exec($prow, $this)) 2314 $results[] = $prow; 2315 $this->user->set_overrides($old_overrides); 2316 return $results; 2317 } 2318 2319 function test_review(PaperInfo $prow, ReviewInfo $rrow) { 2320 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 2321 $qe = $this->term(); 2322 $this->test_review = $rrow; 2323 $x = $this->test_limit($prow) && $qe->exec($prow, $this); 2324 $this->test_review = null; 2325 $this->user->set_overrides($old_overrides); 2326 return $x; 2327 } 2328 2329 function simple_search_options() { 2330 $limit = $xlimit = $this->limit(); 2331 if ($this->q === "re:me" 2332 && ($xlimit === "s" || $xlimit === "act" || $xlimit === "rout" || $xlimit === "rable")) 2333 $xlimit = "r"; 2334 if ($this->_matches !== null 2335 || ($this->q !== "" 2336 && ($this->q !== "re:me" || $xlimit !== "r")) 2337 || (!$this->privChair 2338 && $this->reviewer_user() !== $this->user) 2339 || ($this->conf->has_tracks() 2340 && !$this->privChair 2341 && !in_array($xlimit, ["a", "r", "ar"])) 2342 || ($this->conf->has_tracks() 2343 && $limit === "rable") 2344 || $this->user->has_hidden_papers()) 2345 return false; 2346 if ($limit === "rable") { 2347 if ($this->reviewer_user()->isPC) 2348 $limit = $this->conf->can_pc_see_all_submissions() ? "act" : "s"; 2349 else 2350 $limit = "r"; 2351 } 2352 $queryOptions = []; 2353 if ($limit === "s") 2354 $queryOptions["finalized"] = 1; 2355 else if ($limit === "unsub") { 2356 $queryOptions["unsub"] = 1; 2357 $queryOptions["active"] = 1; 2358 } else if ($limit === "acc") { 2359 if ($this->privChair || $this->conf->can_all_author_view_decision()) { 2360 $queryOptions["accepted"] = 1; 2361 $queryOptions["finalized"] = 1; 2362 } else 2363 return false; 2364 } else if ($limit === "und") { 2365 $queryOptions["undecided"] = 1; 2366 $queryOptions["finalized"] = 1; 2367 } else if ($limit === "r") 2368 $queryOptions["myReviews"] = 1; 2369 else if ($limit === "rout") 2370 $queryOptions["myOutstandingReviews"] = 1; 2371 else if ($limit === "a") { 2372 // If complex author SQL, always do search the long way 2373 if ($this->user->act_author_view_sql("%", true)) 2374 return false; 2375 $queryOptions["author"] = 1; 2376 } else if ($limit === "req" || $limit === "reqrevs") 2377 $queryOptions["myReviewRequests"] = 1; 2378 else if ($limit === "act") 2379 $queryOptions["active"] = 1; 2380 else if ($limit === "lead") 2381 $queryOptions["myLead"] = 1; 2382 else if ($limit === "unm") 2383 $queryOptions["finalized"] = $queryOptions["unmanaged"] = 1; 2384 else if ($limit !== "all" 2385 && ($limit !== "vis" || !$this->privChair)) 2386 return false; /* don't understand limit */ 2387 if ($this->q === "re:me" && $limit !== "rout") 2388 $queryOptions["myReviews"] = 1; 2389 return $queryOptions; 2390 } 2391 2392 function alternate_query() { 2393 if ($this->q !== "" 2394 && $this->q[0] !== "#" 2395 && preg_match('/\A' . TAG_REGEX . '\z/', $this->q) 2396 && $this->user->can_view_tags(null) 2397 && in_array($this->limit(), ["s", "all", "r"])) { 2398 if ($this->q[0] === "~") 2399 return "#" . $this->q; 2400 $result = $this->conf->qe("select paperId from PaperTag where tag=? limit 1", $this->q); 2401 if (count(Dbl::fetch_first_columns($result))) 2402 return "#" . $this->q; 2403 } 2404 return false; 2405 } 2406 2407 function url_site_relative_raw($q = null) { 2408 $url = $this->urlbase; 2409 if ($q === null) 2410 $q = $this->q; 2411 if ($q !== "" || substr($this->urlbase, 0, 6) === "search") 2412 $url .= (strpos($url, "?") === false ? "?" : "&") 2413 . "q=" . urlencode($q); 2414 return $url; 2415 } 2416 2417 private function _tag_description() { 2418 if ($this->q === "") 2419 return false; 2420 else if (strlen($this->q) <= 24) 2421 return htmlspecialchars($this->q); 2422 else if (!preg_match(',\A(#|-#|tag:|-tag:|notag:|order:|rorder:)(.*)\z,', $this->q, $m)) 2423 return false; 2424 $tagger = new Tagger($this->user); 2425 if (!$tagger->check($m[2])) 2426 return false; 2427 else if ($m[1] === "-tag:") 2428 return "no" . substr($this->q, 1); 2429 else 2430 return $this->q; 2431 } 2432 2433 function description($listname) { 2434 if ($listname) 2435 $lx = $this->conf->_($listname); 2436 else { 2437 $limit = $this->limit(); 2438 if ($this->q === "re:me" && ($limit === "s" || $limit === "act")) 2439 $limit = "r"; 2440 $lx = $this->conf->_c("search_types", get(self::$search_type_names, $limit, "Papers")); 2441 } 2442 if ($this->q === "" 2443 || ($this->q === "re:me" && $this->limit() === "s") 2444 || ($this->q === "re:me" && $this->limit() === "act")) 2445 return $lx; 2446 else if (($td = $this->_tag_description())) 2447 return "$td in $lx"; 2448 else 2449 return "$lx search"; 2450 } 2451 2452 function listid($sort = null) { 2453 $rest = []; 2454 if ($this->_reviewer_user && $this->_reviewer_user->contactId !== $this->cid) 2455 $rest[] = "reviewer=" . urlencode($this->_reviewer_user->email); 2456 if ((string) $sort !== "") 2457 $rest[] = "sort=" . urlencode($sort); 2458 return "p/" . $this->limitName . "/" . urlencode($this->q) 2459 . ($rest ? "/" . join("&", $rest) : ""); 2460 } 2461 2462 static function unparse_listid($listid) { 2463 if (preg_match('{\Ap/([^/]+)/([^/]*)(?:|/([^/]*))\z}', $listid, $m)) { 2464 $args = ["t" => $m[1], "q" => urldecode($m[2])]; 2465 if (isset($m[3]) && $m[3] !== "") { 2466 foreach (explode("&", $m[3]) as $arg) { 2467 if (str_starts_with($arg, "sort=")) 2468 $args["sort"] = urldecode(substr($arg, 5)); 2469 else 2470 // XXX `reviewer` 2471 error_log(caller_landmark() . ": listid includes $arg"); 2472 } 2473 } 2474 return $args; 2475 } else 2476 return null; 2477 } 2478 2479 function create_session_list_object($ids, $listname, $sort = null) { 2480 $sort = $sort !== null ? $sort : $this->_default_sort; 2481 $l = new SessionList($this->listid($sort), $ids, 2482 $this->description($listname), $this->urlbase); 2483 if ($this->field_highlighters()) 2484 $l->highlight = $this->_match_preg_query ? : true; 2485 return $l; 2486 } 2487 2488 function session_list_object() { 2489 return $this->create_session_list_object($this->sorted_paper_ids(), null); 2490 } 2491 2492 function highlight_tags() { 2493 if ($this->_highlight_tags === null) { 2494 $this->_prepare(); 2495 $this->_highlight_tags = array(); 2496 foreach ($this->sorters ? : array() as $s) 2497 if ($s->type[0] === "#") 2498 $this->_highlight_tags[] = substr($s->type, 1); 2499 } 2500 return $this->_highlight_tags; 2501 } 2502 2503 2504 function set_field_highlighter_query($q) { 2505 $ps = new PaperSearch($this->user, ["q" => $q]); 2506 $this->_match_preg = $ps->field_highlighters(); 2507 $this->_match_preg_query = $q; 2508 } 2509 2510 function field_highlighters() { 2511 if ($this->_match_preg === null) { 2512 $this->_match_preg = []; 2513 $this->term(); 2514 if (!empty($this->regex)) { 2515 foreach (TextMatch_SearchTerm::$map as $k => $v) 2516 if (isset($this->regex[$k]) && !empty($this->regex[$k])) 2517 $this->_match_preg[$v] = Text::merge_pregexes($this->regex[$k]); 2518 } 2519 } 2520 return $this->_match_preg; 2521 } 2522 2523 function field_highlighter($field) { 2524 return get($this->field_highlighters(), $field, ""); 2525 } 2526 2527 2528 static function search_types(Contact $user, $reqtype = null) { 2529 $ts = []; 2530 if ($user->isPC) { 2531 if ($user->conf->can_pc_see_all_submissions()) 2532 $ts[] = "act"; 2533 $ts[] = "s"; 2534 if ($user->conf->timePCViewDecision(false) && $user->conf->has_any_accepted()) 2535 $ts[] = "acc"; 2536 } 2537 if ($user->privChair) { 2538 $ts[] = "all"; 2539 if (!$user->conf->can_pc_see_all_submissions() && $reqtype === "act") 2540 $ts[] = "act"; 2541 } 2542 if ($user->is_reviewer()) 2543 $ts[] = "r"; 2544 if ($user->has_outstanding_review() 2545 || ($user->is_reviewer() && $reqtype === "rout")) 2546 $ts[] = "rout"; 2547 if ($user->isPC) { 2548 if ($user->is_requester() || $reqtype === "req") 2549 $ts[] = "req"; 2550 if (($user->conf->has_any_lead_or_shepherd() && $user->is_discussion_lead()) 2551 || $reqtype === "lead") 2552 $ts[] = "lead"; 2553 if (($user->privChair ? $user->conf->has_any_manager() : $user->is_manager()) 2554 || $reqtype === "manager") 2555 $ts[] = "manager"; 2556 } 2557 if ($user->is_author() || $reqtype === "a") 2558 $ts[] = "a"; 2559 return self::expand_search_types($user, $ts); 2560 } 2561 2562 static function manager_search_types(Contact $user) { 2563 if ($user->privChair) { 2564 if ($user->conf->has_any_manager()) 2565 $ts = ["manager", "unm", "s"]; 2566 else 2567 $ts = ["s"]; 2568 array_push($ts, "acc", "und", "all"); 2569 } else 2570 $ts = ["manager"]; 2571 return self::expand_search_types($user, $ts); 2572 } 2573 2574 static private function expand_search_types(Contact $user, $ts) { 2575 $topt = []; 2576 foreach ($ts as $t) 2577 $topt[$t] = $user->conf->_c("search_type", self::$search_type_names[$t]); 2578 return $topt; 2579 } 2580 2581 static function searchTypeSelector($tOpt, $type, $tabindex) { 2582 if (count($tOpt) > 1) { 2583 $sel_opt = array(); 2584 foreach ($tOpt as $k => $v) { 2585 if (count($sel_opt) && $k === "a") 2586 $sel_opt["xxxa"] = null; 2587 if (count($sel_opt) > 2 && ($k === "lead" || $k === "r") && !isset($sel_opt["xxxa"])) 2588 $sel_opt["xxxb"] = null; 2589 $sel_opt[$k] = $v; 2590 } 2591 $sel_extra = array(); 2592 if ($tabindex) 2593 $sel_extra["tabindex"] = $tabindex; 2594 return Ht::select("t", $sel_opt, $type, $sel_extra); 2595 } else 2596 return current($tOpt); 2597 } 2598 2599 private static function simple_search_completion($prefix, $map, $flags = 0) { 2600 $x = array(); 2601 foreach ($map as $id => $str) { 2602 $match = null; 2603 foreach (preg_split(',[^a-z0-9_]+,', strtolower($str)) as $word) 2604 if ($word !== "" 2605 && ($m = Text::simple_search($word, $map, $flags)) 2606 && isset($m[$id]) && count($m) == 1 2607 && !Text::is_boring_word($word)) { 2608 $match = $word; 2609 break; 2610 } 2611 $x[] = $prefix . ($match ? : "\"$str\""); 2612 } 2613 return $x; 2614 } 2615 2616 function search_completion($category = "") { 2617 $res = []; 2618 $old_overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 2619 2620 if ($this->amPC && (!$category || $category === "ss")) { 2621 foreach ($this->conf->saved_searches() as $k => $v) 2622 $res[] = "ss:" . $k; 2623 } 2624 2625 array_push($res, "has:submission", "has:abstract"); 2626 if ($this->amPC && $this->conf->has_any_manager()) 2627 $res[] = "has:admin"; 2628 if ($this->conf->has_any_lead_or_shepherd() 2629 && $this->user->can_view_lead(null)) 2630 $res[] = "has:lead"; 2631 if ($this->user->can_view_some_decision()) { 2632 $res[] = "has:decision"; 2633 if (!$category || $category === "dec") { 2634 $res[] = array("pri" => -1, "nosort" => true, "i" => array("dec:any", "dec:none", "dec:yes", "dec:no")); 2635 $dm = $this->conf->decision_map(); 2636 unset($dm[0]); 2637 $res = array_merge($res, self::simple_search_completion("dec:", $dm, Text::SEARCH_UNPRIVILEGE_EXACT)); 2638 } 2639 if ($this->conf->setting("final_open")) 2640 $res[] = "has:final"; 2641 } 2642 if ($this->conf->has_any_lead_or_shepherd() 2643 && $this->user->can_view_shepherd(null)) 2644 $res[] = "has:shepherd"; 2645 if ($this->user->is_reviewer()) 2646 array_push($res, "has:review", "has:creview", "has:ireview", "has:preview", "has:primary", "has:secondary", "has:external", "has:comment", "has:aucomment"); 2647 else if ($this->user->can_view_some_review()) 2648 array_push($res, "has:review", "has:comment"); 2649 if ($this->amPC && $this->conf->setting("extrev_approve") && $this->conf->setting("pcrev_editdelegate") 2650 && $this->user->is_requester()) 2651 array_push($res, "has:approvable"); 2652 foreach ($this->conf->resp_rounds() as $rrd) { 2653 if (!in_array("has:response", $res)) 2654 $res[] = "has:response"; 2655 if ($rrd->number) 2656 $res[] = "has:{$rrd->name}response"; 2657 } 2658 if ($this->user->can_view_some_draft_response()) 2659 foreach ($this->conf->resp_rounds() as $rrd) { 2660 if (!in_array("has:draftresponse", $res)) 2661 $res[] = "has:draftresponse"; 2662 if ($rrd->number) 2663 $res[] = "has:draft{$rrd->name}response"; 2664 } 2665 if ($this->user->can_view_tags()) { 2666 array_push($res, "has:color", "has:style"); 2667 if ($this->conf->tags()->has_badges) 2668 $res[] = "has:badge"; 2669 } 2670 foreach ($this->user->user_option_list() as $o) 2671 if ($this->user->can_view_some_paper_option($o)) 2672 $o->add_search_completion($res); 2673 if ($this->user->is_reviewer() && $this->conf->has_rounds() 2674 && (!$category || $category === "round")) { 2675 $res[] = array("pri" => -1, "nosort" => true, "i" => array("round:any", "round:none")); 2676 $rlist = array(); 2677 foreach ($this->conf->round_list() as $rnum => $round) 2678 if ($rnum && $round !== ";") 2679 $rlist[$rnum] = $round; 2680 $res = array_merge($res, self::simple_search_completion("round:", $rlist)); 2681 } 2682 if ($this->conf->has_topics() && (!$category || $category === "topic")) { 2683 foreach ($this->conf->topic_map() as $tname) 2684 $res[] = "topic:\"{$tname}\""; 2685 } 2686 if ((!$category || $category === "style") && $this->user->can_view_tags()) { 2687 $res[] = array("pri" => -1, "nosort" => true, "i" => array("style:any", "style:none", "color:any", "color:none")); 2688 foreach ($this->conf->tags()->canonical_colors() as $t) { 2689 $res[] = "style:$t"; 2690 if ($this->conf->tags()->is_known_style($t, TagMap::STYLE_BG)) 2691 $res[] = "color:$t"; 2692 } 2693 } 2694 if (!$category || $category === "show" || $category === "hide") { 2695 $cats = array(); 2696 $pl = new PaperList($this); 2697 foreach ($this->conf->paper_column_map() as $cname => $cj) { 2698 $cj = $this->conf->basic_paper_column($cname, $this->user); 2699 if ($cj && isset($cj->completion) && $cj->completion 2700 && ($c = PaperColumn::make($this->conf, $cj)) 2701 && ($cat = $c->completion_name()) 2702 && $c->prepare($pl, 0)) { 2703 $cats[$cat] = true; 2704 } 2705 } 2706 foreach ($this->conf->paper_column_factories() as $fxj) { 2707 if (!$this->conf->xt_allowed($fxj, $this->user) 2708 || Conf::xt_disabled($fxj)) 2709 continue; 2710 if (isset($fxj->completion_callback)) { 2711 Conf::xt_resolve_require($fxj); 2712 foreach (call_user_func($fxj->completion_callback, $this->user, $fxj) as $c) 2713 $cats[$c] = true; 2714 } else if (isset($fxj->completion) && is_string($fxj->completion)) 2715 $cats[$fxj->completion] = true; 2716 } 2717 foreach (array_keys($cats) as $cat) 2718 array_push($res, "show:$cat", "hide:$cat"); 2719 array_push($res, "show:compact", "show:statistics", "show:rownumbers"); 2720 } 2721 2722 $this->user->set_overrides($old_overrides); 2723 return $res; 2724 } 2725} 2726