1<?php 2// settingvalues.php -- HotCRP conference settings management helper classes 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5// setting information 6class Si { 7 public $name; 8 public $base_name; 9 public $title; 10 public $group; 11 public $type; 12 public $internal; 13 public $extensible = 0; 14 public $storage_type; 15 public $storage = null; 16 public $optional = false; 17 public $values; 18 public $size; 19 public $placeholder; 20 public $parser_class; 21 public $novalue = false; 22 public $disabled = false; 23 public $invalid_value = null; 24 public $default_value = null; 25 public $autogrow = null; 26 public $ifnonempty; 27 public $message_default; 28 public $date_backup; 29 30 static public $option_is_value = []; 31 32 const SI_VALUE = 1; 33 const SI_DATA = 2; 34 const SI_SLICE = 4; 35 const SI_OPT = 8; 36 37 const X_YES = 1; 38 const X_WORD = 2; 39 40 static private $type_storage = [ 41 "emailheader" => self::SI_DATA, "emailstring" => self::SI_DATA, 42 "htmlstring" => self::SI_DATA, "simplestring" => self::SI_DATA, 43 "string" => self::SI_DATA, "tag" => self::SI_DATA, 44 "tagbase" => self::SI_DATA, "taglist" => self::SI_DATA, 45 "urlstring" => self::SI_DATA 46 ]; 47 48 private function store($key, $j, $jkey, $typecheck) { 49 if (isset($j->$jkey) && call_user_func($typecheck, $j->$jkey)) 50 $this->$key = $j->$jkey; 51 else if (isset($j->$jkey)) 52 trigger_error("setting {$j->name}.$jkey format error"); 53 } 54 55 function __construct($j) { 56 assert(!preg_match('/_(?:\$|n|m?\d+)\z/', $j->name)); 57 $this->name = $this->base_name = $this->title = $j->name; 58 foreach (["title", "type", "storage", "parser_class", "ifnonempty", "message_default", "placeholder", "invalid_value", "date_backup"] as $k) 59 $this->store($k, $j, $k, "is_string"); 60 foreach (["internal", "optional", "novalue", "disabled", "autogrow"] as $k) 61 $this->store($k, $j, $k, "is_bool"); 62 $this->store("size", $j, "size", "is_int"); 63 $this->store("values", $j, "values", "is_array"); 64 if (isset($j->default_value) && (is_int($j->default_value) || is_string($j->default_value))) 65 $this->default_value = $j->default_value; 66 if (isset($j->extensible) && $j->extensible === true) 67 $this->extensible = self::X_YES; 68 else if (isset($j->extensible) && $j->extensible === "word") 69 $this->extensible = self::X_WORD; 70 else if (isset($j->extensible) && $j->extensible !== false) 71 trigger_error("setting {$j->name}.extensible format error"); 72 if (isset($j->group)) { 73 if (is_string($j->group)) 74 $this->group = $j->group; 75 else if (is_array($j->group)) { 76 $this->group = []; 77 foreach ($j->group as $g) 78 if (is_string($g)) 79 $this->group[] = $g; 80 else 81 trigger_error("setting {$j->name}.group format error"); 82 } 83 } 84 85 if (!$this->type && $this->parser_class) 86 $this->type = "special"; 87 $s = $this->storage ? : $this->name; 88 $pfx = substr($s, 0, 4); 89 if ($pfx === "opt.") 90 $this->storage_type = self::SI_DATA | self::SI_OPT; 91 else if ($pfx === "ova.") { 92 $this->storage_type = self::SI_VALUE | self::SI_OPT; 93 $this->storage = "opt." . substr($s, 4); 94 } else if ($pfx === "val.") { 95 $this->storage_type = self::SI_VALUE | self::SI_SLICE; 96 $this->storage = substr($s, 4); 97 } else if ($pfx === "dat.") { 98 $this->storage_type = self::SI_DATA | self::SI_SLICE; 99 $this->storage = substr($s, 4); 100 } else if (isset(self::$type_storage[$this->type])) 101 $this->storage_type = self::$type_storage[$this->type]; 102 else 103 $this->storage_type = self::SI_VALUE; 104 if ($this->storage_type & self::SI_OPT) { 105 $is_value = !!($this->storage_type & self::SI_VALUE); 106 $oname = substr($this->storage ? : $this->name, 4); 107 if (!isset(self::$option_is_value[$oname])) 108 self::$option_is_value[$oname] = $is_value; 109 if (self::$option_is_value[$oname] != $is_value) 110 error_log("$oname: conflicting option_is_value"); 111 } 112 113 // defaults for size, placeholder 114 if (str_ends_with($this->type, "date")) { 115 if ($this->size === null) 116 $this->size = 32; 117 if ($this->placeholder === null) 118 $this->placeholder = "N/A"; 119 } else if ($this->type == "grace") { 120 if ($this->size === null) 121 $this->size = 15; 122 if ($this->placeholder === null) 123 $this->placeholder = "none"; 124 } 125 } 126 127 function is_date() { 128 return str_ends_with($this->type, "date"); 129 } 130 131 function storage() { 132 return $this->storage ? : $this->name; 133 } 134 135 function is_interesting(SettingValues $sv) { 136 if (!$this->group) { 137 error_log("$this->name: missing group"); 138 return false; 139 } 140 $groups = $this->group; 141 foreach (is_string($groups) ? [$groups] : $groups as $g) { 142 if ($sv->group_is_interesting($g)) 143 return true; 144 } 145 return false; 146 } 147 148 static function get($conf, $name, $k = null) { 149 if (!isset($conf->_setting_info[$name]) 150 && preg_match('/\A(.*)(_(?:[^_\s]+))\z/', $name, $m) 151 && isset($conf->_setting_info[$m[1]])) { 152 $si = clone $conf->_setting_info[$m[1]]; 153 if (!$si->extensible 154 || ($si->extensible === self::X_YES 155 && !preg_match('/\A_(?:\$|n|m?\d+)\z/', $m[2]))) 156 error_log("$name: cloning non-extensible setting $si->name"); 157 $si->name = $name; 158 if ($si->storage) 159 $si->storage .= $m[2]; 160 if ($si->extensible === self::X_WORD) 161 $si->title .= " (" . htmlspecialchars(substr($m[2], 1)) . ")"; 162 $conf->_setting_info[$name] = $si; 163 } 164 if (!isset($conf->_setting_info[$name])) 165 return null; 166 $si = $conf->_setting_info[$name]; 167 return $k ? $si->$k : $si; 168 } 169 170 171 static private function read($info, $text, $fname) { 172 $j = json_decode($text, true); 173 if (is_array($j)) 174 $info = array_replace_recursive($info, $j); 175 else if (json_last_error() !== JSON_ERROR_NONE) { 176 Json::decode($text); // our JSON decoder provides error positions 177 trigger_error("$fname: Invalid JSON, " . Json::last_error_msg()); 178 } 179 return $info; 180 } 181 182 static function initialize(Conf $conf) { 183 $last_problem = 0; 184 $hook = function ($v, $k, $landmark) use ($conf, &$last_problem) { 185 if (is_object($v) && isset($v->name) && is_string($v->name)) { 186 $conf->_setting_info[] = $v; 187 return true; 188 } else if ($k === 0 && is_object($v) && !isset($v->name)) { 189 error_log("$landmark: old-style keyed-object settinginfo deprecated"); 190 $ok = true; 191 foreach (get_object_vars($v) as $kk => $vv) { 192 if (is_object($vv) && !isset($vv->name)) 193 $vv->name = $kk; 194 else if (is_object($vv)) 195 assert($vv->name === $kk); 196 if (is_object($vv) && isset($vv->name) && is_string($vv->name)) { 197 $vv->__subposition = ++Conf::$next_xt_subposition; 198 $conf->_setting_info[] = $vv; 199 } else if ($kk !== "__subposition") 200 $ok = false; 201 } 202 return $ok; 203 } else 204 return false; 205 }; 206 207 $conf->_setting_info = []; 208 expand_json_includes_callback(["etc/settings.json"], $hook); 209 if (($olist = $conf->opt("settingSpecs"))) 210 expand_json_includes_callback($olist, $hook); 211 usort($conf->_setting_info, "Conf::xt_priority_compare"); 212 213 $all = []; 214 $nall = count($conf->_setting_info); 215 for ($i = 0; $i < $nall; ++$i) { 216 $j = $conf->_setting_info[$i]; 217 if ($conf->xt_allowed($j) && !isset($all[$j->name])) { 218 while (isset($j->merge) && $j->merge && $i + 1 < $nall 219 && $j->name === $conf->_setting_info[$i + 1]->name) { 220 unset($j->merge); 221 $j = object_replace_recursive($conf->_setting_info[$i + 1], $j); 222 ++$i; 223 } 224 Conf::xt_resolve_require($j); 225 $class = get_s($j, "setting_class", "Si"); 226 $all[$j->name] = new $class($j); 227 } 228 } 229 $conf->_setting_info = $all; 230 } 231} 232 233class SettingParser { 234 function parse(SettingValues $sv, Si $si) { 235 return false; 236 } 237 function save(SettingValues $sv, Si $si) { 238 } 239 240 static function parse_grace($v) { 241 $t = 0; 242 $v = trim($v); 243 if ($v == "" || strtoupper($v) == "N/A" || strtoupper($v) == "NONE" || $v == "0") 244 return -1; 245 if (ctype_digit($v)) 246 return $v * 60; 247 if (preg_match('/^\s*([\d]+):([\d.]+)\s*$/', $v, $m)) 248 return $m[1] * 60 + $m[2]; 249 if (preg_match('/^\s*([\d.]+)\s*d(ays?)?(?![a-z])/i', $v, $m)) { 250 $t += $m[1] * 3600 * 24; 251 $v = substr($v, strlen($m[0])); 252 } 253 if (preg_match('/^\s*([\d.]+)\s*h(rs?|ours?)?(?![a-z])/i', $v, $m)) { 254 $t += $m[1] * 3600; 255 $v = substr($v, strlen($m[0])); 256 } 257 if (preg_match('/^\s*([\d.]+)\s*m(in(ute)?s?)?(?![a-z])/i', $v, $m)) { 258 $t += $m[1] * 60; 259 $v = substr($v, strlen($m[0])); 260 } 261 if (preg_match('/^\s*([\d.]+)\s*s(ec(ond)?s?)?(?![a-z])/i', $v, $m)) { 262 $t += $m[1]; 263 $v = substr($v, strlen($m[0])); 264 } 265 if (trim($v) == "") 266 return $t; 267 else 268 return null; 269 } 270} 271 272class SettingValues extends MessageSet { 273 public $conf; 274 public $user; 275 public $interesting_groups = []; 276 277 private $parsers = []; 278 private $saved_si = []; 279 private $cleanup_callbacks = []; 280 public $need_lock = []; 281 public $changes = []; 282 283 public $req = array(); 284 public $req_files = array(); 285 public $savedv = array(); 286 public $explicit_oldv = array(); 287 private $hint_status = array(); 288 private $has_req = array(); 289 private $near_msgs = null; 290 private $null_mailer; 291 292 private $_gxt = null; 293 294 function __construct(Contact $user) { 295 parent::__construct(); 296 $this->conf = $user->conf; 297 $this->user = $user; 298 $this->near_msgs = new MessageSet; 299 // maybe set $Opt["contactName"] and $Opt["contactEmail"] 300 $this->conf->site_contact(); 301 // maybe initialize _setting_info 302 if ($this->conf->_setting_info === null) 303 Si::initialize($this->conf); 304 } 305 static function make_request(Contact $user, $qreq) { 306 $sv = new SettingValues($user); 307 $got = []; 308 foreach ($qreq as $k => $v) { 309 $sv->req[$k] = $v; 310 if (preg_match('/\A(?:has_)?(\S+?)(|_n|_m?\d+)\z/', $k, $m)) { 311 if (!isset($sv->has_req[$m[1]])) 312 $sv->has_req[$m[1]] = []; 313 if (!isset($got[$m[1] . $m[2]])) { 314 $sv->has_req[$m[1]][] = $m[2]; 315 $got[$m[1] . $m[2]] = true; 316 } 317 } 318 } 319 if ($qreq instanceof Qrequest) { 320 foreach ($qreq->files() as $f => $finfo) 321 $sv->req_files[$f] = $finfo; 322 } 323 return $sv; 324 } 325 function session_highlight() { 326 foreach ($this->conf->session("settings_highlight", []) as $f => $v) 327 $this->msg($f, null, $v); 328 $this->conf->save_session("settings_highlight", null); 329 } 330 331 332 private function gxt() { 333 if ($this->_gxt === null) 334 $this->_gxt = new GroupedExtensions($this->user, ["etc/settinggroups.json"], $this->conf->opt("settingGroups")); 335 return $this->_gxt; 336 } 337 function canonical_group($g) { 338 return $this->gxt()->canonical_group(strtolower($g)); 339 } 340 function is_titled_group($g) { 341 $gj = $this->gxt()->get($g); 342 return $gj && $gj->name == $gj->group && isset($gj->title); 343 } 344 function group_titles() { 345 return array_map(function ($gj) { return $gj->title; }, $this->gxt()->groups()); 346 } 347 function group_members($g) { 348 return $this->gxt()->members(strtolower($g)); 349 } 350 function mark_interesting_group($g) { 351 foreach ($this->group_members($g) as $gj) { 352 $this->interesting_groups[$gj->name] = true; 353 foreach ($gj->synonym as $syn) 354 $this->interesting_groups[$syn] = true; 355 } 356 } 357 function crosscheck() { 358 foreach ($this->gxt()->all() as $gj) { 359 if (isset($gj->crosscheck_callback)) { 360 Conf::xt_resolve_require($gj); 361 call_user_func($gj->crosscheck_callback, $this, $gj); 362 } 363 } 364 } 365 function render_group($g) { 366 $last_title = null; 367 foreach ($this->group_members($g) as $gj) { 368 GroupedExtensions::render_heading($gj, $last_title, 3, "settings"); 369 if (isset($gj->render_callback)) { 370 Conf::xt_resolve_require($gj); 371 call_user_func($gj->render_callback, $this, $gj); 372 } else if (isset($gj->render_html)) 373 echo $gj->render_html; 374 } 375 } 376 377 378 function use_req() { 379 return $this->has_error(); 380 } 381 static private function check_error_field($field, &$html) { 382 if ($field instanceof Si) { 383 if ($field->title && $html !== false) 384 $html = htmlspecialchars($field->title) . ": " . $html; 385 return $field->name; 386 } else 387 return $field; 388 } 389 function error_at($field, $html = false) { 390 $fname = self::check_error_field($field, $html); 391 parent::error_at($fname, $html); 392 } 393 function warning_at($field, $html = false) { 394 $fname = self::check_error_field($field, $html); 395 parent::warning_at($fname, $html); 396 } 397 function error_near($field, $html) { 398 $this->near_msgs->error_at($field, $html); 399 } 400 function warning_near($field, $html) { 401 $this->near_msgs->warning_at($field, $html); 402 } 403 function info_near($field, $html) { 404 $this->near_msgs->info_at($field, $html); 405 } 406 function report($is_update = false) { 407 $msgs = array(); 408 if ($is_update && $this->has_error()) 409 $msgs[] = "Your changes were not saved. Please fix these errors and try again."; 410 foreach ($this->messages(true) as $mx) 411 $msgs[] = ($mx[2] == MessageSet::WARNING ? "Warning: " : "") . $mx[1]; 412 if (!empty($msgs) && $this->has_error()) 413 Conf::msg_error($msgs, true); 414 else if (!empty($msgs)) 415 Conf::msg_warning($msgs, true); 416 } 417 function parser(Si $si) { 418 if (($class = $si->parser_class)) { 419 if (!isset($this->parsers[$class])) 420 $this->parsers[$class] = new $class($this, $si); 421 return $this->parsers[$class]; 422 } else 423 return null; 424 } 425 function group_is_interesting($g) { 426 return isset($this->interesting_groups[$g]); 427 } 428 429 function sclass($name, $class = null) { 430 $ps = $this->problem_status_at($name); 431 if ($ps > 1) 432 return $class ? $class . " has-error" : "has-error"; 433 else if ($ps > 0) 434 return $class ? $class . " has-warning" : "has-warning"; 435 else 436 return $class; 437 } 438 function label($name, $html, $label_js = null) { 439 $name1 = is_array($name) ? $name[0] : $name; 440 foreach (is_array($name) ? $name : array($name) as $n) { 441 if (($sc = $this->sclass($n))) { 442 if ($label_js && ($ec = get_s($label_js, "class")) !== "") 443 $sc = $ec . " " . $sc; 444 $label_js["class"] = $sc; 445 break; 446 } 447 } 448 $post = ""; 449 if (($pos = strpos($html, "<input")) !== false) 450 list($html, $post) = [substr($html, 0, $pos), substr($html, $pos)]; 451 return Ht::label($html, $name1, $label_js) . $post; 452 } 453 function sjs($name, $js = array()) { 454 $x = ["id" => $name]; 455 if (Si::get($this->conf, $name, "disabled")) 456 $x["disabled"] = true; 457 foreach ($js ? : [] as $k => $v) 458 $x[$k] = $v; 459 if ($this->has_problem_at($name)) 460 $x["class"] = $this->sclass($name, get($x, "class")); 461 return $x; 462 } 463 464 function si($name) { 465 $si = Si::get($this->conf, $name); 466 if (!$si) 467 error_log(caller_landmark(2) . ": setting $name: missing information"); 468 return $si; 469 } 470 private function req_has($xname, $suffix) { 471 $x = get($this->req, "has_$xname$suffix"); 472 return $x && $x !== "false"; 473 } 474 function req_si(Si $si) { 475 $xname = str_replace(".", "_", $si->name); 476 $xsis = []; 477 foreach (get($this->has_req, $xname, []) as $suffix) { 478 $xsi = $this->si($si->name . $suffix); 479 if ($xsi->parser_class) 480 $has_value = $this->req_has($xname, $suffix); 481 else 482 $has_value = isset($this->req["$xname$suffix"]) 483 || (($xsi->type === "cdate" || $xsi->type === "checkbox") 484 && $this->req_has($xname, $suffix)); 485 if ($has_value) 486 $xsis[] = $xsi; 487 } 488 return $xsis; 489 } 490 491 function curv($name, $default_value = null) { 492 return $this->si_curv($name, $this->si($name), $default_value); 493 } 494 function oldv($name, $default_value = null) { 495 return $this->si_oldv($this->si($name), $default_value); 496 } 497 function reqv($name, $default_value = null) { 498 $name = str_replace(".", "_", $name); 499 return get($this->req, $name, $default_value); 500 } 501 function has_savedv($name) { 502 $si = $this->si($name); 503 return array_key_exists($si->storage(), $this->savedv); 504 } 505 function has_interest($name) { 506 $si = $this->si($name); 507 return array_key_exists($si->storage(), $this->savedv) 508 || $si->is_interesting($this); 509 } 510 function savedv($name, $default_value = null) { 511 $si = $this->si($name); 512 return $this->si_savedv($si->storage(), $si, $default_value); 513 } 514 function newv($name, $default_value = null) { 515 $si = $this->si($name); 516 $s = $si->storage(); 517 if (array_key_exists($s, $this->savedv)) 518 return $this->si_savedv($s, $si, $default_value); 519 else 520 return $this->si_oldv($si, $default_value); 521 } 522 523 function set_oldv($name, $value) { 524 $this->explicit_oldv[$name] = $value; 525 } 526 function save($name, $value) { 527 $si = $this->si($name); 528 if (!$si) 529 return; 530 if ($value !== null 531 && !($si->storage_type & Si::SI_DATA ? is_string($value) : is_int($value))) { 532 error_log(caller_landmark() . ": setting $name: invalid value " . var_export($value, true)); 533 return; 534 } 535 $s = $si->storage(); 536 if ($value === $si->default_value 537 || ($value === "" && ($si->storage_type & Si::SI_DATA))) 538 $value = null; 539 if ($si->storage_type & Si::SI_SLICE) { 540 if (!isset($this->savedv[$s])) { 541 if (!array_key_exists($s, $this->savedv)) 542 $this->savedv[$s] = [$this->conf->setting($s, 0), $this->conf->setting_data($s, null)]; 543 else 544 $this->savedv[$s] = [0, null]; 545 } 546 $idx = $si->storage_type & Si::SI_DATA ? 1 : 0; 547 $this->savedv[$s][$idx] = $value; 548 if ($this->savedv[$s][0] === 0 && $this->savedv[$s][1] === null) 549 $this->savedv[$s] = null; 550 } else if ($value === null) 551 $this->savedv[$s] = null; 552 else if ($si->storage_type & Si::SI_DATA) 553 $this->savedv[$s] = [1, $value]; 554 else 555 $this->savedv[$s] = [$value, null]; 556 } 557 function update($name, $value) { 558 if ($value !== $this->oldv($name)) { 559 $this->save($name, $value); 560 return true; 561 } else 562 return false; 563 } 564 function cleanup_callback($name, $func, $arg = null) { 565 if (!isset($this->cleanup_callbacks[$name])) 566 $this->cleanup_callbacks[$name] = [$func, null]; 567 if (func_num_args() > 2) 568 $this->cleanup_callbacks[$name][1][] = $arg; 569 } 570 571 private function si_curv($name, Si $si, $default_value) { 572 if ($si->group && !$si->is_interesting($this)) 573 error_log("$name: bad group $si->group, not interesting here"); 574 if ($this->use_req()) 575 return get($this->req, str_replace(".", "_", $name), $default_value); 576 else 577 return $this->si_oldv($si, $default_value); 578 } 579 private function si_oldv(Si $si, $default_value) { 580 if ($default_value === null) 581 $default_value = $si->default_value; 582 if (isset($this->explicit_oldv[$si->name])) 583 $val = $this->explicit_oldv[$si->name]; 584 else if ($si->storage_type & Si::SI_OPT) { 585 $val = $this->conf->opt(substr($si->storage(), 4), $default_value); 586 if (($si->storage_type & Si::SI_VALUE) && is_bool($val)) 587 $val = (int) $val; 588 } else if ($si->storage_type & Si::SI_DATA) 589 $val = $this->conf->setting_data($si->storage(), $default_value); 590 else 591 $val = $this->conf->setting($si->storage(), $default_value); 592 if ($val === $si->invalid_value) 593 $val = ""; 594 return $val; 595 } 596 private function si_savedv($s, Si $si, $default_value) { 597 if (!isset($this->savedv[$s])) 598 return $default_value; 599 else if ($si->storage_type & Si::SI_DATA) 600 return $this->savedv[$s][1]; 601 else 602 return $this->savedv[$s][0]; 603 } 604 605 function echo_messages_near($name) { 606 $msgs = []; 607 $status = MessageSet::INFO; 608 foreach ($this->near_msgs->messages_at($name, true) as $mx) { 609 $msgs[] = ($mx[2] == MessageSet::WARNING ? "Warning: " : "") . $mx[1]; 610 $status = max($status, $mx[2]); 611 } 612 if (!empty($msgs)) { 613 $xtype = ["xinfo", "xwarning", "xmerror"]; 614 $this->conf->msg($xtype[$status], $msgs); 615 } 616 } 617 function echo_checkbox_only($name, $js = null) { 618 $js["id"] = "cb$name"; 619 $x = $this->curv($name); 620 echo Ht::hidden("has_$name", 1), 621 Ht::checkbox($name, 1, $x !== null && $x > 0, $this->sjs($name, $js)); 622 } 623 function echo_checkbox($name, $text, $js = null, $hint = null) { 624 $item_class = get($js, "item_class"); 625 $hint_class = get($js, "hint_class"); 626 $item_open = get($js, "item_open"); 627 unset($js["item_class"], $js["hint_class"], $js["item_open"]); 628 629 echo '<div class="checki', ($item_class ? " " . $item_class : ""), 630 '"><span class="checkc">'; 631 $this->echo_checkbox_only($name, $js); 632 echo ' </span>', $this->label($name, $text, ["for" => "cb$name"]); 633 if ($hint) 634 echo '<p class="settings-ap f-hx', ($hint_class ? " " . $hint_class : ""), '">', $hint, '</p>'; 635 if (!$item_open) 636 echo "</div>\n"; 637 } 638 function echo_radio_table($name, $varr, $heading = null, $after = null) { 639 $x = $this->curv($name); 640 if ($x === null || !isset($varr[$x])) 641 $x = 0; 642 echo '<div class="settings-radio">'; 643 if ($heading) 644 echo '<div class="settings-radioheading">', $heading, '</div>'; 645 foreach ($varr as $k => $text) { 646 $hint = ""; 647 if (is_array($text)) 648 list($text, $hint) = $text; 649 echo '<div class="settings-radioitem checki ', 650 ($k == $x ? "foldo" : "foldc"), '"><label><span class="checkc">', 651 Ht::radio($name, $k, $k == $x, 652 $this->sjs($name, ["id" => "{$name}_{$k}", "class" => "js-settings-radio"])), 653 '</span>', $text, '</label>', $hint, '</div>'; 654 } 655 if ($after) 656 echo $after; 657 echo "</div>\n"; 658 } 659 function render_entry($name, $js = []) { 660 $v = $this->curv($name); 661 $t = ""; 662 if (($si = $this->si($name))) { 663 if ($si->size && !isset($js["size"])) 664 $js["size"] = $si->size; 665 if ($si->placeholder && !isset($js["placeholder"])) 666 $js["placeholder"] = $si->placeholder; 667 if ($si->autogrow) 668 $js["class"] = ltrim(get($js, "class", "") . " need-autogrow"); 669 if ($si->is_date()) 670 $v = $this->si_render_date_value($v, $si); 671 else if ($si->type === "grace") 672 $v = $this->si_render_grace_value($v, $si); 673 if ($si->parser_class) 674 $t = Ht::hidden("has_$name", 1); 675 } 676 return Ht::entry($name, $v, $this->sjs($name, $js)) . $t; 677 } 678 function echo_entry($name) { 679 echo $this->render_entry($name); 680 } 681 function echo_entry_group($name, $description, $js = null, $hint = null) { 682 $after_entry = get($js, "after_entry"); 683 $horizontal = get($js, "horizontal"); 684 $item_open = get($js, "item_open"); 685 unset($js["after_entry"], $js["horizontal"], $js["item_open"]); 686 $klass = $horizontal ? "entryi" : "f-i"; 687 $si = $this->si($name); 688 if ($description === null && $si) 689 $description = $si->title; 690 691 echo '<div class="', $this->sclass($name, $klass), '">', 692 $this->label($name, $description, ["class" => false]), 693 $this->render_entry($name, $js), ($after_entry ? : ""); 694 $thint = $si ? $this->type_hint($si->type) : null; 695 if ($hint || $thint) { 696 echo '<div class="f-h">'; 697 if ($hint && $thint) 698 echo '<div>', $hint, '</div><div>', $thint, '</div>'; 699 else if ($hint || $thint) 700 echo $hint ? $hint : $thint; 701 echo '</div>'; 702 } 703 if (!$item_open) 704 echo "</div>\n"; 705 } 706 function render_select($name, $values, $js = []) { 707 $v = $this->curv($name); 708 $t = ""; 709 if (($si = $this->si($name)) && $si->parser_class) 710 $t = Ht::hidden("has_$name", 1); 711 return Ht::select($name, $values, $v !== null ? $v : 0, $this->sjs($name, $js)) . $t; 712 } 713 function render_textarea($name, $js = []) { 714 $v = $this->curv($name); 715 $t = ""; 716 $rows = 10; 717 if (($si = $this->si($name))) { 718 if ($si->size) 719 $rows = $si->size; 720 if ($si->placeholder) 721 $js["placeholder"] = $si->placeholder; 722 if ($si->autogrow || $si->autogrow === null) 723 $js["class"] = ltrim(get($js, "class", "") . " need-autogrow"); 724 if ($si->parser_class) 725 $t = Ht::hidden("has_$name", 1); 726 } 727 if (!isset($js["rows"])) 728 $js["rows"] = $rows; 729 if (!isset($js["cols"])) 730 $js["cols"] = 80; 731 return Ht::textarea($name, $v, $this->sjs($name, $js)) . $t; 732 } 733 private function echo_message_base($name, $description, $hint, $xclass) { 734 $si = $this->si($name); 735 $si->default_value = $this->conf->message_default_html($name); 736 $current = $this->curv($name); 737 $description = '<a class="ui q js-foldup" href="">' 738 . expander(null, 0) . $description . '</a>'; 739 echo '<div class="f-i has-fold fold', ($current == $si->default_value ? "c" : "o"), '">', 740 '<div class="f-c', $xclass, ' ui js-foldup">', 741 $this->label($name, $description), 742 ' <span class="n fx">(HTML allowed)</span></div>', 743 $this->render_textarea($name, ["class" => "fx"]), 744 $hint, "</div>\n"; 745 } 746 function echo_message($name, $description, $hint = "") { 747 $this->echo_message_base($name, $description, $hint, ""); 748 } 749 function echo_message_minor($name, $description, $hint = "") { 750 $this->echo_message_base($name, $description, $hint, " n"); 751 } 752 753 private function si_render_date_value($v, Si $si) { 754 if ($v !== null && $this->use_req()) 755 return $v; 756 else if ($si->date_backup && $this->curv($si->date_backup) == $v) 757 return ""; 758 else if ($si->placeholder !== "N/A" && $si->placeholder !== "none" && $v === 0) 759 return "none"; 760 else if ($v <= 0) 761 return ""; 762 else if ($v == 1) 763 return "now"; 764 else 765 return $this->conf->parseableTime($v, true); 766 } 767 private function si_render_grace_value($v, Si $si) { 768 if ($v === null || $v <= 0 || !is_numeric($v)) 769 return "none"; 770 if ($v % 3600 == 0) 771 return ($v / 3600) . " hr"; 772 if ($v % 60 == 0) 773 return ($v / 60) . " min"; 774 return sprintf("%d:%02d", intval($v / 60), $v % 60); 775 } 776 777 function type_hint($type) { 778 if (str_ends_with($type, "date") && !isset($this->hint_status["date"])) { 779 $this->hint_status["date"] = true; 780 return "Date examples: “now”, “10 Dec 2006 11:59:59pm PST”, “2014-10-31 00:00 UTC-1100” <a href='http://php.net/manual/en/datetime.formats.php'>(more examples)</a>"; 781 } else if ($type === "grace" && !isset($this->hint_status["grace"])) { 782 $this->hint_status["grace"] = true; 783 return "Example: “15 min”"; 784 } else 785 return false; 786 } 787 788 function expand_mail_template($name, $default) { 789 if (!$this->null_mailer) 790 $this->null_mailer = new HotCRPMailer($this->conf, null, null, array("width" => false)); 791 return $this->null_mailer->expand_template($name, $default); 792 } 793 794 795 function execute() { 796 global $Now; 797 // parse settings 798 foreach ($this->conf->_setting_info as $si) 799 $this->account($si); 800 801 // check date relationships 802 foreach (array("sub_reg" => "sub_sub", "final_soft" => "final_done") 803 as $dn1 => $dn2) 804 list($dv1, $dv2) = [$this->savedv($dn1), $this->savedv($dn2)]; 805 if (!$dv1 && $dv2) 806 $this->save($dn1, $dv2); 807 else if ($dv2 && $dv1 > $dv2) { 808 $si = Si::get($this->conf, $dn1); 809 $this->error_at($si, "Must come before " . Si::get($this->conf, $dn2, "title") . "."); 810 $this->error_at($dn2); 811 } 812 if ($this->has_savedv("sub_sub")) 813 $this->save("sub_update", $this->savedv("sub_sub")); 814 if ($this->conf->opt("defaultSiteContact")) { 815 if ($this->has_savedv("opt.contactName") 816 && $this->conf->opt("contactName") === $this->savedv("opt.contactName")) 817 $this->save("opt.contactName", null); 818 if ($this->has_savedv("opt.contactEmail") 819 && $this->conf->opt("contactEmail") === $this->savedv("opt.contactEmail")) 820 $this->save("opt.contactEmail", null); 821 } 822 if ($this->has_savedv("resp_active") && $this->savedv("resp_active")) 823 foreach (explode(" ", $this->newv("resp_rounds")) as $i => $rname) { 824 $isuf = $i ? "_$i" : ""; 825 if ($this->newv("resp_open$isuf") > $this->newv("resp_done$isuf")) { 826 $si = Si::get($this->conf, "resp_open$isuf"); 827 $this->error_at($si, "Must come before " . Si::get($this->conf, "resp_done", "title") . "."); 828 $this->error_at("resp_done$isuf"); 829 } 830 } 831 832 // Setting relationships 833 if ($this->has_savedv("sub_open") 834 && $this->newv("sub_open", 1) <= 0 835 && $this->oldv("sub_open") > 0 836 && $this->newv("sub_sub") <= 0) 837 $this->save("sub_close", $Now); 838 if ($this->has_savedv("msg.clickthrough_submit")) 839 $this->save("clickthrough_submit", null); 840 841 // make settings 842 $this->changes = []; 843 if (!$this->has_error() 844 && (!empty($this->savedv) || !empty($this->saved_si))) { 845 $tables = "Settings write"; 846 foreach ($this->need_lock as $t => $need) 847 if ($need) 848 $tables .= ", $t write"; 849 $this->conf->qe_raw("lock tables $tables"); 850 851 // load db settings, pre-crosscheck 852 $dbsettings = array(); 853 $result = $this->conf->qe("select name, value, data from Settings"); 854 while (($row = edb_row($result))) 855 $dbsettings[$row[0]] = $row; 856 Dbl::free($result); 857 858 // apply settings 859 foreach ($this->saved_si as $si) { 860 $this->parser($si)->save($this, $si); 861 } 862 863 $dv = $av = array(); 864 foreach ($this->savedv as $n => $v) { 865 if (substr($n, 0, 4) === "opt.") { 866 $okey = substr($n, 4); 867 if (array_key_exists($okey, $this->conf->opt_override)) 868 $oldv = $this->conf->opt_override[$okey]; 869 else 870 $oldv = $this->conf->opt($okey); 871 $vi = Si::$option_is_value[$okey] ? 0 : 1; 872 $basev = $vi ? "" : 0; 873 $newv = $v === null ? $basev : $v[$vi]; 874 if ($oldv === $newv) 875 $v = null; // delete override value in database 876 else if ($v === null && $oldv !== $basev && $oldv !== null) 877 $v = $vi ? [0, ""] : [0, null]; 878 } 879 if ($v === null 880 ? !isset($dbsettings[$n]) 881 : isset($dbsettings[$n]) && (int) $dbsettings[$n][1] === $v[0] && $dbsettings[$n][2] === $v[1]) 882 continue; 883 $this->changes[] = $n; 884 if ($v !== null) 885 $av[] = [$n, $v[0], $v[1]]; 886 else 887 $dv[] = $n; 888 } 889 if (!empty($dv)) { 890 $this->conf->qe("delete from Settings where name?a", $dv); 891 //Conf::msg_info(Ht::pre_text_wrap(Dbl::format_query("delete from Settings where name?a", $dv))); 892 } 893 if (!empty($av)) { 894 $this->conf->qe("insert into Settings (name, value, data) values ?v on duplicate key update value=values(value), data=values(data)", $av); 895 //Conf::msg_info(Ht::pre_text_wrap(Dbl::format_query("insert into Settings (name, value, data) values ?v on duplicate key update value=values(value), data=values(data)", $av))); 896 } 897 898 $this->conf->qe_raw("unlock tables"); 899 if (!empty($this->changes)) 900 $this->user->log_activity("Updated settings " . join(", ", $this->changes)); 901 $this->conf->load_settings(); 902 foreach ($this->cleanup_callbacks as $cb) 903 call_user_func($cb[0], $this, $cb[1]); 904 905 // contactdb may need to hear about changes to shortName 906 if ($this->has_savedv("opt.shortName") && ($cdb = $this->conf->contactdb())) 907 Dbl::ql($cdb, "update Conferences set shortName=? where dbName=?", $this->conf->short_name, $this->conf->dbname); 908 } 909 return !$this->has_error(); 910 } 911 function account(Si $si1) { 912 if ($si1->internal) 913 return; 914 foreach ($this->req_si($si1) as $si) { 915 if ($si->disabled || $si->novalue || !$si->type || $si->type === "none") { 916 /* ignore changes to disabled/novalue settings */; 917 } else if ($si->parser_class) { 918 if ($this->parser($si)->parse($this, $si)) { 919 $this->saved_si[] = $si; 920 } 921 } else { 922 $v = $this->parse_value($si); 923 if ($v === null || $v === false) 924 return; 925 if (is_int($v) && $v <= 0 && $si->type !== "radio" && $si->type !== "zint") 926 $v = null; 927 $this->save($si->name, $v); 928 if ($si->ifnonempty) 929 $this->save($si->ifnonempty, $v === null || $v === "" ? null : 1); 930 } 931 } 932 } 933 function parse_value(Si $si) { 934 global $Now; 935 936 if (!isset($sv->req[$si->name])) { 937 $xname = str_replace(".", "_", $si->name); 938 if (isset($this->req[$xname])) 939 $this->req[$si->name] = $this->req[$xname]; 940 else if ($si->type === "checkbox" || $si->type === "cdate") 941 return 0; 942 else 943 return null; 944 } 945 946 $v = trim($this->req[$si->name]); 947 if (($si->placeholder && $si->placeholder === $v) 948 || ($si->invalid_value && $si->invalid_value === $v)) 949 $v = ""; 950 951 if ($si->type === "checkbox") 952 return $v != "" ? 1 : 0; 953 else if ($si->type === "cdate" && $v == "1") 954 return 1; 955 else if ($si->type === "date" || $si->type === "cdate" 956 || $si->type === "ndate") { 957 if ($v == "" || !strcasecmp($v, "N/A") || !strcasecmp($v, "same as PC") 958 || $v == "0" || ($si->type !== "ndate" && !strcasecmp($v, "none"))) 959 return -1; 960 else if (!strcasecmp($v, "none")) 961 return 0; 962 else if (($v = $this->conf->parse_time($v)) !== false) 963 return $v; 964 $err = "Invalid date."; 965 } else if ($si->type === "grace") { 966 if (($v = SettingParser::parse_grace($v)) !== null) 967 return intval($v); 968 $err = "Invalid grace period."; 969 } else if ($si->type === "int" || $si->type === "zint") { 970 if (preg_match("/\\A[-+]?[0-9]+\\z/", $v)) 971 return intval($v); 972 if ($v == "" && $si->placeholder) 973 return 0; 974 $err = "Should be a number."; 975 } else if ($si->type === "string") { 976 // Avoid storing the default message in the database 977 if (substr($si->name, 0, 9) == "mailbody_") { 978 $t = $this->expand_mail_template(substr($si->name, 9), true); 979 $v = cleannl($v); 980 if ($t["body"] == $v) 981 return ""; 982 } 983 return $v; 984 } else if ($si->type === "simplestring") { 985 return simplify_whitespace($v); 986 } else if ($si->type === "tag" || $si->type === "tagbase") { 987 $tagger = new Tagger($this->user); 988 $v = trim($v); 989 if ($v === "" && $si->optional) 990 return $v; 991 $v = $tagger->check($v, $si->type === "tagbase" ? Tagger::NOVALUE : 0); 992 if ($v) 993 return $v; 994 $err = $tagger->error_html; 995 } else if ($si->type === "emailheader") { 996 $v = MimeText::encode_email_header("", $v); 997 if ($v !== false) 998 return ($v == "" ? "" : MimeText::decode_header($v)); 999 $err = "Invalid email header."; 1000 } else if ($si->type === "emailstring") { 1001 $v = trim($v); 1002 if ($v === "" && $si->optional) 1003 return ""; 1004 else if (validate_email($v) || $v === $this->oldv($si->name, null)) 1005 return $v; 1006 $err = "Invalid email."; 1007 } else if ($si->type === "urlstring") { 1008 $v = trim($v); 1009 if (($v === "" && $si->optional) 1010 || preg_match(',\A(?:https?|ftp)://\S+\z,', $v)) 1011 return $v; 1012 $err = "Invalid URL."; 1013 } else if ($si->type === "htmlstring") { 1014 if (($v = CleanHTML::basic_clean($v, $err)) !== false) { 1015 if ($si->message_default 1016 && $v === $this->conf->message_default_html($si->message_default)) 1017 return ""; 1018 return $v; 1019 } 1020 /* $err set by CleanHTML::basic_clean */ 1021 } else if ($si->type === "radio") { 1022 foreach ($si->values as $allowedv) 1023 if ((string) $allowedv === $v) 1024 return $allowedv; 1025 $err = "Parse error (unexpected value)."; 1026 } else 1027 return $v; 1028 1029 $this->error_at($si, $err); 1030 return null; 1031 } 1032 1033 function changes() { 1034 return $this->changes; 1035 } 1036} 1037