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