1<?php 2// conference.php -- HotCRP central helper class (singleton) 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class Track { 6 const VIEW = 0; 7 const VIEWPDF = 1; 8 const VIEWREV = 2; 9 const VIEWREVID = 3; 10 const ASSREV = 4; 11 const UNASSREV = 5; 12 const VIEWTRACKER = 6; 13 const ADMIN = 7; 14 const VIEWREVOVERRIDE = 8; 15 const HIDDENTAG = 9; 16 const VIEWALLREV = 10; 17 18 const BITS_VIEW = 0x1; // 1 << VIEW 19 const BITS_REVIEW = 0x30; // (1 << ASSREV) | (1 << UNASSREV) 20 const BITS_ADMIN = 0x80; // 1 << ADMIN 21 22 static public $map = [ 23 "view" => 0, "viewpdf" => 1, "viewrev" => 2, "viewrevid" => 3, 24 "assrev" => 4, "unassrev" => 5, "viewtracker" => 6, "admin" => 7, 25 "viewrevover" => 8, "hiddentag" => 9, "viewallrev" => 10 26 ]; 27 static public $zero = [null, null, null, null, null, null, null, null, null, null, null]; 28 static function match_perm(Contact $user, $perm) { 29 if ($perm) { 30 $has_tag = $user->has_tag(substr($perm, 1)); 31 return $perm[0] === "-" ? !$has_tag : $has_tag; 32 } else 33 return true; 34 } 35 static function permission_required($perm) { 36 return $perm === self::ADMIN || $perm === self::VIEWREVOVERRIDE 37 || $perm === self::HIDDENTAG; 38 } 39} 40 41class ResponseRound { 42 public $name; 43 public $number; 44 public $open; 45 public $done; 46 public $grace; 47 public $words; 48 public $search; 49 function time_allowed($with_grace) { 50 global $Now; 51 if ($this->open === null || $this->open <= 0 || $this->open > $Now) 52 return false; 53 $t = $this->done; 54 if ($t !== null && $t > 0 && $with_grace && $this->grace) 55 $t += $this->grace; 56 return $t === null || $t <= 0 || $t >= $Now; 57 } 58 function instructions(Conf $conf) { 59 $m = false; 60 if ($this->number) 61 $m = $conf->message_html("resp_instrux_$this->number", ["wordlimit" => $this->words]); 62 if ($m === false) 63 $m = $conf->message_html("resp_instrux", ["wordlimit" => $this->words]); 64 return $m; 65 } 66} 67 68class Conf { 69 public $dblink = null; 70 71 public $settings; 72 private $settingTexts; 73 public $sversion; 74 private $_pc_seeall_cache = null; 75 private $_pc_see_pdf = false; 76 77 public $dbname; 78 public $dsn = null; 79 80 public $short_name; 81 public $long_name; 82 public $default_format; 83 public $download_prefix; 84 public $au_seerev; 85 public $tag_au_seerev; 86 public $any_response_open; 87 public $tag_seeall; 88 public $sort_by_last; 89 public $opt; 90 public $opt_override = null; 91 private $_opt_timestamp = null; 92 public $paper_opts; 93 94 public $headerPrinted = false; 95 private $_save_logs = false; 96 public $_session_handler; 97 98 private $usertimeId = 1; 99 100 private $rounds = null; 101 private $_defined_rounds = null; 102 private $_round_settings = null; 103 private $_resp_rounds = null; 104 private $tracks = null; 105 private $_taginfo = null; 106 private $_track_tags = null; 107 private $_track_sensitivity = 0; 108 private $_decisions = null; 109 private $_topic_map = null; 110 private $_topic_order_map = null; 111 private $_topic_separator_cache = null; 112 private $_topic_abbrev_matcher = null; 113 private $_pc_members_cache = null; 114 private $_pc_tags_cache = null; 115 private $_pc_members_and_admins_cache = null; 116 private $_pc_members_fully_loaded = false; 117 private $_user_cache = null; 118 private $_user_cache_missing = null; 119 private $_site_contact; 120 private $_review_form_cache = null; 121 private $_abbrev_matcher = null; 122 private $_date_format_initialized = false; 123 private $_formatspec_cache = []; 124 private $_docstore = false; 125 private $_defined_formulas = null; 126 private $_emoji_codes = null; 127 private $_s3_document = false; 128 private $_ims = null; 129 private $_format_info = null; 130 private $_updating_autosearch_tags = false; 131 private $_cdb = false; 132 133 private $_formula_functions = null; 134 private $_search_keyword_base = null; 135 private $_search_keyword_factories = null; 136 private $_assignment_parsers = null; 137 private $_api_map = null; 138 private $_list_action_map = null; 139 private $_list_action_renderers = null; 140 private $_list_action_factories = null; 141 private $_paper_column_map = null; 142 private $_paper_column_factories = null; 143 private $_option_type_map = null; 144 private $_option_type_factories = null; 145 private $_hook_map = null; 146 private $_hook_factories = null; 147 public $_file_filters = null; // maintained externally 148 public $_setting_info = null; // maintained externally 149 private $_mail_keyword_map = null; 150 private $_mail_keyword_factories = null; 151 152 public $paper = null; // current paper row 153 private $_active_list = false; 154 155 static public $g; 156 static public $no_invalidate_caches = false; 157 static public $next_xt_subposition = 0; 158 static private $xt_require_resolved = []; 159 160 const BLIND_NEVER = 0; 161 const BLIND_OPTIONAL = 1; 162 const BLIND_ALWAYS = 2; 163 const BLIND_UNTILREVIEW = 3; 164 165 const SEEDEC_ADMIN = 0; 166 const SEEDEC_REV = 1; 167 const SEEDEC_ALL = 2; 168 const SEEDEC_NCREV = 3; 169 170 const AUSEEREV_NO = 0; 171 const AUSEEREV_UNLESSINCOMPLETE = 1; 172 const AUSEEREV_YES = 2; 173 const AUSEEREV_TAGS = 3; 174 175 const PCSEEREV_IFCOMPLETE = 0; 176 const PCSEEREV_YES = 1; 177 const PCSEEREV_UNLESSINCOMPLETE = 3; 178 const PCSEEREV_UNLESSANYINCOMPLETE = 4; 179 180 static public $review_deadlines = array("pcrev_soft", "pcrev_hard", "extrev_soft", "extrev_hard"); 181 182 static public $hoturl_defaults = null; 183 184 function __construct($options, $make_dsn) { 185 // unpack dsn, connect to database, load current settings 186 if ($make_dsn && ($this->dsn = Dbl::make_dsn($options))) 187 list($this->dblink, $options["dbName"]) = Dbl::connect_dsn($this->dsn); 188 if (!isset($options["confid"])) 189 $options["confid"] = get($options, "dbName"); 190 $this->opt = $options; 191 $this->dbname = $options["dbName"]; 192 $this->paper_opts = new PaperOptionList($this); 193 if ($this->dblink && !Dbl::$default_dblink) { 194 Dbl::set_default_dblink($this->dblink); 195 Dbl::set_error_handler(array($this, "query_error_handler")); 196 } 197 if ($this->dblink) { 198 Dbl::$landmark_sanitizer = "/^(?:Dbl::|Conf::q|Conf::fetch|call_user_func)/"; 199 $this->load_settings(); 200 } else 201 $this->crosscheck_options(); 202 } 203 204 205 // 206 // Initialization functions 207 // 208 209 function load_settings() { 210 global $Now; 211 212 // load settings from database 213 $this->settings = array(); 214 $this->settingTexts = array(); 215 foreach ($this->opt_override ? : [] as $k => $v) { 216 if ($v === null) 217 unset($this->opt[$k]); 218 else 219 $this->opt[$k] = $v; 220 } 221 $this->opt_override = []; 222 223 $result = $this->q_raw("select name, value, data from Settings"); 224 while ($result && ($row = $result->fetch_row())) { 225 $this->settings[$row[0]] = (int) $row[1]; 226 if ($row[2] !== null) 227 $this->settingTexts[$row[0]] = $row[2]; 228 if (substr($row[0], 0, 4) == "opt.") { 229 $okey = substr($row[0], 4); 230 $this->opt_override[$okey] = get($this->opt, $okey); 231 $this->opt[$okey] = ($row[2] === null ? (int) $row[1] : $row[2]); 232 } 233 } 234 Dbl::free($result); 235 236 // update schema 237 $this->sversion = $this->settings["allowPaperOption"]; 238 if ($this->sversion < 199) { 239 require_once("updateschema.php"); 240 $old_nerrors = Dbl::$nerrors; 241 updateSchema($this); 242 Dbl::$nerrors = $old_nerrors; 243 } 244 if ($this->sversion < 95) 245 self::msg_error("Warning: The database could not be upgraded to the current version; expect errors. A system administrator must solve this problem."); 246 247 // invalidate all caches after loading from backup 248 if (isset($this->settings["frombackup"]) 249 && $this->invalidate_caches()) { 250 $this->qe_raw("delete from Settings where name='frombackup' and value=" . $this->settings["frombackup"]); 251 unset($this->settings["frombackup"]); 252 } 253 254 // update options 255 if (isset($this->opt["ldapLogin"]) && !$this->opt["ldapLogin"]) 256 unset($this->opt["ldapLogin"]); 257 if (isset($this->opt["httpAuthLogin"]) && !$this->opt["httpAuthLogin"]) 258 unset($this->opt["httpAuthLogin"]); 259 260 // set conferenceKey 261 if (!isset($this->opt["conferenceKey"])) { 262 if (!isset($this->settingTexts["conf_key"]) 263 && ($key = random_bytes(32)) !== false) 264 $this->__save_setting("conf_key", 1, $key); 265 $this->opt["conferenceKey"] = get($this->settingTexts, "conf_key", ""); 266 } 267 268 // set capability key 269 if (!get($this->settings, "cap_key") 270 && !get($this->opt, "disableCapabilities") 271 && !(($key = random_bytes(16)) !== false 272 && ($key = base64_encode($key)) 273 && $this->__save_setting("cap_key", 1, $key))) 274 $this->opt["disableCapabilities"] = true; 275 276 // GC old capabilities 277 if (get($this->settings, "__capability_gc", 0) < $Now - 86400) { 278 foreach (array($this->dblink, $this->contactdb()) as $db) 279 if ($db) 280 Dbl::ql($db, "delete from Capability where timeExpires>0 and timeExpires<$Now"); 281 $this->q_raw("insert into Settings (name, value) values ('__capability_gc', $Now) on duplicate key update value=values(value)"); 282 $this->settings["__capability_gc"] = $Now; 283 } 284 285 $this->crosscheck_settings(); 286 $this->crosscheck_options(); 287 } 288 289 private function crosscheck_settings() { 290 global $Now; 291 292 // enforce invariants 293 foreach (array("pcrev_any", "extrev_view") as $x) 294 if (!isset($this->settings[$x])) 295 $this->settings[$x] = 0; 296 if (!isset($this->settings["sub_blind"])) 297 $this->settings["sub_blind"] = self::BLIND_ALWAYS; 298 if (!isset($this->settings["rev_blind"])) 299 $this->settings["rev_blind"] = self::BLIND_ALWAYS; 300 if (!isset($this->settings["seedec"])) { 301 if (get($this->settings, "au_seedec")) 302 $this->settings["seedec"] = self::SEEDEC_ALL; 303 else if (get($this->settings, "rev_seedec")) 304 $this->settings["seedec"] = self::SEEDEC_REV; 305 } 306 if (get($this->settings, "pc_seeallrev") == 2) { 307 $this->settings["pc_seeblindrev"] = 1; 308 $this->settings["pc_seeallrev"] = self::PCSEEREV_YES; 309 } 310 if (($sub_update = get($this->settings, "sub_update", -1)) > 0 311 && ($sub_reg = get($this->settings, "sub_reg", -1)) <= 0) { 312 $this->settings["sub_reg"] = $sub_update; 313 $this->settings["__sub_reg"] = $sub_reg; 314 } 315 316 // rounds 317 $this->crosscheck_round_settings(); 318 319 // S3 settings 320 foreach (array("s3_bucket", "s3_key", "s3_secret") as $k) 321 if (!get($this->settingTexts, $k) && ($x = get($this->opt, $k))) 322 $this->settingTexts[$k] = $x; 323 if (!get($this->settingTexts, "s3_key") 324 || !get($this->settingTexts, "s3_secret") 325 || !get($this->settingTexts, "s3_bucket")) 326 unset($this->settingTexts["s3_key"], $this->settingTexts["s3_secret"], 327 $this->settingTexts["s3_bucket"]); 328 if (get($this->opt, "dbNoPapers") && !get($this->opt, "docstore") 329 && !get($this->opt, "filestore") && !get($this->settingTexts, "s3_bucket")) 330 unset($this->opt["dbNoPapers"]); 331 if ($this->_s3_document 332 && (!isset($this->settingTexts["s3_bucket"]) 333 || !$this->_s3_document->check_key_secret_bucket($this->settingTexts["s3_key"], $this->settingTexts["s3_secret"], $this->settingTexts["s3_bucket"]))) 334 $this->_s3_document = false; 335 336 // tracks settings 337 $this->tracks = $this->_track_tags = null; 338 $this->_track_sensitivity = 0; 339 if (($j = get($this->settingTexts, "tracks"))) 340 $this->crosscheck_track_settings($j); 341 342 // clear caches 343 $this->_decisions = null; 344 $this->_pc_seeall_cache = null; 345 $this->_defined_rounds = null; 346 $this->_resp_rounds = null; 347 // digested settings 348 $this->_pc_see_pdf = true; 349 if (get($this->settings, "sub_freeze", 0) <= 0 350 && ($so = get($this->settings, "sub_open", 0)) > 0 351 && $so < $Now 352 && ($ss = get($this->settings, "sub_sub", 0)) > 0 353 && $ss > $Now 354 && (get($this->settings, "pc_seeallpdf", 0) <= 0 355 || !$this->can_pc_see_all_submissions())) 356 $this->_pc_see_pdf = false; 357 358 $this->au_seerev = get($this->settings, "au_seerev", 0); 359 $this->tag_au_seerev = null; 360 if ($this->au_seerev == self::AUSEEREV_TAGS) 361 $this->tag_au_seerev = explode(" ", get_s($this->settingTexts, "tag_au_seerev")); 362 $this->any_response_open = get($this->settings, "resp_active", 0) > 0 363 && $this->time_author_respond_all_rounds(); 364 $this->tag_seeall = get($this->settings, "tag_seeall", 0) > 0; 365 } 366 367 private function crosscheck_round_settings() { 368 $this->rounds = [""]; 369 if (isset($this->settingTexts["tag_rounds"])) { 370 foreach (explode(" ", $this->settingTexts["tag_rounds"]) as $r) 371 if ($r != "") 372 $this->rounds[] = $r; 373 } 374 $this->_round_settings = null; 375 if (isset($this->settingTexts["round_settings"])) { 376 $this->_round_settings = json_decode($this->settingTexts["round_settings"]); 377 $max_rs = []; 378 foreach ($this->_round_settings as $rs) { 379 if ($rs && isset($rs->pc_seeallrev) 380 && self::pcseerev_compare($rs->pc_seeallrev, get($max_rs, "pc_seeallrev", 0)) > 0) 381 $max_rs["pc_seeallrev"] = $rs->pc_seeallrev; 382 if ($rs && isset($rs->extrev_view) 383 && $rs->extrev_view > get($max_rs, "extrev_view", 0)) 384 $max_rs["extrev_view"] = $rs->extrev_view; 385 } 386 $this->_round_settings["max"] = (object) $max_rs; 387 } 388 389 // review times 390 foreach ($this->rounds as $i => $rname) { 391 $suf = $i ? "_$i" : ""; 392 if (!isset($this->settings["extrev_soft$suf"]) && isset($this->settings["pcrev_soft$suf"])) 393 $this->settings["extrev_soft$suf"] = $this->settings["pcrev_soft$suf"]; 394 if (!isset($this->settings["extrev_hard$suf"]) && isset($this->settings["pcrev_hard$suf"])) 395 $this->settings["extrev_hard$suf"] = $this->settings["pcrev_hard$suf"]; 396 } 397 } 398 399 private function crosscheck_track_settings($j) { 400 if (is_string($j) && !($j = json_decode($j))) 401 return; 402 $this->tracks = []; 403 $default_track = Track::$zero; 404 $this->_track_tags = []; 405 foreach ((array) $j as $k => $v) { 406 if ($k !== "_") 407 $this->_track_tags[] = $k; 408 if (!isset($v->viewpdf) && isset($v->view)) 409 $v->viewpdf = $v->view; 410 $t = Track::$zero; 411 foreach (Track::$map as $tname => $idx) 412 if (isset($v->$tname)) { 413 $t[$idx] = $v->$tname; 414 $this->_track_sensitivity |= 1 << $idx; 415 } 416 if ($k === "_") 417 $default_track = $t; 418 else 419 $this->tracks[$k] = $t; 420 } 421 $this->tracks["_"] = $default_track; 422 } 423 424 function crosscheck_options() { 425 global $ConfSitePATH; 426 427 // set longName, downloadPrefix, etc. 428 $confid = $this->opt["confid"]; 429 if ((!isset($this->opt["longName"]) || $this->opt["longName"] == "") 430 && (!isset($this->opt["shortName"]) || $this->opt["shortName"] == "")) { 431 $this->opt["shortNameDefaulted"] = true; 432 $this->opt["longName"] = $this->opt["shortName"] = $confid; 433 } else if (!isset($this->opt["longName"]) || $this->opt["longName"] == "") 434 $this->opt["longName"] = $this->opt["shortName"]; 435 else if (!isset($this->opt["shortName"]) || $this->opt["shortName"] == "") 436 $this->opt["shortName"] = $this->opt["longName"]; 437 if (!isset($this->opt["downloadPrefix"]) || $this->opt["downloadPrefix"] == "") 438 $this->opt["downloadPrefix"] = $confid . "-"; 439 $this->short_name = $this->opt["shortName"]; 440 $this->long_name = $this->opt["longName"]; 441 442 // expand ${confid}, ${confshortname} 443 foreach (array("sessionName", "downloadPrefix", "conferenceSite", 444 "paperSite", "defaultPaperSite", "contactName", 445 "contactEmail", "docstore") as $k) 446 if (isset($this->opt[$k]) && is_string($this->opt[$k]) 447 && strpos($this->opt[$k], "$") !== false) { 448 $this->opt[$k] = preg_replace(',\$\{confid\}|\$confid\b,', $confid, $this->opt[$k]); 449 $this->opt[$k] = preg_replace(',\$\{confshortname\}|\$confshortname\b,', $this->short_name, $this->opt[$k]); 450 } 451 $this->download_prefix = $this->opt["downloadPrefix"]; 452 453 foreach (array("emailFrom", "emailSender", "emailCc", "emailReplyTo") as $k) 454 if (isset($this->opt[$k]) && is_string($this->opt[$k]) 455 && strpos($this->opt[$k], "$") !== false) { 456 $this->opt[$k] = preg_replace(',\$\{confid\}|\$confid\b,', $confid, $this->opt[$k]); 457 if (strpos($this->opt[$k], "confshortname") !== false) { 458 $v = rfc2822_words_quote($this->short_name); 459 if ($v[0] === "\"" && strpos($this->opt[$k], "\"") !== false) 460 $v = substr($v, 1, strlen($v) - 2); 461 $this->opt[$k] = preg_replace(',\$\{confshortname\}|\$confshortname\b,', $v, $this->opt[$k]); 462 } 463 } 464 465 // remove final slash from $Opt["paperSite"] 466 if (!isset($this->opt["paperSite"]) || $this->opt["paperSite"] == "") 467 $this->opt["paperSite"] = Navigation::site_absolute(); 468 if ($this->opt["paperSite"] == "" && isset($this->opt["defaultPaperSite"])) 469 $this->opt["paperSite"] = $this->opt["defaultPaperSite"]; 470 $this->opt["paperSite"] = preg_replace('|/+\z|', "", $this->opt["paperSite"]); 471 472 // option name updates (backwards compatibility) 473 foreach (array("assetsURL" => "assetsUrl", 474 "jqueryURL" => "jqueryUrl", "jqueryCDN" => "jqueryCdn", 475 "disableCSV" => "disableCsv") as $kold => $knew) 476 if (isset($this->opt[$kold]) && !isset($this->opt[$knew])) 477 $this->opt[$knew] = $this->opt[$kold]; 478 479 // set assetsUrl and scriptAssetsUrl 480 if (!isset($this->opt["scriptAssetsUrl"]) && isset($_SERVER["HTTP_USER_AGENT"]) 481 && strpos($_SERVER["HTTP_USER_AGENT"], "MSIE") !== false) 482 $this->opt["scriptAssetsUrl"] = Navigation::siteurl(); 483 if (!isset($this->opt["assetsUrl"])) 484 $this->opt["assetsUrl"] = Navigation::siteurl(); 485 if ($this->opt["assetsUrl"] !== "" && !str_ends_with($this->opt["assetsUrl"], "/")) 486 $this->opt["assetsUrl"] .= "/"; 487 if (!isset($this->opt["scriptAssetsUrl"])) 488 $this->opt["scriptAssetsUrl"] = $this->opt["assetsUrl"]; 489 Ht::$img_base = $this->opt["assetsUrl"] . "images/"; 490 Ht::$default_button_class = "btn"; 491 492 // set docstore 493 if (get($this->opt, "docstore") === true) 494 $this->opt["docstore"] = "docs"; 495 else if (!get($this->opt, "docstore") && get($this->opt, "filestore")) { // backwards compat 496 $this->opt["docstore"] = $this->opt["filestore"]; 497 if ($this->opt["docstore"] === true) 498 $this->opt["docstore"] = "filestore"; 499 $this->opt["docstoreSubdir"] = get($this->opt, "filestoreSubdir"); 500 } 501 if (get($this->opt, "docstore") && $this->opt["docstore"][0] !== "/") 502 $this->opt["docstore"] = $ConfSitePATH . "/" . $this->opt["docstore"]; 503 $this->_docstore = false; 504 if (($dpath = get($this->opt, "docstore"))) { 505 if (strpos($dpath, "%") !== false) 506 $this->_docstore = $dpath; 507 else { 508 if ($dpath[strlen($dpath) - 1] === "/") 509 $dpath = substr($dpath, 0, strlen($dpath) - 1); 510 $use_subdir = get($this->opt, "docstoreSubdir"); 511 if ($use_subdir && ($use_subdir === true || $use_subdir > 0)) 512 $dpath .= "/%" . ($use_subdir === true ? 2 : $use_subdir) . "h"; 513 $this->_docstore = $dpath . "/%h%x"; 514 } 515 } 516 517 // handle timezone 518 if (function_exists("date_default_timezone_set")) { 519 if (isset($this->opt["timezone"])) { 520 if (!date_default_timezone_set($this->opt["timezone"])) { 521 self::msg_error("Timezone option “" . htmlspecialchars($this->opt["timezone"]) . "” is invalid; falling back to “America/New_York”."); 522 date_default_timezone_set("America/New_York"); 523 } 524 } else if (!ini_get("date.timezone") && !getenv("TZ")) 525 date_default_timezone_set("America/New_York"); 526 } 527 $this->_date_format_initialized = false; 528 529 // set safePasswords 530 if (!get($this->opt, "safePasswords") 531 || (is_int($this->opt["safePasswords"]) && $this->opt["safePasswords"] < 1)) 532 $this->opt["safePasswords"] = 0; 533 else if ($this->opt["safePasswords"] === true) 534 $this->opt["safePasswords"] = 1; 535 if (!isset($this->opt["contactdb_safePasswords"])) 536 $this->opt["contactdb_safePasswords"] = $this->opt["safePasswords"]; 537 538 // set defaultFormat 539 $this->default_format = (int) get($this->opt, "defaultFormat"); 540 $this->_format_info = null; 541 542 // other caches 543 $sort_by_last = !!get($this->opt, "sortByLastName"); 544 if (!$this->sort_by_last != !$sort_by_last) 545 $this->invalidate_caches("pc"); 546 $this->sort_by_last = $sort_by_last; 547 548 $this->_api_map = null; 549 $this->_list_action_map = $this->_list_action_renderers = $this->_list_action_factories = null; 550 $this->_file_filters = null; 551 $this->_site_contact = null; 552 } 553 554 function has_setting($name) { 555 return isset($this->settings[$name]); 556 } 557 558 function setting($name, $defval = null) { 559 return get($this->settings, $name, $defval); 560 } 561 562 function setting_data($name, $defval = false) { 563 return get($this->settingTexts, $name, $defval); 564 } 565 566 function setting_json($name, $defval = false) { 567 $x = get($this->settingTexts, $name, $defval); 568 return is_string($x) ? json_decode($x) : $x; 569 } 570 571 function opt($name, $defval = null) { 572 return get($this->opt, $name, $defval); 573 } 574 575 function set_opt($name, $value) { 576 global $Opt; 577 $Opt[$name] = $this->opt[$name] = $value; 578 } 579 580 function opt_timestamp() { 581 if ($this->_opt_timestamp === null) { 582 $this->_opt_timestamp = 1; 583 foreach (get($this->opt, "loaded", []) as $fn) 584 $this->_opt_timestamp = max($this->_opt_timestamp, +@filemtime($fn)); 585 } 586 return $this->_opt_timestamp; 587 } 588 589 590 static function pcseerev_compare($sr1, $sr2) { 591 if ($sr1 == $sr2) 592 return 0; 593 else if ($sr1 == self::PCSEEREV_YES || $sr2 == self::PCSEEREV_YES) 594 return $sr1 == self::PCSEEREV_YES ? 1 : -1; 595 else 596 return $sr1 > $sr2 ? 1 : -1; 597 } 598 599 600 // database 601 602 function q(/* $qstr, ... */) { 603 return Dbl::do_query_on($this->dblink, func_get_args(), 0); 604 } 605 function q_raw(/* $qstr */) { 606 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_RAW); 607 } 608 function q_apply(/* $qstr, $args */) { 609 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_APPLY); 610 } 611 612 function ql(/* $qstr, ... */) { 613 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_LOG); 614 } 615 function ql_raw(/* $qstr */) { 616 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_RAW | Dbl::F_LOG); 617 } 618 function ql_apply(/* $qstr, $args */) { 619 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_APPLY | Dbl::F_LOG); 620 } 621 622 function qe(/* $qstr, ... */) { 623 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR); 624 } 625 function qe_raw(/* $qstr */) { 626 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_RAW | Dbl::F_ERROR); 627 } 628 function qe_apply(/* $qstr, $args */) { 629 return Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_APPLY | Dbl::F_ERROR); 630 } 631 632 function fetch_rows(/* $qstr, ... */) { 633 return Dbl::fetch_rows(Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR)); 634 } 635 function fetch_first_row(/* $qstr, ... */) { 636 return Dbl::fetch_first_row(Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR)); 637 } 638 function fetch_first_object(/* $qstr, ... */) { 639 return Dbl::fetch_first_object(Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR)); 640 } 641 function fetch_value(/* $qstr, ... */) { 642 return Dbl::fetch_value(Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR)); 643 } 644 function fetch_ivalue(/* $qstr, ... */) { 645 return Dbl::fetch_ivalue(Dbl::do_query_on($this->dblink, func_get_args(), Dbl::F_ERROR)); 646 } 647 648 function db_error_html($getdb = true, $while = "") { 649 $text = "<p>Database error"; 650 if ($while) 651 $text .= " $while"; 652 if ($getdb) 653 $text .= ": " . htmlspecialchars($this->dblink->error); 654 return $text . "</p>"; 655 } 656 657 function db_error_text($getdb = true, $while = "") { 658 $text = "Database error"; 659 if ($while) 660 $text .= " $while"; 661 if ($getdb) 662 $text .= ": " . $this->dblink->error; 663 return $text; 664 } 665 666 function query_error_handler($dblink, $query) { 667 $landmark = caller_landmark(1, "/^(?:Dbl::|Conf::q|call_user_func)/"); 668 if (PHP_SAPI == "cli") 669 fwrite(STDERR, "$landmark: database error: $dblink->error in $query\n"); 670 else { 671 error_log("$landmark: database error: $dblink->error in $query"); 672 self::msg_error("<p>" . htmlspecialchars($landmark) . ": database error: " . htmlspecialchars($this->dblink->error) . " in " . Ht::pre_text_wrap($query) . "</p>"); 673 } 674 } 675 676 677 // name 678 679 function full_name() { 680 if ($this->short_name && $this->short_name != $this->long_name) 681 return $this->long_name . " (" . $this->short_name . ")"; 682 else 683 return $this->long_name; 684 } 685 686 687 function format_spec($dtype) { 688 if (!isset($this->_formatspec_cache[$dtype])) { 689 $o = $this->paper_opts->get($dtype); 690 $spec = $o ? $o->format_spec() : null; 691 $this->_formatspec_cache[$dtype] = $spec ? : new FormatSpec; 692 } 693 return $this->_formatspec_cache[$dtype]; 694 } 695 696 function docstore() { 697 return $this->_docstore; 698 } 699 700 function s3_docstore() { 701 global $Now; 702 if ($this->_s3_document === false) { 703 if ($this->setting_data("s3_bucket")) { 704 $opts = ["key" => $this->setting_data("s3_key"), 705 "secret" => $this->setting_data("s3_secret"), 706 "bucket" => $this->setting_data("s3_bucket"), 707 "scope" => $this->setting_data("__s3_scope"), 708 "signing_key" => $this->setting_data("__s3_signing_key")]; 709 $this->_s3_document = S3Document::make($opts); 710 list($scope, $signing_key) = $this->_s3_document->scope_and_signing_key($Now); 711 if ($opts["scope"] !== $scope || $opts["signing_key"] !== $signing_key) { 712 $this->__save_setting("__s3_scope", 1, $scope); 713 $this->__save_setting("__s3_signing_key", 1, $signing_key); 714 } 715 } else 716 $this->_s3_document = null; 717 } 718 return $this->_s3_document; 719 } 720 721 722 function _add_emoji_code($val, $key) { 723 if (is_string($val) && str_starts_with($key, ":") && str_ends_with($key, ":")) { 724 $this->_emoji_codes[$key] = $val; 725 return true; 726 } else 727 return false; 728 } 729 function emoji_code_map() { 730 global $ConfSitePATH; 731 if ($this->_emoji_codes === null) { 732 $this->_emoji_codes = json_decode(file_get_contents("$ConfSitePATH/etc/emojicodes.json"), true); 733 if (($olist = $this->opt("emojiCodes"))) 734 expand_json_includes_callback($olist, [$this, "_add_emoji_code"]); 735 } 736 return $this->_emoji_codes; 737 } 738 739 740 static function xt_priority($xt) { 741 return $xt ? get($xt, "priority", 0) : -PHP_INT_MAX; 742 } 743 static function xt_priority_compare($xta, $xtb) { 744 $ap = self::xt_priority($xta); 745 $bp = self::xt_priority($xtb); 746 if ($ap == $bp) { 747 $ap = $xta ? get($xta, "__subposition", 0) : -PHP_INT_MAX; 748 $bp = $xtb ? get($xtb, "__subposition", 0) : -PHP_INT_MAX; 749 } 750 return $ap < $bp ? 1 : ($ap == $bp ? 0 : -1); 751 } 752 static function xt_position_compare($xta, $xtb) { 753 $ap = get($xta, "position", 0); 754 $bp = get($xtb, "position", 0); 755 if ($ap == $bp) { 756 $ap = get($xta, "__subposition", 0); 757 $bp = get($xtb, "__subposition", 0); 758 } 759 return $ap < $bp ? -1 : ($ap == $bp ? 0 : 1); 760 } 761 static function xt_add(&$a, $name, $xt) { 762 $a[$name][] = $xt; 763 if (($syn = get($xt, "synonym"))) 764 foreach (is_string($syn) ? [$syn] : $syn as $synname) 765 $a[$synname][] = $xt; 766 return true; 767 } 768 static private function xt_combine($xt1, $xt2) { 769 foreach (get_object_vars($xt2) as $k => $v) 770 if (!property_exists($xt1, $k) 771 && $k !== "match" 772 && $k !== "expand_callback") 773 $xt1->$k = $v; 774 } 775 static function xt_enabled($xt) { 776 return $xt && (!isset($xt->disabled) || !$xt->disabled); 777 } 778 static function xt_disabled($xt) { 779 return !$xt || (isset($xt->disabled) && $xt->disabled); 780 } 781 static function xt_resolve_require($xt) { 782 if ($xt 783 && isset($xt->require) 784 && !isset(self::$xt_require_resolved[$xt->require])) { 785 foreach (expand_includes($xt->require, ["autoload" => true]) as $f) 786 require_once($f); 787 self::$xt_require_resolved[$xt->require] = true; 788 } 789 return $xt && (!isset($xt->disabled) || !$xt->disabled) ? $xt : null; 790 } 791 function xt_check($expr, $xt, Contact $user = null) { 792 $es = is_array($expr) ? $expr : [$expr]; 793 foreach ($es as $e) { 794 $not = false; 795 if (is_string($e) && ($not = str_starts_with($e, "!"))) 796 $e = substr($e, 1); 797 if (!is_string($e)) 798 $b = $e; 799 else if ($e === "chair") 800 $b = !$user || $user->privChair; 801 else if ($e === "manager") 802 $b = !$user || $user->is_manager(); 803 else if ($e === "pc") 804 $b = !$user || $user->isPC; 805 else if ($e === "reviewer") 806 $b = !$user || $user->is_reviewer(); 807 else if ($e === "view_review") 808 $b = !$user || $user->can_view_some_review(); 809 else if ($e === "lead" || $e === "shepherd") 810 $b = $this->has_any_lead_or_shepherd(); 811 else if (strpos($e, "::") !== false) { 812 self::xt_resolve_require($xt); 813 $b = call_user_func($e, $xt, $user, $this); 814 } else { 815 // check if setting exists 816 if (str_starts_with($e, "opt.")) 817 $b = !!$this->opt(substr($e, 4)); 818 else if (str_starts_with($e, "setting.")) 819 $b = !!$this->setting(substr($e, 8)); 820 else 821 $b = !!$this->setting($e); 822 } 823 if ($not ? $b : !$b) 824 return false; 825 } 826 return true; 827 } 828 function xt_allowed($xt, Contact $user = null) { 829 return $xt && (!isset($xt->allow_if) 830 || $this->xt_check($xt->allow_if, $xt, $user)); 831 } 832 static function xt_allow_list($xt) { 833 if ($xt && isset($xt->allow_if)) 834 return is_array($xt->allow_if) ? $xt->allow_if : [$xt->allow_if]; 835 else 836 return []; 837 } 838 function xt_search_name($map, $name, $checkf, $found = null) { 839 foreach (get($map, $name, []) as $xt) 840 if (self::xt_priority_compare($xt, $found) <= 0 841 && call_user_func($checkf, $xt)) 842 $found = $xt; 843 return $found; 844 } 845 function xt_search_factories($factories, $name, $checkf, $found, 846 Contact $user = null, $reflags = "") { 847 $this->xt_user = $user; 848 $this->_xt_factory_match = false; 849 $this->_xt_factory_error = null; 850 $xts = []; 851 foreach ($factories as $fxt) { 852 if (self::xt_priority_compare($fxt, $found) >= 0) 853 break; 854 if ($fxt->match === ".*") 855 $m = [$name]; 856 else if (!preg_match("\1\\A(?:{$fxt->match})\\z\1{$reflags}", $name, $m)) 857 continue; 858 if (!call_user_func($checkf, $fxt)) 859 continue; 860 self::xt_resolve_require($fxt); 861 if (isset($fxt->expand_callback)) 862 $r = call_user_func($fxt->expand_callback, $name, $this, $fxt, $m); 863 else 864 $r = (object) ["name" => $name, "match_data" => $m]; 865 if (is_object($r)) 866 $r = [$r]; 867 foreach ($r ? : [] as $xt) { 868 self::xt_combine($xt, $fxt); 869 $prio = self::xt_priority_compare($xt, $found); 870 if ($prio <= 0 && call_user_func($checkf, $xt)) { 871 if ($prio < 0) 872 $xts = []; 873 $xts[] = $found = $xt; 874 } 875 } 876 } 877 $this->xt_user = null; 878 return $xts; 879 } 880 function xt_factory_mark_matched() { 881 $this->_xt_factory_match = true; 882 } 883 function xt_factory_error($message) { 884 $this->_xt_factory_error[] = $message; 885 } 886 function xt_factory_matched() { 887 return $this->_xt_factory_match; 888 } 889 function xt_factory_errors() { 890 return $this->_xt_factory_error; 891 } 892 893 894 function _add_search_keyword_json($kwj) { 895 if (isset($kwj->name) && is_string($kwj->name)) 896 return self::xt_add($this->_search_keyword_base, $kwj->name, $kwj); 897 else if (is_string($kwj->match) && is_string($kwj->expand_callback)) { 898 $this->_search_keyword_factories[] = $kwj; 899 return true; 900 } else 901 return false; 902 } 903 private function make_search_keyword_map() { 904 $this->_search_keyword_base = $this->_search_keyword_factories = []; 905 expand_json_includes_callback(["etc/searchkeywords.json"], [$this, "_add_search_keyword_json"]); 906 if (($olist = $this->opt("searchKeywords"))) 907 expand_json_includes_callback($olist, [$this, "_add_search_keyword_json"]); 908 usort($this->_search_keyword_factories, "Conf::xt_priority_compare"); 909 } 910 function search_keyword($keyword, Contact $user = null) { 911 if ($this->_search_keyword_base === null) 912 $this->make_search_keyword_map(); 913 $checkf = function ($xt) use ($user) { return $this->xt_allowed($xt, $user); }; 914 $uf = $this->xt_search_name($this->_search_keyword_base, $keyword, $checkf); 915 if (($expansions = $this->xt_search_factories($this->_search_keyword_factories, $keyword, $checkf, $uf, $user))) 916 $uf = $expansions[0]; 917 return self::xt_resolve_require($uf); 918 } 919 920 921 function _add_assignment_parser_json($uf) { 922 if (isset($uf->name) && is_string($uf->name)) 923 return self::xt_add($this->_assignment_parsers, $uf->name, $uf); 924 return false; 925 } 926 function assignment_parser($keyword, Contact $user = null) { 927 require_once("assignmentset.php"); 928 if ($this->_assignment_parsers === null) { 929 $this->_assignment_parsers = []; 930 expand_json_includes_callback(["etc/assignmentparsers.json"], [$this, "_add_assignment_parser_json"]); 931 if (($olist = $this->opt("assignmentParsers"))) 932 expand_json_includes_callback($olist, [$this, "_add_assignment_parser_json"]); 933 } 934 $checkf = function ($xt) use ($user) { return $this->xt_allowed($xt, $user); }; 935 $uf = $this->xt_search_name($this->_assignment_parsers, $keyword, $checkf); 936 $uf = self::xt_resolve_require($uf); 937 if ($uf && !isset($uf->__parser)) { 938 $p = $uf->parser_class; 939 $uf->__parser = new $p($this, $uf); 940 } 941 return $uf ? $uf->__parser : null; 942 } 943 944 945 function _add_formula_function_json($fj) { 946 if (isset($fj->name) && is_string($fj->name)) 947 return self::xt_add($this->_formula_functions, $fj->name, $fj); 948 return false; 949 } 950 function formula_function($fname, Contact $user) { 951 if ($this->_formula_functions === null) { 952 $this->_formula_functions = []; 953 expand_json_includes_callback(["etc/formulafunctions.json"], [$this, "_add_formula_function_json"]); 954 if (($olist = $this->opt("formulaFunctions"))) 955 expand_json_includes_callback($olist, [$this, "_add_formula_function_json"]); 956 } 957 $checkf = function ($xt) use ($user) { return $this->xt_allowed($xt, $user); }; 958 $uf = $this->xt_search_name($this->_formula_functions, $fname, $checkf); 959 return self::xt_resolve_require($uf); 960 } 961 962 963 function named_formulas() { 964 if ($this->_defined_formulas === null) { 965 $this->_defined_formulas = []; 966 if ($this->setting("formulas")) { 967 $result = $this->q("select * from Formula order by lower(name)"); 968 while ($result && ($f = Formula::fetch($this, $result))) 969 $this->_defined_formulas[$f->formulaId] = $f; 970 Dbl::free($result); 971 } 972 } 973 return $this->_defined_formulas; 974 } 975 976 function invalidate_named_formulas() { 977 $this->_defined_formulas = null; 978 } 979 980 function find_named_formula($text) { 981 return $this->abbrev_matcher()->find1($text, self::FSRCH_FORMULA); 982 } 983 984 function viewable_named_formulas(Contact $user, $author_only = false) { 985 return array_filter($this->named_formulas(), function ($f) use ($user, $author_only) { 986 return $user->can_view_formula($f, $author_only); 987 }); 988 } 989 990 991 function decision_map() { 992 if ($this->_decisions === null) { 993 $dmap = array(); 994 if (($j = get($this->settingTexts, "outcome_map")) 995 && ($j = json_decode($j, true)) 996 && is_array($j)) 997 $dmap = $j; 998 $dmap[0] = "Unspecified"; 999 $this->_decisions = $dmap; 1000 uksort($this->_decisions, function ($ka, $kb) use ($dmap) { 1001 if ($ka == 0 || $kb == 0) 1002 return $ka == 0 ? -1 : 1; 1003 else if (($ka > 0) !== ($kb > 0)) 1004 return $ka > 0 ? 1 : -1; 1005 else 1006 return strcasecmp($dmap[$ka], $dmap[$kb]); 1007 }); 1008 } 1009 return $this->_decisions; 1010 } 1011 1012 function decision_name($dnum) { 1013 if ($this->_decisions === null) 1014 $this->decision_map(); 1015 if (($dname = get($this->_decisions, $dnum))) 1016 return $dname; 1017 else 1018 return false; 1019 } 1020 1021 static function decision_name_error($dname) { 1022 $dname = simplify_whitespace($dname); 1023 if ((string) $dname === "") 1024 return "Empty decision name."; 1025 else if (preg_match(',\A(?:yes|no|any|none|unknown|unspecified)\z,i', $dname)) 1026 return "Decision name “{$dname}” is reserved."; 1027 else 1028 return false; 1029 } 1030 1031 1032 1033 function topic_map() { 1034 if ($this->_topic_map === null) { 1035 $this->_topic_map = $tx = []; 1036 $result = $this->qe_raw("select topicId, topicName from TopicArea"); 1037 while (($row = edb_row($result))) { 1038 if (preg_match('{\A(?:None of |Others?(?: |\z))}', $row[1])) 1039 $tx[(int) $row[0]] = $row[1]; 1040 else 1041 $this->_topic_map[(int) $row[0]] = $row[1]; 1042 } 1043 Dbl::free($result); 1044 asort($this->_topic_map, SORT_NATURAL | SORT_FLAG_CASE); 1045 if (!empty($tx)) { 1046 asort($tx, SORT_NATURAL | SORT_FLAG_CASE); 1047 foreach ($tx as $tid => $tname) 1048 $this->_topic_map[$tid] = $tname; 1049 } 1050 } 1051 return $this->_topic_map; 1052 } 1053 1054 function topic_order_map() { 1055 if ($this->_topic_order_map === null) { 1056 $this->_topic_order_map = []; 1057 foreach ($this->topic_map() as $tid => $tname) 1058 $this->_topic_order_map[$tid] = count($this->_topic_order_map); 1059 } 1060 return $this->_topic_order_map; 1061 } 1062 1063 function topic_abbrev_matcher() { 1064 if ($this->_topic_abbrev_matcher === null) { 1065 $this->_topic_abbrev_matcher = new AbbreviationMatcher; 1066 foreach ($this->topic_map() as $tid => $tname) 1067 $this->_topic_abbrev_matcher->add($tname, $tid); 1068 } 1069 return $this->_topic_abbrev_matcher; 1070 } 1071 1072 function has_topics() { 1073 return get($this->settings, "has_topics", 0) !== 0; 1074 } 1075 1076 function topic_count() { 1077 return count($this->topic_map()); 1078 } 1079 1080 function topic_separator() { 1081 if ($this->_topic_separator_cache === null) { 1082 $this->_topic_separator_cache = ", "; 1083 foreach ($this->topic_map() as $tname) 1084 if (strpos($tname, ",") !== false) { 1085 $this->_topic_separator_cache = "; "; 1086 break; 1087 } 1088 } 1089 return $this->_topic_separator_cache; 1090 } 1091 1092 function invalidate_topics() { 1093 $this->_topic_map = $this->_topic_order_map = null; 1094 $this->_topic_separator_cache = $this->_topic_abbrev_matcher = null; 1095 } 1096 1097 1098 const FSRCH_OPTION = 1; 1099 const FSRCH_REVIEW = 2; 1100 const FSRCH_FORMULA = 4; 1101 1102 function abbrev_matcher() { 1103 if (!$this->_abbrev_matcher) { 1104 $this->_abbrev_matcher = new AbbreviationMatcher; 1105 $this->_abbrev_matcher->add("paper", $this->paper_opts->get(DTYPE_SUBMISSION), self::FSRCH_OPTION, 1); 1106 $this->_abbrev_matcher->add("submission", $this->paper_opts->get(DTYPE_SUBMISSION), self::FSRCH_OPTION, 1); 1107 if ($this->has_any_accepted()) { 1108 $ol = $this->paper_opts->option_list(); 1109 $this->_abbrev_matcher->add("final", $this->paper_opts->get(DTYPE_FINAL), self::FSRCH_OPTION, 1); 1110 } else 1111 $ol = $this->paper_opts->nonfinal_option_list(); 1112 // XXX exposes invisible paper options, review fields 1113 foreach ($ol as $o) { 1114 $this->_abbrev_matcher->add($o->name, $o, self::FSRCH_OPTION); 1115 $this->_abbrev_matcher->add("opt" . $o->id, $o, self::FSRCH_OPTION, 1); 1116 } 1117 foreach ($this->all_review_fields() as $f) 1118 if ($f->displayed) 1119 $this->_abbrev_matcher->add($f->name, $f, self::FSRCH_REVIEW); 1120 foreach ($this->named_formulas() as $f) 1121 if ($f->name) 1122 $this->_abbrev_matcher->add($f->name, $f, self::FSRCH_FORMULA); 1123 } 1124 return $this->_abbrev_matcher; 1125 } 1126 1127 function find_all_fields($text, $tflags = 0) { 1128 return $this->abbrev_matcher()->find_all($text, $tflags); 1129 } 1130 1131 1132 function review_form_json() { 1133 $x = get($this->settingTexts, "review_form"); 1134 if (is_string($x)) 1135 $x = $this->settingTexts["review_form"] = json_decode($x); 1136 return is_object($x) ? $x : null; 1137 } 1138 1139 function review_form() { 1140 if (!$this->_review_form_cache) 1141 $this->_review_form_cache = new ReviewForm($this->review_form_json(), $this); 1142 return $this->_review_form_cache; 1143 } 1144 1145 function all_review_fields() { 1146 return $this->review_form()->all_fields(); 1147 } 1148 1149 function review_field($fid) { 1150 return $this->review_form()->field($fid); 1151 } 1152 1153 function find_review_field($text) { 1154 return $this->abbrev_matcher()->find1($text, self::FSRCH_REVIEW); 1155 } 1156 1157 1158 1159 function tags() { 1160 if (!$this->_taginfo) 1161 $this->_taginfo = TagMap::make($this); 1162 return $this->_taginfo; 1163 } 1164 1165 1166 1167 function has_tracks() { 1168 return $this->tracks !== null; 1169 } 1170 1171 function has_track_tags() { 1172 return $this->_track_tags !== null; 1173 } 1174 1175 function track_tags() { 1176 return $this->_track_tags ? $this->_track_tags : array(); 1177 } 1178 1179 function permissive_track_tag_for(Contact $user, $perm) { 1180 foreach ($this->tracks ? : [] as $t => $tr) 1181 if (Track::match_perm($user, $tr[$perm])) 1182 return $t; 1183 return null; 1184 } 1185 1186 function check_tracks(PaperInfo $prow, Contact $contact, $ttype) { 1187 $unmatched = true; 1188 if ($this->tracks) { 1189 foreach ($this->tracks as $t => $tr) 1190 if ($t === "_" ? $unmatched : $prow->has_tag($t)) { 1191 $unmatched = false; 1192 if (Track::match_perm($contact, $tr[$ttype])) 1193 return true; 1194 } 1195 } 1196 return $unmatched; 1197 } 1198 1199 function check_required_tracks(PaperInfo $prow, Contact $contact, $ttype) { 1200 if ($this->_track_sensitivity & (1 << $ttype)) { 1201 $unmatched = true; 1202 foreach ($this->tracks as $t => $tr) 1203 if ($t === "_" ? $unmatched : $prow->has_tag($t)) { 1204 $unmatched = false; 1205 if ($tr[$ttype] && Track::match_perm($contact, $tr[$ttype])) 1206 return true; 1207 } 1208 } 1209 return false; 1210 } 1211 1212 function check_admin_tracks(PaperInfo $prow, Contact $contact) { 1213 return $this->check_required_tracks($prow, $contact, Track::ADMIN); 1214 } 1215 1216 function check_default_track(Contact $contact, $ttype) { 1217 return !$this->tracks || Track::match_perm($contact, $this->tracks["_"][$ttype]); 1218 } 1219 1220 function check_any_tracks(Contact $contact, $ttype) { 1221 if ($this->tracks) 1222 foreach ($this->tracks as $t => $tr) 1223 if (($ttype === Track::VIEW 1224 || Track::match_perm($contact, $tr[Track::VIEW])) 1225 && Track::match_perm($contact, $tr[$ttype])) 1226 return true; 1227 return !$this->tracks; 1228 } 1229 1230 function check_any_admin_tracks(Contact $contact) { 1231 if ($this->_track_sensitivity & Track::BITS_ADMIN) 1232 foreach ($this->tracks as $t => $tr) 1233 if ($tr[Track::ADMIN] && Track::match_perm($contact, $tr[Track::ADMIN])) 1234 return true; 1235 return false; 1236 } 1237 1238 function check_all_tracks(Contact $contact, $ttype) { 1239 if ($this->tracks) 1240 foreach ($this->tracks as $t => $tr) 1241 if (!(($ttype === Track::VIEW 1242 || Track::match_perm($contact, $tr[Track::VIEW])) 1243 && Track::match_perm($contact, $tr[$ttype]))) 1244 return false; 1245 return true; 1246 } 1247 1248 function check_track_sensitivity($ttype) { 1249 return ($this->_track_sensitivity & (1 << $ttype)) !== 0; 1250 } 1251 1252 function check_track_view_sensitivity() { 1253 return ($this->_track_sensitivity & Track::BITS_VIEW) !== 0; 1254 } 1255 1256 function check_track_review_sensitivity() { 1257 return ($this->_track_sensitivity & Track::BITS_REVIEW) !== 0; 1258 } 1259 1260 function track_permission($tag, $ttype) { 1261 if ($this->tracks) 1262 foreach ($this->tracks as $t => $tr) 1263 if (strcasecmp($t, $tag) == 0) 1264 return $tr[$ttype]; 1265 return null; 1266 } 1267 1268 function dangerous_track_mask(Contact $user) { 1269 $m = 0; 1270 if ($this->tracks) { 1271 foreach ($this->tracks as $t => $tr) 1272 foreach ($tr as $i => $perm) 1273 if ($perm && $perm[0] === "-" 1274 && !Track::match_perm($user, $perm)) 1275 $m |= 1 << $i; 1276 } 1277 return $m; 1278 } 1279 1280 1281 function has_rounds() { 1282 return count($this->rounds) > 1; 1283 } 1284 1285 function round_list() { 1286 return $this->rounds; 1287 } 1288 1289 function round0_defined() { 1290 return isset($this->defined_round_list()[0]); 1291 } 1292 1293 function defined_round_list() { 1294 if ($this->_defined_rounds === null) { 1295 $r = $dl = []; 1296 foreach ($this->rounds as $i => $rname) 1297 if (!$i || $rname !== ";") { 1298 foreach (self::$review_deadlines as $rd) 1299 if (($dl[$i] = get($this->settings, $rd . ($i ? "_$i" : "")))) 1300 break; 1301 $i && ($r[$i] = $rname); 1302 } 1303 if (!$dl[0]) { 1304 $result = $this->qe("select exists (select * from PaperReview where reviewRound=0)"); 1305 if (!$result || !$result->num_rows) 1306 unset($dl[0]); 1307 Dbl::free($result); 1308 } 1309 array_key_exists(0, $dl) && ($r[0] = "unnamed"); 1310 uasort($r, function ($a, $b) use ($dl) { 1311 $adl = get($dl, $a); 1312 $bdl = get($dl, $b); 1313 if ($adl && $bdl && $adl != $bdl) 1314 return $adl < $bdl ? -1 : 1; 1315 else if (!$adl != !$bdl) 1316 return $adl ? -1 : 1; 1317 else 1318 return strcmp($a !== "unnamed" ? $a : "", 1319 $b !== "unnamed" ? $b : ""); 1320 }); 1321 $this->_defined_rounds = $r; 1322 } 1323 return $this->_defined_rounds; 1324 } 1325 1326 function round_name($roundno) { 1327 if ($roundno > 0) { 1328 if (($rname = get($this->rounds, $roundno)) && $rname !== ";") 1329 return $rname; 1330 error_log($this->dbname . ": round #$roundno undefined"); 1331 } 1332 return ""; 1333 } 1334 1335 function round_suffix($roundno) { 1336 if ($roundno > 0) { 1337 if (($rname = get($this->rounds, $roundno)) && $rname !== ";") 1338 return "_$rname"; 1339 } 1340 return ""; 1341 } 1342 1343 static function round_name_error($rname) { 1344 if ((string) $rname === "") 1345 return "Empty round name."; 1346 else if (!preg_match('/\A[a-zA-Z][a-zA-Z0-9]*\z/', $rname)) 1347 return "Round names must start with a letter and contain only letters and numbers."; 1348 else if (preg_match('/\A(?:none|any|all|default|unnamed|.*response|pri(?:mary)|sec(?:ondary)|opt(?:ional)|pc(?:review)|ext(?:ernal)|meta(?:review))\z/i', $rname)) 1349 return "Round name $rname is reserved."; 1350 else 1351 return false; 1352 } 1353 1354 function sanitize_round_name($rname) { 1355 if ($rname === null) 1356 return $this->assignment_round_name(false); 1357 else if ($rname === "" || preg_match('/\A(?:\(none\)|none|unnamed)\z/i', $rname)) 1358 return ""; 1359 else if (self::round_name_error($rname)) 1360 return false; 1361 else 1362 return $rname; 1363 } 1364 1365 function assignment_round_name($external) { 1366 if ($external && ($x = get($this->settingTexts, "extrev_roundtag")) !== null) 1367 return $x; 1368 else 1369 return (string) get($this->settingTexts, "rev_roundtag"); 1370 } 1371 1372 function assignment_round($external) { 1373 return $this->round_number($this->assignment_round_name($external), false); 1374 } 1375 1376 function round_number($rname, $add) { 1377 if (!$rname || !strcasecmp($rname, "none") || !strcasecmp($rname, "unnamed")) 1378 return 0; 1379 for ($i = 1; $i != count($this->rounds); ++$i) 1380 if (!strcasecmp($this->rounds[$i], $rname)) 1381 return $i; 1382 if ($add && !self::round_name_error($rname)) { 1383 $rtext = $this->setting_data("tag_rounds", ""); 1384 $rtext = ($rtext ? "$rtext$rname " : " $rname "); 1385 $this->__save_setting("tag_rounds", 1, $rtext); 1386 $this->crosscheck_round_settings(); 1387 return $this->round_number($rname, false); 1388 } else 1389 return false; 1390 } 1391 1392 function round_selector_options($isexternal) { 1393 $opt = $arounds = []; 1394 if (($isexternal === null || $isexternal === false) 1395 && ($r = $this->assignment_round_name(false)) !== null) 1396 $arounds[$r === "" ? "unnamed" : $r] = true; 1397 if (($isexternal === null || $isexternal === true) 1398 && ($r = $this->assignment_round_name(true)) !== null) 1399 $arounds[$r === "" ? "unnamed" : $r] = true; 1400 if (isset($arounds["unnamed"])) 1401 $opt["unnamed"] = "unnamed"; 1402 foreach ($this->defined_round_list() as $rname) 1403 $opt[$rname] = $rname; 1404 foreach (array_keys($arounds) as $r) 1405 $opt[$r] = $r; 1406 return $opt; 1407 } 1408 1409 function round_setting($name, $round, $defval = null) { 1410 if ($this->_round_settings !== null 1411 && $round !== null 1412 && isset($this->_round_settings[$round]) 1413 && isset($this->_round_settings[$round]->$name)) 1414 return $this->_round_settings[$round]->$name; 1415 else 1416 return get($this->settings, $name, $defval); 1417 } 1418 1419 1420 1421 function resp_rounds() { 1422 if ($this->_resp_rounds === null) { 1423 $this->_resp_rounds = []; 1424 $x = get($this->settingTexts, "resp_rounds", "1"); 1425 foreach (explode(" ", $x) as $i => $rname) { 1426 $r = new ResponseRound; 1427 $r->number = $i; 1428 $r->name = $rname; 1429 $isuf = $i ? "_$i" : ""; 1430 $r->open = get($this->settings, "resp_open$isuf"); 1431 $r->done = get($this->settings, "resp_done$isuf"); 1432 $r->grace = get($this->settings, "resp_grace$isuf"); 1433 $r->words = get($this->settings, "resp_words$isuf", 500); 1434 if (($s = get($this->settingTexts, "resp_search$isuf"))) 1435 $r->search = new PaperSearch($this->site_contact(), $s); 1436 $this->_resp_rounds[] = $r; 1437 } 1438 } 1439 return $this->_resp_rounds; 1440 } 1441 1442 function resp_round_name($rnum) { 1443 $rrd = get($this->resp_rounds(), $rnum); 1444 return $rrd ? $rrd->name : "1"; 1445 } 1446 1447 function resp_round_text($rnum) { 1448 $rname = $this->resp_round_name($rnum); 1449 return $rname == "1" ? "" : $rname; 1450 } 1451 1452 static function resp_round_name_error($rname) { 1453 if ((string) $rname === "") 1454 return "Empty round name."; 1455 else if (!strcasecmp($rname, "none") || !strcasecmp($rname, "any") 1456 || stri_ends_with($rname, "response")) 1457 return "Round name “{$rname}” is reserved."; 1458 else if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $rname)) 1459 return "Round names must start with a letter and contain letters and numbers."; 1460 else 1461 return false; 1462 } 1463 1464 function resp_round_number($rname) { 1465 if (!$rname || $rname === 1 || $rname === "1" || $rname === true 1466 || !strcasecmp($rname, "none")) 1467 return 0; 1468 foreach ($this->resp_rounds() as $rrd) 1469 if (!strcasecmp($rname, $rrd->name)) 1470 return $rrd->number; 1471 return false; 1472 } 1473 1474 1475 function format_info($format) { 1476 if ($this->_format_info === null) { 1477 $this->_format_info = []; 1478 if (!isset($this->opt["formatInfo"])) 1479 /* OK */; 1480 else if (is_array($this->opt["formatInfo"])) 1481 $this->_format_info = $this->opt["formatInfo"]; 1482 else if (is_string($this->opt["formatInfo"])) 1483 $this->_format_info = json_decode($this->opt["formatInfo"], true); 1484 foreach ($this->_format_info as $format => &$fi) 1485 $fi = new TextFormat($format, $fi); 1486 } 1487 if ($format === null) 1488 $format = $this->default_format; 1489 return get($this->_format_info, $format); 1490 } 1491 1492 function check_format($format, $text = null) { 1493 if ($format === null) 1494 $format = $this->default_format; 1495 if ($format && $text !== null && ($f = $this->format_info($format)) 1496 && $f->simple_regex && preg_match($f->simple_regex, $text)) 1497 $format = 0; 1498 return $format; 1499 } 1500 1501 1502 function saved_searches() { 1503 $ss = []; 1504 foreach ($this->settingTexts as $k => $v) 1505 if (substr($k, 0, 3) === "ss:" && ($v = json_decode($v))) 1506 $ss[substr($k, 3)] = $v; 1507 return $ss; 1508 } 1509 1510 1511 // users 1512 1513 function external_login() { 1514 return isset($this->opt["ldapLogin"]) || isset($this->opt["httpAuthLogin"]); 1515 } 1516 1517 function site_contact() { 1518 if (!$this->_site_contact) { 1519 $args = [ 1520 "fullName" => $this->opt("contactName"), 1521 "email" => $this->opt("contactEmail"), 1522 "isChair" => true, "isPC" => true, "is_site_contact" => true, 1523 "contactTags" => null 1524 ]; 1525 if (!$args["email"] || $args["email"] === "you@example.com") { 1526 $result = $this->ql("select firstName, lastName, email from ContactInfo where roles!=0 and (roles&" . (Contact::ROLE_CHAIR | Contact::ROLE_ADMIN) . ")!=0 order by (roles&" . Contact::ROLE_CHAIR . ") desc limit 1"); 1527 if ($result && ($row = $result->fetch_object())) { 1528 $this->set_opt("defaultSiteContact", true); 1529 $this->set_opt("contactName", Text::name_text($row)); 1530 $this->set_opt("contactEmail", $row->email); 1531 unset($args["fullName"]); 1532 $args["email"] = $row->email; 1533 $args["firstName"] = $row->firstName; 1534 $args["lastName"] = $row->lastName; 1535 } 1536 Dbl::free($result); 1537 } 1538 $this->_site_contact = new Contact((object) $args, $this); 1539 } 1540 return $this->_site_contact; 1541 } 1542 1543 function user_by_id($id) { 1544 $result = $this->qe("select * from ContactInfo where contactId=?", $id); 1545 $acct = Contact::fetch($result, $this); 1546 Dbl::free($result); 1547 return $acct; 1548 } 1549 1550 function cached_user_by_id($id, $missing = false) { 1551 global $Me; 1552 if ($id && $Me && $Me->contactId == $id) 1553 return $Me; 1554 else if (isset($this->_pc_members_and_admins_cache[$id])) 1555 return $this->_pc_members_and_admins_cache[$id]; 1556 else if (isset($this->_user_cache[$id])) 1557 return $this->_user_cache[$id]; 1558 else if ($missing) { 1559 $this->_user_cache_missing[$id] = true; 1560 return null; 1561 } else 1562 return $this->user_by_id($id); 1563 } 1564 1565 function load_missing_cached_users() { 1566 $n = 0; 1567 if ($this->_user_cache_missing) { 1568 $result = $this->qe("select " . $this->_cached_user_query() . " from ContactInfo where contactId?a", array_keys($this->_user_cache_missing)); 1569 while ($result && ($u = Contact::fetch($result, $this))) { 1570 $this->_user_cache[$u->contactId] = $u; 1571 ++$n; 1572 } 1573 Dbl::free($result); 1574 $this->_user_cache_missing = null; 1575 } 1576 return $n > 0; 1577 } 1578 1579 function user_by_email($email) { 1580 $acct = null; 1581 if (($email = trim((string) $email)) !== "") { 1582 $result = $this->qe("select * from ContactInfo where email=?", $email); 1583 $acct = Contact::fetch($result, $this); 1584 Dbl::free($result); 1585 } 1586 return $acct; 1587 } 1588 1589 function user_id_by_email($email) { 1590 $result = $this->qe("select contactId from ContactInfo where email=?", trim($email)); 1591 $row = edb_row($result); 1592 Dbl::free($result); 1593 return $row ? (int) $row[0] : false; 1594 } 1595 1596 function cached_user_by_email($email) { 1597 global $Me; 1598 if ($email && $Me && strcasecmp($Me->email, $email) == 0) 1599 return $Me; 1600 else if (($u = $this->pc_member_by_email($email))) 1601 return $u; 1602 else 1603 return $this->user_by_email($email); 1604 } 1605 1606 private function _cached_user_query() { 1607 if ($this->_pc_members_fully_loaded) 1608 return "*"; 1609 else 1610 return "contactId, firstName, lastName, unaccentedName, affiliation, email, roles, contactTags, disabled"; 1611 } 1612 1613 function pc_members() { 1614 if ($this->_pc_members_cache === null) { 1615 $pc = array(); 1616 $result = $this->q("select " . $this->_cached_user_query() . " from ContactInfo where roles!=0 and (roles&" . Contact::ROLE_PCLIKE . ")!=0"); 1617 $by_name_text = array(); 1618 $this->_pc_tags_cache = ["pc" => "pc"]; 1619 while ($result && ($row = Contact::fetch($result, $this))) { 1620 $pc[$row->contactId] = $row; 1621 if ($row->firstName || $row->lastName) { 1622 $name_text = Text::name_text($row); 1623 $row2 = get($by_name_text, $name_text); 1624 if ($row2) { 1625 $pc1 = ($row->roles & Contact::ROLE_PC) != 0; 1626 $pc2 = ($row2->roles & Contact::ROLE_PC) != 0; 1627 if (!$pc1 || $pc2) 1628 $row->nameAmbiguous = true; 1629 if (!$pc2 || $pc1) 1630 $row2->nameAmbiguous = true; 1631 if (!$pc2) 1632 $by_name_text[$name_text] = $row; 1633 } else 1634 $by_name_text[$name_text] = $row; 1635 } 1636 if ($row->contactTags) 1637 foreach (explode(" ", $row->contactTags) as $t) { 1638 list($tag, $value) = TagInfo::unpack($t); 1639 if ($tag) 1640 $this->_pc_tags_cache[strtolower($tag)] = $tag; 1641 } 1642 } 1643 Dbl::free($result); 1644 uasort($pc, "Contact::compare"); 1645 $this->_pc_members_and_admins_cache = $pc; 1646 $pc = array_filter($pc, function ($p) { return ($p->roles & Contact::ROLE_PC) != 0; }); 1647 $order = 0; 1648 foreach ($pc as $row) { 1649 $row->sort_position = $order; 1650 ++$order; 1651 } 1652 $this->_pc_members_cache = $pc; 1653 ksort($this->_pc_tags_cache); 1654 } 1655 return $this->_pc_members_cache; 1656 } 1657 1658 function pc_members_and_admins() { 1659 if ($this->_pc_members_and_admins_cache === null) 1660 $this->pc_members(); 1661 return $this->_pc_members_and_admins_cache; 1662 } 1663 1664 function full_pc_members() { 1665 if (!$this->_pc_members_fully_loaded) { 1666 if ($this->_pc_members_cache !== null) { 1667 $result = $this->q("select * from ContactInfo where roles!=0 and (roles&" . Contact::ROLE_PCLIKE . ")!=0"); 1668 while ($result && ($row = $result->fetch_object())) { 1669 if (($pc = get($this->_pc_members_and_admins_cache, $row->contactId))) 1670 $pc->merge_secondary_properties($row); 1671 } 1672 Dbl::free($result); 1673 } 1674 $this->_user_cache = null; 1675 $this->_pc_members_fully_loaded = true; 1676 } 1677 return $this->pc_members(); 1678 } 1679 1680 function pc_member_by_id($cid) { 1681 return get($this->pc_members(), $cid); 1682 } 1683 1684 function pc_member_by_email($email) { 1685 foreach ($this->pc_members() as $p) 1686 if (strcasecmp($p->email, $email) == 0) 1687 return $p; 1688 return null; 1689 } 1690 1691 function pc_tags() { 1692 if ($this->_pc_tags_cache === null) 1693 $this->pc_members(); 1694 return $this->_pc_tags_cache; 1695 } 1696 1697 function pc_tag_exists($tag) { 1698 if ($this->_pc_tags_cache === null) 1699 $this->pc_members(); 1700 return isset($this->_pc_tags_cache[strtolower($tag)]); 1701 } 1702 1703 function pc_completion_map() { 1704 $map = $bylevel = []; 1705 foreach ($this->pc_members_and_admins() as $pc) 1706 if (!$pc->disabled) { 1707 foreach ($pc->completion_items() as $k => $level) { 1708 if (!isset($bylevel[$k]) 1709 || $bylevel[$k] < $level 1710 || get($map, $k) === $pc) { 1711 $map[$k] = $pc; 1712 $bylevel[$k] = $level; 1713 } else 1714 unset($map[$k]); 1715 } 1716 } 1717 return $map; 1718 } 1719 1720 1721 // contactdb 1722 1723 function contactdb() { 1724 if ($this->_cdb === false) { 1725 $this->_cdb = null; 1726 if (($dsn = $this->opt("contactdb_dsn"))) 1727 list($this->_cdb, $dbname) = Dbl::connect_dsn($dsn); 1728 } 1729 return $this->_cdb; 1730 } 1731 1732 private function contactdb_user_by_key($key, $value) { 1733 if (($cdb = $this->contactdb())) { 1734 $q = "select ContactInfo.*, roles, activity_at"; 1735 $qv = []; 1736 if (($confid = $this->opt("contactdb_confid"))) { 1737 $q .= ", ? confid from ContactInfo left join Roles on (Roles.contactDbId=ContactInfo.contactDbId and Roles.confid=?)"; 1738 array_push($qv, $confid, $confid); 1739 } else { 1740 $q .= ", Conferences.confid from ContactInfo left join Conferences on (Conferences.`dbname`=?) left join Roles on (Roles.contactDbId=ContactInfo.contactDbId and Roles.confid=Conferences.confid)"; 1741 $qv[] = $this->dbname; 1742 } 1743 $qv[] = $value; 1744 $result = Dbl::ql_apply($cdb, "$q where ContactInfo.$key=?", $qv); 1745 $acct = Contact::fetch($result, $this); 1746 Dbl::free($result); 1747 return $acct; 1748 } else 1749 return null; 1750 } 1751 1752 function contactdb_user_by_email($email) { 1753 return $this->contactdb_user_by_key("email", $email); 1754 } 1755 1756 function contactdb_user_by_id($id) { 1757 return $this->contactdb_user_by_key("contactDbId", $id); 1758 } 1759 1760 1761 // session data 1762 1763 function session($name, $defval = null) { 1764 if (isset($_SESSION[$this->dsn]) 1765 && isset($_SESSION[$this->dsn][$name])) 1766 return $_SESSION[$this->dsn][$name]; 1767 else 1768 return $defval; 1769 } 1770 1771 function save_session($name, $value) { 1772 if ($value !== null) { 1773 if (empty($_SESSION)) 1774 ensure_session(); 1775 $_SESSION[$this->dsn][$name] = $value; 1776 } else if (isset($_SESSION[$this->dsn])) { 1777 unset($_SESSION[$this->dsn][$name]); 1778 if (empty($_SESSION[$this->dsn])) 1779 unset($_SESSION[$this->dsn]); 1780 } 1781 } 1782 1783 function capability_text($prow, $capType) { 1784 // A capability has the following representation (. is concatenation): 1785 // capFormat . paperId . capType . hashPrefix 1786 // capFormat -- Character denoting format (currently 0). 1787 // paperId -- Decimal representation of paper number. 1788 // capType -- Capability type (e.g. "a" for author view). 1789 // To create hashPrefix, calculate a SHA-1 hash of: 1790 // capFormat . paperId . capType . paperCapVersion . capKey 1791 // where paperCapVersion is a decimal representation of the paper's 1792 // capability version (usually 0, but could allow conference admins 1793 // to disable old capabilities paper-by-paper), and capKey 1794 // is a random string specific to the conference, stored in Settings 1795 // under cap_key (created in load_settings). Then hashPrefix 1796 // is the base-64 encoding of the first 8 bytes of this hash, except 1797 // that "+" is re-encoded as "-", "/" is re-encoded as "_", and 1798 // trailing "="s are removed. 1799 // 1800 // Any user who knows the conference's cap_key can construct any 1801 // capability for any paper. Longer term, one might set each paper's 1802 // capVersion to a random value; but the only way to get cap_key is 1803 // database access, which would give you all the capVersions anyway. 1804 1805 if (!isset($this->settingTexts["cap_key"])) 1806 return false; 1807 $start = "0" . $prow->paperId . $capType; 1808 $hash = sha1($start . $prow->capVersion . $this->settingTexts["cap_key"], true); 1809 $suffix = str_replace(array("+", "/", "="), array("-", "_", ""), 1810 base64_encode(substr($hash, 0, 8))); 1811 return $start . $suffix; 1812 } 1813 1814 1815 // update the 'papersub' setting: are there any submitted papers? 1816 function update_papersub_setting($adding) { 1817 if ($this->setting("no_papersub", 0) > 0 ? $adding >= 0 : $adding <= 0) { 1818 $this->qe("delete from Settings where name='no_papersub'"); 1819 $this->qe("insert into Settings (name, value) select 'no_papersub', 1 from dual where exists (select * from Paper where timeSubmitted>0) = 0"); 1820 $this->settings["no_papersub"] = $this->fetch_ivalue("select value from Settings where name='no_papersub'"); 1821 } 1822 } 1823 1824 function update_paperacc_setting($adding) { 1825 if ($this->setting("paperacc", 0) <= 0 ? $adding >= 0 : $adding <= 0) { 1826 $this->qe_raw("insert into Settings (name, value) select 'paperacc', exists (select * from Paper where outcome>0) on duplicate key update value=values(value)"); 1827 $this->settings["paperacc"] = $this->fetch_ivalue("select value from Settings where name='paperacc'"); 1828 } 1829 } 1830 1831 function update_rev_tokens_setting($adding) { 1832 if ($this->setting("rev_tokens", 0) === -1) 1833 $adding = 0; 1834 if ($this->setting("rev_tokens", 0) <= 0 ? $adding >= 0 : $adding <= 0) { 1835 $this->qe_raw("insert into Settings (name, value) select 'rev_tokens', exists (select * from PaperReview where reviewToken!=0) on duplicate key update value=values(value)"); 1836 $this->settings["rev_tokens"] = $this->fetch_ivalue("select value from Settings where name='rev_tokens'"); 1837 } 1838 } 1839 1840 function update_paperlead_setting($adding) { 1841 if ($this->setting("paperlead", 0) <= 0 ? $adding >= 0 : $adding <= 0) { 1842 $this->qe_raw("insert into Settings (name, value) select 'paperlead', exists (select * from Paper where leadContactId>0 or shepherdContactId>0) on duplicate key update value=values(value)"); 1843 $this->settings["paperlead"] = $this->fetch_ivalue("select value from Settings where name='paperlead'"); 1844 } 1845 } 1846 1847 function update_papermanager_setting($adding) { 1848 if ($this->setting("papermanager", 0) <= 0 ? $adding >= 0 : $adding <= 0) { 1849 $this->qe_raw("insert into Settings (name, value) select 'papermanager', exists (select * from Paper where managerContactId>0) on duplicate key update value=values(value)"); 1850 $this->settings["papermanager"] = $this->fetch_ivalue("select value from Settings where name='papermanager'"); 1851 } 1852 } 1853 1854 function update_metareviews_setting($adding) { 1855 if ($this->setting("metareviews", 0) <= 0 ? $adding >= 0 : $adding <= 0) { 1856 $this->qe_raw("insert into Settings (name, value) select 'metareviews', exists (select * from PaperReview where reviewType=" . REVIEW_META . ") on duplicate key update value=values(value)"); 1857 $this->settings["metareviews"] = $this->fetch_ivalue("select value from Settings where name='metareviews'"); 1858 } 1859 } 1860 1861 function update_autosearch_tags($paper = null) { 1862 if ((!$this->setting("tag_autosearch") && !$this->opt("definedTags")) 1863 || !$this->tags()->has_autosearch 1864 || $this->_updating_autosearch_tags) 1865 return; 1866 $csv = ["paper,tag"]; 1867 if (!$paper) { 1868 foreach ($this->tags()->filter("autosearch") as $dt) { 1869 $csv[] = CsvGenerator::quote("#{$dt->tag}") . "," . CsvGenerator::quote("{$dt->tag}#clear"); 1870 $csv[] = CsvGenerator::quote($dt->autosearch) . "," . CsvGenerator::quote($dt->tag); 1871 } 1872 } else { 1873 if (is_object($paper)) 1874 $paper = $paper->paperId; 1875 $rowset = $this->paper_set(null, ["paperId" => $paper]); 1876 foreach ($this->tags()->filter("autosearch") as $dt) { 1877 $search = new PaperSearch($this->site_contact(), ["q" => $dt->autosearch, "t" => "all"]); 1878 foreach ($rowset as $prow) { 1879 $want = $search->test($prow); 1880 if ($prow->has_tag($dt->tag) !== $want) 1881 $csv[] = "{$prow->paperId}," . CsvGenerator::quote($dt->tag . ($want ? "" : "#clear")); 1882 } 1883 } 1884 } 1885 $this->_update_autosearch_tags_csv($csv); 1886 } 1887 1888 function _update_autosearch_tags_csv($csv) { 1889 if (count($csv) > 1) { 1890 $this->_updating_autosearch_tags = true; 1891 $aset = new AssignmentSet($this->site_contact(), true); 1892 $aset->set_search_type("all"); 1893 $aset->parse($csv); 1894 $aset->execute(); 1895 $this->_updating_autosearch_tags = false; 1896 } 1897 } 1898 1899 1900 static private $invariant_row = null; 1901 1902 private function invariantq($q, $args = []) { 1903 $result = $this->ql_apply($q, $args); 1904 if ($result) { 1905 self::$invariant_row = $result->fetch_row(); 1906 $result->close(); 1907 return !!self::$invariant_row; 1908 } else 1909 return null; 1910 } 1911 1912 function check_invariants() { 1913 $any = $this->invariantq("select paperId from Paper where timeSubmitted>0 limit 1"); 1914 if ($any !== !get($this->settings, "no_papersub")) 1915 trigger_error("$this->dbname invariant error: no_papersub"); 1916 1917 $any = $this->invariantq("select paperId from Paper where outcome>0 and timeSubmitted>0 limit 1"); 1918 if ($any !== !!get($this->settings, "paperacc")) 1919 trigger_error("$this->dbname invariant error: paperacc"); 1920 1921 $any = $this->invariantq("select reviewId from PaperReview where reviewToken!=0 limit 1"); 1922 if ($any !== !!get($this->settings, "rev_tokens")) 1923 trigger_error("$this->dbname invariant error: rev_tokens"); 1924 1925 $any = $this->invariantq("select paperId from Paper where leadContactId>0 or shepherdContactId>0 limit 1"); 1926 if ($any !== !!get($this->settings, "paperlead")) 1927 trigger_error("$this->dbname invariant error: paperlead"); 1928 1929 $any = $this->invariantq("select paperId from Paper where managerContactId>0 limit 1"); 1930 if ($any !== !!get($this->settings, "papermanager")) 1931 trigger_error("$this->dbname invariant error: papermanager"); 1932 1933 $any = $this->invariantq("select paperId from PaperReview where reviewType=" . REVIEW_META . " limit 1"); 1934 if ($any !== !!get($this->settings, "metareviews")) 1935 trigger_error("$this->dbname invariant error: metareviews"); 1936 1937 // no empty text options 1938 $text_options = array(); 1939 foreach ($this->paper_opts->option_list() as $ox) 1940 if ($ox->type === "text") 1941 $text_options[] = $ox->id; 1942 if (count($text_options)) { 1943 $any = $this->invariantq("select paperId from PaperOption where optionId?a and data='' limit 1", [$text_options]); 1944 if ($any) 1945 trigger_error("$this->dbname invariant error: text option with empty text"); 1946 } 1947 1948 // no funky PaperConflict entries 1949 $any = $this->invariantq("select paperId from PaperConflict where conflictType<=0 limit 1"); 1950 if ($any) 1951 trigger_error("$this->dbname invariant error: PaperConflict with zero conflictType"); 1952 1953 // reviewNeedsSubmit is defined correctly 1954 $any = $this->invariantq("select r.paperId, r.reviewId from PaperReview r 1955 left join (select paperId, requestedBy, count(reviewId) ct, count(reviewSubmitted) cs 1956 from PaperReview where reviewType<" . REVIEW_SECONDARY . " 1957 group by paperId, requestedBy) q 1958 on (q.paperId=r.paperId and q.requestedBy=r.contactId) 1959 where r.reviewType=" . REVIEW_SECONDARY . " and reviewSubmitted is null 1960 and if(coalesce(q.ct,0)=0,1,if(q.cs=0,-1,0))!=r.reviewNeedsSubmit 1961 limit 1"); 1962 if ($any) 1963 trigger_error("$this->dbname invariant error: bad reviewNeedsSubmit for review #" . self::$invariant_row[0] . "/" . self::$invariant_row[1]); 1964 1965 // anonymous users are disabled 1966 $any = $this->invariantq("select email from ContactInfo where email regexp '^anonymous[0-9]*\$' and not disabled limit 1"); 1967 if ($any) 1968 trigger_error("$this->dbname invariant error: anonymous user is not disabled"); 1969 1970 // paper denormalizations match 1971 $any = $this->invariantq("select p.paperId from Paper p join PaperStorage ps on (ps.paperStorageId=p.paperStorageId) where p.finalPaperStorageId<=0 and p.paperStorageId>1 and (p.sha1!=ps.sha1 or p.size!=ps.size or p.mimetype!=ps.mimetype or p.timestamp!=ps.timestamp) limit 1"); 1972 if ($any) 1973 trigger_error("$this->dbname invariant error: bad Paper denormalization, paper #" . self::$invariant_row[0]); 1974 $any = $this->invariantq("select p.paperId from Paper p join PaperStorage ps on (ps.paperStorageId=p.finalPaperStorageId) where p.finalPaperStorageId>1 and (p.sha1 != ps.sha1 or p.size!=ps.size or p.mimetype!=ps.mimetype or p.timestamp!=ps.timestamp) limit 1"); 1975 if ($any) 1976 trigger_error("$this->dbname invariant error: bad Paper final denormalization, paper #" . self::$invariant_row[0]); 1977 1978 // filterType is never zero 1979 $any = $this->invariantq("select paperStorageId from PaperStorage where filterType=0 limit 1"); 1980 if ($any) 1981 trigger_error("$this->dbname invariant error: bad PaperStorage filterType, id #" . self::$invariant_row[0]); 1982 1983 // has_colontag is defined 1984 $any = $this->invariantq("select tag from PaperTag where tag like '%:' limit 1"); 1985 if ($any && !$this->setting("has_colontag")) 1986 trigger_error("$this->dbname invariant error: has tag " . self::$invariant_row[0] . " but no has_colontag"); 1987 1988 // has_topics is defined 1989 $any = $this->invariantq("select topicId from TopicArea limit 1"); 1990 if (!$any !== !$this->setting("has_topics")) 1991 trigger_error("$this->dbname invariant error: has_topics setting incorrect"); 1992 1993 $this->check_document_inactive_invariants(); 1994 } 1995 1996 function check_document_inactive_invariants() { 1997 $result = $this->ql("select paperStorageId, finalPaperStorageId from Paper"); 1998 $pids = []; 1999 while ($result && ($row = $result->fetch_row())) { 2000 if ($row[0] > 1) 2001 $pids[] = (int) $row[0]; 2002 if ($row[1] > 1) 2003 $pids[] = (int) $row[1]; 2004 } 2005 sort($pids); 2006 $any = $this->invariantq("select s.paperId, s.paperStorageId from PaperStorage s where s.paperStorageId?a and s.inactive limit 1", [$pids]); 2007 if ($any) 2008 trigger_error("$this->dbname invariant error: paper " . self::$invariant_row[0] . " document " . self::$invariant_row[1] . " is inappropriately inactive"); 2009 2010 $oids = []; 2011 $nonempty_oids = []; 2012 foreach ($this->paper_opts->full_option_list() as $o) 2013 if ($o->has_document()) { 2014 $oids[] = $o->id; 2015 if (!$o->allow_empty_document()) 2016 $nonempty_oids[] = $o->id; 2017 } 2018 2019 if (!empty($oids)) { 2020 $any = $this->invariantq("select o.paperId, o.optionId, s.paperStorageId from PaperOption o join PaperStorage s on (s.paperStorageId=o.value and s.inactive and s.paperStorageId>1) where o.optionId?a limit 1", [$oids]); 2021 if ($any) 2022 trigger_error("$this->dbname invariant error: paper " . self::$invariant_row[0] . " option " . self::$invariant_row[1] . " document " . self::$invariant_row[2] . " is inappropriately inactive"); 2023 2024 $any = $this->invariantq("select o.paperId, o.optionId, s.paperId from PaperOption o join PaperStorage s on (s.paperStorageId=o.value and s.paperStorageId>1 and s.paperId!=o.paperId) where o.optionId?a limit 1", [$oids]); 2025 if ($any) 2026 trigger_error("$this->dbname invariant error: paper " . self::$invariant_row[0] . " option " . self::$invariant_row[1] . " document belongs to different paper " . self::$invariant_row[2]); 2027 } 2028 2029 if (!empty($nonempty_oids)) { 2030 $any = $this->invariantq("select o.paperId, o.optionId from PaperOption o where o.optionId?a and o.value<=1 limit 1", [$nonempty_oids]); 2031 if ($any) 2032 trigger_error("$this->dbname invariant error: paper " . self::$invariant_row[0] . " option " . self::$invariant_row[1] . " links to empty document"); 2033 } 2034 2035 $any = $this->invariantq("select l.paperId, l.linkId, s.paperStorageId from DocumentLink l join PaperStorage s on (l.documentId=s.paperStorageId and s.inactive) limit 1"); 2036 if ($any) 2037 trigger_error("$this->dbname invariant error: paper " . self::$invariant_row[0] . " link " . self::$invariant_row[1] . " document " . self::$invariant_row[2] . " is inappropriately inactive"); 2038 } 2039 2040 2041 private function __save_setting($name, $value, $data = null) { 2042 $change = false; 2043 if ($value === null && $data === null) { 2044 if ($this->qe("delete from Settings where name=?", $name)) { 2045 unset($this->settings[$name]); 2046 unset($this->settingTexts[$name]); 2047 $change = true; 2048 } 2049 } else { 2050 $dval = $data; 2051 if (is_array($dval) || is_object($dval)) 2052 $dval = json_encode_db($dval); 2053 if ($this->qe("insert into Settings (name, value, data) values (?, ?, ?) on duplicate key update value=values(value), data=values(data)", $name, (int) $value, $dval)) { 2054 $this->settings[$name] = $value; 2055 $this->settingTexts[$name] = $data; 2056 $change = true; 2057 } 2058 } 2059 if ($change && str_starts_with($name, "opt.")) { 2060 $oname = substr($name, 4); 2061 if ($value === null && $data === null) 2062 $this->opt[$oname] = get($this->opt_override, $oname); 2063 else 2064 $this->opt[$oname] = $data === null ? $value : $data; 2065 } 2066 return $change; 2067 } 2068 2069 function save_setting($name, $value, $data = null) { 2070 $change = $this->__save_setting($name, $value, $data); 2071 if ($change) { 2072 $this->crosscheck_settings(); 2073 if (str_starts_with($name, "opt.")) 2074 $this->crosscheck_options(); 2075 if (str_starts_with($name, "tag_") || $name === "tracks") 2076 $this->invalidate_caches(["taginfo" => true, "tracks" => true]); 2077 } 2078 return $change; 2079 } 2080 2081 function update_schema_version($n) { 2082 if (!$n) 2083 $n = $this->fetch_ivalue("select value from Settings where name='allowPaperOption'"); 2084 if ($n && $this->ql("update Settings set value=? where name='allowPaperOption'", $n)) { 2085 $this->sversion = $this->settings["allowPaperOption"] = $n; 2086 return true; 2087 } else 2088 return false; 2089 } 2090 2091 function invalidate_caches($caches = null) { 2092 if (!self::$no_invalidate_caches) { 2093 if (is_string($caches)) 2094 $caches = [$caches => true]; 2095 if (!$caches || isset($caches["pc"])) 2096 $this->_pc_members_cache = $this->_pc_tags_cache = $this->_pc_members_and_admins_cache = $this->_user_cache = null; 2097 if (!$caches || isset($caches["options"])) { 2098 $this->paper_opts->invalidate_option_list(); 2099 $this->_formatspec_cache = []; 2100 $this->_abbrev_matcher = null; 2101 } 2102 if (!$caches || isset($caches["rf"])) { 2103 $this->_review_form_cache = $this->_defined_rounds = null; 2104 $this->_abbrev_matcher = null; 2105 } 2106 if (!$caches || isset($caches["taginfo"]) || isset($caches["tracks"])) 2107 $this->_taginfo = null; 2108 if (!$caches || isset($caches["formulas"])) 2109 $this->_formula_functions = null; 2110 if (!$caches || isset($caches["assigners"])) 2111 $this->_assignment_parsers = null; 2112 if (!$caches || isset($caches["tracks"])) 2113 Contact::update_rights(); 2114 } 2115 } 2116 2117 2118 // times 2119 2120 private function _dateFormat($type) { 2121 if (!$this->_date_format_initialized) { 2122 if (!isset($this->opt["time24hour"]) && isset($this->opt["time24Hour"])) 2123 $this->opt["time24hour"] = $this->opt["time24Hour"]; 2124 if (!isset($this->opt["dateFormatLong"]) && isset($this->opt["dateFormat"])) 2125 $this->opt["dateFormatLong"] = $this->opt["dateFormat"]; 2126 if (!isset($this->opt["dateFormat"])) 2127 $this->opt["dateFormat"] = get($this->opt, "time24hour") ? "j M Y H:i:s" : "j M Y g:i:sa"; 2128 if (!isset($this->opt["dateFormatLong"])) 2129 $this->opt["dateFormatLong"] = "l " . $this->opt["dateFormat"]; 2130 if (!isset($this->opt["dateFormatObscure"])) 2131 $this->opt["dateFormatObscure"] = "j M Y"; 2132 if (!isset($this->opt["timestampFormat"])) 2133 $this->opt["timestampFormat"] = $this->opt["dateFormat"]; 2134 if (!isset($this->opt["dateFormatSimplifier"])) 2135 $this->opt["dateFormatSimplifier"] = get($this->opt, "time24hour") ? "/:00(?!:)/" : "/:00(?::00|)(?= ?[ap]m)/"; 2136 if (!isset($this->opt["dateFormatTimezone"])) 2137 $this->opt["dateFormatTimezone"] = null; 2138 $this->_date_format_initialized = true; 2139 } 2140 if ($type == "timestamp") 2141 return $this->opt["timestampFormat"]; 2142 else if ($type == "obscure") 2143 return $this->opt["dateFormatObscure"]; 2144 else if ($type) 2145 return $this->opt["dateFormatLong"]; 2146 else 2147 return $this->opt["dateFormat"]; 2148 } 2149 private function _unparse_timezone($value) { 2150 $z = $this->opt["dateFormatTimezone"]; 2151 if ($z === null) { 2152 $z = date("T", $value); 2153 if ($z === "-12") 2154 $z = "AoE"; 2155 else if ($z && ($z[0] === "+" || $z[0] === "-")) 2156 $z = "UTC" . $z; 2157 } 2158 return $z; 2159 } 2160 2161 function parseableTime($value, $include_zone) { 2162 $f = $this->_dateFormat(false); 2163 $d = date($f, $value); 2164 if ($this->opt["dateFormatSimplifier"]) 2165 $d = preg_replace($this->opt["dateFormatSimplifier"], "", $d); 2166 if ($include_zone && ($z = $this->_unparse_timezone($value))) 2167 $d .= " $z"; 2168 return $d; 2169 } 2170 function parse_time($d, $reference = null) { 2171 global $Now; 2172 if ($reference === null) 2173 $reference = $Now; 2174 if (!isset($this->opt["dateFormatTimezoneRemover"])) { 2175 $x = array(); 2176 if (function_exists("timezone_abbreviations_list")) { 2177 $mytz = date_default_timezone_get(); 2178 foreach (timezone_abbreviations_list() as $tzname => $tzinfo) { 2179 foreach ($tzinfo as $tz) 2180 if ($tz["timezone_id"] == $mytz) 2181 $x[] = preg_quote($tzname); 2182 } 2183 } 2184 if (empty($x)) { 2185 $z = date("T", $reference); 2186 if ($z === "-12") 2187 $x[] = "AoE"; 2188 $x[] = preg_quote($z); 2189 } 2190 $this->opt["dateFormatTimezoneRemover"] = 2191 "/(?:\\s|\\A)(?:" . join("|", $x) . ")(?:\\s|\\z)/i"; 2192 } 2193 if ($this->opt["dateFormatTimezoneRemover"]) 2194 $d = preg_replace($this->opt["dateFormatTimezoneRemover"], " ", $d); 2195 $d = preg_replace_callback('/\b(utc(?=[-+])|aoe(?=\s|\z))/i', function ($m) { 2196 return strcasecmp($m[1], "aoe") === 0 ? "GMT-1200" : "GMT"; 2197 }, $d); 2198 return strtotime($d, $reference); 2199 } 2200 2201 function _printableTime($value, $type, $useradjust, $preadjust = null) { 2202 if ($value <= 0) 2203 return "N/A"; 2204 $t = date($this->_dateFormat($type), $value); 2205 if ($this->opt["dateFormatSimplifier"]) 2206 $t = preg_replace($this->opt["dateFormatSimplifier"], "", $t); 2207 if ($type !== "obscure" && ($z = $this->_unparse_timezone($value))) 2208 $t .= " $z"; 2209 if ($preadjust) 2210 $t .= $preadjust; 2211 if ($useradjust) { 2212 $sp = strpos($useradjust, " "); 2213 $t .= "<$useradjust class=\"usertime hidden\" id=\"usertime$this->usertimeId\"></" . ($sp ? substr($useradjust, 0, $sp) : $useradjust) . ">"; 2214 Ht::stash_script("setLocalTime('usertime$this->usertimeId',$value)"); 2215 ++$this->usertimeId; 2216 } 2217 return $t; 2218 } 2219 function printableTime($value, $useradjust = false, $preadjust = null) { 2220 return $this->_printableTime($value, true, $useradjust, $preadjust); 2221 } 2222 function obscure_time($timestamp) { 2223 if ($timestamp !== null) 2224 $timestamp = (int) ($timestamp + 0.5); 2225 if ($timestamp > 0) { 2226 $offset = 0; 2227 if (($zone = timezone_open(date_default_timezone_get()))) 2228 $offset = $zone->getOffset(new DateTime("@$timestamp")); 2229 $timestamp += 43200 - ($timestamp + $offset) % 86400; 2230 } 2231 return $timestamp; 2232 } 2233 function unparse_time_short($value) { 2234 return $this->_printableTime($value, false, false, null); 2235 } 2236 function unparse_time_full($value) { 2237 return $this->_printableTime($value, "timestamp", false, null); 2238 } 2239 function unparse_time_obscure($value) { 2240 return $this->_printableTime($value, "obscure", false, null); 2241 } 2242 function unparse_time_log($value) { 2243 return date("d/M/Y:H:i:s O", $value); 2244 } 2245 2246 function printableTimeSetting($what, $useradjust = false, $preadjust = null) { 2247 return $this->printableTime(defval($this->settings, $what, 0), $useradjust, $preadjust); 2248 } 2249 function printableDeadlineSetting($what, $useradjust = false, $preadjust = null) { 2250 if (!isset($this->settings[$what]) || $this->settings[$what] <= 0) 2251 return "No deadline"; 2252 else 2253 return "Deadline: " . $this->printableTime($this->settings[$what], $useradjust, $preadjust); 2254 } 2255 2256 function settingsAfter($name) { 2257 global $Now; 2258 $t = get($this->settings, $name); 2259 return $t !== null && $t > 0 && $t <= $Now; 2260 } 2261 function deadlinesAfter($name, $grace = null) { 2262 global $Now; 2263 $t = get($this->settings, $name); 2264 if ($t !== null && $t > 0 && $grace && ($g = get($this->settings, $grace))) 2265 $t += $g; 2266 return $t !== null && $t > 0 && $t <= $Now; 2267 } 2268 function deadlinesBetween($name1, $name2, $grace = null) { 2269 // see also ResponseRound::time_allowed 2270 global $Now; 2271 $t = get($this->settings, $name1); 2272 if (($t === null || $t <= 0 || $t > $Now) && $name1) 2273 return false; 2274 $t = get($this->settings, $name2); 2275 if ($t !== null && $t > 0 && $grace && ($g = get($this->settings, $grace))) 2276 $t += $g; 2277 return $t === null || $t <= 0 || $t >= $Now; 2278 } 2279 2280 function timeStartPaper() { 2281 return $this->deadlinesBetween("sub_open", "sub_reg", "sub_grace"); 2282 } 2283 function timeUpdatePaper($prow = null) { 2284 return $this->deadlinesBetween("sub_open", "sub_update", "sub_grace") 2285 && (!$prow || $prow->timeSubmitted <= 0 || $this->setting("sub_freeze") <= 0); 2286 } 2287 function timeFinalizePaper($prow = null) { 2288 return $this->deadlinesBetween("sub_open", "sub_sub", "sub_grace") 2289 && (!$prow || $prow->timeSubmitted <= 0 || $this->setting('sub_freeze') <= 0); 2290 } 2291 function collectFinalPapers() { 2292 return $this->setting("final_open") > 0; 2293 } 2294 function time_submit_final_version() { 2295 return $this->deadlinesBetween("final_open", "final_done", "final_grace"); 2296 } 2297 function can_some_author_view_review($reviewsOutstanding = false) { 2298 return $this->any_response_open 2299 || ($this->au_seerev > 0 2300 && ($this->au_seerev != self::AUSEEREV_UNLESSINCOMPLETE 2301 || !$reviewsOutstanding)); 2302 } 2303 private function time_author_respond_all_rounds() { 2304 $allowed = []; 2305 foreach ($this->resp_rounds() as $rrd) 2306 if ($rrd->time_allowed(true)) 2307 $allowed[$rrd->number] = $rrd->name; 2308 return $allowed; 2309 } 2310 function time_author_respond($round = null) { 2311 if (!$this->any_response_open) 2312 return $round === null ? [] : false; 2313 else if ($round === null) 2314 return $this->time_author_respond_all_rounds(); 2315 else { 2316 $rrd = get($this->resp_rounds(), $round); 2317 return $rrd && $rrd->time_allowed(true); 2318 } 2319 } 2320 function can_all_author_view_decision() { 2321 return $this->setting("seedec") == self::SEEDEC_ALL; 2322 } 2323 function can_some_author_view_decision() { 2324 return $this->setting("seedec") == self::SEEDEC_ALL; 2325 } 2326 function time_review_open() { 2327 global $Now; 2328 $rev_open = +get($this->settings, "rev_open"); 2329 return 0 < $rev_open && $rev_open <= $Now; 2330 } 2331 function review_deadline($round, $isPC, $hard) { 2332 $dn = ($isPC ? "pcrev_" : "extrev_") . ($hard ? "hard" : "soft"); 2333 if ($round === null) 2334 $round = $this->assignment_round(!$isPC); 2335 else if (is_object($round)) 2336 $round = $round->reviewRound ? : 0; 2337 if ($round && isset($this->settings["{$dn}_$round"])) 2338 $dn .= "_$round"; 2339 return $dn; 2340 } 2341 function missed_review_deadline($round, $isPC, $hard) { 2342 global $Now; 2343 $rev_open = +get($this->settings, "rev_open"); 2344 if (!(0 < $rev_open && $rev_open <= $Now)) 2345 return "rev_open"; 2346 $dn = $this->review_deadline($round, $isPC, $hard); 2347 $dv = +get($this->settings, $dn); 2348 if ($dv > 0 && $dv < $Now) 2349 return $dn; 2350 return false; 2351 } 2352 function time_review($round, $isPC, $hard) { 2353 return !$this->missed_review_deadline($round, $isPC, $hard); 2354 } 2355 function timePCReviewPreferences() { 2356 return $this->can_pc_see_all_submissions() || $this->has_any_submitted(); 2357 } 2358 function timePCViewDecision($conflicted) { 2359 $s = $this->setting("seedec"); 2360 if ($conflicted) 2361 return $s == self::SEEDEC_ALL || $s == self::SEEDEC_REV; 2362 else 2363 return $s >= self::SEEDEC_REV; 2364 } 2365 function time_reviewer_view_decision() { 2366 return $this->setting("seedec") >= self::SEEDEC_REV; 2367 } 2368 function time_reviewer_view_accepted_authors() { 2369 return $this->setting("seedec") == self::SEEDEC_ALL; 2370 } 2371 function timePCViewPaper($prow, $pdf) { 2372 if ($prow->timeWithdrawn > 0) 2373 return false; 2374 else if ($prow->timeSubmitted > 0) 2375 return !$pdf || $this->_pc_see_pdf; 2376 else 2377 return !$pdf && $this->can_pc_see_all_submissions(); 2378 } 2379 2380 function submission_blindness() { 2381 return $this->settings["sub_blind"]; 2382 } 2383 function subBlindAlways() { 2384 return $this->settings["sub_blind"] == self::BLIND_ALWAYS; 2385 } 2386 function subBlindNever() { 2387 return $this->settings["sub_blind"] == self::BLIND_NEVER; 2388 } 2389 function subBlindOptional() { 2390 return $this->settings["sub_blind"] == self::BLIND_OPTIONAL; 2391 } 2392 function subBlindUntilReview() { 2393 return $this->settings["sub_blind"] == self::BLIND_UNTILREVIEW; 2394 } 2395 2396 function is_review_blind($rrow) { 2397 $rb = $this->settings["rev_blind"]; 2398 if ($rb == self::BLIND_ALWAYS) 2399 return true; 2400 else if ($rb != self::BLIND_OPTIONAL) 2401 return false; 2402 if (is_object($rrow)) 2403 $rrow = (bool) $rrow->reviewBlind; 2404 return $rrow === null || $rrow; 2405 } 2406 function review_blindness() { 2407 return $this->settings["rev_blind"]; 2408 } 2409 function can_some_external_reviewer_view_comment() { 2410 return $this->settings["extrev_view"] == 2; 2411 } 2412 2413 function has_any_submitted() { 2414 return !get($this->settings, "no_papersub"); 2415 } 2416 function has_any_pc_visible_pdf() { 2417 return $this->has_any_submitted() && $this->_pc_see_pdf; 2418 } 2419 function has_any_accepted() { 2420 return !!get($this->settings, "paperacc"); 2421 } 2422 2423 function count_submitted_accepted() { 2424 $dlt = max($this->setting("sub_sub"), $this->setting("sub_close")); 2425 $result = $this->qe("select outcome, count(paperId) from Paper where timeSubmitted>0 " . ($dlt ? "or (timeSubmitted=-100 and timeWithdrawn>=$dlt) " : "") . "group by outcome"); 2426 $n = $nyes = 0; 2427 while (($row = edb_row($result))) { 2428 $n += $row[1]; 2429 if ($row[0] > 0) 2430 $nyes += $row[1]; 2431 } 2432 Dbl::free($result); 2433 return [$n, $nyes]; 2434 } 2435 2436 function has_any_lead_or_shepherd() { 2437 return !!get($this->settings, "paperlead"); 2438 } 2439 2440 function has_any_manager() { 2441 return ($this->_track_sensitivity & Track::BITS_ADMIN) 2442 || !!get($this->settings, "papermanager"); 2443 } 2444 2445 function has_any_metareviews() { 2446 return !!get($this->settings, "metareviews"); 2447 } 2448 2449 function can_pc_see_all_submissions() { 2450 if ($this->_pc_seeall_cache === null) { 2451 $this->_pc_seeall_cache = get($this->settings, "pc_seeall") ? : 0; 2452 if ($this->_pc_seeall_cache > 0 && !$this->timeFinalizePaper()) 2453 $this->_pc_seeall_cache = 0; 2454 } 2455 return $this->_pc_seeall_cache > 0; 2456 } 2457 2458 2459 function set_siteurl($base) { 2460 $old_siteurl = Navigation::siteurl(); 2461 $base = Navigation::set_siteurl($base); 2462 if ($this->opt["assetsUrl"] === $old_siteurl) { 2463 $this->opt["assetsUrl"] = $base; 2464 Ht::$img_base = $this->opt["assetsUrl"] . "images/"; 2465 } 2466 if ($this->opt["scriptAssetsUrl"] === $old_siteurl) 2467 $this->opt["scriptAssetsUrl"] = $base; 2468 } 2469 2470 const HOTURL_RAW = 1; 2471 const HOTURL_POST = 2; 2472 const HOTURL_ABSOLUTE = 4; 2473 const HOTURL_SITE_RELATIVE = 8; 2474 const HOTURL_NO_DEFAULTS = 16; 2475 2476 function hoturl($page, $options = null, $flags = 0) { 2477 global $Me; 2478 $amp = ($flags & self::HOTURL_RAW ? "&" : "&"); 2479 $t = $page . Navigation::php_suffix(); 2480 // parse options, separate anchor; see also redirectSelf 2481 $anchor = ""; 2482 if (is_array($options)) { 2483 $x = ""; 2484 foreach ($options as $k => $v) 2485 if ($v === null || $v === false) 2486 /* skip */; 2487 else if ($k !== "anchor") 2488 $x .= ($x === "" ? "" : $amp) . $k . "=" . urlencode($v); 2489 else 2490 $anchor = "#" . urlencode($v); 2491 $options = $x; 2492 } else if (is_string($options)) { 2493 if (preg_match('/\A(.*?)(#.*)\z/', $options, $m)) 2494 list($options, $anchor) = array($m[1], $m[2]); 2495 } else 2496 $options = ""; 2497 if ($flags & self::HOTURL_POST) 2498 $options .= ($options === "" ? "" : $amp) . "post=" . post_value(); 2499 // append defaults 2500 $are = '/\A(|.*?(?:&|&))'; 2501 $zre = '(?:&(?:amp;)?|\z)(.*)\z/'; 2502 if (Conf::$hoturl_defaults && !($flags & self::HOTURL_NO_DEFAULTS)) 2503 foreach (Conf::$hoturl_defaults as $k => $v) 2504 if (!preg_match($are . preg_quote($k) . '=/', $options)) 2505 $options .= $amp . $k . "=" . $v; 2506 // append forceShow to links to same paper if appropriate 2507 $is_paper_page = preg_match('/\A(?:paper|review|comment|assign)\z/', $page); 2508 if ($is_paper_page && $this->paper 2509 && preg_match($are . 'p=' . $this->paper->paperId . $zre, $options) 2510 && $Me->conf === $this 2511 && $Me->can_administer($this->paper) 2512 && $this->paper->has_conflict($Me) 2513 && !preg_match($are . 'forceShow=/', $options)) 2514 $options .= $amp . "forceShow=1"; 2515 // create slash-based URLs if appropriate 2516 if ($options) { 2517 if ($page == "review" 2518 && preg_match($are . 'r=(\d+[A-Z]+)' . $zre, $options, $m)) { 2519 $t .= "/" . $m[2]; 2520 $options = $m[1] . $m[3]; 2521 if (preg_match($are . 'p=\d+' . $zre, $options, $m)) 2522 $options = $m[1] . $m[2]; 2523 } else if ($page == "paper" 2524 && preg_match($are . 'p=(\d+|%\w+%|new)' . $zre, $options, $m) 2525 && preg_match($are . 'm=(\w+)' . $zre, $m[1] . $m[3], $m2)) { 2526 $t .= "/" . $m[2] . "/" . $m2[2]; 2527 $options = $m2[1] . $m2[3]; 2528 } else if (($is_paper_page 2529 && preg_match($are . 'p=(\d+|%\w+%|new)' . $zre, $options, $m)) 2530 || ($page == "profile" 2531 && preg_match($are . 'u=([^&?]+)' . $zre, $options, $m)) 2532 || ($page == "help" 2533 && preg_match($are . 't=(\w+)' . $zre, $options, $m)) 2534 || ($page == "settings" 2535 && preg_match($are . 'group=(\w+)' . $zre, $options, $m)) 2536 || ($page == "graph" 2537 && preg_match($are . 'g=([^&?]+)' . $zre, $options, $m)) 2538 || ($page == "doc" 2539 && preg_match($are . 'file=([^&]+)' . $zre, $options, $m))) { 2540 $t .= "/" . str_replace("%2F", "/", $m[2]); 2541 $options = $m[1] . $m[3]; 2542 } else if (preg_match($are . '__PATH__=([^&]+)' . $zre, $options, $m)) { 2543 $t .= "/" . urldecode($m[2]); 2544 $options = $m[1] . $m[3]; 2545 } 2546 $options = preg_replace('/&(?:amp;)?\z/', "", $options); 2547 } 2548 if ($options && preg_match('/\A&(?:amp;)?(.*)\z/', $options, $m)) 2549 $options = $m[1]; 2550 if ($options !== "") 2551 $t .= "?" . $options; 2552 if ($anchor !== "") 2553 $t .= $anchor; 2554 if ($flags & self::HOTURL_SITE_RELATIVE) 2555 return $t; 2556 $need_site_path = false; 2557 if ($page === "index") { 2558 $expect = "index" . Navigation::php_suffix(); 2559 if (substr($t, 0, strlen($expect)) === $expect 2560 && ($t === $expect || $t[strlen($expect)] === "?" || $t[strlen($expect)] === "#")) { 2561 $need_site_path = true; 2562 $t = substr($t, strlen($expect)); 2563 } 2564 } 2565 if (($flags & self::HOTURL_ABSOLUTE) || $this !== Conf::$g) 2566 return $this->opt("paperSite") . "/" . $t; 2567 else { 2568 $siteurl = Navigation::siteurl(); 2569 if ($need_site_path && $siteurl === "") 2570 $siteurl = Navigation::site_path(); 2571 return $siteurl . $t; 2572 } 2573 } 2574 2575 function hoturl_site_relative($page, $options = null) { 2576 return $this->hoturl($page, $options, self::HOTURL_SITE_RELATIVE); 2577 } 2578 2579 function hoturl_site_relative_raw($page, $options = null) { 2580 return $this->hoturl($page, $options, self::HOTURL_SITE_RELATIVE | self::HOTURL_RAW); 2581 } 2582 2583 function hoturl_post($page, $options = null) { 2584 return $this->hoturl($page, $options, self::HOTURL_POST); 2585 } 2586 2587 function hoturl_raw($page, $options = null) { 2588 return $this->hoturl($page, $options, self::HOTURL_RAW); 2589 } 2590 2591 2592 // 2593 // Paper storage 2594 // 2595 2596 function active_document_ids() { 2597 $q = array("select paperStorageId from Paper where paperStorageId>1", 2598 "select finalPaperStorageId from Paper where finalPaperStorageId>1", 2599 "select paperStorageId from PaperComment where paperStorageId>1"); 2600 $document_option_ids = array(); 2601 foreach ($this->paper_opts->option_list() as $id => $o) 2602 if ($o->has_document()) 2603 $document_option_ids[] = $id; 2604 if (!empty($document_option_ids)) 2605 $q[] = "select value from PaperOption where optionId in (" 2606 . join(",", $document_option_ids) . ") and value>1"; 2607 2608 $result = $this->qe_raw(join(" UNION ", $q)); 2609 $ids = array(); 2610 while (($row = edb_row($result))) 2611 $ids[(int) $row[0]] = true; 2612 Dbl::free($result); 2613 ksort($ids); 2614 return array_keys($ids); 2615 } 2616 2617 function document_by_id($did, PaperInfo $prow = null) { 2618 $result = $this->qe("select * from PaperStorage where paperStorageId=?" 2619 . ($prow ? " and paperId={$prow->paperId}" : ""), $did); 2620 $doc = DocumentInfo::fetch($result, $this, $prow); 2621 Dbl::free($result); 2622 return $doc; 2623 } 2624 2625 function download_documents($docs, $attachment) { 2626 if (count($docs) == 1 2627 && $docs[0]->paperStorageId <= 1 2628 && (!isset($docs[0]->content) || $docs[0]->content === "")) { 2629 self::msg_error("Paper #" . $docs[0]->paperId . " hasn’t been uploaded yet."); 2630 return false; 2631 } 2632 2633 foreach ($docs as $doc) 2634 $doc->filename = $doc->export_filename(); 2635 $downloadname = false; 2636 if (count($docs) > 1) { 2637 $o = $this->paper_opts->get($docs[0]->documentType); 2638 $name = $o->dtype_name(); 2639 if ($docs[0]->documentType <= 0) 2640 $name = pluralize($name); 2641 $downloadname = $this->download_prefix . "$name.zip"; 2642 } 2643 $result = Filer::multidownload($docs, $downloadname, $attachment); 2644 if ($result->error) { 2645 self::msg_error($result->error_html); 2646 return false; 2647 } else 2648 return true; 2649 } 2650 2651 2652 // 2653 // Paper search 2654 // 2655 2656 static private function _cvt_numeric_set($optarr) { 2657 $ids = array(); 2658 if (is_object($optarr)) 2659 $optarr = $optarr->selection(); 2660 foreach (mkarray($optarr) as $x) 2661 if (($x = cvtint($x)) > 0) 2662 $ids[] = $x; 2663 return $ids; 2664 } 2665 2666 function query_all_reviewer_preference() { 2667 return "group_concat(contactId,' ',preference,' ',coalesce(expertise,'.'))"; 2668 } 2669 2670 private function paperQuery(Contact $contact = null, $options = array()) { 2671 // Options: 2672 // "paperId" => $pid Only paperId $pid (if array, any of those) 2673 // "reviewId" => $rid Only paper reviewed by $rid 2674 // "commentId" => $c Only paper where comment is $c 2675 // "finalized" Only submitted papers 2676 // "unsub" Only unsubmitted papers 2677 // "accepted" Only accepted papers 2678 // "active" Only nonwithdrawn papers 2679 // "author" Only papers authored by $contactId 2680 // "myReviewRequests" Only reviews requested by $contactId 2681 // "myReviews" All reviews authored by $contactId 2682 // "myOutstandingReviews" All unsubmitted reviews auth by $contactId 2683 // "myConflicts" Only conflicted papers 2684 // "commenterName" Include commenter names 2685 // "tags" Include paperTags 2686 // "minimal" Only include minimal paper fields 2687 // "tagIndex" => $tag Include tagIndex of named tag 2688 // "tagIndex" => tag array -- include tagIndex, tagIndex1, ... 2689 // "topics" 2690 // "options" 2691 // "scores" => array(fields to score) 2692 // "assignments" 2693 // "order" => $sql $sql is SQL 'order by' clause (or empty) 2694 2695 $contactId = $contact ? $contact->contactId : 0; 2696 2697 // paper selection 2698 $paperset = array(); 2699 if (isset($options["paperId"])) 2700 $paperset[] = self::_cvt_numeric_set($options["paperId"]); 2701 if (isset($options["reviewId"])) { 2702 if (is_numeric($options["reviewId"])) { 2703 $result = $this->qe("select paperId from PaperReview where reviewId=?", $options["reviewId"]); 2704 $paperset[] = self::_cvt_numeric_set(edb_first_columns($result)); 2705 } else if (preg_match('/^(\d+)([A-Z][A-Z]?)$/i', $options["reviewId"], $m)) { 2706 $result = $this->qe("select paperId from PaperReview where paperId=? and reviewOrdinal=?", $m[1], parseReviewOrdinal($m[2])); 2707 $paperset[] = self::_cvt_numeric_set(edb_first_columns($result)); 2708 } else 2709 $paperset[] = array(); 2710 } 2711 if (isset($options["commentId"])) { 2712 $result = $this->qe("select paperId from PaperComment where commentId?a", self::_cvt_numeric_set($options["commentId"])); 2713 $paperset[] = self::_cvt_numeric_set(edb_first_columns($result)); 2714 } 2715 if (count($paperset) > 1) 2716 $paperset = array(call_user_func_array("array_intersect", $paperset)); 2717 $papersel = ""; 2718 if (!empty($paperset)) 2719 $papersel = "paperId" . sql_in_numeric_set($paperset[0]) . " and "; 2720 2721 // prepare query: basic tables 2722 // * Every table in `$joins` can have at most one row per paperId, 2723 // except for `PaperReview`. 2724 $where = array(); 2725 2726 $joins = array("Paper"); 2727 2728 if (get($options, "minimal")) 2729 $cols = ["Paper.paperId, Paper.timeSubmitted, Paper.timeWithdrawn, Paper.outcome, Paper.leadContactId"]; 2730 else 2731 $cols = ["Paper.*"]; 2732 2733 if ($contact) { 2734 $aujoinwhere = null; 2735 if (get($options, "author") 2736 && ($aujoinwhere = $contact->act_author_view_sql("PaperConflict", true))) 2737 $where[] = $aujoinwhere; 2738 if (get($options, "author") && !$aujoinwhere) 2739 $joins[] = "join PaperConflict on (PaperConflict.paperId=Paper.paperId and PaperConflict.contactId=$contactId and PaperConflict.conflictType>=" . CONFLICT_AUTHOR . ")"; 2740 else 2741 $joins[] = "left join PaperConflict on (PaperConflict.paperId=Paper.paperId and PaperConflict.contactId=$contactId)"; 2742 $cols[] = "PaperConflict.conflictType"; 2743 } else if (get($options, "author")) 2744 $where[] = "false"; 2745 2746 // my review 2747 $no_paperreview = $paperreview_is_my_reviews = false; 2748 $reviewjoin = "PaperReview.paperId=Paper.paperId and " . ($contact ? $contact->act_reviewer_sql("PaperReview") : "false"); 2749 if (get($options, "myReviews")) { 2750 $joins[] = "join PaperReview on ($reviewjoin)"; 2751 $paperreview_is_my_reviews = true; 2752 } else if (get($options, "myOutstandingReviews")) 2753 $joins[] = "join PaperReview on ($reviewjoin and reviewNeedsSubmit!=0)"; 2754 else if (get($options, "myReviewRequests")) 2755 $joins[] = "join PaperReview on (PaperReview.paperId=Paper.paperId and requestedBy=" . ($contactId ? : -100) . " and reviewType=" . REVIEW_EXTERNAL . ")"; 2756 else 2757 $no_paperreview = true; 2758 2759 // review signatures 2760 if (get($options, "reviewSignatures") 2761 || get($options, "scores") 2762 || get($options, "reviewWordCounts")) { 2763 $cols[] = "(select " . ReviewInfo::review_signature_sql() . " from PaperReview r where r.paperId=Paper.paperId) reviewSignatures"; 2764 foreach (get($options, "scores", []) as $fid) 2765 if (($f = $this->review_field($fid)) && $f->main_storage) 2766 $cols[] = "(select group_concat({$f->main_storage} order by reviewId) from PaperReview where PaperReview.paperId=Paper.paperId) {$fid}Signature"; 2767 if (get($options, "reviewWordCounts")) 2768 $cols[] = "(select group_concat(coalesce(reviewWordCount,'.') order by reviewId) from PaperReview where PaperReview.paperId=Paper.paperId) reviewWordCountSignature"; 2769 } else if ($contact) { 2770 // need myReviewPermissions 2771 if ($no_paperreview) 2772 $joins[] = "left join PaperReview on ($reviewjoin)"; 2773 if ($no_paperreview || $paperreview_is_my_reviews) 2774 $cols[] = PaperInfo::my_review_permissions_sql("PaperReview.") . " myReviewPermissions"; 2775 else 2776 $cols[] = "(select " . PaperInfo::my_review_permissions_sql() . " from PaperReview where $reviewjoin group by paperId) myReviewPermissions"; 2777 } 2778 2779 // fields 2780 if (get($options, "topics")) 2781 $cols[] = "(select group_concat(topicId) from PaperTopic where PaperTopic.paperId=Paper.paperId) topicIds"; 2782 2783 if (get($options, "options") 2784 && (isset($this->settingTexts["options"]) || isset($this->opt["fixedOptions"])) 2785 && $this->paper_opts->count_option_list()) 2786 $cols[] = "(select group_concat(PaperOption.optionId, '#', value) from PaperOption where paperId=Paper.paperId) optionIds"; 2787 else if (get($options, "options")) 2788 $cols[] = "'' as optionIds"; 2789 2790 if (get($options, "tags") 2791 || ($contact && $contact->isPC) 2792 || $this->has_tracks()) 2793 $cols[] = "(select group_concat(' ', tag, '#', tagIndex order by tag separator '') from PaperTag where PaperTag.paperId=Paper.paperId) paperTags"; 2794 if (get($options, "tagIndex") && !is_array($options["tagIndex"])) 2795 $options["tagIndex"] = array($options["tagIndex"]); 2796 if (get($options, "tagIndex")) 2797 foreach ($options["tagIndex"] as $i => $tag) 2798 $cols[] = "(select tagIndex from PaperTag where PaperTag.paperId=Paper.paperId and PaperTag.tag='" . sqlq($tag) . "') tagIndex" . ($i ? : ""); 2799 2800 if (get($options, "reviewerPreference")) { 2801 $joins[] = "left join PaperReviewPreference on (PaperReviewPreference.paperId=Paper.paperId and PaperReviewPreference.contactId=$contactId)"; 2802 $cols[] = "coalesce(PaperReviewPreference.preference, 0) as reviewerPreference"; 2803 $cols[] = "PaperReviewPreference.expertise as reviewerExpertise"; 2804 } 2805 2806 if (get($options, "allReviewerPreference")) 2807 $cols[] = "(select " . $this->query_all_reviewer_preference() . " from PaperReviewPreference where PaperReviewPreference.paperId=Paper.paperId) allReviewerPreference"; 2808 2809 if (get($options, "allConflictType")) 2810 // See also SearchQueryInfo::add_allConflictType_column 2811 $cols[] = "(select group_concat(contactId, ' ', conflictType) from PaperConflict where PaperConflict.paperId=Paper.paperId) allConflictType"; 2812 2813 if (get($options, "watch") && $contactId) { 2814 $joins[] = "left join PaperWatch on (PaperWatch.paperId=Paper.paperId and PaperWatch.contactId=$contactId)"; 2815 $cols[] = "PaperWatch.watch"; 2816 } 2817 2818 // conditions 2819 if (!empty($paperset)) 2820 $where[] = "Paper.paperId" . sql_in_numeric_set($paperset[0]); 2821 if (get($options, "finalized")) 2822 $where[] = "timeSubmitted>0"; 2823 else if (get($options, "unsub")) 2824 $where[] = "timeSubmitted<=0"; 2825 if (get($options, "accepted")) 2826 $where[] = "outcome>0"; 2827 if (get($options, "undecided")) 2828 $where[] = "outcome=0"; 2829 if (get($options, "active") 2830 || get($options, "myReviews") 2831 || get($options, "myReviewRequests")) 2832 $where[] = "timeWithdrawn<=0"; 2833 if (get($options, "myLead")) 2834 $where[] = "leadContactId=$contactId"; 2835 if (get($options, "unmanaged")) 2836 $where[] = "managerContactId=0"; 2837 if (get($options, "myManaged")) 2838 $where[] = "managerContactId=$contactId"; 2839 if (get($options, "myWatching") && $contactId) { 2840 // return the papers with explicit or implicit WATCH_REVIEW 2841 // (i.e., author/reviewer/commenter); or explicitly managed 2842 // papers 2843 $owhere = [ 2844 "PaperConflict.conflictType>=" . CONFLICT_AUTHOR, 2845 "PaperReview.reviewType>0", 2846 "exists (select * from PaperComment where paperId=Paper.paperId and contactId=$contactId)", 2847 "(PaperWatch.watch&" . Contact::WATCH_REVIEW . ")!=0" 2848 ]; 2849 if ($this->has_any_lead_or_shepherd()) 2850 $owhere[] = "leadContactId=$contactId"; 2851 if ($this->has_any_manager() && $contact->is_explicit_manager()) 2852 $owhere[] = "managerContactId=$contactId"; 2853 $where[] = "(" . join(" or ", $owhere) . ")"; 2854 } 2855 if (get($options, "myConflicts")) 2856 $where[] = $contactId ? "PaperConflict.conflictType>0" : "false"; 2857 2858 $pq = "select " . join(",\n ", $cols) 2859 . "\nfrom " . join("\n ", $joins); 2860 if (!empty($where)) 2861 $pq .= "\nwhere " . join("\n and ", $where); 2862 if (get($options, "tags") === "require") 2863 $pq .= "\nhaving paperTags!=''"; 2864 2865 // grouping and ordering 2866 $pq .= "\ngroup by Paper.paperId\n" 2867 . get($options, "order", "order by Paper.paperId") . "\n"; 2868 2869 //Conf::msg_debugt($pq); 2870 return $pq; 2871 } 2872 2873 function paperRow($sel, Contact $contact = null, &$whyNot = null) { 2874 $ret = null; 2875 $whyNot = ["conf" => $this]; 2876 2877 if (!is_array($sel)) 2878 $sel = array("paperId" => $sel); 2879 if (isset($sel["paperId"])) 2880 $whyNot["paperId"] = $sel["paperId"]; 2881 if (isset($sel["reviewId"])) 2882 $whyNot["reviewId"] = $sel["reviewId"]; 2883 2884 if (isset($sel["paperId"]) && cvtint($sel["paperId"]) < 0) 2885 $whyNot["invalidId"] = "paper"; 2886 else if (isset($sel["reviewId"]) && cvtint($sel["reviewId"]) < 0 2887 && !preg_match('/^\d+[A-Z][A-Z]?$/i', $sel["reviewId"])) 2888 $whyNot["invalidId"] = "review"; 2889 else { 2890 $q = $this->paperQuery($contact, $sel); 2891 $result = $this->qe_raw($q); 2892 2893 if (!$result) 2894 $whyNot["dbError"] = "Database error while fetching paper (" . htmlspecialchars($q) . "): " . htmlspecialchars($this->dblink->error); 2895 else if ($result->num_rows == 0) { 2896 if (!$contact || $contact->isPC) 2897 $whyNot["noPaper"] = 1; 2898 else 2899 $whyNot["permission"] = "view_paper"; 2900 } else 2901 $ret = PaperInfo::fetch($result, $contact, $this); 2902 2903 Dbl::free($result); 2904 } 2905 2906 return $ret; 2907 } 2908 2909 function paper_result(Contact $user = null, $options = []) { 2910 return $this->qe_raw($this->paperQuery($user, $options)); 2911 } 2912 2913 function paper_set(Contact $user = null, $options = []) { 2914 $rowset = new PaperInfoSet; 2915 $result = $this->paper_result($user, $options); 2916 while (($prow = PaperInfo::fetch($result, $user, $this))) 2917 $rowset->add($prow); 2918 Dbl::free($result); 2919 return $rowset; 2920 } 2921 2922 function preferenceConflictQuery($type, $extra) { 2923 $q = "select PRP.paperId, PRP.contactId, PRP.preference 2924 from PaperReviewPreference PRP 2925 join ContactInfo c on (c.contactId=PRP.contactId and c.roles!=0 and (c.roles&" . Contact::ROLE_PC . ")!=0) 2926 join Paper P on (P.paperId=PRP.paperId) 2927 left join PaperConflict PC on (PC.paperId=PRP.paperId and PC.contactId=PRP.contactId) 2928 where PRP.preference<=-100 and coalesce(PC.conflictType,0)<=0 2929 and P.timeWithdrawn<=0"; 2930 if ($type != "all" && ($type || !$this->can_pc_see_all_submissions())) 2931 $q .= " and P.timeSubmitted>0"; 2932 if ($extra) 2933 $q .= " " . $extra; 2934 return $q; 2935 } 2936 2937 2938 // 2939 // Message routines 2940 // 2941 2942 static function msg_on(Conf $conf = null, $type, $text) { 2943 if (PHP_SAPI == "cli") { 2944 if (is_array($text)) 2945 $text = join("\n", $text); 2946 if ($type === "xmerror" || $type === "merror" || $type === 2) 2947 fwrite(STDERR, "$text\n"); 2948 else if ($type === "xwarning" || $type === "warning" || $type === 1 2949 || !defined("HOTCRP_TESTHARNESS")) 2950 fwrite(STDOUT, "$text\n"); 2951 } else if ($conf && !$conf->headerPrinted) { 2952 ensure_session(); 2953 $_SESSION[$conf->dsn]["msgs"][] = [$type, $text]; 2954 } else if ($type[0] == "x" || is_int($type)) 2955 echo Ht::xmsg($type, $text); 2956 else { 2957 if (is_array($text)) 2958 $text = '<div class="multimessage">' . join("", array_map(function ($x) { return '<div class="mmm">' . $x . '</div>'; }, $text)) . '</div>'; 2959 echo "<div class=\"$type\">$text</div>"; 2960 } 2961 } 2962 2963 function msg($type, $text) { 2964 self::msg_on($this, $type, $text); 2965 } 2966 2967 function infoMsg($text, $minimal = false) { 2968 $this->msg($minimal ? "xinfo" : "info", $text); 2969 } 2970 2971 static function msg_info($text, $minimal = false) { 2972 self::msg_on(self::$g, $minimal ? "xinfo" : "info", $text); 2973 } 2974 2975 function warnMsg($text, $minimal = false) { 2976 $this->msg($minimal ? "xwarning" : "warning", $text); 2977 } 2978 2979 static function msg_warning($text, $minimal = false) { 2980 self::msg_on(self::$g, $minimal ? "xwarning" : "warning", $text); 2981 } 2982 2983 function confirmMsg($text, $minimal = false) { 2984 $this->msg($minimal ? "xconfirm" : "confirm", $text); 2985 } 2986 2987 static function msg_confirm($text, $minimal = false) { 2988 self::msg_on(self::$g, $minimal ? "xconfirm" : "confirm", $text); 2989 } 2990 2991 function errorMsg($text, $minimal = false) { 2992 $this->msg($minimal ? "xmerror" : "merror", $text); 2993 return false; 2994 } 2995 2996 static function msg_error($text, $minimal = false) { 2997 self::msg_on(self::$g, $minimal ? "xmerror" : "merror", $text); 2998 return false; 2999 } 3000 3001 static function msg_debugt($text) { 3002 if (is_object($text) || is_array($text) || $text === null || $text === false || $text === true) 3003 $text = json_encode_browser($text); 3004 self::msg_on(self::$g, "merror", Ht::pre_text_wrap($text)); 3005 return false; 3006 } 3007 3008 function post_missing_msg() { 3009 $this->msg("merror", "Your uploaded data wasn’t received. This can happen on unusually slow connections, or if you tried to upload a file larger than I can accept."); 3010 } 3011 3012 3013 // 3014 // Conference header, footer 3015 // 3016 3017 function has_active_list() { 3018 return !!$this->_active_list; 3019 } 3020 3021 function active_list() { 3022 if ($this->_active_list === false) 3023 $this->_active_list = null; 3024 return $this->_active_list; 3025 } 3026 3027 function set_active_list(SessionList $list = null) { 3028 assert($this->_active_list === false); 3029 $this->_active_list = $list; 3030 } 3031 3032 function make_css_link($url, $media = null) { 3033 global $ConfSitePATH; 3034 if (str_starts_with($url, "<meta") || str_starts_with($url, "<link")) 3035 return $url; 3036 $t = '<link rel="stylesheet" type="text/css" href="'; 3037 $absolute = preg_match(',\A(?:https:?:|/),i', $url); 3038 if (!$absolute) 3039 $t .= $this->opt["assetsUrl"]; 3040 $t .= htmlspecialchars($url); 3041 if (!$absolute && ($mtime = @filemtime("$ConfSitePATH/$url")) !== false) 3042 $t .= "?mtime=$mtime"; 3043 if ($media) 3044 $t .= '" media="' . $media; 3045 return $t . '">'; 3046 } 3047 3048 function make_script_file($url, $no_strict = false, $integrity = null) { 3049 global $ConfSitePATH; 3050 if (str_starts_with($url, "scripts/")) { 3051 $post = ""; 3052 if (($mtime = @filemtime("$ConfSitePATH/$url")) !== false) 3053 $post = "mtime=$mtime"; 3054 if (get($this->opt, "strictJavascript") && !$no_strict) 3055 $url = $this->opt["scriptAssetsUrl"] . "cacheable.php?file=" . urlencode($url) 3056 . "&strictjs=1" . ($post ? "&$post" : ""); 3057 else 3058 $url = $this->opt["scriptAssetsUrl"] . $url . ($post ? "?$post" : ""); 3059 if ($this->opt["scriptAssetsUrl"] === Navigation::siteurl()) 3060 return Ht::script_file($url); 3061 } 3062 return Ht::script_file($url, ["crossorigin" => "anonymous", "integrity" => $integrity]); 3063 } 3064 3065 private function make_jquery_script_file($jqueryVersion) { 3066 $integrity = null; 3067 if ($this->opt("jqueryCdn")) { 3068 if ($jqueryVersion === "1.12.4") 3069 $integrity = "sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="; 3070 else if ($jqueryVersion === "3.1.1") 3071 $integrity = "sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="; 3072 else if ($jqueryVersion === "3.2.1") 3073 $integrity = "sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="; 3074 else if ($jqueryVersion === "3.3.1") 3075 $integrity = "sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="; 3076 $jquery = "//code.jquery.com/jquery-{$jqueryVersion}.min.js"; 3077 } else 3078 $jquery = "scripts/jquery-{$jqueryVersion}.min.js"; 3079 return $this->make_script_file($jquery, true, $integrity); 3080 } 3081 3082 function prepare_content_security_policy() { 3083 if (($csp = $this->opt("contentSecurityPolicy"))) { 3084 if (is_string($csp)) 3085 $csp = [$csp]; 3086 else if ($csp === true) 3087 $csp = []; 3088 $report_only = false; 3089 if (($pos = array_search("'report-only'", $csp)) !== false) { 3090 $report_only = true; 3091 array_splice($csp, $pos, 1); 3092 } 3093 if (empty($csp)) 3094 array_push($csp, "script-src", "'nonce'"); 3095 if (($pos = array_search("'nonce'", $csp)) !== false) { 3096 $nonceval = base64_encode(random_bytes(16)); 3097 $csp[$pos] = "'nonce-$nonceval'"; 3098 Ht::set_script_nonce($nonceval); 3099 } 3100 header("Content-Security-Policy" 3101 . ($report_only ? "-Report-Only: " : ": ") 3102 . join(" ", $csp)); 3103 } 3104 } 3105 3106 function set_cookie($name, $value, $expires_at) { 3107 setcookie($name, $value, $expires_at, Navigation::site_path(), 3108 $this->opt("sessionDomain", ""), $this->opt("sessionSecure", false)); 3109 } 3110 3111 function header_head($title, $extra = null) { 3112 global $Me, $Now, $ConfSitePATH; 3113 // clear session list cookie 3114 if (isset($_COOKIE["hotlist-info"])) 3115 $this->set_cookie("hotlist-info", "", $Now - 86400); 3116 3117 echo "<!DOCTYPE html> 3118<html lang=\"en\"> 3119<head> 3120<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"> 3121<meta name=\"google\" content=\"notranslate\">\n"; 3122 if (!$this->opt("allowIndexPapers") && $this->paper) 3123 echo "<meta name=\"robots\" content=\"noindex,noarchive\">\n"; 3124 3125 if (($font_script = $this->opt("fontScript"))) { 3126 if (!str_starts_with($font_script, "<script")) 3127 $font_script = Ht::script($font_script); 3128 echo $font_script, "\n"; 3129 } 3130 3131 foreach (mkarray($this->opt("prependStylesheets", [])) as $css) 3132 echo $this->make_css_link($css), "\n"; 3133 echo $this->make_css_link("stylesheets/style.css"), "\n"; 3134 if ($this->opt("mobileStylesheet")) { 3135 echo '<meta name="viewport" content="width=device-width, initial-scale=1">', "\n"; 3136 echo $this->make_css_link("stylesheets/mobile.css", "screen and (max-width: 768px)"), "\n"; 3137 } 3138 foreach (mkarray($this->opt("stylesheets", [])) as $css) 3139 echo $this->make_css_link($css), "\n"; 3140 3141 // favicon 3142 $favicon = $this->opt("favicon", "images/review24.png"); 3143 if ($favicon) { 3144 if (strpos($favicon, "://") === false && $favicon[0] != "/") { 3145 if ($this->opt["assetsUrl"] && substr($favicon, 0, 7) === "images/") 3146 $favicon = $this->opt["assetsUrl"] . $favicon; 3147 else 3148 $favicon = Navigation::siteurl() . $favicon; 3149 } 3150 if (substr($favicon, -4) == ".png") 3151 echo "<link rel=\"icon\" type=\"image/png\" href=\"$favicon\">\n"; 3152 else if (substr($favicon, -4) == ".ico") 3153 echo "<link rel=\"shortcut icon\" href=\"$favicon\">\n"; 3154 else if (substr($favicon, -4) == ".gif") 3155 echo "<link rel=\"icon\" type=\"image/gif\" href=\"$favicon\">\n"; 3156 else 3157 echo "<link rel=\"icon\" href=\"$favicon\">\n"; 3158 } 3159 3160 // title 3161 echo "<title>"; 3162 if ($title) { 3163 $title = preg_replace("/<([^>\"']|'[^']*'|\"[^\"]*\")*>/", "", $title); 3164 $title = preg_replace(",(?: | |\302\240)+,", " ", $title); 3165 $title = str_replace("∕", "-", $title); 3166 } 3167 if ($title && $title !== "Home") 3168 echo $title, " - "; 3169 echo htmlspecialchars($this->short_name), "</title>\n</head>\n"; 3170 3171 // jQuery 3172 $stash = Ht::unstash(); 3173 if (isset($this->opt["jqueryUrl"])) 3174 Ht::stash_html($this->make_script_file($this->opt["jqueryUrl"], true) . "\n"); 3175 else { 3176 $jqueryVersion = get($this->opt, "jqueryVersion", "3.3.1"); 3177 if ($jqueryVersion[0] === "3") { 3178 Ht::stash_html("<!--[if lt IE 9]>" . $this->make_jquery_script_file("1.12.4") . "<![endif]-->\n"); 3179 Ht::stash_html("<![if !IE|gte IE 9]>" . $this->make_jquery_script_file($jqueryVersion) . "<![endif]>\n"); 3180 } else 3181 Ht::stash_html($this->make_jquery_script_file($jqueryVersion) . "\n"); 3182 } 3183 if ($this->opt("jqueryMigrate")) 3184 Ht::stash_html($this->make_script_file("//code.jquery.com/jquery-migrate-3.0.0.js", true)); 3185 3186 // Javascript settings to set before script.js 3187 Ht::stash_script("siteurl=" . json_encode_browser(Navigation::siteurl()) . ";siteurl_suffix=\"" . Navigation::php_suffix() . "\""); 3188 if (session_id() !== "") { 3189 $p = ""; 3190 if (($x = $this->opt("sessionDomain"))) 3191 $p .= "; domain=" . $x; 3192 if ($this->opt("sessionSecure")) 3193 $p .= "; secure"; 3194 Ht::stash_script("siteurl_postvalue=" . json_encode(post_value()) . ";siteurl_cookie_params=" . json_encode($p)); 3195 } 3196 if (($urldefaults = hoturl_defaults())) 3197 Ht::stash_script("siteurl_defaults=" . json_encode_browser($urldefaults) . ";"); 3198 Ht::stash_script("assetsurl=" . json_encode_browser($this->opt["assetsUrl"]) . ";"); 3199 $huser = (object) array(); 3200 if ($Me && $Me->email) 3201 $huser->email = $Me->email; 3202 if ($Me && $Me->is_pclike()) 3203 $huser->is_pclike = true; 3204 if ($Me && $Me->has_database_account()) 3205 $huser->cid = $Me->contactId; 3206 Ht::stash_script("hotcrp_user=" . json_encode_browser($huser) . ";"); 3207 3208 $pid = $extra ? get($extra, "paperId") : null; 3209 $pid = $pid && ctype_digit($pid) ? (int) $pid : 0; 3210 if (!$pid && $this->paper) 3211 $pid = $this->paper->paperId; 3212 if ($pid) 3213 Ht::stash_script("hotcrp_paperid=$pid"); 3214 if ($pid && $Me && $Me->is_admin_force()) 3215 Ht::stash_script("hotcrp_want_override_conflict=true"); 3216 3217 // script.js 3218 if (!$this->opt("noDefaultScript")) 3219 Ht::stash_html($this->make_script_file("scripts/script.js") . "\n"); 3220 3221 // other scripts 3222 foreach ($this->opt("scripts", []) as $file) 3223 Ht::stash_html($this->make_script_file($file) . "\n"); 3224 3225 if ($stash) 3226 Ht::stash_html($stash); 3227 } 3228 3229 function has_interesting_deadline($my_deadlines) { 3230 global $Now; 3231 if (get($my_deadlines->sub, "open")) 3232 foreach (["reg", "update", "sub"] as $k) 3233 if ($Now <= get($my_deadlines->sub, $k, 0) || get($my_deadlines->sub, "{$k}_ingrace")) 3234 return true; 3235 if (get($my_deadlines, "is_author") && get($my_deadlines, "resps")) 3236 foreach (get($my_deadlines, "resps") as $r) 3237 if ($r->open && ($Now <= $r->done || get($r, "ingrace"))) 3238 return true; 3239 return false; 3240 } 3241 3242 static function echo_header(Conf $conf, $is_home, $site_div, $title_div, 3243 $profile_html, $actions_html, $my_deadlines) { 3244 echo $site_div, '<div id="header_right">', $profile_html; 3245 if ($my_deadlines && $conf->has_interesting_deadline($my_deadlines)) 3246 echo '<div id="maindeadline"> </div>'; 3247 else 3248 echo '<div id="maindeadline" class="hidden"></div>'; 3249 echo '</div>', ($title_div ? : ""), ($actions_html ? : ""); 3250 } 3251 3252 function header_body($title, $id, $extra = null) { 3253 global $ConfSitePATH, $Me, $Now; 3254 echo "<body"; 3255 if ($id) 3256 echo ' id="', $id, '"'; 3257 $class = get($extra, "class"); 3258 if (($list = $this->active_list())) 3259 $class = ($class ? $class . " " : "") . "has-hotlist"; 3260 if ($class) 3261 echo ' class="', $class, '"'; 3262 if ($list) 3263 echo ' data-hotlist="', htmlspecialchars($list->info_string()), '"'; 3264 echo ">\n"; 3265 3266 // initial load (JS's timezone offsets are negative of PHP's) 3267 Ht::stash_script("hotcrp_load.time(" . (-date("Z", $Now) / 60) . "," . ($this->opt("time24hour") ? 1 : 0) . ")"); 3268 3269 // deadlines settings 3270 $my_deadlines = null; 3271 if ($Me) { 3272 $my_deadlines = $Me->my_deadlines($this->paper); 3273 Ht::stash_script("hotcrp_deadlines.init(" . json_encode_browser($my_deadlines) . ")"); 3274 } 3275 if ($this->default_format) 3276 Ht::stash_script("render_text.set_default_format(" . $this->default_format . ")"); 3277 3278 // meeting tracker 3279 $trackerowner = ($trackerstate = $this->setting_json("tracker")) 3280 && $trackerstate->trackerid 3281 && $trackerstate->sessionid == session_id(); 3282 if ($trackerowner) 3283 Ht::stash_script("hotcrp_deadlines.tracker_ui(0)"); 3284 3285 echo '<div id="prebody"><div id="header">'; 3286 3287 // $header_site 3288 $is_home = $id === "home"; 3289 $site_div = '<div id="header_site" class="' 3290 . ($is_home ? "header_site_home" : "header_site_page") 3291 . '"><h1><a class="qq" href="' . $this->hoturl("index") . '">' 3292 . '<span class="header-site-name">' 3293 . htmlspecialchars($this->short_name) . '</span>'; 3294 if (!$is_home) 3295 $site_div .= ' Home'; 3296 $site_div .= '</a></h1></div>'; 3297 3298 // $header_profile 3299 $profile_html = ""; 3300 if ($Me && !$Me->is_empty()) { 3301 // profile link 3302 $profile_parts = []; 3303 if ($Me->has_email() && !$Me->disabled) { 3304 $profile_parts[] = '<a class="q" href="' . $this->hoturl("profile") . '"><strong>' 3305 . htmlspecialchars($Me->email) 3306 . '</strong></a> <a href="' . $this->hoturl("profile") . '">Profile</a>'; 3307 } 3308 3309 // "act as" link 3310 if (($actas = get($_SESSION, "last_actas")) 3311 && get($_SESSION, "trueuser") 3312 && ($Me->privChair || Contact::$trueuser_privChair === $Me)) { 3313 // Link becomes true user if not currently chair. 3314 if (!$Me->privChair || strcasecmp($Me->email, $actas) == 0) 3315 $actas = $_SESSION["trueuser"]->email; 3316 if (strcasecmp($Me->email, $actas) != 0) 3317 $profile_parts[] = "<a href=\"" . selfHref(array("actas" => $actas)) . "\">" 3318 . ($Me->privChair ? htmlspecialchars($actas) : "Admin") 3319 . " " . Ht::img("viewas.png", "Act as " . htmlspecialchars($actas)) 3320 . "</a>"; 3321 } 3322 3323 // help, sign out 3324 $x = ($id == "search" ? "t=$id" : ($id == "settings" ? "t=chair" : "")); 3325 if (!$Me->disabled) 3326 $profile_parts[] = '<a href="' . $this->hoturl("help", $x) . '">Help</a>'; 3327 if (!$Me->has_email() && !isset($this->opt["httpAuthLogin"])) 3328 $profile_parts[] = '<a href="' . $this->hoturl("index", "signin=1") . '" class="nw">Sign in</a>'; 3329 if (!$Me->is_empty() || isset($this->opt["httpAuthLogin"])) 3330 $profile_parts[] = '<a href="' . $this->hoturl_post("index", "signout=1") . '" class="nw">Sign out</a>'; 3331 3332 if (!empty($profile_parts)) 3333 $profile_html .= join(' <span class="barsep">·</span> ', $profile_parts); 3334 } 3335 3336 $action_bar = get($extra, "action_bar"); 3337 if ($action_bar === null) 3338 $action_bar = actionBar(); 3339 3340 $title_div = get($extra, "title_div"); 3341 if (!$title_div) { 3342 if ($title && $title !== "Home") 3343 $title_div = '<div id="header_page"><h1>' . $title . '</h1></div>'; 3344 else if ($action_bar) 3345 $title_div = '<hr class="c">'; 3346 } 3347 3348 $renderf = $this->opt("headerRenderer"); 3349 if (!$renderf) 3350 $renderf = "Conf::echo_header"; 3351 if (is_array($renderf)) { 3352 require_once($renderf[0]); 3353 $renderf = $renderf[1]; 3354 } 3355 call_user_func($renderf, $this, $is_home, $site_div, $title_div, $profile_html, $action_bar, $my_deadlines); 3356 3357 echo " <hr class=\"c\"></div>\n"; 3358 3359 $this->headerPrinted = true; 3360 echo "<div id=\"initialmsgs\">\n"; 3361 if (($x = $this->opt("maintenance"))) 3362 echo Ht::xmsg(2, is_string($x) ? $x : "<strong>The site is down for maintenance.</strong> Please check back later."); 3363 if (($msgs = $this->session("msgs")) && !empty($msgs)) { 3364 $this->save_session("msgs", null); 3365 foreach ($msgs as $m) 3366 $this->msg($m[0], $m[1]); 3367 } 3368 echo "</div>\n"; 3369 3370 echo "</div>\n<div id=\"body\" class=\"body\">\n"; 3371 3372 // If browser owns tracker, send it the script immediately 3373 if ($trackerowner) 3374 echo Ht::unstash(); 3375 3376 // Callback for version warnings 3377 if ($Me && $Me->privChair 3378 && (!isset($_SESSION["updatecheck"]) 3379 || $_SESSION["updatecheck"] + 3600 <= $Now) 3380 && (!isset($this->opt["updatesSite"]) || $this->opt["updatesSite"])) { 3381 $m = isset($this->opt["updatesSite"]) ? $this->opt["updatesSite"] : "//hotcrp.lcdf.org/updates"; 3382 $m .= (strpos($m, "?") === false ? "?" : "&") 3383 . "addr=" . urlencode($_SERVER["SERVER_ADDR"]) 3384 . "&base=" . urlencode(Navigation::siteurl()) 3385 . "&version=" . HOTCRP_VERSION; 3386 $v = HOTCRP_VERSION; 3387 if (is_dir("$ConfSitePATH/.git")) { 3388 $args = array(); 3389 exec("export GIT_DIR=" . escapeshellarg($ConfSitePATH) . "/.git; git rev-parse HEAD 2>/dev/null; git merge-base origin/master HEAD 2>/dev/null", $args); 3390 if (count($args) >= 1) { 3391 $m .= "&git-head=" . urlencode($args[0]); 3392 $v .= " " . $args[0]; 3393 } 3394 if (count($args) >= 2) { 3395 $m .= "&git-upstream=" . urlencode($args[1]); 3396 $v .= " " . $args[1]; 3397 } 3398 } 3399 Ht::stash_script("check_version(\"$m\",\"$v\")"); 3400 $_SESSION["updatecheck"] = $Now; 3401 } 3402 } 3403 3404 function header($title, $id, $extra = null) { 3405 if (!$this->headerPrinted) { 3406 $this->header_head($title, $extra); 3407 $this->header_body($title, $id, $extra); 3408 } 3409 } 3410 3411 static function git_status() { 3412 global $ConfSitePATH; 3413 $args = array(); 3414 if (is_dir("$ConfSitePATH/.git")) 3415 exec("export GIT_DIR=" . escapeshellarg($ConfSitePATH) . "/.git; git rev-parse HEAD 2>/dev/null; git rev-parse v" . HOTCRP_VERSION . " 2>/dev/null", $args); 3416 return count($args) == 2 ? $args : null; 3417 } 3418 3419 function footer() { 3420 global $Me, $ConfSitePATH; 3421 echo "</div>\n", // class='body' 3422 '<div id="footer"><div id="footer_crp">', 3423 $this->opt("extraFooter", ""), 3424 '<a href="http://read.seas.harvard.edu/~kohler/hotcrp/">HotCRP</a>'; 3425 if (!$this->opt("noFooterVersion")) { 3426 if ($Me && $Me->privChair) { 3427 echo " v", HOTCRP_VERSION; 3428 if (($git_data = self::git_status()) && $git_data[0] !== $git_data[1]) 3429 echo " [", substr($git_data[0], 0, 7), "...]"; 3430 } else 3431 echo "<!-- Version ", HOTCRP_VERSION, " -->"; 3432 } 3433 echo "</div>\n <hr class=\"c\"></div>\n"; 3434 echo Ht::unstash(), "</body>\n</html>\n"; 3435 } 3436 3437 function stash_hotcrp_pc(Contact $user) { 3438 if (!Ht::mark_stash("hotcrp_pc")) 3439 return; 3440 $hpcj = $list = []; 3441 foreach ($this->pc_members() as $pcm) { 3442 $hpcj[$pcm->contactId] = $j = (object) ["name" => $user->name_text_for($pcm), "email" => $pcm->email]; 3443 if (($color_classes = $user->user_color_classes_for($pcm))) 3444 $j->color_classes = $color_classes; 3445 if ($this->sort_by_last && $pcm->lastName) { 3446 $r = Text::analyze_name($pcm); 3447 if (strlen($r->lastName) !== strlen($r->name)) 3448 $j->lastpos = strlen($r->firstName) + 1; 3449 if ($r->nameAmbiguous && $r->name !== "" && $r->email !== "") 3450 $j->emailpos = strlen($r->name) + 1; 3451 } 3452 $list[] = $pcm->contactId; 3453 } 3454 $hpcj["__order__"] = $list; 3455 if ($this->sort_by_last) 3456 $hpcj["__sort__"] = "last"; 3457 Ht::stash_script("hotcrp_pc=" . json_encode_browser($hpcj) . ";"); 3458 } 3459 3460 3461 // 3462 // Action recording 3463 // 3464 3465 const action_log_query = "insert into ActionLog (ipaddr, contactId, destContactId, paperId, action) values ?v"; 3466 3467 function save_logs($on) { 3468 if ($on && $this->_save_logs === false) 3469 $this->_save_logs = array(); 3470 else if (!$on && $this->_save_logs !== false) { 3471 $qv = []; 3472 $last_pids = null; 3473 foreach ($this->_save_logs as $cid_text => $pids) { 3474 $pos = strpos($cid_text, "|"); 3475 list($user, $dest_user) = explode(",", substr($cid_text, 0, $pos)); 3476 $what = substr($cid_text, $pos + 1); 3477 $pids = array_keys($pids); 3478 3479 // Combine `Tag:` messages 3480 if (substr($what, 0, 5) === "Tag: " 3481 && ($n = count($qv)) > 0 3482 && substr($qv[$n-1][4], 0, 5) === "Tag: " 3483 && $last_pids === $pids) { 3484 $qv[$n-1][4] = $what . substr($qv[$n-1][4], 4); 3485 continue; 3486 } 3487 3488 $qv[] = self::format_log_values($what, $user, $dest_user, $pids); 3489 $last_pids = $pids; 3490 } 3491 if (!empty($qv)) 3492 $this->qe(self::action_log_query, $qv); 3493 $this->_save_logs = false; 3494 } 3495 } 3496 3497 private static function log_clean_user($user, &$text) { 3498 if (!$user) 3499 return 0; 3500 else if (!is_numeric($user)) { 3501 if ($user->email && !$user->contactId && !$user->is_site_contact) 3502 $text .= " <{$user->email}>"; 3503 return $user->contactId; 3504 } else 3505 return $user; 3506 } 3507 3508 function log_for($user, $dest_user, $text, $pids = null) { 3509 $user = self::log_clean_user($user, $text); 3510 $dest_user = self::log_clean_user($dest_user, $text); 3511 3512 if (is_object($pids)) 3513 $pids = array($pids->paperId); 3514 else if (!is_array($pids)) 3515 $pids = $pids > 0 ? array($pids) : array(); 3516 $ps = array(); 3517 foreach ($pids as $p) 3518 $ps[] = is_object($p) ? $p->paperId : $p; 3519 3520 if ($this->_save_logs === false) 3521 $this->qe(self::action_log_query, [self::format_log_values($text, $user, $dest_user, $ps)]); 3522 else { 3523 $key = "$user,$dest_user|$text"; 3524 if (!isset($this->_save_logs[$key])) 3525 $this->_save_logs[$key] = []; 3526 foreach ($ps as $p) 3527 $this->_save_logs[$key][$p] = true; 3528 } 3529 } 3530 3531 private static function format_log_values($text, $user, $dest_user, $pids) { 3532 $pid = null; 3533 if (count($pids) == 1) 3534 $pid = $pids[0]; 3535 else if (count($pids) > 1) 3536 $text .= " (papers " . join(", ", $pids) . ")"; 3537 return [get($_SERVER, "REMOTE_ADDR"), (int) $user, (int) $dest_user, $pid, substr($text, 0, 4096)]; 3538 } 3539 3540 3541 // capabilities 3542 3543 function capability_manager($for = null) { 3544 if ($for && substr($for, 0, 1) === "U") { 3545 if (($cdb = $this->contactdb())) 3546 return new CapabilityManager($cdb, "U"); 3547 else 3548 return null; 3549 } else 3550 return new CapabilityManager($this->dblink, ""); 3551 } 3552 3553 3554 // messages 3555 3556 function message_name($name) { 3557 if (str_starts_with($name, "msg.")) 3558 $name = substr($name, 4); 3559 if ($name === "revprefdescription" && $this->has_topics()) 3560 $name .= ".withtopics"; 3561 else if (str_starts_with($name, "resp_instrux") && $this->setting("resp_words", 500) > 0) 3562 $name .= ".wordlimit"; 3563 return $name; 3564 } 3565 3566 function message_html($name, $expansions = null) { 3567 $name = $this->message_name($name); 3568 $html = get($this->settingTexts, "msg.$name"); 3569 if ($html === null && ($p = strrpos($name, ".")) !== false) 3570 $html = get($this->settingTexts, "msg." . substr($name, 0, $p)); 3571 if ($html === null) 3572 $html = Message::default_html($name); 3573 if ($html && $expansions) 3574 foreach ($expansions as $k => $v) 3575 $html = str_ireplace("%$k%", $v, $html); 3576 return $html; 3577 } 3578 3579 function message_default_html($name) { 3580 return Message::default_html($this->message_name($name)); 3581 } 3582 3583 3584 function ims() { 3585 if (!$this->_ims) { 3586 $this->_ims = new IntlMsgSet; 3587 $m = ["?etc/msgs.json"]; 3588 if (($lang = $this->opt("lang"))) 3589 $m[] = "?etc/msgs.$lang.json"; 3590 $this->_ims->set_default_priority(-1.0); 3591 expand_json_includes_callback($m, [$this->_ims, "addj"]); 3592 $this->_ims->clear_default_priority(); 3593 if (($mlist = $this->opt("messageOverrides"))) 3594 expand_json_includes_callback($mlist, [$this->_ims, "addj"]); 3595 } 3596 return $this->_ims; 3597 } 3598 3599 function _($itext) { 3600 return call_user_func_array([$this->ims(), "x"], func_get_args()); 3601 } 3602 3603 function _c($context, $itext) { 3604 return call_user_func_array([$this->ims(), "xc"], func_get_args()); 3605 } 3606 3607 function _i($id, $itext) { 3608 return call_user_func_array([$this->ims(), "xi"], func_get_args()); 3609 } 3610 3611 function _ci($context, $id, $itext) { 3612 return call_user_func_array([$this->ims(), "xci"], func_get_args()); 3613 } 3614 3615 3616 // API 3617 function _add_api_json($fj) { 3618 if (isset($fj->name) && is_string($fj->name) 3619 && isset($fj->callback) && is_string($fj->callback)) 3620 return self::xt_add($this->_api_map, $fj->name, $fj); 3621 else 3622 return false; 3623 } 3624 private function api_map() { 3625 if ($this->_api_map === null) { 3626 $this->_api_map = []; 3627 expand_json_includes_callback(["etc/apifunctions.json"], [$this, "_add_api_json"]); 3628 if (($olist = $this->opt("apiFunctions"))) 3629 expand_json_includes_callback($olist, [$this, "_add_api_json"]); 3630 } 3631 return $this->_api_map; 3632 } 3633 private function check_api_json($fj, Contact $user = null, $method) { 3634 if (isset($fj->allow_if) && !$this->xt_allowed($fj, $user)) 3635 return false; 3636 else if (!$method) 3637 return true; 3638 else { 3639 $methodx = get($fj, strtolower($method)); 3640 return $methodx 3641 || ($method === "POST" && $methodx === null && get($fj, "get")); 3642 } 3643 } 3644 function has_api($fn, Contact $user = null, $method = null) { 3645 return !!$this->api($fn, $user, $method); 3646 } 3647 function api($fn, Contact $user = null, $method = null) { 3648 $checkf = function ($xt) use ($user, $method) { 3649 return $this->check_api_json($xt, $user, $method); 3650 }; 3651 $uf = $this->xt_search_name($this->api_map(), $fn, $checkf); 3652 return self::xt_enabled($uf) ? $uf : null; 3653 } 3654 private function call_api($fn, $uf, Contact $user, Qrequest $qreq, $prow) { 3655 $method = $qreq->method(); 3656 if ($method !== "GET" && $method !== "HEAD" && $method !== "OPTIONS" 3657 && (!$uf || !get($uf, "allow_xss")) && !$qreq->post_ok()) 3658 return new JsonResult(403, ["ok" => false, "error" => "Missing credentials."]); 3659 if (!$uf) { 3660 if ($this->has_api($fn, $user, null)) 3661 return new JsonResult(405, ["ok" => false, "error" => "Method not supported."]); 3662 else if ($this->has_api($fn, null, $qreq->method())) 3663 return new JsonResult(403, ["ok" => false, "error" => "Permission error."]); 3664 else 3665 return new JsonResult(404, ["ok" => false, "error" => "Function not found."]); 3666 } 3667 if (!$prow && get($uf, "paper")) { 3668 $result = ["ok" => false]; 3669 if (($whynot = $qreq->attachment("paper_whynot"))) { 3670 $status = isset($result["noPaper"]) ? 404 : 403; 3671 $result["error"] = whyNotText($whynot, true); 3672 if (isset($whynot["signin"])) 3673 $result["loggedout"] = true; 3674 } else { 3675 $status = 400; 3676 $result["error"] = "No paper specified."; 3677 } 3678 return new JsonResult($status, $result); 3679 } 3680 self::xt_resolve_require($uf); 3681 return call_user_func($uf->callback, $user, $qreq, $prow, $uf); 3682 } 3683 function call_api_exit($fn, Contact $user, Qrequest $qreq, PaperInfo $prow = null) { 3684 // XXX precondition: $user->can_view_paper($prow) || !$prow 3685 $uf = $this->api($fn, $user, $qreq->method()); 3686 if ($uf && get($uf, "redirect") && $qreq->redirect 3687 && preg_match('@\A(?![a-z]+:|/).+@', $qreq->redirect)) { 3688 try { 3689 JsonResultException::$capturing = true; 3690 $j = $this->call_api($fn, $uf, $user, $qreq, $prow); 3691 } catch (JsonResultException $ex) { 3692 $j = $ex->result; 3693 } 3694 if (is_object($j) && $j instanceof JsonResult) 3695 $j = $j->content; 3696 if (!get($j, "ok") && !get($j, "error")) 3697 Conf::msg_error("Internal error."); 3698 else if (($x = get($j, "error"))) 3699 Conf::msg_error(htmlspecialchars($x)); 3700 else if (($x = get($j, "error_html"))) 3701 Conf::msg_error($x); 3702 Navigation::redirect_site($qreq->redirect); 3703 } else { 3704 $j = $this->call_api($fn, $uf, $user, $qreq, $prow); 3705 json_exit($j); 3706 } 3707 } 3708 3709 3710 // List action API 3711 function _add_list_action_json($fj) { 3712 $ok = false; 3713 if (isset($fj->name) && is_string($fj->name)) { 3714 if (isset($fj->render_callback) && is_string($fj->render_callback)) 3715 $ok = self::xt_add($this->_list_action_renderers, $fj->name, $fj); 3716 if (isset($fj->callback) && is_string($fj->callback)) 3717 $ok = self::xt_add($this->_list_action_map, $fj->name, $fj); 3718 } else if (is_string($fj->match) && is_string($fj->expand_callback)) { 3719 $this->_list_action_factories[] = $fj; 3720 $ok = true; 3721 } 3722 return $ok; 3723 } 3724 function list_action_map() { 3725 if ($this->_list_action_map === null) { 3726 $this->_list_action_map = $this->_list_action_renderers = $this->_list_action_factories = []; 3727 expand_json_includes_callback(["etc/listactions.json"], [$this, "_add_list_action_json"]); 3728 if (($olist = $this->opt("listActions"))) 3729 expand_json_includes_callback($olist, [$this, "_add_list_action_json"]); 3730 usort($this->_list_action_factories, "Conf::xt_priority_compare"); 3731 } 3732 return $this->_list_action_map; 3733 } 3734 function list_action_renderers() { 3735 $this->list_action_map(); 3736 return $this->_list_action_renderers; 3737 } 3738 function has_list_action($name, Contact $user = null, $method = null) { 3739 return !!$this->list_action($name, $user, $method); 3740 } 3741 function list_action($name, Contact $user = null, $method = null) { 3742 $checkf = function ($xt) use ($user, $method) { 3743 return $this->check_api_json($xt, $user, $method); 3744 }; 3745 $uf = $this->xt_search_name($this->list_action_map(), $name, $checkf); 3746 if (($s = strpos($name, "/")) !== false) 3747 $uf = $this->xt_search_name($this->list_action_map(), substr($name, 0, $s), $checkf, $uf); 3748 if (($expansions = $this->xt_search_factories($this->_list_action_factories, $name, $checkf, $uf, $user))) 3749 $uf = $expansions[0]; 3750 return self::xt_resolve_require($uf); 3751 } 3752 3753 function make_csvg($basename, $flags = 0) { 3754 $csv = new CsvGenerator($flags); 3755 $csv->set_filename($this->download_prefix . $basename . $csv->extension()); 3756 return $csv; 3757 } 3758 3759 3760 // Paper columns 3761 function _add_paper_column_json($fj) { 3762 $cb = isset($fj->callback) && is_string($fj->callback); 3763 if (isset($fj->name) && is_string($fj->name) && $cb) { 3764 return self::xt_add($this->_paper_column_map, $fj->name, $fj); 3765 } else if (is_string($fj->match) && (isset($fj->expand_callback) ? is_string($fj->expand_callback) : $cb)) { 3766 $this->_paper_column_factories[] = $fj; 3767 return true; 3768 } else 3769 return false; 3770 } 3771 function paper_column_map() { 3772 if ($this->_paper_column_map === null) { 3773 require_once("papercolumn.php"); 3774 $this->_paper_column_map = $this->_paper_column_factories = []; 3775 expand_json_includes_callback(["etc/papercolumns.json"], [$this, "_add_paper_column_json"]); 3776 if (($olist = $this->opt("paperColumns"))) 3777 expand_json_includes_callback($olist, [$this, "_add_paper_column_json"]); 3778 usort($this->_paper_column_factories, "Conf::xt_priority_compare"); 3779 } 3780 return $this->_paper_column_map; 3781 } 3782 function paper_column_factories() { 3783 $this->paper_column_map(); 3784 return $this->_paper_column_factories; 3785 } 3786 function basic_paper_column($name, Contact $user = null) { 3787 $checkf = function ($xt) use ($user) { return $this->xt_allowed($xt, $user); }; 3788 $uf = $this->xt_search_name($this->paper_column_map(), $name, $checkf); 3789 return self::xt_enabled($uf) ? $uf : null; 3790 } 3791 function paper_columns($name, Contact $user) { 3792 $checkf = function ($xt) use ($user) { return $this->xt_allowed($xt, $user); }; 3793 $uf = $this->xt_search_name($this->paper_column_map(), $name, $checkf); 3794 $expansions = $this->xt_search_factories($this->_paper_column_factories, $name, $checkf, $uf, $user, "i"); 3795 return array_filter($expansions ? : [$uf], "Conf::xt_resolve_require"); 3796 } 3797 3798 3799 // Option types 3800 function _add_option_type_json($fj) { 3801 $cb = isset($fj->callback) && is_string($fj->callback); 3802 if (isset($fj->name) && is_string($fj->name) && $cb) 3803 return self::xt_add($this->_option_type_map, $fj->name, $fj); 3804 else if (is_string($fj->match) && (isset($fj->expand_callback) ? is_string($fj->expand_callback) : $cb)) { 3805 $this->_option_type_factories[] = $fj; 3806 return true; 3807 } else 3808 return false; 3809 } 3810 function option_type_map() { 3811 if ($this->_option_type_map === null) { 3812 require_once("paperoption.php"); 3813 $this->_option_type_map = $this->_option_type_factories = []; 3814 expand_json_includes_callback(["etc/optiontypes.json"], [$this, "_add_option_type_json"]); 3815 if (($olist = $this->opt("optionTypes"))) 3816 expand_json_includes_callback($olist, [$this, "_add_option_type_json"]); 3817 usort($this->_option_type_factories, "Conf::xt_priority_compare"); 3818 // option types are global (cannot be allowed per user) 3819 $m = []; 3820 foreach (array_keys($this->_option_type_map) as $name) { 3821 if (($uf = $this->xt_search_name($this->_option_type_map, $name, [$this, "xt_allowed"]))) 3822 $m[$name] = $uf; 3823 } 3824 $this->_option_type_map = $m; 3825 } 3826 return $this->_option_type_map; 3827 } 3828 function option_type($name) { 3829 $uf = get($this->option_type_map(), $name); 3830 if (($expansions = $this->xt_search_factories($this->_option_type_factories, $name, [$this, "xt_allowed"], $uf, null, "i"))) 3831 $uf = $expansions[0]; 3832 return $uf; 3833 } 3834 3835 3836 // Mail keywords 3837 function _add_mail_keyword_json($fj) { 3838 $cb = isset($fj->callback) && is_string($fj->callback); 3839 if (isset($fj->name) && is_string($fj->name) && $cb) 3840 return self::xt_add($this->_mail_keyword_map, $fj->name, $fj); 3841 else if (is_string($fj->match) && (isset($fj->expand_callback) ? is_string($fj->expand_callback) : $cb)) { 3842 $this->_mail_keyword_factories[] = $fj; 3843 return true; 3844 } else 3845 return false; 3846 } 3847 function mail_keyword_map() { 3848 if ($this->_mail_keyword_map === null) { 3849 $this->_mail_keyword_map = $this->_mail_keyword_factories = []; 3850 expand_json_includes_callback(["etc/mailkeywords.json"], [$this, "_add_mail_keyword_json"]); 3851 if (($mks = $this->opt("mailKeywords"))) 3852 expand_json_includes_callback($mks, [$this, "_add_mail_keyword_json"]); 3853 usort($this->_mail_keyword_factories, "Conf::xt_priority_compare"); 3854 } 3855 return $this->_mail_keyword_map; 3856 } 3857 function mail_keywords($name) { 3858 $checkf = [$this, "xt_allowed"]; 3859 $uf = $this->xt_search_name($this->mail_keyword_map(), $name, $checkf); 3860 $expansions = $this->xt_search_factories($this->_mail_keyword_factories, $name, $checkf, $uf, null, ""); 3861 return array_filter($expansions ? : [$uf], "Conf::xt_resolve_require"); 3862 } 3863 3864 3865 // Hooks 3866 function _add_hook_json($fj) { 3867 if (isset($fj->callback) && is_string($fj->callback) && !isset($fj->synonym)) { 3868 if (isset($fj->event) && is_string($fj->event)) 3869 return self::xt_add($this->_hook_map, $fj->event, $fj); 3870 else if (isset($fj->match) && is_string($fj->match)) { 3871 $this->_hook_factories[] = $fj; 3872 return true; 3873 } 3874 } 3875 return false; 3876 } 3877 function add_hook($name, $callback = null, $priority = null) { 3878 if ($this->_hook_map === null) 3879 $this->hook_map(); 3880 $fj = is_object($name) ? $name : $callback; 3881 if (is_string($fj)) 3882 $fj = (object) ["callback" => $fj]; 3883 if (is_string($name)) 3884 $fj->event = $name; 3885 if ($priority !== null) 3886 $fj->priority = $priority; 3887 return $this->_add_hook_json($fj) ? $fj : false; 3888 } 3889 function remove_hook($fj) { 3890 if (isset($fj->event) && is_string($fj->event) 3891 && isset($this->_hook_map[$fj->event]) 3892 && ($i = array_search($fj, $this->_hook_map[$fj->event], true)) !== false) { 3893 array_splice($this->_hook_map[$fj->event], $i, 1); 3894 return true; 3895 } else if (isset($fj->match) && is_string($fj->match) 3896 && ($i = array_search($fj, $this->_hook_factories, true)) !== false) { 3897 array_splice($this->_hook_factories, $i, 1); 3898 return true; 3899 } 3900 return false; 3901 } 3902 private function hook_map() { 3903 if ($this->_hook_map === null) { 3904 $this->_hook_map = $this->_hook_factories = []; 3905 if (($hlist = $this->opt("hooks"))) 3906 expand_json_includes_callback($hlist, [$this, "_add_hook_json"]); 3907 } 3908 return $this->_hook_map; 3909 } 3910 function call_hooks($name, Contact $user = null /* ... args */) { 3911 $hs = get($this->hook_map(), $name); 3912 foreach ($this->_hook_factories as $fj) { 3913 if ($fj->match === ".*" 3914 || preg_match("\1\\A(?:{$fxt->match})\\z\1", $name, $m)) { 3915 $xfj = clone $fj; 3916 $xfj->event = $name; 3917 $xfj->match_data = $m; 3918 $hs[] = $xfj; 3919 } 3920 } 3921 if ($hs !== null) { 3922 $args = array_slice(func_get_args(), 1); 3923 usort($hs, "Conf::xt_priority_compare"); 3924 $ids = []; 3925 foreach ($hs as $fj) { 3926 if ((!isset($fj->id) || !isset($ids[$fj->id])) 3927 && $this->xt_allowed($fj, $user)) { 3928 if (isset($fj->id)) 3929 $ids[$fj->id] = true; 3930 if (!self::xt_disabled($fj)) { 3931 $fj->conf = $this; 3932 $fj->user = $user; 3933 $args[0] = $fj; 3934 $x = call_user_func_array($fj->callback, $args); 3935 unset($fj->conf, $fj->user); 3936 if ($x === false) 3937 return false; 3938 } 3939 } 3940 } 3941 } 3942 } 3943} 3944