1<?php
2// paperstatus.php -- HotCRP helper for reading/storing papers as JSON
3// Copyright (c) 2008-2018 Eddie Kohler; see LICENSE.
4
5class PaperStatus extends MessageSet {
6    public $conf;
7    public $user;
8    private $uploaded_documents;
9    private $no_email = false;
10    private $export_ids = false;
11    private $hide_docids = false;
12    private $export_content = false;
13    private $disable_users = false;
14    private $allow_any_content_file = false;
15    private $content_file_prefix = false;
16    private $add_topics = false;
17    public $prow;
18    public $paperId;
19    private $_on_document_export = [];
20    private $_on_document_import = [];
21
22    public $diffs;
23    private $_paper_upd;
24    private $_topic_ins;
25    private $_option_delid;
26    private $_option_ins;
27    private $_new_conflicts;
28    private $_conflict_ins;
29    private $_paper_submitted;
30    private $_document_change;
31
32    function __construct(Conf $conf, Contact $user = null, $options = array()) {
33        $this->conf = $conf;
34        $this->user = $user;
35        foreach (array("no_email", "export_ids", "hide_docids",
36                       "export_content", "disable_users",
37                       "allow_any_content_file", "content_file_prefix",
38                       "add_topics") as $k)
39            if (array_key_exists($k, $options))
40                $this->$k = $options[$k];
41        $this->_on_document_import[] = [$this, "document_import_check_filename"];
42        $this->clear();
43    }
44
45    function clear() {
46        parent::clear();
47        $this->uploaded_documents = [];
48        $this->prow = null;
49        $this->diffs = [];
50        $this->_paper_upd = [];
51        $this->_topic_ins = null;
52        $this->_option_delid = $this->_option_ins = [];
53        $this->_new_conflicts = $this->_conflict_ins = null;
54        $this->_paper_submitted = $this->_document_change = null;
55    }
56
57    function on_document_export($cb) {
58        // arguments: $document_json, DocumentInfo $doc, $dtype, PaperStatus $pstatus
59        $this->_on_document_export[] = $cb;
60    }
61
62    function on_document_import($cb) {
63        // arguments: $document_json, $prow
64        $this->_on_document_import[] = $cb;
65    }
66
67    function user() {
68        return $this->user;
69    }
70
71    function paper_row() {
72        return $this->prow;
73    }
74
75    function _() {
76        return call_user_func_array([$this->conf->ims(), "x"], func_get_args());
77    }
78
79    function document_to_json($dtype, $docid) {
80        if (!is_object($docid))
81            $doc = $this->prow ? $this->prow->document($dtype, $docid) : null;
82        else {
83            $doc = $docid;
84            $docid = $doc->paperStorageId;
85        }
86        if (!$doc)
87            return null;
88        assert($doc instanceof DocumentInfo);
89
90        $d = (object) array();
91        if ($docid && !$this->hide_docids)
92            $d->docid = $docid;
93        if ($doc->mimetype)
94            $d->mimetype = $doc->mimetype;
95        if ($doc->has_hash())
96            $d->hash = $doc->text_hash();
97        if ($doc->timestamp)
98            $d->timestamp = $doc->timestamp;
99        if ($doc->size)
100            $d->size = $doc->size;
101        if ($doc->filename)
102            $d->filename = $doc->filename;
103        $meta = null;
104        if (isset($doc->infoJson) && is_object($doc->infoJson))
105            $meta = $doc->infoJson;
106        else if (isset($doc->infoJson) && is_string($doc->infoJson))
107            $meta = json_decode($doc->infoJson);
108        if ($meta)
109            $d->metadata = $meta;
110        if ($this->export_content
111            && ($content = $doc->content()) !== false)
112            $d->content_base64 = base64_encode($content);
113        foreach ($this->_on_document_export as $cb)
114            if (call_user_func($cb, $d, $doc, $dtype, $this) === false)
115                return null;
116        if (!count(get_object_vars($d)))
117            $d = null;
118        return $d;
119    }
120
121    function paper_json($prow, $args = array()) {
122        if (is_int($prow))
123            $prow = $this->conf->paperRow(["paperId" => $prow, "topics" => true, "options" => true], $this->user);
124        $original_user = $user = $this->user;
125        if (get($args, "forceShow"))
126            $user = null;
127
128        if (!$prow || ($user && !$user->can_view_paper($prow)))
129            return null;
130        $this->user = $user;
131        $original_no_msgs = $this->ignore_msgs;
132        $this->ignore_msgs = !get($args, "msgs");
133
134        $this->prow = $prow;
135        $this->paperId = $prow->paperId;
136
137        $pj = (object) array();
138        $pj->pid = (int) $prow->paperId;
139        $pj->title = $prow->title;
140
141        $submitted_status = "submitted";
142        if ($prow->outcome != 0
143            && (!$user || $user->can_view_decision($prow))) {
144            $pj->decision = $this->conf->decision_name($prow->outcome);
145            if ($pj->decision === false) {
146                $pj->decision = (int) $prow->outcome;
147                $submitted_status = $pj->decision > 0 ? "accepted" : "rejected";
148            } else
149                $submitted_status = $pj->decision;
150        }
151
152        if ($prow->timeWithdrawn > 0) {
153            $pj->status = "withdrawn";
154            $pj->withdrawn = true;
155            $pj->withdrawn_at = (int) $prow->timeWithdrawn;
156            if (get($prow, "withdrawReason"))
157                $pj->withdraw_reason = $prow->withdrawReason;
158        } else if ($prow->timeSubmitted > 0) {
159            $pj->status = $submitted_status;
160            $pj->submitted = true;
161        } else {
162            $pj->status = "inprogress";
163            $pj->draft = true;
164        }
165        if (($t = $prow->submitted_at()))
166            $pj->submitted_at = $t;
167
168        $can_view_authors = !$user
169            || $user->can_view_authors($prow);
170        if ($can_view_authors) {
171            $contacts = array();
172            foreach ($prow->named_contacts() as $cflt)
173                $contacts[strtolower($cflt->email)] = $cflt;
174
175            $pj->authors = array();
176            foreach ($prow->author_list() as $au) {
177                $aux = (object) array();
178                if ($au->email)
179                    $aux->email = $au->email;
180                if ($au->firstName)
181                    $aux->first = $au->firstName;
182                if ($au->lastName)
183                    $aux->last = $au->lastName;
184                if ($au->affiliation)
185                    $aux->affiliation = $au->affiliation;
186                $lemail = strtolower((string) $au->email);
187                if ($lemail && ($cflt = get($contacts, $lemail))
188                    && $cflt->conflictType >= CONFLICT_AUTHOR) {
189                    $aux->contact = true;
190                    unset($contacts[$lemail]);
191                }
192                $pj->authors[] = $aux;
193            }
194
195            $other_contacts = array();
196            foreach ($contacts as $cflt)
197                if ($cflt->conflictType >= CONFLICT_AUTHOR) {
198                    $aux = (object) array("email" => $cflt->email);
199                    if ($cflt->firstName)
200                        $aux->first = $cflt->firstName;
201                    if ($cflt->lastName)
202                        $aux->last = $cflt->lastName;
203                    if ($cflt->affiliation)
204                        $aux->affiliation = $cflt->affiliation;
205                    $other_contacts[] = $aux;
206                }
207            if (!empty($other_contacts))
208                $pj->contacts = $other_contacts;
209        }
210
211        if ($this->conf->submission_blindness() == Conf::BLIND_OPTIONAL)
212            $pj->nonblind = !$prow->blind;
213
214        if ($prow->abstract !== "" || !$this->conf->opt("noAbstract"))
215            $pj->abstract = $prow->abstract;
216
217        $topics = array();
218        foreach ($prow->named_topic_map() as $tid => $tname)
219            $topics[$this->export_ids ? $tid : $tname] = true;
220        if (!empty($topics))
221            $pj->topics = (object) $topics;
222
223        if ($prow->paperStorageId > 1
224            && (!$user || $user->can_view_pdf($prow))
225            && ($doc = $this->document_to_json(DTYPE_SUBMISSION, (int) $prow->paperStorageId)))
226            $pj->submission = $doc;
227
228        if ($prow->finalPaperStorageId > 1
229            && (!$user || $user->can_view_pdf($prow))
230            && ($doc = $this->document_to_json(DTYPE_FINAL, (int) $prow->finalPaperStorageId)))
231            $pj->final = $doc;
232        if ($prow->timeFinalSubmitted > 0) {
233            $pj->final_submitted = true;
234            $pj->final_submitted_at = (int) $prow->timeFinalSubmitted;
235        }
236
237        $options = array();
238        foreach ($this->conf->paper_opts->option_list() as $o) {
239            if ($user && !$user->can_view_paper_option($prow, $o))
240                continue;
241            $ov = $prow->option($o->id) ? : new PaperOptionValue($prow, $o);
242            $oj = $o->unparse_json($ov, $this, $user);
243            if ($oj !== null)
244                $options[$this->export_ids ? $o->id : $o->json_key()] = $oj;
245        }
246        if (!empty($options))
247            $pj->options = (object) $options;
248
249        if ($can_view_authors) {
250            $pcconflicts = array();
251            foreach ($prow->pc_conflicts(true) as $id => $cflt) {
252                if (($ctname = get(Conflict::$type_names, $cflt->conflictType)))
253                    $pcconflicts[$cflt->email] = $ctname;
254            }
255            if (!empty($pcconflicts))
256                $pj->pc_conflicts = (object) $pcconflicts;
257            if ($prow->collaborators)
258                $pj->collaborators = $prow->collaborators;
259        }
260
261        // Now produce messages.
262        if (!$this->ignore_msgs
263            && $pj->title === "")
264            $this->error_at("title", $this->_("Each submission must have a title."));
265        if (!$this->ignore_msgs
266            && (!isset($pj->abstract) || $pj->abstract === "")
267            && !$this->conf->opt("noAbstract"))
268            $this->error_at("abstract", $this->_("Each submission must have an abstract."));
269        if (!$this->ignore_msgs
270            && $can_view_authors) {
271            $msg1 = $msg2 = false;
272            foreach ($prow->author_list() as $n => $au) {
273                if (strpos($au->email, "@") === false
274                    && strpos($au->affiliation, "@") !== false) {
275                    $msg1 = true;
276                    $this->warning_at("author" . ($n + 1), null);
277                } else if ($au->firstName === "" && $au->lastName === ""
278                           && $au->email === "" && $au->affiliation !== "") {
279                    $msg2 = true;
280                    $this->warning_at("author" . ($n + 1), null);
281                }
282            }
283            $max_authors = $this->conf->opt("maxAuthors");
284            if (!$prow->author_list())
285                $this->error_at("authors", $this->_("Each submission must have at least one author.", $max_authors));
286            if ($max_authors > 0 && count($prow->author_list()) > $max_authors)
287                $this->error_at("authors", $this->_("Each submission can have at most %d authors.", $max_authors));
288            if ($msg1)
289                $this->warning_at("authors", "You may have entered an email address in the wrong place. The first author field is for author name, the second for email address, and the third for affiliation.");
290            if ($msg2)
291                $this->warning_at("authors", "Please enter a name and optional email address for every author.");
292        }
293        if (!$this->ignore_msgs
294            && $can_view_authors
295            && $this->conf->setting("sub_collab")
296            && ($prow->outcome <= 0 || ($user && !$user->can_view_decision($prow)))) {
297            $field = $this->_($this->conf->setting("sub_pcconf") ? "Other conflicts" : "Potential conflicts");
298            if (!$prow->collaborators)
299                $this->warning_at("collaborators", $this->_("Enter the authors’ external conflicts of interest in the %s field. If none of the authors have external conflicts, enter “None”.", $field));
300        }
301        if (!$this->ignore_msgs
302            && $can_view_authors
303            && $this->conf->setting("sub_pcconf")
304            && ($prow->outcome <= 0 || ($user && !$user->can_view_decision($prow)))) {
305            $pcs = [];
306            foreach ($this->conf->full_pc_members() as $p) {
307                if (!$prow->has_conflict($p)
308                    && $prow->potential_conflict($p))
309                    $pcs[] = Text::name_html($p);
310            }
311            if (!empty($pcs)) {
312                $this->warning_at("pcconf", $this->_("<p>You may have missed conflicts of interest for %s. These conflicts are highlighted below; hover for more information. Please verify that all conflicts are correctly marked.</p>", commajoin($pcs, "and"))
313                    . $this->_('<p class="hint">This warning will not prevent submission.</p>'));
314            }
315        }
316
317        $this->ignore_msgs = $original_no_msgs;
318        $this->user = $original_user;
319        return $pj;
320    }
321
322
323    function error_at_option(PaperOption $o, $html) {
324        $this->error_at($o->field_key(), htmlspecialchars($o->name) . ": " . $html);
325    }
326    function warning_at_option(PaperOption $o, $html) {
327        $this->warning_at($o->field_key(), htmlspecialchars($o->name) . ": " . $html);
328    }
329
330    function format_error_at($key, $value) {
331        $this->error_at($key, "Format error [" . htmlspecialchars($key) . "]");
332        error_log($this->conf->dbname . ": PaperStatus: format error $key " . gettype($value));
333    }
334
335
336    function document_import_check_filename($docj, PaperOption $o, PaperStatus $pstatus) {
337        if (isset($docj->content_file)
338            && is_string($docj->content_file)
339            && !($docj instanceof DocumentInfo)) {
340            if (!$this->allow_any_content_file && preg_match(',\A/|(?:\A|/)\.\.(?:/|\z),', $docj->content_file)) {
341                $pstatus->error_at_option($o, "Bad content_file: only simple filenames allowed.");
342                return false;
343            }
344            if ((string) $this->content_file_prefix !== "")
345                $docj->content_file = $this->content_file_prefix . $docj->content_file;
346        }
347    }
348
349    function upload_document($docj, PaperOption $o) {
350        // $docj can be a DocumentInfo or a JSON.
351        // If it is a JSON, its format is set by document_to_json.
352        if (is_array($docj) && count($docj) === 1 && isset($docj[0]))
353            $docj = $docj[0];
354        if (!is_object($docj)) {
355            $this->format_error_at($o->field_key(), $docj);
356            return null;
357        } else if (get($docj, "error") || get($docj, "error_html")) {
358            $this->error_at_option($o, get($docj, "error_html", "Upload error."));
359            return null;
360        }
361        assert(!isset($docj->filter));
362
363        // check on_document_import
364        foreach ($this->_on_document_import as $cb)
365            if (call_user_func($cb, $docj, $o, $this) === false)
366                return null;
367
368        // validate JSON
369        if ($docj instanceof DocumentInfo)
370            $doc = $docj;
371        else {
372            $doc = null;
373            if (!isset($docj->hash) && isset($docj->sha1))
374                $dochash = (string) Filer::sha1_hash_as_text($docj->sha1);
375            else
376                $dochash = (string) get($docj, "hash");
377
378            if ($this->prow
379                && ($docid = get($docj, "docid"))
380                && is_int($docid)) {
381                $result = $this->conf->qe("select * from PaperStorage where paperId=? and paperStorageId=? and documentType=?", $this->prow->paperId, $docid, $o->id);
382                $doc = DocumentInfo::fetch($result, $this->conf, $this->prow);
383                Dbl::free($result);
384                if (!$doc || ($dochash !== "" && !Filer::check_text_hash($doc->sha1, $dochash)))
385                    $doc = null;
386            }
387
388            if (!$doc) {
389                $args = ["paperId" => $this->paperId, "sha1" => $dochash, "documentType" => $o->id];
390                foreach (["timestamp", "mimetype", "content", "content_base64",
391                          "content_file", "metadata", "filename"] as $k)
392                    if (isset($docj->$k))
393                        $args[$k] = $docj->$k;
394                DocumentInfo::fix_mimetype($args);
395                $doc = new DocumentInfo($args, $this->conf, $this->prow);
396            }
397        }
398
399        // save
400        if ($doc->paperStorageId > 0 || $doc->save()) {
401            $this->uploaded_documents[] = $doc->paperStorageId;
402            return $doc;
403        } else {
404            error_log($doc->error_html);
405            $this->error_at_option($o, $doc->error_html);
406            return false;
407        }
408    }
409
410    private function normalize_string($pj, $k, $simplify) {
411        if (isset($pj->$k) && is_string($pj->$k)) {
412            $pj->$k = $simplify ? simplify_whitespace($pj->$k) : trim($pj->$k);
413        } else if (isset($pj->$k)) {
414            $this->format_error_at($k, $pj->$k);
415            unset($pj, $k);
416        }
417    }
418
419    private function normalize_author($pj, $au, &$au_by_lemail) {
420        $aux = Text::analyze_name($au);
421        $aux->first = simplify_whitespace($aux->firstName);
422        $aux->last = simplify_whitespace($aux->lastName);
423        $aux->email = simplify_whitespace($aux->email);
424        $aux->affiliation = simplify_whitespace($aux->affiliation);
425        // borrow from old author information
426        if ($aux->email && $aux->first === "" && $aux->last === "" && $this->prow
427            && ($old_au = $this->prow->author_by_email($aux->email))) {
428            $aux->first = get($old_au, "first", "");
429            $aux->last = get($old_au, "last", "");
430            if ($aux->affiliation === "")
431                $aux->affiliation = get($old_au, "affiliation", "");
432        }
433        // set contactness and author index
434        if (is_object($au) && isset($au->contact))
435            $aux->contact = !!$au->contact;
436        if (is_object($au) && isset($au->index) && is_int($au->index))
437            $aux->index = $au->index;
438        else
439            $aux->index = count($pj->authors) + count($pj->bad_authors);
440
441        if ($aux->first !== "" || $aux->last !== ""
442            || $aux->email !== "" || $aux->affiliation !== "")
443            $pj->authors[] = $aux;
444        else
445            $pj->bad_authors[] = $aux;
446        if ($aux->email) {
447            $lemail = strtolower($aux->email);
448            $au_by_lemail[$lemail] = $aux;
449            if (!validate_email($lemail)
450                && (!$this->prow || !$this->prow->author_by_email($lemail)))
451                $pj->bad_email_authors[] = $aux;
452        }
453    }
454
455    private function normalize_topics($pj) {
456        $topics = $pj->topics;
457        unset($pj->topics);
458        if (is_string($topics))
459            $topics = explode("\n", cleannl($topics));
460        if (is_array($topics)) {
461            $new_topics = (object) array();
462            foreach ($topics as $v) {
463                if ($v && (is_int($v) || is_string($v)))
464                    $new_topics->$v = true;
465                else if ($v)
466                    $this->format_error_at("topics", $v);
467            }
468            $topics = $new_topics;
469        }
470        if (is_object($topics)) {
471            $topic_map = $this->conf->topic_map();
472            $pj->topics = (object) array();
473            foreach ($topics as $k => $v) {
474                if (!$v)
475                    /* skip */;
476                else if (isset($topic_map[$k]))
477                    $pj->topics->$k = true;
478                else {
479                    $tid = array_search($k, $topic_map, true);
480                    if ($tid === false && $k !== "" && !ctype_digit($k)) {
481                        $tmatches = [];
482                        foreach ($topic_map as $tid => $tname)
483                            if (strcasecmp($k, $tname) == 0)
484                                $tmatches[] = $tid;
485                        if (empty($tmatches) && $this->add_topics) {
486                            $this->conf->qe("insert into TopicArea set topicName=?", $k);
487                            if (!$this->conf->has_topics())
488                                $this->conf->save_setting("has_topics", 1);
489                            $this->conf->invalidate_topics();
490                            $topic_map = $this->conf->topic_map();
491                            if (($tid = array_search($k, $topic_map, true)) !== false)
492                                $tmatches[] = $tid;
493                        }
494                        $tid = (count($tmatches) == 1 ? $tmatches[0] : false);
495                    }
496                    if ($tid !== false)
497                        $pj->topics->$tid = true;
498                    else
499                        $pj->bad_topics[] = $k;
500                }
501            }
502        } else if ($topics)
503            $this->format_error_at("topics", $topics);
504    }
505
506    private function normalize_options($pj, $options) {
507        // canonicalize option values to use IDs, not abbreviations
508        $pj->options = (object) array();
509        foreach ($options as $id => $oj) {
510            $omatches = $this->conf->paper_opts->find_all($id);
511            if (count($omatches) != 1)
512                $pj->bad_options[$id] = true;
513            else {
514                $o = current($omatches);
515                // XXX setting decision in JSON?
516                if (($o->final && (!$this->prow || $this->prow->outcome <= 0))
517                    || $o->id <= 0)
518                    continue;
519                $oid = $o->id;
520                $pj->options->$oid = $oj;
521            }
522        }
523    }
524
525    private function normalize_pc_conflicts($pj) {
526        $conflicts = get($pj, "pc_conflicts");
527        $pj->pc_conflicts = (object) array();
528        if (is_object($conflicts))
529            $conflicts = (array) $conflicts;
530        foreach ($conflicts as $email => $ct) {
531            if (is_int($email) && is_string($ct))
532                list($email, $ct) = array($ct, true);
533            if (!($pccid = $this->conf->pc_member_by_email($email)))
534                $pj->bad_pc_conflicts->$email = true;
535            else if (!is_bool($ct) && !is_int($ct) && !is_string($ct))
536                $this->format_error_at("pc_conflicts", $ct);
537            else {
538                if (is_int($ct) && isset(Conflict::$type_names[$ct]))
539                    $ctn = $ct;
540                else if ((is_bool($ct) || is_string($ct))
541                         && ($ctn = Conflict::parse($ct, CONFLICT_AUTHORMARK)) !== false)
542                    /* OK */;
543                else {
544                    $pj->bad_pc_conflicts->$email = $ct;
545                    $ctn = Conflict::parse("other", 1);
546                }
547                $pj->pc_conflicts->$email = $ctn;
548            }
549        }
550    }
551
552    private function valid_contact($email) {
553        global $Me;
554        if ($email) {
555            if (validate_email($email) || strcasecmp($email, $Me->email) == 0)
556                return true;
557            foreach ($this->prow ? $this->prow->contacts(true) : [] as $cflt)
558                if (strcasecmp($cflt->email, $email) == 0)
559                    return true;
560        }
561        return false;
562    }
563
564    private function normalize($pj) {
565        // Errors prevent saving
566        global $Now;
567
568        // Title, abstract
569        $this->normalize_string($pj, "title", true);
570        $this->normalize_string($pj, "abstract", false);
571        $this->normalize_string($pj, "collaborators", false);
572        if (isset($pj->collaborators)) {
573            $collab = rtrim(cleannl($pj->collaborators));
574            if (!$this->prow || $collab !== rtrim(cleannl($this->prow->collaborators))) {
575                $old_collab = $collab;
576                $collab = (string) AuthorMatcher::fix_collaborators($old_collab);
577                if ($collab !== $old_collab) {
578                    $name = $this->conf->setting("sub_pcconf") ? "Other conflicts" : "Potential conflicts";
579                    $this->warning_at("collaborators", "$name changed to follow our required format. You may want to look them over.");
580                }
581            }
582            $pj->collaborators = $collab;
583        }
584
585        // Authors
586        $au_by_lemail = [];
587        $pj->bad_authors = $pj->bad_email_authors = [];
588        if (isset($pj->authors)) {
589            if (is_array($pj->authors))
590                $input_authors = $pj->authors;
591            else {
592                $this->format_error_at("authors", $pj->authors);
593                $input_authors = [];
594            }
595            $pj->authors = [];
596            foreach ($input_authors as $k => $au) {
597                if (is_string($au) || is_object($au))
598                    $this->normalize_author($pj, $au, $au_by_lemail);
599                else
600                    $this->format_error_at("authors", $au);
601            }
602        }
603
604        // Status
605        foreach (array("withdrawn_at", "submitted_at", "final_submitted_at") as $k)
606            if (isset($pj->$k)) {
607                if (is_numeric($pj->$k))
608                    $pj->$k = (int) $pj->$k;
609                else if (is_string($pj->$k))
610                    $pj->$k = $this->conf->parse_time($pj->$k, $Now);
611                else
612                    $pj->$k = false;
613                if ($pj->$k === false || $pj->$k < 0)
614                    $pj->$k = $Now;
615            }
616
617        // Blindness
618        if (isset($pj->nonblind)) {
619            if (($x = friendly_boolean($pj->nonblind)) !== null)
620                $pj->nonblind = $x;
621            else {
622                $this->format_error_at("nonblind", $pj->nonblind);
623                unset($pj->nonblind);
624            }
625        }
626
627        // Topics
628        $pj->bad_topics = array();
629        if (isset($pj->topics))
630            $this->normalize_topics($pj);
631
632        // Options
633        $pj->bad_options = array();
634        if (isset($pj->options)) {
635            if (is_associative_array($pj->options) || is_object($pj->options))
636                $this->normalize_options($pj, $pj->options);
637            else if (is_array($pj->options) && count($pj->options) == 1 && is_object($pj->options[0]))
638                $this->normalize_options($pj, $pj->options[0]);
639            else if ($pj->options === false)
640                $pj->options = (object) array();
641            else {
642                $this->format_error_at("options", $pj->options);
643                unset($pj->options);
644            }
645        }
646
647        // PC conflicts
648        $pj->bad_pc_conflicts = (object) array();
649        if (get($pj, "pc_conflicts")
650            && (is_object($pj->pc_conflicts) || is_array($pj->pc_conflicts)))
651            $this->normalize_pc_conflicts($pj);
652        else if (get($pj, "pc_conflicts") === false)
653            $pj->pc_conflicts = (object) array();
654        else if (isset($pj->pc_conflicts)) {
655            $this->format_error_at("pc_conflicts", $pj->pc_conflicts);
656            unset($pj->pc_conflicts);
657        }
658
659        // verify emails on authors marked as contacts
660        $pj->bad_contacts = array();
661        foreach (get($pj, "authors") ? : array() as $au)
662            if (get($au, "contact")
663                && (!isset($au->email) || !$this->valid_contact($au->email)))
664                $pj->bad_contacts[] = $au;
665
666        // Contacts
667        $contacts = get($pj, "contacts");
668        if ($contacts !== null) {
669            if (is_object($contacts) || is_array($contacts))
670                $contacts = (array) $contacts;
671            else {
672                $this->format_error_at("contacts", $contacts);
673                $contacts = [];
674            }
675            $pj->contacts = [];
676            // verify emails on explicitly named contacts
677            foreach ($contacts as $k => $v) {
678                if (!$v)
679                    continue;
680                if ($v === true)
681                    $v = (object) array();
682                else if (is_string($v) && is_int($k)) {
683                    $v = trim($v);
684                    if ($this->valid_contact($v))
685                        $v = (object) array("email" => $v);
686                    else
687                        $v = Text::analyze_name($v);
688                }
689                if (is_object($v) && !get($v, "email") && is_string($k))
690                    $v->email = $k;
691                if (is_object($v) && get($v, "email")) {
692                    if ($this->valid_contact($v->email))
693                        $pj->contacts[] = (object) array_merge((array) get($au_by_lemail, strtolower($v->email)), (array) $v);
694                    else
695                        $pj->bad_contacts[] = $v;
696                } else
697                    $this->format_error_at("contacts", $v);
698            }
699        }
700
701        // Inherit contactness
702        if (isset($pj->authors) && $this->prow) {
703            foreach ($this->prow->contacts(true) as $cflt)
704                if ($cflt->conflictType >= CONFLICT_CONTACTAUTHOR
705                    && ($aux = get($au_by_lemail, strtolower($cflt->email)))
706                    && !isset($aux->contact))
707                    $aux->contact = true;
708        }
709        // If user modifies paper, make them a contact (not just an author)
710        if ($this->prow
711            && $this->user
712            && !$this->user->allow_administer($this->prow)
713            && $this->prow->conflict_type($this->user) === CONFLICT_AUTHOR) {
714            if (!isset($pj->contacts)) {
715                $pj->contacts = [];
716                foreach ($this->prow->contacts(true) as $cflt)
717                    if ($cflt->conflictType >= CONFLICT_CONTACTAUTHOR)
718                        $pj->contacts[] = (object) ["email" => $cflt->email];
719            }
720            if (!array_filter($pj->contacts, function ($cflt) {
721                    return strcasecmp($this->user->email, $cflt->email) === 0;
722                }))
723                $pj->contacts[] = (object) ["email" => $this->user->email];
724        }
725    }
726
727    static function check_title(PaperStatus $ps, $pj) {
728        $v = convert_to_utf8(get_s($pj, "title"));
729        if ($v === ""
730            && (isset($pj->title) || !$ps->prow || (string) $ps->prow->title === ""))
731            $ps->error_at("title", $ps->_("Each submission must have a title."));
732        if (!$ps->prow
733            || (!$ps->has_error_at("title")
734                && isset($pj->title)
735                && $v !== (string) $ps->prow->title))
736            $ps->save_paperf("title", $v, "title");
737    }
738
739    static function check_abstract(PaperStatus $ps, $pj) {
740        $v = convert_to_utf8(get_s($pj, "abstract"));
741        if ($v === ""
742            && (isset($pj->abstract) || !$ps->prow || (string) $ps->prow->abstract === "")) {
743            if (!$ps->conf->opt("noAbstract"))
744                $ps->error_at("abstract", $ps->_("Each submission must have an abstract."));
745        }
746        if (!$ps->prow
747            || (!$ps->has_error_at("abstract")
748                && isset($pj->abstract)
749                && $v !== (string) $ps->prow->abstract))
750            $ps->save_paperf("abstract", $v, "abstract");
751    }
752
753    static private function author_information($pj) {
754        $x = "";
755        foreach ($pj && get($pj, "authors") ? $pj->authors : [] as $au) {
756            $x .= get($au, "first", get($au, "firstName", "")) . "\t"
757                . get($au, "last", get($au, "lastName", "")) . "\t"
758                . get($au, "email", "") . "\t"
759                . get($au, "affiliation", "") . "\n";
760        }
761        return $x;
762    }
763
764    static function check_authors(PaperStatus $ps, $pj) {
765        $authors = get($pj, "authors");
766        $max_authors = $ps->conf->opt("maxAuthors");
767        if ((is_array($authors) && empty($authors))
768            || ($authors === null && (!$ps->prow || !$ps->prow->author_list())))
769            $ps->error_at("authors", $ps->_("Each submission must have at least one author.", $max_authors));
770        if ($max_authors > 0 && is_array($authors) && count($authors) > $max_authors)
771            $ps->error_at("authors", $ps->_("Each submission can have at most %d authors.", $max_authors));
772        if (!empty($pj->bad_authors))
773            $ps->error_at("authors", $ps->_("Some authors ignored."));
774        foreach ($pj->bad_email_authors as $aux) {
775            $ps->error_at("authors", null);
776            $ps->error_at("auemail" . $aux->index, $ps->_("“%s” is not a valid email address.", htmlspecialchars($aux->email)));
777        }
778        if (!$ps->prow || isset($pj->authors)) {
779            $v = convert_to_utf8(self::author_information($pj));
780            if (!$ps->prow
781                || (!$ps->has_error_at("authors")
782                    && $v !== $ps->prow->authorInformation))
783                $ps->save_paperf("authorInformation", $v, "authors");
784        }
785    }
786
787    static function check_collaborators(PaperStatus $ps, $pj) {
788        $v = convert_to_utf8(get_s($pj, "collaborators"));
789        if (!$ps->prow
790            || (isset($pj->collaborators)
791                && $v !== (string) $ps->prow->collaborators))
792            $ps->save_paperf("collaborators", $v, "collaborators");
793    }
794
795    static function check_nonblind(PaperStatus $ps, $pj) {
796        if ($ps->conf->submission_blindness() == Conf::BLIND_OPTIONAL
797            && (!$ps->prow
798                || (isset($pj->nonblind)
799                    && !$pj->nonblind !== !!$ps->prow->blind))) {
800            $ps->save_paperf("blind", get($pj, "nonblind") ? 0 : 1, "nonblind");
801        }
802    }
803
804    static function check_pdfs(PaperStatus $ps, $pj) {
805        // store documents (XXX should attach to paper even if error)
806        foreach (["submission", "final"] as $i => $k) {
807            if (isset($pj->$k) && $pj->$k) {
808                $pj->$k = $ps->upload_document($pj->$k, $ps->conf->paper_opts->get($i ? DTYPE_FINAL : DTYPE_SUBMISSION));
809            }
810            if (!$ps->prow
811                || (isset($pj->$k)
812                    && !$ps->has_error_at($i ? "final" : "paper"))) {
813                $null_id = $i ? 0 : 1;
814                $new_id = isset($pj->$k) && $pj->$k ? $pj->$k->paperStorageId : $null_id;
815                $prowk = $i ? "finalPaperStorageId" : "paperStorageId";
816                if (($ps->prow ? $ps->prow->$prowk : $null_id) != $new_id)
817                    $ps->save_paperf($prowk, $new_id, $k);
818                else if (!$ps->prow)
819                    $ps->save_paperf($prowk, $new_id);
820            }
821        }
822    }
823
824    static function check_status(PaperStatus $ps, $pj) {
825        global $Now;
826        $pj_withdrawn = get($pj, "withdrawn");
827        $pj_submitted = get($pj, "submitted");
828        $pj_draft = get($pj, "draft");
829        if ($pj_withdrawn === null
830            && $pj_submitted === null
831            && $pj_draft === null) {
832            $pj_status = get($pj, "status");
833            if ($pj_status === "submitted")
834                $pj_submitted = true;
835            else if ($pj_status === "withdrawn")
836                $pj_withdrawn = true;
837            else if ($pj_status === "draft")
838                $pj_draft = true;
839        }
840        if ($ps->has_error()
841            && ($pj_submitted || $pj_draft === false)
842            && !$pj_withdrawn
843            && (!$ps->prow || $ps->prow->timeSubmitted == 0)) {
844            $pj_submitted = false;
845            $pj_draft = true;
846        }
847
848        $submitted = false;
849        if ($pj_withdrawn !== null
850            || $pj_submitted !== null
851            || $pj_draft !== null) {
852            if ($pj_submitted !== null)
853                $submitted = $pj_submitted;
854            else if ($pj_draft !== null)
855                $submitted = !$pj_draft;
856            else if ($ps->prow)
857                $submitted = $ps->prow->timeSubmitted != 0;
858            if (isset($pj->submitted_at))
859                $submitted_at = $pj->submitted_at;
860            else if ($ps->prow)
861                $submitted_at = $ps->prow->submitted_at();
862            else
863                $submitted_at = 0;
864            if ($pj_withdrawn) {
865                if ($submitted && $submitted_at <= 0)
866                    $submitted_at = -100;
867                else if (!$submitted)
868                    $submitted_at = 0;
869                else
870                    $submitted_at = -$submitted_at;
871                if (!$ps->prow || $ps->prow->timeWithdrawn <= 0) {
872                    $ps->save_paperf("timeWithdrawn", get($pj, "withdrawn_at") ? : $Now, "status");
873                    $ps->save_paperf("timeSubmitted", $submitted_at);
874                } else if (($ps->prow->submitted_at() > 0) !== $submitted)
875                    $ps->save_paperf("timeSubmitted", $submitted_at, "status");
876            } else if ($submitted) {
877                if (!$ps->prow || $ps->prow->timeSubmitted <= 0) {
878                    if ($submitted_at <= 0 || $submitted_at === PaperInfo::SUBMITTED_AT_FOR_WITHDRAWN)
879                        $submitted_at = $Now;
880                    $ps->save_paperf("timeSubmitted", $submitted_at, "status");
881                }
882                if ($ps->prow && $ps->prow->timeWithdrawn != 0)
883                    $ps->save_paperf("timeWithdrawn", 0, "status");
884            } else if ($ps->prow && ($ps->prow->timeWithdrawn > 0 || $ps->prow->timeSubmitted > 0)) {
885                $ps->save_paperf("timeSubmitted", 0, "status");
886                $ps->save_paperf("timeWithdrawn", 0);
887            }
888        }
889        $ps->_paper_submitted = !$pj_withdrawn && $submitted;
890    }
891
892    static function check_final_status(PaperStatus $ps, $pj) {
893        global $Now;
894        if (isset($pj->final_submitted)) {
895            if ($pj->final_submitted)
896                $time = get($pj, "final_submitted_at") ? : $Now;
897            else
898                $time = 0;
899            if (!$ps->prow || $ps->prow->timeFinalSubmitted != $time)
900                $ps->save_paperf("timeFinalSubmitted", $time, "final_status");
901        }
902    }
903
904    static function check_topics(PaperStatus $ps, $pj) {
905        if (!empty($pj->bad_topics))
906            $ps->warning_at("topics", $ps->_("Unknown topics ignored (%2\$s).", count($pj->bad_topics), htmlspecialchars(join("; ", $pj->bad_topics))));
907        if (isset($pj->topics)) {
908            $old_topics = $ps->prow ? $ps->prow->topic_list() : [];
909            $new_topics = array_map("intval", array_keys((array) $pj->topics));
910            sort($old_topics);
911            sort($new_topics);
912            if ($old_topics !== $new_topics) {
913                $ps->diffs["topics"] = true;
914                $ps->_topic_ins = $new_topics;
915            }
916        }
917    }
918
919    static function execute_topics(PaperStatus $ps) {
920        if (isset($ps->_topic_ins)) {
921            $ps->conf->qe("delete from PaperTopic where paperId=?", $ps->paperId);
922            if (!empty($ps->_topic_ins)) {
923                $ti = array_map(function ($tid) use ($ps) {
924                    return [$ps->paperId, $tid];
925                }, $ps->_topic_ins);
926                $ps->conf->qe("insert into PaperTopic (paperId,topicId) values ?v", $ti);
927            }
928        }
929    }
930
931    static function check_options(PaperStatus $ps, $pj) {
932        if (!empty($pj->bad_options))
933            $ps->warning_at("options", $ps->_("Unknown options ignored (%2\$s).", count($pj->bad_options), htmlspecialchars(join("; ", array_keys($pj->bad_options)))));
934        if (!isset($pj->options))
935            return;
936
937        $parsed_options = array();
938        foreach ($pj->options as $oid => $oj) {
939            $o = $ps->conf->paper_opts->get($oid);
940            $result = null;
941            if ($oj !== null)
942                $result = $o->store_json($oj, $ps);
943            if ($result === null || $result === false)
944                $result = [];
945            else if (!is_array($result))
946                $result = [[$result]];
947            else if (count($result) == 2 && !is_int($result[1]))
948                $result = [$result];
949            if (!$ps->has_error_at($o->field_key()))
950                $parsed_options[$o->id] = $result;
951        }
952
953        ksort($parsed_options);
954        foreach ($parsed_options as $id => $parsed_vs) {
955            // old values
956            $ov = $od = [];
957            if ($ps->prow) {
958                list($ov, $od) = $ps->prow->option_value_data($id);
959            }
960
961            // new values
962            $nv = $nd = [];
963            foreach ($parsed_vs as $vx) {
964                $nv[] = is_int($vx) ? $vx : $vx[0];
965                $nd[] = is_int($vx) ? null : get($vx, 1);
966            }
967
968            // save difference
969            if ($ov !== $nv || $od !== $nd) {
970                $opt = $ps->conf->paper_opts->get($id);
971                $ps->_option_delid[] = $id;
972                $ps->diffs[$opt->json_key()] = true;
973                for ($i = 0; $i < count($nv); ++$i) {
974                    $qv0 = [-1, $id, $nv[$i], null, null];
975                    if ($nd[$i] !== null) {
976                        $qv0[strlen($nd[$i]) < 32768 ? 3 : 4] = $nd[$i];
977                    }
978                    $ps->_option_ins[] = $qv0;
979                }
980                if ($opt->has_document())
981                    $ps->_document_change = true;
982            }
983        }
984    }
985
986    static function execute_options(PaperStatus $ps) {
987        if (!empty($ps->_option_delid))
988            $ps->conf->qe("delete from PaperOption where paperId=? and optionId?a", $ps->paperId, $ps->_option_delid);
989        if (!empty($ps->_option_ins)) {
990            foreach ($ps->_option_ins as &$x)
991                $x[0] = $ps->paperId;
992            $ps->conf->qe("insert into PaperOption (paperId, optionId, value, data, dataOverflow) values ?v", $ps->_option_ins);
993        }
994    }
995
996    static private function contacts_array($pj) {
997        $contacts = array();
998        foreach (get($pj, "authors") ? : [] as $au)
999            if (get($au, "email") && validate_email($au->email)) {
1000                $c = clone $au;
1001                $contacts[strtolower($c->email)] = $c;
1002            }
1003        foreach (get($pj, "contacts") ? : array() as $v) {
1004            $lemail = strtolower($v->email);
1005            $c = (object) array_merge((array) get($contacts, $lemail), (array) $v);
1006            $c->contact = true;
1007            $contacts[$lemail] = $c;
1008        }
1009        return $contacts;
1010    }
1011
1012    function conflicts_array($pj) {
1013        $cflts = [];
1014
1015        // extract PC conflicts
1016        if (isset($pj->pc_conflicts)) {
1017            foreach ((array) $pj->pc_conflicts as $email => $type)
1018                $cflts[strtolower($email)] = $type;
1019        } else if ($this->prow) {
1020            foreach ($this->prow->conflicts(true) as $cflt)
1021                if ($cflt->conflictType < CONFLICT_AUTHOR)
1022                    $cflts[strtolower($cflt->email)] = $cflt->conflictType;
1023        }
1024
1025        // extract contacts
1026        if (isset($pj->contacts)) {
1027            foreach ($pj->contacts as $aux) {
1028                $cflts[strtolower($aux->email)] = CONFLICT_CONTACTAUTHOR;
1029            }
1030        } else if ($this->prow) {
1031            foreach ($this->prow->contacts(true) as $cflt) {
1032                if ($cflt->conflictType == CONFLICT_CONTACTAUTHOR)
1033                    $cflts[strtolower($cflt->email)] = CONFLICT_CONTACTAUTHOR;
1034            }
1035        }
1036
1037        // extract authors
1038        if (isset($pj->authors)) {
1039            foreach ($pj->authors as $aux) {
1040                if (isset($aux->email)) {
1041                    $lemail = strtolower($aux->email);
1042                    if (!isset($aux->contact))
1043                        $ctype = max(get_i($cflts, $lemail), CONFLICT_AUTHOR);
1044                    else
1045                        $ctype = $aux->contact ? CONFLICT_CONTACTAUTHOR : CONFLICT_AUTHOR;
1046                    $cflts[$lemail] = $ctype;
1047                }
1048            }
1049        } else if ($this->prow) {
1050            foreach ($this->prow->contacts(true) as $cflt) {
1051                $lemail = strtolower($cflt->email);
1052                $cflts[$lemail] = max(get_i($cflts, $lemail), $cflt->conflictType);
1053            }
1054            foreach ($this->prow->author_list() as $au)
1055                if ($au->email !== "") {
1056                    $lemail = strtolower($au->email);
1057                    $cflts[$lemail] = max(get_i($cflts, $lemail), CONFLICT_AUTHOR);
1058                }
1059        }
1060
1061        // chair conflicts cannot be overridden
1062        if ($this->prow) {
1063            foreach ($this->prow->conflicts(true) as $cflt) {
1064                if ($cflt->conflictType == CONFLICT_CHAIRMARK) {
1065                    $lemail = strtolower($cflt->email);
1066                    if (get_i($cflts, $lemail) < CONFLICT_CHAIRMARK
1067                        && $this->user
1068                        && !$this->user->can_administer($this->prow))
1069                        $cflts[$lemail] = CONFLICT_CHAIRMARK;
1070                }
1071            }
1072        }
1073
1074        ksort($cflts);
1075        return $cflts;
1076    }
1077
1078    static private function check_contacts(PaperStatus $ps, $pj) {
1079        $cflts = $ps->conflicts_array($pj);
1080        if (!array_filter($cflts, function ($cflt) { return $cflt >= CONFLICT_CONTACTAUTHOR; })
1081            && $ps->prow
1082            && array_filter($ps->prow->contacts(), function ($cflt) { return $cflt->conflictType >= CONFLICT_CONTACTAUTHOR; })) {
1083            $ps->error_at("contacts", $ps->_("Each submission must have at least one contact."));
1084        }
1085        if ($ps->prow
1086            && $ps->user
1087            && !$ps->user->allow_administer($ps->prow)
1088            && get($cflts, strtolower($ps->user->email), 0) < CONFLICT_AUTHOR) {
1089            $ps->error_at("contacts", $ps->_("You can’t remove yourself as submission contact. (Ask another contact to remove you.)"));
1090        }
1091        foreach ($pj->bad_contacts as $reg) {
1092            if (!isset($reg->email))
1093                $ps->error_at("contacts", $ps->_("Contact %s has no associated email.", Text::user_html($reg)));
1094            else
1095                $ps->error_at("contacts", $ps->_("Contact email %s is invalid.", htmlspecialchars($reg->email)));
1096        }
1097    }
1098
1099    static function check_conflicts(PaperStatus $ps, $pj) {
1100        if (isset($pj->contacts))
1101            self::check_contacts($ps, $pj);
1102
1103        $ps->_new_conflicts = $new_cflts = $ps->conflicts_array($pj);
1104        $old_cflts = $ps->conflicts_array((object) []);
1105        foreach ($new_cflts + $old_cflts as $lemail => $v) {
1106            $new_ctype = get_i($new_cflts, $lemail);
1107            $old_ctype = get_i($old_cflts, $lemail);
1108            if ($new_ctype !== $old_ctype) {
1109                if ($new_ctype >= CONFLICT_AUTHOR || $old_ctype >= CONFLICT_AUTHOR)
1110                    $ps->diffs["contacts"] = true;
1111                if (($new_ctype > 0 && $new_ctype < CONFLICT_AUTHOR)
1112                    || ($old_ctype > 0 && $old_ctype < CONFLICT_AUTHOR))
1113                    $ps->diffs["pc_conflicts"] = true;
1114            }
1115        }
1116    }
1117
1118    static function postcheck_contacts(PaperStatus $ps, $pj) {
1119        if (isset($ps->diffs["contacts"]) && !$ps->has_error_at("contacts")) {
1120            foreach (self::contacts_array($pj) as $c) {
1121                $flags = (get($c, "contact") ? 0 : Contact::SAVE_IMPORT)
1122                    | ($ps->no_email ? 0 : Contact::SAVE_NOTIFY);
1123                $c->disabled = !!$ps->disable_users;
1124                if (!Contact::create($ps->conf, $ps->user, $c, $flags)
1125                    && !($flags & Contact::SAVE_IMPORT))
1126                    $ps->error_at("contacts", $ps->_("Could not create an account for contact %s.", Text::user_html($c)));
1127            }
1128        }
1129        if ((isset($ps->diffs["contacts"]) || isset($ps->diffs["pc_conflicts"]))
1130            && !$ps->has_error_at("contacts")
1131            && !$ps->has_error_at("pc_conflicts")) {
1132            $ps->_conflict_ins = [];
1133            if (!empty($ps->_new_conflicts)) {
1134                $result = $ps->conf->qe("select contactId, email from ContactInfo where email?a", array_keys($ps->_new_conflicts));
1135                while (($row = edb_row($result)))
1136                    $ps->_conflict_ins[] = [-1, $row[0], $ps->_new_conflicts[strtolower($row[1])]];
1137                Dbl::free($result);
1138            }
1139        }
1140    }
1141
1142    static function execute_conflicts(PaperStatus $ps) {
1143        if ($ps->_conflict_ins !== null) {
1144            $ps->conf->qe("delete from PaperConflict where paperId=?", $ps->paperId);
1145            foreach ($ps->_conflict_ins as &$x)
1146                $x[0] = $ps->paperId;
1147            if (!empty($ps->_conflict_ins))
1148                $ps->conf->qe("insert into PaperConflict (paperId,contactId,conflictType) values ?v", $ps->_conflict_ins);
1149        }
1150    }
1151
1152    private function save_paperf($f, $v, $diff = null) {
1153        assert(!isset($this->_paper_upd[$f]));
1154        $this->_paper_upd[$f] = $v;
1155        if ($diff)
1156            $this->diffs[$diff] = true;
1157    }
1158
1159    function prepare_save_paper_json($pj) {
1160        assert(!$this->hide_docids);
1161        assert(is_object($pj));
1162
1163        $paperid = get($pj, "pid", get($pj, "id", null));
1164        if ($paperid !== null && is_int($paperid) && $paperid <= 0)
1165            $paperid = null;
1166        if ($paperid !== null && !is_int($paperid)) {
1167            $key = isset($pj->pid) ? "pid" : "id";
1168            $this->format_error_at($key, $paperid);
1169            return false;
1170        }
1171
1172        if (get($pj, "error") || get($pj, "error_html")) {
1173            $this->error_at("error", $this->_("Refusing to save submission with error"));
1174            return false;
1175        }
1176
1177        $this->clear();
1178        $this->paperId = $paperid ? : -1;
1179        if ($paperid)
1180            $this->prow = $this->conf->paperRow(["paperId" => $paperid, "topics" => true, "options" => true], $this->user);
1181        if ($pj && $this->prow && $paperid !== $this->prow->paperId) {
1182            $this->error_at("pid", $this->_("Saving submission with different ID"));
1183            return false;
1184        }
1185
1186        // normalize and check format
1187        $this->normalize($pj);
1188        if ($this->has_error())
1189            return false;
1190
1191        // save parts and track diffs
1192        self::check_title($this, $pj);
1193        self::check_abstract($this, $pj);
1194        self::check_authors($this, $pj);
1195        self::check_collaborators($this, $pj);
1196        self::check_nonblind($this, $pj);
1197        self::check_conflicts($this, $pj);
1198        self::check_pdfs($this, $pj);
1199        self::check_topics($this, $pj);
1200        self::check_options($this, $pj);
1201        self::check_status($this, $pj);
1202        self::check_final_status($this, $pj);
1203        self::postcheck_contacts($this, $pj);
1204        return true;
1205    }
1206
1207    private function unused_random_pid() {
1208        $n = max(100, 3 * $this->conf->fetch_ivalue("select count(*) from Paper"));
1209        while (1) {
1210            $pids = [];
1211            while (count($pids) < 10)
1212                $pids[] = mt_rand(1, $n);
1213
1214            $result = $this->conf->qe("select paperId from Paper where paperId?a", $pids);
1215            while ($result && ($row = $result->fetch_row()))
1216                $pids = array_values(array_diff($pids, [(int) $row[0]]));
1217            Dbl::free($result);
1218
1219            if (!empty($pids))
1220                return $pids[0];
1221        }
1222    }
1223
1224    function execute_save_paper_json($pj) {
1225        global $Now;
1226        if (!empty($this->_paper_upd)) {
1227            if ($this->conf->submission_blindness() == Conf::BLIND_NEVER)
1228                $this->save_paperf("blind", 0);
1229            else if ($this->conf->submission_blindness() != Conf::BLIND_OPTIONAL)
1230                $this->save_paperf("blind", 1);
1231
1232            $old_joindoc = $this->prow ? $this->prow->joindoc() : null;
1233            $old_joinid = $old_joindoc ? $old_joindoc->paperStorageId : 0;
1234
1235            $new_final_docid = get($this->_paper_upd, "finalPaperStorageId");
1236            $new_sub_docid = get($this->_paper_upd, "paperStorageId");
1237            if ($new_final_docid !== null || $new_sub_docid !== null)
1238                $this->_document_change = true;
1239
1240            if ($new_final_docid > 0)
1241                $new_joindoc = $pj->final;
1242            else if ($new_final_docid === null
1243                     && $this->prow
1244                     && $this->prow->finalPaperStorageId > 0)
1245                $new_joindoc = $this->prow->document(DTYPE_FINAL);
1246            else if ($new_sub_docid > 1)
1247                $new_joindoc = $pj->submission;
1248            else if ($new_sub_docid === null
1249                     && $this->prow
1250                     && $this->prow->paperStorageId > 1)
1251                $new_joindoc = $this->prow->document(DTYPE_SUBMISSION);
1252            else
1253                $new_joindoc = null;
1254            $new_joinid = $new_joindoc ? $new_joindoc->paperStorageId : 0;
1255
1256            if ($new_joindoc && $new_joinid != $old_joinid) {
1257                if ($new_joindoc->ensure_size())
1258                    $this->save_paperf("size", $new_joindoc->size);
1259                else
1260                    $this->save_paperf("size", 0);
1261                $this->save_paperf("mimetype", $new_joindoc->mimetype);
1262                $this->save_paperf("sha1", $new_joindoc->binary_hash());
1263                $this->save_paperf("timestamp", $new_joindoc->timestamp);
1264                if ($this->conf->sversion >= 145)
1265                    $this->save_paperf("pdfFormatStatus", 0);
1266            } else if (!$this->prow || $new_joinid != $old_joinid) {
1267                $this->save_paperf("size", 0);
1268                $this->save_paperf("mimetype", "");
1269                $this->save_paperf("sha1", "");
1270                $this->save_paperf("timestamp", 0);
1271                if ($this->conf->sversion >= 145)
1272                    $this->save_paperf("pdfFormatStatus", 0);
1273            }
1274
1275            $this->save_paperf("timeModified", $Now);
1276
1277            $need_insert = $this->paperId <= 0;
1278            if (!$need_insert) {
1279                $qv = array_values($this->_paper_upd);
1280                $qv[] = $this->paperId;
1281                $result = $this->conf->qe_apply("update Paper set " . join("=?, ", array_keys($this->_paper_upd)) . "=? where paperId=?", $qv);
1282                if ($result
1283                    && $result->affected_rows === 0
1284                    && !$this->conf->fetch_value("select paperId from Paper where paperId=?", $this->paperId)) {
1285                    $this->_paper_upd["paperId"] = $this->paperId;
1286                    $need_insert = true;
1287                }
1288            }
1289            if ($need_insert) {
1290                if (($random_pids = $this->conf->setting("random_pids"))) {
1291                    $this->conf->qe("lock tables Paper write");
1292                    $this->_paper_upd["paperId"] = $this->unused_random_pid();
1293                }
1294                $result = $this->conf->qe_apply("insert into Paper set " . join("=?, ", array_keys($this->_paper_upd)) . "=?", array_values($this->_paper_upd));
1295                if ($random_pids)
1296                    $this->conf->qe("unlock tables");
1297                if (!$result || !$result->insert_id)
1298                    return $this->error_at(false, $this->_("Could not create paper."));
1299                $pj->pid = $this->paperId = (int) $result->insert_id;
1300                if (!empty($this->uploaded_documents))
1301                    $this->conf->qe("update PaperStorage set paperId=? where paperStorageId?a", $this->paperId, $this->uploaded_documents);
1302            }
1303
1304            // maybe update `papersub` settings
1305            $was_submitted = $this->prow && $this->prow->timeWithdrawn <= 0 && $this->prow->timeSubmitted > 0;
1306            if ($this->_paper_submitted != $was_submitted)
1307                $this->conf->update_papersub_setting($this->_paper_submitted ? 1 : -1);
1308        }
1309
1310        self::execute_conflicts($this);
1311        self::execute_topics($this);
1312        self::execute_options($this);
1313
1314        // update autosearch
1315        $this->conf->update_autosearch_tags($this->paperId);
1316
1317        // update document inactivity
1318        if ($this->_document_change) {
1319            $pset = $this->conf->paper_set(null, ["paperId" => $this->paperId, "options" => true]);
1320            foreach ($pset as $prow)
1321                $prow->mark_inactive_documents();
1322        }
1323
1324        return true;
1325    }
1326
1327    function save_paper_json($pj) {
1328        if ($this->prepare_save_paper_json($pj)) {
1329            $this->execute_save_paper_json($pj);
1330            return $this->paperId;
1331        } else
1332            return false;
1333    }
1334}
1335