diff --git a/.phan/config.php b/.phan/config.php index 964e948e3..b6102f418 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -26,8 +26,8 @@ */ return (function () { $config = [ - "minimum_target_php_version" => "7.0", - "target_php_version" => "8.2", + "minimum_target_php_version" => "7.2", + "target_php_version" => "8.3", // If true, missing properties will be created when // they are first seen. If false, we'll report an diff --git a/NEWS.md b/NEWS.md index 93e73091f..d9e11e268 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,12 +1,13 @@ HotCRP NEWS =========== -## Version 3.0xx +## Version 3.0.0 – 14.Mar.2024 * Upgrade notes - * PHP 8.2 and 8.3 are supported, PHP 7.0 is not. - * Remove support for `#perm` tags; the experiment failed. + * PHP 8.2 and 8.3 are supported, PHP 7.0 and 7.1 are not. + * Remove support for `#perm` tags. + * Prefer `$Opt["oAuthProviders"]` (not `$Opt["oAuthTypes"]`). * Batch scripts @@ -17,6 +18,9 @@ HotCRP NEWS * Add support for multiple submission classes! Different deadlines for different kinds of submission. + * Submission updates are transactional. Conflicting parallel updates + should be caught by the system; users get the submission form back and + can try again. * Reviews @@ -26,19 +30,34 @@ HotCRP NEWS and other systems may “click” those links automatically. * Fix longstanding bug with accepting a review with clickthrough terms. (Infinite recursion.) + * Comments being edited pop out when the user scrolls up, so you can + edit a comment and read reviews in parallel. * Search * Deprecated shorthand score searches `ovemer:X...Y`, `ovemer:X-Y`. Instead be explicit: `ovemer:all:X-Y` and `ovemer:span:X-Y`. + * Add `sort:color`. -* Navigation: Support deployment within Apache, proxied or not. +* Signin + + * Improve OAuth signin support, and add support to be an OAuth provider. + +* Navigation + + * Support deployment within Apache, proxied or not. + * Validate redirect parameters to avoid open redirect vulnerabilities. * Support decision variants: “Desk-reject” decisions are immediately visible to authors, regardless of other settings. There is also support for “other” decisions, which are neither acceptish nor rejectish. -* More internal improvements; for example, the autoassigner is extensible. +* Add initial developer manual. + +* More internal improvements; for example, allow autoassigner extensions, and + support TLS database connections. + +* Many, many, many bug fixes and other improvements. ## Version 3.0b3 – 30.Aug.2022 @@ -48,7 +67,7 @@ HotCRP NEWS * Add dropdown menu for help, settings, and accounts. -## Version 3.0b2 - 22.Aug.2022 +## Version 3.0b2 – 22.Aug.2022 * Upgrade notes @@ -129,7 +148,7 @@ HotCRP NEWS * Add support for bearer-token API access. -## Version 3.0b1 - 12.Nov.2020 +## Version 3.0b1 – 12.Nov.2020 * Upgrade notes @@ -280,7 +299,7 @@ HotCRP NEWS * Many other bug fixes, tests, and improvements. -## Version 2.102 - 9.Aug.2018 +## Version 2.102 – 9.Aug.2018 * Support integration with Lutz Prechelt’s [Review Quality Collector][]. @@ -325,7 +344,7 @@ HotCRP NEWS * Support PHP 7.2; stop supporting PHP 5.5. -## Version 2.101 - 18.Oct.2017 +## Version 2.101 – 18.Oct.2017 * Support metareviewers. @@ -381,7 +400,7 @@ HotCRP NEWS * Thanks for feature requests and bug reports to many users. -## Version 2.100 - 15.Jun.2016 +## Version 2.100 – 15.Jun.2016 * Sort reviews & comments by post time, rather than putting all the reviews first and all the comments later. @@ -432,14 +451,14 @@ HotCRP NEWS Oleg Vaskevich, Eijiro Sumii, Marcos Aguilera. -## Version 2.99 - 21.Nov.2015 +## Version 2.99 – 21.Nov.2015 * Support real-valued tag indexes and tag indexes for PC members. * Fix some bugs in 2.98. -## Version 2.98 - 19.Nov.2015 +## Version 2.98 – 19.Nov.2015 * MySQL improvements: Use InnoDB; set the connection charset to binary, which is required on newer MySQL instances; support emoji in reviews. @@ -452,7 +471,7 @@ HotCRP NEWS and improvements. -## Version 2.97 - 28.Sep.2015 +## Version 2.97 – 28.Sep.2015 * Add `re:words` search term and formula term. @@ -460,7 +479,7 @@ HotCRP NEWS (a regular expression's backtracking went exponential). -## Version 2.96 - 24.Sep.2015 +## Version 2.96 – 24.Sep.2015 * New improved look. @@ -489,7 +508,7 @@ HotCRP NEWS Emery Berger also suggested a feature or two. -## Version 2.95 - 19.Jun.2015 +## Version 2.95 – 19.Jun.2015 * Graphs!!!!! @@ -515,7 +534,7 @@ HotCRP NEWS * Hundreds of bug fixes and minor improvements, and some performance work. -## Version 2.94 - 15.Mar.2015 +## Version 2.94 – 15.Mar.2015 * Add buzzer, a discussion status page based on the tracker. Many tracker stability improvements. @@ -534,7 +553,7 @@ HotCRP NEWS Dan Tsafrir, Peter Sewell, Gail Murphy, George Candea, and others. -## Version 2.93 - 2.Oct.2014 +## Version 2.93 – 2.Oct.2014 * Improve autoassigner to spread out user unhappiness. @@ -573,19 +592,19 @@ HotCRP NEWS Douglis, and others. -## Version 2.92 - 13.May.2014 +## Version 2.92 – 13.May.2014 * Bug fixes for bugs reported by Shriram Krishnamurthi, Aditya Akella, Yoshi Kohno, Garth Gibson. -## Version 2.91 - 1.May.2014 +## Version 2.91 – 1.May.2014 * Bug fixes to profile editing and submission options problems reported by Lars Eggert and Kevin Fu. -## Version 2.90 - 25.Apr.2014 +## Version 2.90 – 25.Apr.2014 * Major refactoring release. @@ -620,7 +639,7 @@ HotCRP NEWS Kevin Fu, Soheil Hassas Yeganeh, Robby Findler, and Johannes Dahse. -## Version 2.61 - 14.Aug.2013 +## Version 2.61 – 14.Aug.2013 * Correct some XSS errors and one SQL injection error reported by Johannes Dahse using a static checking tool of his design. The XSS @@ -633,7 +652,7 @@ HotCRP NEWS Thanks to Anil Madhavapeddy and Peter Sewell. -## Version 2.60 - 19.Jul.2013 +## Version 2.60 – 19.Jul.2013 * Major new feature: Paper managers. Administrators can assign PC members to "manage" individual papers. These PC members gain admin @@ -658,7 +677,7 @@ HotCRP NEWS and Jeff Mogul. -## Version 2.59 - 14.Jun.2013 +## Version 2.59 – 14.Jun.2013 * Bug fix: "Monitor external reviews" works. Reported by Peter Sewell. @@ -668,14 +687,14 @@ HotCRP NEWS papers" searches. Reported by Nickolai Zeldovich and Jeff Mogul. -## Version 2.58 - 23.Mar.2013 +## Version 2.58 – 23.Mar.2013 * More information leak plugging: explicit search for review fields that should be hidden from authors, and review rounds. Reported by John Heidemann. -## Version 2.57 - 16.Mar.2013 +## Version 2.57 – 16.Mar.2013 * Bug fix: The search page's score graphs exposed score values for authored papers during the rebuttal phase. This is normally OK, but @@ -687,7 +706,7 @@ HotCRP NEWS * Add a random-walk-based paper ranking method (John Douceur). -## Version 2.56 - 29.Jan.2013 +## Version 2.56 – 29.Jan.2013 * This is a major refactoring release. Internals, particularly for paper list display, are cleaner and more extensible. But bugs are @@ -724,12 +743,12 @@ HotCRP NEWS * Thanks to Jeff Mogul and John Douceur. -## Version 2.55 - 31.Dec.2012 +## Version 2.55 – 31.Dec.2012 * Minor bugfix release. -## Version 2.54 - 30.Dec.2012 +## Version 2.54 – 30.Dec.2012 * Fix bug in 2.53 where long papers could not be uploaded. Kamin Whitehouse report. @@ -739,7 +758,7 @@ HotCRP NEWS * Some other bug fixes. -## Version 2.53 - 26.Dec.2012 +## Version 2.53 – 26.Dec.2012 * Support sending mail to PC members about their new review assignments. @@ -751,14 +770,14 @@ HotCRP NEWS Petros Maniatis, Jeff Mogul, Antoine Picard, and Anthony Riley. -## Version 2.52 - 23.Jul.2012 +## Version 2.52 – 23.Jul.2012 * Allow chairs to change all PC conflicts on papers' Edit screens. * Other bug fixes and improvements. -## Version 2.51 - 22.Jun.2012 +## Version 2.51 – 22.Jun.2012 * Fix bug with setting tags on per-paper pages (caused by cross-site request forgery protection). @@ -766,7 +785,7 @@ HotCRP NEWS * Other fixes and improvements. -## Version 2.50 - 10.May.2012 +## Version 2.50 – 10.May.2012 * Fix database error on response submissions (a problem since v2.48). Problem reported by Robby Findler. @@ -778,7 +797,7 @@ HotCRP NEWS * Thanks to Dan Tsafrir, Wilson Hsieh, Giuliano Casale, and Geoff Voelker. -## Version 2.49 - 29.Mar.2012 +## Version 2.49 – 29.Mar.2012 * Add update notification. Chairs' browsers contact an updates server, hotcrp.lcdf.org/updates, to check whether the HotCRP installation should @@ -786,7 +805,7 @@ HotCRP NEWS with version information, set `$Opt["updatesSite"] = false`. -## Version 2.48 - 28.Mar.2012 +## Version 2.48 – 28.Mar.2012 * Correct major information exposure with author-view capabilities. Author-view capability URLs, when entered by users not otherwise logged @@ -804,7 +823,7 @@ HotCRP NEWS Jane-Ellen Long, and others. -## Version 2.47 - 14.Dec.2011 +## Version 2.47 – 14.Dec.2011 * Add author-view capabilities. These parameters, when appended to any HotCRP URL, grant the client the right to view a paper like an author. @@ -835,7 +854,7 @@ HotCRP NEWS Mogul, Clay Shepard, Gareth Gale, and Amit Sahai. -## Version 2.46 - 5.Aug.2011 +## Version 2.46 – 5.Aug.2011 * Support multiple final-version uploads. @@ -846,7 +865,7 @@ HotCRP NEWS * Other bug fixes. -## Version 2.45 - 24.Apr.2011 +## Version 2.45 – 24.Apr.2011 * New, improved visual appearance for paper pages. @@ -873,7 +892,7 @@ HotCRP NEWS Stamatogiannakis, and Michael Hicks. -## Version 2.44 - 8.Feb.2011 +## Version 2.44 – 8.Feb.2011 * Correct recent bugs: improve Ajax return values (which lacked "b" characters due to a quoting mishap); do not ask authors for responses @@ -886,14 +905,14 @@ HotCRP NEWS * Thanks especially to Jeff Mogul and John Byers. -## Version 2.43 - 3.Jan.2011 +## Version 2.43 – 3.Jan.2011 * Correct 2.41 bug that could cause SQL errors on the home page when users had many comments to view. Double ouch! Apologies to Tony Del Porto and Usenix. -## Version 2.42 - 2.Jan.2011 +## Version 2.42 – 2.Jan.2011 * Correct 2.41 bug that broke `ovemer:3` searches (ouch). @@ -903,7 +922,7 @@ HotCRP NEWS * Style nits (paragraph breaks in abstracts, reviewer icon alignment). -## Version 2.41 - 13.Dec.2010 +## Version 2.41 – 13.Dec.2010 * The "Recent activity" on the home page includes information about submitted reviews as well as submitted comments (frequent request, @@ -957,7 +976,7 @@ HotCRP NEWS Andersen. -## Version 2.40 - 30.Jul.2010 +## Version 2.40 – 30.Jul.2010 * Search expression improvements: Allow parenthesized expressions, `AND` keywords, and `THEN` searches. `THEN` is the lowest precedence operator. @@ -980,7 +999,7 @@ HotCRP NEWS Long, and Dana Randall. -## Version 2.39 - 20.May.2010 +## Version 2.39 – 20.May.2010 * PC member tags. Each PC member can be associated with a list of tags, which use the same format as paper tags. This list is only set by @@ -998,7 +1017,7 @@ HotCRP NEWS * Thanks especially to Jeff Mogul and Ian Goldberg. -## Version 2.38 - 27.Jan.2010 +## Version 2.38 – 27.Jan.2010 * Add "Recent comments" section to the home page for PC members. This lists recent viewable comments, newest comments first. @@ -1018,12 +1037,12 @@ HotCRP NEWS Tony Del Porto, Jane-Ellen Long, and Casey Henderson. -## Version 2.37 - 19.Dec.2009 +## Version 2.37 – 19.Dec.2009 * Bug-fix release. -## Version 2.36 - 17.Dec.2009 +## Version 2.36 – 17.Dec.2009 * Formulas @@ -1067,7 +1086,7 @@ HotCRP NEWS and John P. John. -## Version 2.35 - 7.Oct.2009 +## Version 2.35 – 7.Oct.2009 * Paper options: Support numeric values, text values, and PDF uploads. @@ -1099,7 +1118,7 @@ HotCRP NEWS addition to bug reports and feature requests. -## Version 2.34 - 21.Mar.2009 +## Version 2.34 – 21.Mar.2009 * Tag colors! After a Dan Wallach suggestion. Tag a paper "red" and it shows up as red in paper lists. Or instruct the system that "reject" @@ -1124,12 +1143,12 @@ HotCRP NEWS * Thanks to Stefan Lorenz and John Wilkes. -## Version 2.33 - 15.Feb.2009 +## Version 2.33 – 15.Feb.2009 * Re-fix "Don't assign (X) and (Y) to the same paper." -## Version 2.32 - 15.Feb.2009 +## Version 2.32 – 15.Feb.2009 * Add `au:pc` search, which returns papers whose contact authors contain at least one PC member. @@ -1143,7 +1162,7 @@ HotCRP NEWS * Thanks to John Wilkes, Jeff Mogul, Stefan Lorenz, and Benjamin Pierce. -## Version 2.31 - 26.Jan.2009 +## Version 2.31 – 26.Jan.2009 * Administrators can delete users. @@ -1155,7 +1174,7 @@ HotCRP NEWS Slightly better support for browsers without Javascript. -## Version 2.30 - 7.Jan.2009 +## Version 2.30 – 7.Jan.2009 * Add chair-only tags: double-twiddle tags, like `~~tag`, are only visible to and changeable by chairs and administrators. Andrew Myers idea. @@ -1167,18 +1186,18 @@ HotCRP NEWS `$Opt["noPapers"]` (C. Craig Ross). -## Version 2.29 - 1.Jan.2009 +## Version 2.29 – 1.Jan.2009 * Bug fix release. Fixes bugs in tag search and tag setting, some reported by John Wilkes. -## Version 2.28 - 20.Dec.2008 +## Version 2.28 – 20.Dec.2008 * Allow periods in email addresses (Jeff Mogul). -## Version 2.27 - 16.Dec.2008 +## Version 2.27 – 16.Dec.2008 * Search results: Add tons of Display options, load them all by Ajax, and chairs gain a "Make these options the default" link. @@ -1200,7 +1219,7 @@ HotCRP NEWS Gebhart, Paolo Faraboschi, John Wilkes, Dina Papagiannaki, and others. -## Version 2.26 - 27.Oct.2008 +## Version 2.26 – 27.Oct.2008 * Submitters can be forced to define what type of conflict a PC member has. Requested by Dina Papagiannaki. @@ -1225,7 +1244,7 @@ HotCRP NEWS * Many help and usability improvements inspired by Benjamin Pierce requests. -## Version 2.25 - 22.Sep.2008 +## Version 2.25 – 22.Sep.2008 * Many bug fixes for new-style paper views. @@ -1260,7 +1279,7 @@ HotCRP NEWS * Thanks also to Benjamin Pierce, Richard Gass, Michael Vrable, and others. -## Version 2.24 - 22.Aug.2008 +## Version 2.24 – 22.Aug.2008 * Major changes @@ -1380,7 +1399,7 @@ HotCRP NEWS * Special thanks to Robbert van Renesse. -## Version 2.23 - 22.Jul.2008 +## Version 2.23 – 22.Jul.2008 * Do not infinite loop when sending mail to non-ASCII names associated with long email addresses. Reported by Robbert van Renesse and Rich Draves. @@ -1392,7 +1411,7 @@ HotCRP NEWS Jeonghee Shin. -## Version 2.22 - 15.Jul.2008 +## Version 2.22 – 15.Jul.2008 * Appearance fixes: use default controls in most cases. @@ -1410,12 +1429,12 @@ HotCRP NEWS * Improve some messages and help text. -## Version 2.21 - 11.May.2008 +## Version 2.21 – 11.May.2008 * Further improve validation and Internet Explorer 6 compatibility. -## Version 2.20 - 11.May.2008 +## Version 2.20 – 11.May.2008 * Improve Internet Explorer 6 compatibility. Reported by Terence Kelly. Includes Drew McLellan's supersleight for transparent PNG support @@ -1430,7 +1449,7 @@ HotCRP NEWS * Bug fixes to preference list, English, and createdb script. -## Version 2.19 - 6.May.2008 +## Version 2.19 – 6.May.2008 * Provide visible feedback on Ajax forms. @@ -1438,7 +1457,7 @@ HotCRP NEWS Isaacs). -## Version 2.18 - 5.May.2008 +## Version 2.18 – 5.May.2008 * Record PC feedback about whether reviews were helpful. PC members and, optionally, external reviewers can rate one another's reviews. Hopefully @@ -1465,7 +1484,7 @@ HotCRP NEWS reviews (Stefan Savage). -## Version 2.17 - 23.Apr.2008 +## Version 2.17 – 23.Apr.2008 * IMPORTANT: Continue reviewer identity leak fix via search rewrite. @@ -1481,7 +1500,7 @@ HotCRP NEWS than "0/1"). -## Version 2.16 - 21.Apr.2008 +## Version 2.16 – 21.Apr.2008 * IMPORTANT: Reviewer identity leak fix. @@ -1489,7 +1508,7 @@ HotCRP NEWS John Wilkes). -## Version 2.15 - 9.Apr.2008 +## Version 2.15 – 9.Apr.2008 * Improve homepage with a right-hand sidebar. @@ -1520,7 +1539,7 @@ HotCRP NEWS * Bug fix: Searching for `cre:>0`, etc. works. -## Version 2.14 - 12.Mar.2008 +## Version 2.14 – 12.Mar.2008 * Review field options can take lettered values, such as A-D or X-Z, as well as numeric values. @@ -1550,7 +1569,7 @@ HotCRP NEWS * Thanks to Michael Vrable, Stefan Savage, and Scott Rose. -## Version 2.13 - 22.Jan.2008 +## Version 2.13 – 22.Jan.2008 * Add support for paper format checking with Geoff Voelker's banal script. Thanks to Geoff for the script and debugging support, and to Harald @@ -1575,7 +1594,7 @@ HotCRP NEWS * Thanks also to Matthew Frank, Joseph Tucek, and Bernhard Ager. -## Version 2.12 - 30.Dec.2007 +## Version 2.12 – 30.Dec.2007 * Introduce "twiddle tags", such as `~tag`, which are visible only to the PC members that created them. Based on a request from Matthew Frank. @@ -1602,7 +1621,7 @@ HotCRP NEWS Birman, and Jon Crowcroft. -## Version 2.11 - 27.Oct.2007 +## Version 2.11 – 27.Oct.2007 * Mail tool allows sending mail to contact authors or reviewers for selected papers. @@ -1614,7 +1633,7 @@ HotCRP NEWS paper downloads when submissions are closed (bug report from V. Arun). -## Version 2.10 - 24.Oct.2007 +## Version 2.10 – 24.Oct.2007 * Add some support for MIME extensions; message bodies are marked UTF-8, and message headers containing UTF-8 characters are quoted according to @@ -1624,7 +1643,7 @@ HotCRP NEWS unrequested reviews, and other things. -## Version 2.9 - 20.Oct.2007 +## Version 2.9 – 20.Oct.2007 * Add a setting allowing PC members to see tags even for conflicted papers. @@ -1637,7 +1656,7 @@ HotCRP NEWS * Setting description improvements. -## Version 2.8 - 11.Oct.2007 +## Version 2.8 – 11.Oct.2007 * Bug fix: Do not reveal authors' identities via responses. @@ -1658,7 +1677,7 @@ HotCRP NEWS * Other behavior improvements. -## Version 2.7 - 23.Aug.2007 +## Version 2.7 – 23.Aug.2007 * Email notification for comments. Authors, reviewers, and PC members can request email notification when comments are added to a paper they are @@ -1683,7 +1702,7 @@ HotCRP NEWS bugs. -## Version 2.6 - 20.Aug.2007 +## Version 2.6 – 20.Aug.2007 * New way to collect author information. Author information is entered using separate text fields for Name, Email, and Affiliation. If a user's @@ -1698,12 +1717,12 @@ HotCRP NEWS * Style changes, especially on settings pages. -## Version 2.5 - 12.Aug.2007 +## Version 2.5 – 12.Aug.2007 * Optionally collect users' addresses and phone numbers. -## Version 2.4 - 12.Aug.2007 +## Version 2.4 – 12.Aug.2007 * Allow setting an info message that appears on the homepage. @@ -1715,7 +1734,7 @@ HotCRP NEWS * Style changes. -## Version 2.3 - 16.Jul.2007 +## Version 2.3 – 16.Jul.2007 * New action log display includes search. @@ -1724,19 +1743,19 @@ HotCRP NEWS * Other fixes. -## Version 2.2 - 11.Jul.2007 +## Version 2.2 – 11.Jul.2007 * Download a text file with reviewer names and emails (Frans). * Better offline reviewing. -## Version 2.1 - 10.Jul.2007 +## Version 2.1 – 10.Jul.2007 * IE compatibility. -## Version 2.0 - 9.Jul.2007 +## Version 2.0 – 9.Jul.2007 * New mail system. @@ -1752,7 +1771,7 @@ HotCRP NEWS * Thanks to Akos Ledeczi. -## Version 2.0b9 - 16.Jun.2007 +## Version 2.0b9 – 16.Jun.2007 * More Ajax. @@ -1778,7 +1797,7 @@ HotCRP NEWS * Thanks to Bernhard Ager, Frans Kaashoek, and Fernando Pereira. -## Version 2.0b8 - 11.Mar.2007 +## Version 2.0b8 – 11.Mar.2007 * Fix policy leak: Do not reveal reviewer identities if reviews are always anonymous! @@ -1788,7 +1807,7 @@ HotCRP NEWS * Thanks to Jeff Chase. -## Version 2.0b7 - 3.Mar.2007 +## Version 2.0b7 – 3.Mar.2007 * Fix policy leak: When sending email, include only information the recipient can see. @@ -1809,7 +1828,7 @@ HotCRP NEWS * Thanks to Bernhard Ager, Jeff Chase, Frans Kaashoek, and Andrew Myers. -## Version 2.0b6 - 1.Feb.2007 +## Version 2.0b6 – 1.Feb.2007 * Fix policy leak: PC members cannot see PC-only fields on review forms for their authored papers. @@ -1820,7 +1839,7 @@ HotCRP NEWS * Other fixes. -## Version 2.0b5 - 27.Jan.2007 +## Version 2.0b5 – 27.Jan.2007 * Improve tags and help. @@ -1831,7 +1850,7 @@ HotCRP NEWS * Other fixes. -## Version 2.0b4 - 13.Jan.2007 +## Version 2.0b4 – 13.Jan.2007 * Add automatic assignments. @@ -1848,7 +1867,7 @@ HotCRP NEWS * Other fixes. -## Version 2.0b3 - 10.Dec.2006 +## Version 2.0b3 – 10.Dec.2006 * Move to Conference Settings pages from deadline settings. @@ -1861,12 +1880,12 @@ HotCRP NEWS * Other fixes. -## Version 2.0b2 - 1.Dec.2006 +## Version 2.0b2 – 1.Dec.2006 * Internal updates. -## Version 2.0b1 - 28.Nov.2006 +## Version 2.0b1 – 28.Nov.2006 * Initial release. diff --git a/authorize.php b/authorize.php new file mode 100644 index 000000000..7d2aa7f90 --- /dev/null +++ b/authorize.php @@ -0,0 +1,5 @@ + $arg - * @param ?callable $attached */ - function __construct(Conf $conf, $arg, Getopt $getopt, $attached = null) { + * @param ?callable $detacher */ + function __construct(Conf $conf, $arg, Getopt $getopt, $detacher = null) { $this->conf = $conf; $this->getopt = $getopt; - $this->attached = $attached; + $this->detacher = $detacher; if (isset($arg["job"])) { - $this->_jtok = Job_Capability::claim($arg["job"], $this->conf, "batch/autoassign"); + $this->_jtok = Job_Capability::claim($this->conf, $arg["job"], "Autoassign"); $this->user = $this->_jtok->user() ?? $conf->root_user(); } else { $this->user = $conf->root_user(); @@ -74,7 +76,7 @@ function __construct(Conf $conf, $arg, Getopt $getopt, $attached = null) { try { $this->_jtok->update_use(); $this->parse_arg($arg); - $this->parse_arg($getopt->parse($this->_jtok->input("argv") ?? [])); + $this->parse_arg($getopt->parse($this->_jtok->input("assign_argv") ?? [])); $this->complete_arg(); } catch (CommandLineException $ex) { $this->report([MessageItem::error("<0>{$ex->getMessage()}")], $ex->exitStatus); @@ -98,7 +100,6 @@ private function report($message_list, $exit_status = null) { $this->_jtok->change_data("exit_status", $exit_status) ->change_data("status", "done"); } - Conf::set_current_time(microtime(true)); $this->_jtok->update_use()->update(); } else { $s = MessageSet::feedback_text($message_list); @@ -123,7 +124,8 @@ private function reportx($message_list, $exit_status = null) { /** @param associative-array $arg */ private function parse_arg($arg) { $this->quiet = $this->quiet || isset($arg["quiet"]); - $this->dry_run = $this->dry_run || isset($arg["dry-run"]); + $this->unsorted_dry_run = $this->unsorted_dry_run || isset($arg["unsorted-dry-run"]); + $this->dry_run = $this->dry_run || $this->unsorted_dry_run || isset($arg["dry-run"]); $this->help_param = $this->help_param || isset($arg["help-param"]); $this->profile = $this->profile || isset($arg["profile"]); if (isset($arg["autoassigner"])) { @@ -134,9 +136,6 @@ private function parse_arg($arg) { if (isset($arg["count"])) { $this->param["count"] = $arg["count"]; } - if (isset($arg["type"])) { - $this->param["type"] = $arg["type"]; - } foreach ($arg["_"] ?? [] as $x) { if (($eq = strpos($x, "=")) === false) { $this->report([MessageItem::error("<0>`NAME=VALUE` format expected for parameter arguments")], 3); @@ -146,6 +145,8 @@ private function parse_arg($arg) { $this->q = $arg["q"] ?? $this->q; if (isset($arg["all"])) { $this->t = "all"; + } else { + $this->t = $arg["type"] ?? "s"; } $pcc = $this->pcc; if (!empty($arg["u"])) { @@ -191,7 +192,10 @@ private function complete_arg() { fwrite(STDOUT, $this->getopt->help()); throw new CommandLineException("", $this->getopt, 0); } - $gj = $this->aaname !== "" ? $this->conf->autoassigner($this->aaname) : null; + $gj = null; + if ($this->aaname !== "" && !str_starts_with($this->aaname, "__")) { + $gj = $this->conf->autoassigner($this->aaname); + } if (!$gj) { $ml = []; if ($this->aaname === "") { @@ -205,16 +209,17 @@ private function complete_arg() { $this->report([MessageItem::error("<0>Invalid autoassigner `{$this->aaname}`")], 3); } $this->gj = $gj; - $parameters = $this->gj->parameters ?? []; - if (isset($this->param["type"]) - && !in_array("type", $parameters) - && in_array("rtype", $parameters)) { - $this->param["rtype"] = $this->param["type"]; + if (isset($this->param["type"])) { + $parameters = Autoassigner::expand_parameters($this->conf, $gj->parameters ?? []); + if (!Autoassigner::find_parameter("type", $parameters) + && Autoassigner::find_parameter("rtype", $parameters)) { + $this->param["rtype"] = $this->param["type"]; + } } } function report_progress($progress) { - $this->_jtok->change_data("progress", $progress)->update(); + $this->_jtok->change_data("progress", $progress)->update_use()->update(); set_time_limit(240); } @@ -241,15 +246,16 @@ function execute() { } else { $aa = call_user_func($this->gj->function, $this->user, $this->pcc, $pids, $this->param, $this->gj); } + '@phan-var-force Autoassigner $aa'; foreach ($this->no_coassign as $pair) { $aa->avoid_coassignment($pair[0], $pair[1]); } $this->report($aa->message_list(), $aa->has_error() ? 1 : null); // run autoassigner - if ($this->attached) { - call_user_func($this->attached, $this); - $this->attached = null; + if ($this->detacher) { + call_user_func($this->detacher, $this); + $this->detacher = null; } if ($this->_jtok) { $aa->add_progress_function([$this, "report_progress"]); @@ -277,6 +283,9 @@ function execute() { // exit if dry run if ($this->dry_run) { + if (!$this->unsorted_dry_run) { + $aa->sort_assignments(); + } if ($this->_jtok) { $this->_jtok->change_output(join("", $aa->assignments())); $this->report([], 0); @@ -316,11 +325,9 @@ function execute() { function run() { if ($this->help_param) { $s = ["{$this->gj->name} parameters:\n"]; - foreach ($this->gj->parameters ?? [] as $p) { - if (($px = Autoassigner::expand_parameter_help($p))) { - $arg = " {$px->name}={$px->argname}" . ($px->required ? " *" : ""); - $s[] = Getopt::format_help_line($arg, $px->description); - } + foreach (Autoassigner::expand_parameters($this->conf, $this->gj->parameters ?? []) as $px) { + $arg = " {$px->name}={$px->argname}" . ($px->required ? " *" : ""); + $s[] = Getopt::format_help_line($arg, $px->description); } $s[] = "\n"; fwrite(STDOUT, join("", $s)); @@ -342,13 +349,14 @@ static function make_getopt() { "config: !", "job:,j: JOBID Run stored job", "dry-run,d Do not perform assignment; output CSV instead", + "unsorted-dry-run,D !", "autoassigner:,a: =AA !", "q:,search: =QUERY Use papers matching QUERY [all]", + "type:,t: =TYPE Set search type [all]", "all Include all papers (default is submitted papers)", "u[],user[] =USER Include users matching USER (`-u -USER` excludes)", "disjoint[],X[] =USER1,USER2 Don’t coassign users", "count:,c: {n} =N Set `count` parameter to N", - "type:,t: =TYPE Set `type`/`rtype` parameter to TYPE", "help-param Print parameters for AUTOASSIGNER", "profile Print profile to standard error", "quiet Don’t warn on empty assignment", @@ -360,11 +368,13 @@ static function make_getopt() { ->interleave(true); } - /** @return Autoassign_Batch */ - static function make_args($argv) { + /** @param list $argv + * @param ?callable $detacher + * @return Autoassign_Batch */ + static function make_args($argv, $detacher = null) { $getopt = self::make_getopt(); $arg = $getopt->parse($argv); $conf = initialize_conf($arg["config"] ?? null, $arg["name"] ?? null); - return new Autoassign_Batch($conf, $arg, $getopt); + return new Autoassign_Batch($conf, $arg, $getopt, $detacher); } } diff --git a/batch/cleandocstore.php b/batch/cleandocstore.php index 21b42acd9..4138c927f 100644 --- a/batch/cleandocstore.php +++ b/batch/cleandocstore.php @@ -1,6 +1,6 @@ need_password) { $this->password = $arg["password"] ?? null; } + if (isset($arg["password-file"])) { + if (($t = @file_get_contents($arg["password-file"])) === false) { + throw new CommandLineException("cannot read `--password-file`"); + } + if (preg_match('/^user\s+(\S+)/m', $t, $m)) { + $this->user = $m[1]; + } + if (preg_match('/^password\s+(.*)$/m', $t, $m)) { + $this->password = trim($m[1]); + $this->need_password = false; + } + } $this->name = $arg["name"] ?? null; $this->write_config = !isset($arg["no-config"]); @@ -98,6 +112,7 @@ function __construct($arg, $interactive) { $this->batch = !$interactive || isset($arg["batch"]); $this->replace = isset($arg["replace"]); + $this->replace_user = isset($arg["replace-user"]); $this->quiet = isset($arg["quiet"]); $this->verbose = isset($arg["verbose"]); $this->interactive = $interactive; @@ -373,7 +388,7 @@ function dbuser_exists() { function check_dbpass() { if ($this->dbpass === null - && $this->dbuser === ($this->configopt["dbUser"] ?? null)) { + && $this->dbuser === ($this->configopt["dbUser"] ?? $this->configopt["dbName"] ?? null)) { $this->dbpass = $this->configopt["dbPassword"] ?? null; } } @@ -401,12 +416,15 @@ function check_replace() { fwrite(STDERR, "* Database {$this->name} already exists!\n"); $replacing[] = "database"; } + if ($this->had_dbuser && $this->replace_user) { + $replacing[] = "user"; + } if ($this->replace) { return; } else if ($this->batch) { throw new CommandLineException("`--replace` required in batch mode"); } else { - $this->replace = $this->confirm("Recreate " . join(" and ", $replacing) . "? [Y/n] "); + $this->replace = $this->confirm("Delete and replace " . join(" and ", $replacing) . "? [Y/n] "); } } @@ -435,6 +453,14 @@ function create_database() { function create_dbuser() { $want_hosts = $this->all_hosts; + if ($this->had_dbuser && $this->replace_user) { + $result = $this->qe("SELECT Host FROM user WHERE User=? AND Host?a", $this->dbuser, $want_hosts); + while (($row = $result->fetch_row())) { + $this->vqe("DROP USER ?@?", $this->dbuser, $row[0]); + } + $result->close(); + $this->vqe("FLUSH PRIVILEGES"); + } if ($this->had_dbuser) { $result = $this->qe("SELECT Host FROM user WHERE User=?", $this->dbuser); while (($row = $result->fetch_row())) { @@ -442,6 +468,7 @@ function create_dbuser() { array_splice($want_hosts, $i, 1); } } + $result->close(); } if (empty($want_hosts)) { return; @@ -454,7 +481,7 @@ function create_dbuser() { if ($this->verbose) { fwrite(STDOUT, Dbl::format_query($this->dblink(), "- CREATE USER ?@? IDENTIFIED BY ;\n", $this->dbuser, $h)); } - $this->vqe("CREATE USER ?@? IDENTIFIED BY ?", $this->dbuser, $h, $this->dbpass); + $this->qe("CREATE USER ?@? IDENTIFIED BY ?", $this->dbuser, $h, $this->dbpass); } $this->vqe("FLUSH PRIVILEGES"); } @@ -596,13 +623,15 @@ function run() { * @return CreateDB_Batch */ static function make_args($argv, $interactive) { $arg = (new Getopt)->long( - "user:,u: =USER Set username for database admin connection", - "password::,p:: =PASSWORD Set password for database admin connection", + "user:,u: =USER Set username for database admin", + "password::,p:: =PASSWORD Set password for database admin", + "password-file: =FILE Read database admin information from FILE", "name:,n: =DBNAME Set name of HotCRP database", "config:,c: =CONFIG Set configuration file [conf/options.php]", "minimal Output minimal configuration file", "batch Batch installation: never stop for input", "replace Replace existing HotCRP database if present", + "replace-user Replace existing HotCRP database user if present", "no-grant Do not create user or grant privileges for HotCRP database access", "dbuser: =USER,PASS Specify database USER and PASS for HotCRP database access", "host: =HOST Specify database host [localhost]", diff --git a/batch/makedist.sh b/batch/makedist.sh index 59b2f3b99..f76ea5d6f 100755 --- a/batch/makedist.sh +++ b/batch/makedist.sh @@ -1,4 +1,4 @@ -export VERSION=3.0b3 +export VERSION=3.0.0 # check that schema.sql and updateschema.php agree on schema version updatenum=`grep 'settings.*allowPaperOption.*=\|update_schema_version' src/updateschema.php | tail -n 1 | sed 's/.*= *//;s/.*[(] *//;s/[;)].*//'` @@ -48,6 +48,7 @@ NEWS.md README.md api.php assign.php +authorize.php autoassign.php bulkassign.php buzzer.php @@ -154,6 +155,7 @@ lib/hashanalysis.php lib/hclcolor.php lib/ht.php lib/icons.php +lib/isovideomimetype.php lib/json.php lib/jsonexception.php lib/jsonparser.php @@ -197,6 +199,7 @@ src/api/api_events.php src/api/api_follow.php src/api/api_formatcheck.php src/api/api_graphdata.php +src/api/api_job.php src/api/api_mail.php src/api/api_paper.php src/api/api_paperpc.php @@ -239,6 +242,7 @@ src/backuppattern.php src/banal src/capabilities/cap_authorview.php src/capabilities/cap_bearer.php +src/capabilities/cap_job.php src/capabilities/cap_reviewaccept.php src/checkformat.php src/commentinfo.php @@ -286,6 +290,7 @@ src/formulas/f_topic.php src/formulas/f_topicscore.php src/help/h_bulkassign.php src/help/h_chairsguide.php +src/help/h_developer.php src/help/h_formulas.php src/help/h_jsonsettings.php src/help/h_keywords.php @@ -347,6 +352,7 @@ src/options/o_topics.php src/pages/p_adminhome.php src/pages/p_api.php src/pages/p_assign.php +src/pages/p_authorize.php src/pages/p_autoassign.php src/pages/p_bulkassign.php src/pages/p_buzzer.php @@ -470,6 +476,7 @@ src/searchoperator.php src/searchselection.php src/searchsplitter.php src/searchterm.php +src/searchviewcommand.php src/searchword.php src/sessionlist.php src/settinginfoset.php @@ -484,6 +491,7 @@ src/settings/s_json.php src/settings/s_messages.php src/settings/s_namedsearch.php src/settings/s_options.php +src/settings/s_preference.php src/settings/s_response.php src/settings/s_review.php src/settings/s_reviewfieldcondition.php @@ -568,12 +576,12 @@ images/view48.png images/viewas.png scripts/.htaccess -scripts/d3-hotcrp.min.js scripts/buzzer.js +scripts/d3-hotcrp.min.js scripts/emojicodes.json scripts/graph.js scripts/jquery-1.12.4.min.js -scripts/jquery-3.6.4.min.js +scripts/jquery-3.7.1.min.js scripts/script.js scripts/settings.js diff --git a/batch/s3test.php b/batch/s3test.php index 80f36c540..67a1eef45 100644 --- a/batch/s3test.php +++ b/batch/s3test.php @@ -1,6 +1,6 @@ silent) { + if (!$this->silent || !$pid) { foreach ($this->ps->decorated_message_list() as $mi) { fwrite(STDERR, $prefix . $mi->message_as(0) . "\n"); } @@ -330,7 +330,7 @@ private function _run_main(&$jl) { $j = $jl[$index]; $jl[$index] = null; $this->run_one($index, $j); - if ($this->nerrors && !$this->ignore_errors) { + if ($this->nerrors > 0 && !$this->ignore_errors) { break; } gc_collect_cycles(); diff --git a/batch/settings.php b/batch/settings.php index 898042d67..a3684e4f0 100644 --- a/batch/settings.php +++ b/batch/settings.php @@ -1,6 +1,6 @@ New {sclass} {submission}", "<0>New {submission}", ["{sclass}="]], ["paper_edit", "<0>new {sclass} {submission}", "<0>new {submission}", ["{sclass}="]], ["paper_edit", "<0>Edit {sclass} {submission}", "<0>Edit {submission}", ["{sclass}="]], - ["paper_edit", "<0>Incomplete {submissions} will not be considered.", "<0>{Submissions} incomplete as of {deadline:time} will not be considered.", ["{deadline}>0"]], - ["paper_edit", "<5>{Submissions} not marked ready for review by {:expandedtime} will not be evaluated.", "<5>{Submissions} not marked ready for review by then will not be evaluated.", ["{0}={1}"]], - ["paper_edit", "<5>Please check {:list} before completing the {submission}.", "<5>Please check the issues highlighted below before completing the {submission}.", ["!#{0}"]], + ["paper_edit", "<0>Edit {sclass} {submission}", "<0>Edit draft {sclass} {submission}", ["{draft}"]], + ["paper_edit", "<0>Edit {sclass} {submission}", "<0>Edit draft {submission}", ["{sclass}=", "{draft}"]], + ["paper_edit", "<0>Incomplete {submissions} will not be evaluated.", "<0>{Submissions} incomplete as of {deadline:time} will not be evaluated.", ["{deadline}>0"]], + ["paper_edit", "<5>{Submissions} marked ready for review as of {:expandedtime} will be evaluated.", "<5>Only {submissions} marked ready for review will be evaluated.", ["{0}={1}"]], + ["paper_edit", "<5>Please check {:list} for potential issues.", "<5>Please check the potential issues highlighted below.", ["!#{0}"]], ["<5>This {submission} is not ready for review. Required fields {:list} are missing.", "<5>This {submission} is not ready for review. Required field {:list} is missing.", ["#{0}=1"]], { diff --git a/etc/pages.json b/etc/pages.json index e187aeba8..860a5ca6c 100644 --- a/etc/pages.json +++ b/etc/pages.json @@ -147,12 +147,14 @@ [ "signin/head", 1000, "Signin_Page::print_signin_head" ], [ "signin/body", 3000, "Signin_Page::print_signin_form" ], [ "signin/form/title", 1, "Signin_Page::print_signin_form_title" ], - [ "signin/form/description", 10, "Signin_Page::print_signin_form_description" ], - [ "signin/form/email", 20, "*Signin_Page::print_signin_form_email" ], - [ "signin/form/password", 30, "*Signin_Page::print_signin_form_password" ], - [ "signin/form/actions", 100, "Signin_Page::print_signin_form_actions" ], - [ "signin/form/create", 150, "Signin_Page::print_signin_form_create" ], - [ "signin/form/oauth", 1000, "Signin_Page::print_signin_form_oauth" ], + [ "signin/form/accounts", 5, "Signin_Page::print_signin_form_accounts" ], + [ "signin/form/local", 20, "Signin_Page::print_signin_form_local" ], + [ "signin/form/oauth", 1000, "*Signin_Page::print_signin_form_oauth" ], + + [ "__local_signin/email", 20, "*Signin_Page::print_signin_form_email" ], + [ "__local_signin/password", 30, "*Signin_Page::print_signin_form_password" ], + [ "__local_signin/actions", 100, "Signin_Page::print_signin_form_actions" ], + [ "__local_signin/create", 150, "Signin_Page::print_signin_form_create" ], { "name": "signout", "allow_disabled": true }, @@ -213,12 +215,10 @@ { "name": "authorize", "print_function": "*Authorize_Page::go", "allow_disabled": true }, [ "authorize/form/title", 1, "*Authorize_Page::print_form_title" ], - [ "authorize/form/description", 10, "*Authorize_Page::print_form_description" ], - [ "authorize/form/active", 15, "*Authorize_Page::print_form_active" ], - [ "authorize/form/email", 20, "signin/form/email" ], - [ "authorize/form/password", 30, "signin/form/password" ], - [ "authorize/form/actions", 100, "*Authorize_Page::print_form_actions" ], - [ "authorize/form/oauth", 1000, "Signin_Page::print_signin_form_oauth" ], + [ "authorize/form/main", 15, "*Authorize_Page::print_form_main" ], + [ "authorize/form/annotation", 2000, "*Authorize_Page::print_form_annotation" ], + [ "authorize/other/local", 15, "signin/form/local" ], + [ "authorize/other/oauth", 1000, "signin/form/oauth" ], { "name": "api", "print_function": "API_Page::go", "allow_disabled": true }, diff --git a/etc/settinginfo.json b/etc/settinginfo.json index e86954077..3060b735c 100644 --- a/etc/settinginfo.json +++ b/etc/settinginfo.json @@ -134,6 +134,18 @@ "type": "htmlstring", "size": 20, "default_value": "auto", "parser_class": "Messages_SettingParser" }, + { + "name": "preference_min", "storage": "pref_min", + "title": "Minimum allowed review preference", + "type": "int", "default_value": -1000000, + "parser_class": "Preference_SettingParser" + }, + { + "name": "preference_max", "storage": "pref_max", + "title": "Maximum allowed review preference", + "type": "int", "default_value": 1000000, + "parser_class": "Preference_SettingParser" + }, { "name": "submission_edit_message", "storage": "msg.submit", "title": "Submission edit message", @@ -1142,16 +1154,29 @@ }, { "name_pattern": "named_search/$/title", "internal": true, "parser_class": "NamedSearch_SettingParser" }, { - "name_pattern": "named_search/$/q", + "name_pattern": "named_search/$/search", "title": "/Search", - "type": "string", "subtype": "search" + "type": "string", "subtype": "search", "storage": "member.q" }, + { "name_pattern": "named_search/$/q", "alias_pattern": "named_search/$/search" }, { "name_pattern": "named_search/$/display", "title": "/Display", "type": "radio", "values": ["default", "none", "highlight"], "default_value": "default" }, + { + "name": "autoassign_review_max_load_tag", + "title": "Autoassignment maximum PC review load tag", + "type": "tag", "subtype": "allow_chair", "required": false + }, + { + "name": "autoassign_review_preference_gadget", + "title": "Review autoassignment method", + "type": "radio", "values": ["default", "expertise"], "default_value": "default", + "storage": "opt.autoassignReviewGadget" + }, + { "name": "extrev_hard_0", "merge": true, "storage": "extrev_hard" }, { "name": "extrev_soft_0", "merge": true, "storage": "extrev_soft" }, { "name": "pcrev_hard_0", "merge": true, "storage": "pcrev_hard" }, diff --git a/index.php b/index.php index 25dc36791..938e4e021 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,6 @@ page === "api") { @@ -48,17 +49,23 @@ function handle_request($nav) { } catch (Redirection $redir) { Conf::$main->redirect($redir->url); } catch (JsonCompletion $jc) { - $jc->result->emit(); + $jc->result->emit($qreq); } catch (PageCompletion $unused) { } } $nav = Navigation::get(); +// handle OPTIONS requests, including CORS preflight +if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") { + include("src/pages/p_api.php"); + API_Page::go_options($nav); +} + // handle `/u/USERINDEX/` if ($nav->page === "u") { $unum = $nav->path_component(0); - if ($unum !== false && ctype_digit($unum)) { + if ($unum !== null && ctype_digit($unum)) { if (!$nav->shift_path_components(2)) { // redirect `/u/USERINDEX` => `/u/USERINDEX/` Navigation::redirect_absolute("{$nav->server}{$nav->base_path}u/{$unum}/{$nav->query}"); diff --git a/lib/base.php b/lib/base.php index 8bccb7d79..1e9c2e3e6 100644 --- a/lib/base.php +++ b/lib/base.php @@ -1,6 +1,6 @@ */ - private $goodtags; - /** @var array */ - private $emptytags; - /** @var ?string */ - public $last_error; + /** @var ?array */ + private $tags; + /** @var list */ + private $ml = []; /** @var CleanHTML */ static private $main; - /** @param int $flags - * @param ?list $goodtags - * @param ?list $emptytags */ - function __construct($flags = 0, $goodtags = null, $emptytags = null) { - if ($goodtags === null) { - $goodtags = ["a", "abbr", "acronym", "address", "area", "b", "bdi", "bdo", "big", "blockquote", "br", "button", "caption", "center", "cite", "code", "col", "colgroup", "dd", "del", "details", "dir", "div", "dfn", "dl", "dt", "em", "figcaption", "figure", "font", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd", "label", "legend", "li", "link", "map", "mark", "menu", "menuitem", "meter", "noscript", "ol", "optgroup", "option", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "span", "strike", "strong", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "tt", "u", "ul", "var", "wbr"]; + const F_BLOCK = 1; + const F_VOID = 2; + const F_NOTEXT = 4; + const F_DISABLED = 8; + const FSS = 8; + const FSP = 16; // must be > FSS + const FSP2 = 24; + const FTM = 0xFF; + + const FT_COLGROUP = 1; + const FT_DL = 2; + const FT_DETAILS = 3; + const FT_FIELDSET = 4; + const FT_FIGURE = 5; + const FT_MEDIA = 6; + const FT_LIST = 7; + const FT_RUBY = 8; + const FT_TABLE = 9; + const FT_TROWS = 10; + const FT_TR = 11; + + /** @var array */ + static private $taginfo = [ + "a" => 0, + "abbr" => 0, + "acronym" => 0, + "address" => 0, + // area + // article + // aside + "audio" => self::F_BLOCK | (self::FT_MEDIA << self::FSS), + "b" => 0, + // base + "bdi" => 0, + "bdo" => 0, + "big" => 0, + "blockquote" => self::F_BLOCK, + // body + "br" => self::F_VOID, + // button + // canvas + "caption" => self::F_BLOCK | (self::FT_TABLE << self::FSP), + "center" => self::F_BLOCK, + "cite" => 0, + "code" => 0, + "col" => self::F_VOID | (self::FT_COLGROUP << self::FSP), + "colgroup" => (self::FT_COLGROUP << self::FSS) | (self::FT_TABLE << self::FSP), + // data + // datalist + "dd" => self::F_BLOCK | (self::FT_DL << self::FSP), + "del" => 0, + "details" => self::F_BLOCK | (self::FT_DETAILS << self::FSS), + "dfn" => 0, + // dialog + "div" => self::F_BLOCK, + "dl" => self::F_BLOCK | self::F_NOTEXT | (self::FT_DL << self::FSS), + "dt" => self::F_BLOCK | (self::FT_DL << self::FSP), + "em" => 0, + // embed + "fieldset" => self::F_BLOCK | (self::FT_FIELDSET << self::FSS), + "figcaption" => self::F_BLOCK | (self::FT_FIGURE << self::FSP), + "figure" => self::F_BLOCK | (self::FT_FIGURE << self::FSS), + // font + // footer + // form + // frame + // frameset + "h1" => self::F_BLOCK, + "h2" => self::F_BLOCK, + "h3" => self::F_BLOCK, + "h4" => self::F_BLOCK, + "h5" => self::F_BLOCK, + "h6" => self::F_BLOCK, + // head + // header + // hgroup + "hr" => self::F_BLOCK | self::F_VOID, + // html + "i" => 0, + // iframe + // image + "img" => self::F_VOID, + // input + "ins" => 0, + "kbd" => 0, + "label" => 0, + "legend" => self::F_BLOCK | (self::FT_FIELDSET << self::FSP), + "li" => self::F_BLOCK | (self::FT_LIST << self::FSP), + // link + // main + // map + "mark" => 0, + // marquee + "menu" => self::F_BLOCK | (self::FT_LIST << self::FSS), + // menuitem + // meta + "meter" => 0, + // nav + // nobr + // noembed + // noframes + "noscript" => 0, + // object + "ol" => self::F_BLOCK | self::F_NOTEXT | (self::FT_LIST << self::FSS), + // optgroup + // option + "p" => self::F_BLOCK, + // param + "picture" => self::F_BLOCK | (self::FT_MEDIA << self::FSS), + // plaintext + "pre" => self::F_BLOCK, + "progress" => 0, + "q" => 0, + // rb + "rp" => self::FT_RUBY << self::FSP, + "rt" => self::FT_RUBY << self::FSP, + // rtc + "ruby" => self::FT_RUBY << self::FSS, + "s" => 0, + "samp" => 0, + // script + // search + // select + // slot + "small" => 0, + "source" => self::F_VOID | (self::FT_MEDIA << self::FSP), + "span" => 0, + "strike" => 0, + "strong" => 0, + // style + "sub" => 0, + "summary" => self::F_BLOCK | (self::FT_DETAILS << self::FSP), + "sup" => 0, + "table" => self::F_BLOCK | self::F_NOTEXT | (self::FT_TABLE << self::FSS), + "tbody" => self::F_BLOCK | self::F_NOTEXT | (self::FT_TROWS << self::FSS) | (self::FT_TABLE << self::FSP), + "td" => self::F_BLOCK | (self::FT_TR << self::FSP), + // template + // textarea + "tfoot" => self::F_BLOCK | self::F_NOTEXT | (self::FT_TROWS << self::FSS) | (self::FT_TABLE << self::FSP), + "th" => self::F_BLOCK | (self::FT_TR << self::FSP), + "thead" => self::F_BLOCK | self::F_NOTEXT | (self::FT_TROWS << self::FSS) | (self::FT_TABLE << self::FSP), + "time" => 0, + // title + "tr" => self::F_BLOCK | self::F_NOTEXT | (self::FT_TR << self::FSS) | (self::FT_TROWS << self::FSP) | (self::FT_TABLE << self::FSP2), + "track" => self::F_BLOCK | (self::FT_MEDIA << self::FSP), + "tt" => 0, + "u" => 0, + "ul" => self::F_BLOCK | self::F_NOTEXT | (self::FT_LIST << self::FSS), + "var" => 0, + "video" => self::F_BLOCK | (self::FT_MEDIA << self::FSS), + "wbr" => self::F_VOID + // xmp + ]; + + /** @param int $flags */ + function __construct($flags = 0) { + $this->flags = $flags; + $this->tags = self::$taginfo; + } + + /** @return $this */ + function disable_all() { + foreach ($this->tags as &$tf) { + $tf |= self::F_DISABLED; + } + return $this; + } + + /** @param string ...$tags + * @return $this */ + function enable(...$tags) { + foreach ($tags as $tag) { + $this->tags[$tag] = ($this->tags[$tag] ?? 0) & ~self::F_DISABLED; + } + return $this; + } + + /** @param string $tag + * @param int $flag + * @return $this */ + function define($tag, $flag) { + $this->tags[$tag] = $flag; + return $this; + } + + /** @return MessageItem */ + private function e($str, $pos1, $pos2, $context) { + $mi = MessageItem::error($str); + $mi->pos1 = $pos1; + $mi->pos2 = $pos2; + $mi->context = $context; + // if (strpos($context, "\n") !== false) { + // $mi->landmark = "line " . (preg_match_all('/\r\n?|\n/', substr($context, 0, $pos1)) + 1); + // } + return $mi; + } + + private function inclusion_context($tag, $tagtf) { + $tp1 = ($tagtf >> self::FSP) & self::FTM; + $tp2 = ($tagtf >> self::FSP2) & self::FTM; + $tlist = []; + foreach ($this->tags as $n => $tf) { + if (($t = ($tf >> self::FSS) & self::FTM) !== 0 + && ($t === $tp1 || $t === $tp2)) { + $tlist[] = "<{$n}>"; + } } - if ($emptytags === null) { - $emptytags = ["area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "wbr"]; + sort($tlist); + return MessageItem::inform("<0>The <{$tag}> tag can only appear inside " . commajoin($tlist) . "."); + } + + private function here($tagstack) { + $nts = count($tagstack); + if ($nts === 0) { + return "here"; + } else { + return "inside <" . $tagstack[$nts - 4] . ">"; } - $this->flags = 0; - $this->goodtags = is_associative_array($goodtags) ? $goodtags : array_flip($goodtags); - $this->emptytags = is_associative_array($emptytags) ? $emptytags : array_flip($emptytags); } - private function _cleanHTMLError($etype) { - $this->last_error = "Your HTML code contains $etype. Only HTML content tags are accepted, such as <p>, <strong>, and <h1>, and attributes are restricted."; - return false; + private function check_text($curtf, $tagstack, $pos1, $pos2, $t) { + if (($curtf & self::F_NOTEXT) !== 0 + && $pos1 !== $pos2 + && !ctype_space(substr($t, $pos1, $pos2 - $pos1))) { + $this->ml[] = $this->e("<0>Text not allowed " . $this->here($tagstack), $pos1, $pos2, $t); + } } /** @param string $t * @return string|false */ function clean($t) { $tagstack = []; - $this->last_error = null; + '@phan-var-force array $tagstack'; + if (($this->flags & self::CLEAN_INLINE) !== 0) { + $curtf = 0; + } else { + $curtf = self::F_BLOCK; + } + $this->ml = []; + $xp = $p = 0; + $len = strlen($t); $x = ""; - while ($t !== "") { - if (($p = strpos($t, "<")) === false) { - $x .= $t; - break; + while ($p !== $len && ($nextp = strpos($t, "<", $p)) !== false) { + if (($curtf & self::F_NOTEXT) !== 0) { + $this->check_text($curtf, $tagstack, $p, $nextp, $t); } - $x .= substr($t, 0, $p); - $t = substr($t, $p); - - if (preg_match('/\A_cleanHTMLError("an Internet Explorer conditional comment"); - } else if (preg_match('/\A(|\z)(.*)\z/s', $t, $m)) { - $x .= $m[1] . "]]>"; - $t = $m[3]; - } else if (preg_match('/\A|\z)(.*)\z/s', $t, $m)) { - $t = $m[2]; - } else if (preg_match('/\A_cleanHTMLError("$m[1] declarations"); - } else if (preg_match('/\A<\s*([A-Za-z0-9]+)\s*(.*)\z/s', $t, $m)) { - $tag = strtolower($m[1]); - if (!isset($this->goodtags[$tag])) { - if (!($this->flags & self::BADTAGS_IGNORE)) { - return $this->_cleanHTMLError("an unacceptable <$tag> tag"); + $p = $nextp; + if (preg_match('/\Gml[] = $this->e("<0>Conditional HTML comments not allowed", $p, $p + strlen($m[0]), $t); + return false; + } else if (preg_match('/\G(|\z)/s', $t, $m, 0, $p)) { + $this->check_text($curtf, $tagstack, $p, $p + strlen($m[0]), $t); + if ($m[2] === "") { + $x .= substr($t, $xp) . "]]>"; + $p = $xp = $len; + } else { + $p += strlen($m[0]); + } + } else if (preg_match('/\G|\z)\z/s', $t, $m, 0, $p)) { + $x .= substr($t, $xp, $p - $xp); + $p = $xp = $p + strlen($m[0]); + } else if (preg_match('/\Gml[] = $this->e("<0>HTML and XML declarations not allowed", $p, $p + strlen($m[0]), $t); + return false; + } else if (preg_match('/\G<(\s*+)([A-Za-z][-A-Za-z0-9]*+)(?=[\s\/>])(\s*+)(?:[^<>\'"]+|\'[^\']*\'|"[^"]*")*+>?/s', $t, $m, 0, $p)) { + $tag = strtolower($m[2]); + $tagp = $p; + $endp = $p + strlen($m[0]); + $tagtf = $this->tags[$tag] ?? self::F_DISABLED; + $x .= substr($t, $xp, $tagp - $xp); + if (($tagtf & self::F_DISABLED) !== 0) { + if (($this->flags & self::CLEAN_STRIP_UNKNOWN) !== 0) { + $p = $xp = $endp; + continue; + } else if (($this->flags & self::CLEAN_IGNORE_UNKNOWN) !== 0) { + $this->check_text($curtf, $tagstack, $tagp, $tagp + 1, $t); + $x .= "<"; + $p = $xp = $tagp + 1; + continue; } - $x .= "<"; - $t = substr($t, 1); - continue; + $this->ml[] = $this->e("<0>HTML tag <{$m[2]}> not allowed", $tagp, $endp, $t); + $tagtf = self::F_VOID; + } + $x .= "<{$tag}"; + if (($tagtf & self::F_BLOCK) !== 0 + && ($curtf & self::F_BLOCK) === 0) { + $this->ml[] = $this->e("<0>Block-level element <{$m[2]}> not allowed " . $this->here($tagstack), $tagp, $endp, $t); } - $t = $m[2]; - $x .= "<" . $tag; + if ($tagtf >= (1 << self::FSP)) { + $pt1 = ($tagtf >> self::FSP) & self::FTM; + $pt2 = ($tagtf >> self::FSP2) & self::FTM; + $curt = ($curtf >> self::FSS) & self::FTM; + if ($curt === 0 + || ($pt1 !== $curt && $pt2 !== $curt)) { + $this->ml[] = $this->e("<0>Element not allowed here", $tagp, $endp, $t); + $this->ml[] = $this->inclusion_context($tag, $tagtf); + } + } + $p = $tagp + 1 + strlen($m[1]) + strlen($m[2]) + strlen($m[3]); // XXX should sanitize 'id', 'class', 'data-', etc. - while ($t !== "" && $t[0] !== "/" && $t[0] !== ">") { - if (!preg_match(',\A([^\s/<>=\'"]+)\s*(.*)\z,s', $t, $m)) { - return $this->_cleanHTMLError("garbage " . htmlspecialchars($t) . " within some <$tag> tag"); + while ($p !== $len && $t[$p] !== "/" && $t[$p] !== ">") { + if (!preg_match('/\G([^\s\/<>=\'"]++)\s*+/s', $t, $m, 0, $p)) { + $this->ml[] = $this->e("<0>Invalid character in HTML tag attributes", $p, $p, $t); + $p = $endp; + break; } + $ap = $p; $attr = strtolower($m[1]); - if (strlen($attr) > 2 && $attr[0] === "o" && $attr[1] === "n") { - return $this->_cleanHTMLError("an event handler attribute in some <$tag> tag"); - } else if ($attr === "style" || $attr === "script" || $attr === "id") { - return $this->_cleanHTMLError("$attr attribute in some <$tag> tag"); + if ((strlen($attr) > 2 && $attr[0] === "o" && $attr[1] === "n") + || $attr === "style" + || $attr === "script" + || $attr === "id") { + $this->ml[] = $this->e("<0>HTML attribute {$m[1]} not allowed", $p, $p + strlen($m[1]), $t); } - $x .= " " . $attr; - $t = $m[2]; - if (preg_match(',\A=\s*(\'.*?\'|".*?"|\w+)\s*(.*)\z,s', $t, $m)) { + $x .= " {$attr}"; + $p += strlen($m[0]); + if (preg_match('/\G=\s*+(\'.*?\'|".*?"|\w++)\s*+/s', $t, $m, 0, $p)) { if ($m[1][0] === "'" || $m[1][0] === "\"") { $m[1] = substr($m[1], 1, -1); } $m[1] = html_entity_decode($m[1], ENT_HTML5); - if ($attr === "href" && preg_match(',\A\s*javascript\s*:,i', $m[1])) { - return $this->_cleanHTMLError("href attribute to JavaScript URL"); + if ($attr === "href" && preg_match('/\A\s*javascript\s*:/i', $m[1])) { + $this->ml[] = $this->e("<5>javascript URLs not allowed", $ap, $p + strlen($m[0]), $t); } $x .= "=\"" . htmlspecialchars($m[1]) . "\""; - $t = $m[2]; + $p += strlen($m[0]); } } - if ($t === "") { - return $this->_cleanHTMLError("an unclosed <$tag> tag"); - } else if ($t[0] === ">") { - $t = substr($t, 1); - if (isset($this->emptytags[$tag]) - && !preg_match(',\A\s*<\s*/' . $tag . '\s*>,si', $t)) - // automagically close empty tags - $x .= " />"; - else { - $x .= ">"; - $tagstack[] = $tag; + if ($p === $endp) { + if ($endp === $len) { + $this->ml[] = $this->e("<0>Unclosed HTML tag", $tagp, $p, $t); } - } else if (preg_match(',\A/\s*>(.*)\z,s', $t, $m)) { - $x .= " />"; - $t = $m[1]; + $x .= ">"; + $xp = $endp - 1; + } else if ($t[$p] === ">") { + $xp = $p; + } else if (preg_match('/\G\/\s*>/s', $t, $m, 0, $p)) { + $xp = $endp - 1; } else { - return $this->_cleanHTMLError("garbage in some <$tag> tag"); + $this->ml[] = $this->e("<0>Unexpected character in HTML tag", $p, $p, $t); + $x .= ">"; + $xp = $p - 1; } - } else if (preg_match(',\A<\s*/\s*([A-Za-z0-9]+)\s*>(.*)\z,s', $t, $m)) { + $p = $xp + 1; + if (($tagtf & self::F_VOID) !== 0) { + continue; + } + array_push($tagstack, $tag, $tagp, $endp, $curtf); + $curtf = $tagtf; + } else if (preg_match('/\G<\s*\/\s*([A-Za-z0-9]+)\s*>/s', $t, $m, 0, $p)) { $tag = strtolower($m[1]); - if (!isset($this->goodtags[$tag])) { - if (!($this->flags & self::BADTAGS_IGNORE)) { - return $this->_cleanHTMLError("an unacceptable </$tag> tag"); + $tagp = $p; + $endp = $tagp + strlen($m[0]); + $tagtf = $this->tags[$tag] ?? self::F_DISABLED; + if (($tagtf & self::F_DISABLED) !== 0) { + $x .= substr($t, $xp, $tagp - $xp); + if (($this->flags & self::CLEAN_IGNORE_UNKNOWN) !== 0) { + $this->check_text($curtf, $tagstack, $tagp, $tagp + 1, $t); + $x .= "<"; + $p = $xp = $tagp + 1; + continue; + } + if (empty($this->ml)) { + $this->e("<0>HTML tag <{$m[1]}> not allowed", $tagp, $endp, $t); } - $x .= "<"; - $t = substr($t, 1); + $p = $xp = $endp; continue; - } else if (empty($tagstack)) { - return $this->_cleanHTMLError("a extra close tag </$tag>"); - } else if (($last = array_pop($tagstack)) !== $tag) { - return $this->_cleanHTMLError("a close tag </$tag that doesn’t match the open tag <$last"); } - $x .= ""; - $t = $m[2]; + if (($tagtf & self::F_VOID) !== 0) { + // ignore close tags for void elements + $x .= substr($t, $xp, $p - $xp); + $xp = $p = $endp; + continue; + } + $nts = count($tagstack); + if ($nts > 0 && $tagstack[$nts - 4] === $tag) { + $curtf = $tagstack[$nts - 1]; + array_splice($tagstack, $nts - 4); + if ($endp !== $tagp + 3 + strlen($tag) + || $tag !== $m[1]) { + $x .= substr($t, $xp, $p - $xp) . "ml[] = $this->e("<0>HTML close tag does not match open tag", $tagp, $endp, $t); + if ($nts > 0) { + $this->ml[] = $mi = MessageItem::inform("<0>Open tag was here"); + $mi->pos1 = $tagstack[$nts - 3]; + $mi->pos2 = $tagstack[$nts - 2]; + $mi->context = $t; + } + } + $p = $endp; } else { - $x .= "<"; - $t = substr($t, 1); + $this->check_text($curtf, $tagstack, $p, $p + 1, $t); + $x .= substr($t, $xp, $p - $xp) . "<"; + $xp = $p = $p + 1; } } - if (!empty($tagstack)) { - return $this->_cleanHTMLError("unclosed tags, including <$tagstack[0]>"); + if ($xp !== $len) { + $this->check_text($curtf, $tagstack, $xp, $len, $t); + $x .= substr($t, $xp); + } + if (($nts = count($tagstack)) !== 0) { + $this->ml[] = $this->e("<0>Unclosed HTML tag", $tagstack[$nts - 3], $tagstack[$nts - 2], $t); + } + if (empty($this->ml)) { + return preg_replace('/\r\n?/', "\n", $x); + } else { + return false; } - - return preg_replace('/\r\n?/', "\n", $x); } /** @param string|list $t @@ -159,6 +447,11 @@ function clean_all($t) { return $x; } + /** @return list */ + function message_list() { + return $this->ml; + } + /** @return CleanHTML */ static function basic() { if (!self::$main) { diff --git a/lib/dbhelper.sh b/lib/dbhelper.sh index fc32e4136..8f38458b2 100644 --- a/lib/dbhelper.sh +++ b/lib/dbhelper.sh @@ -1,5 +1,5 @@ ## dbhelper.sh -- shell program helpers for HotCRP database access -## Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. +## Copyright (c) 2006-2024 Eddie Kohler; see LICENSE. echo_n () { # suns can't echo -n, and Mac OS X can't echo "x\c" @@ -103,7 +103,7 @@ sub fixshell ($) { if ($Opt{"multiconference"} && $Confname ne "") { foreach my $i ("dbName", "dbUser", "dbPassword", "sessionName", "downloadPrefix", "conferenceSite") { - $Opt{$i} =~ s,\*|\*\{conf(?:id|name)\}|\$conf(?:id|name)\b,$Confname,g if exists($Opt{$i}); + $Opt{$i} =~ s,\*|\$\{conf(?:id|name)\}|\$conf(?:id|name)\b,$Confname,g if exists($Opt{$i}); } } diff --git a/lib/dbl.php b/lib/dbl.php index 4565dbfc5..2759540ff 100644 --- a/lib/dbl.php +++ b/lib/dbl.php @@ -1,6 +1,6 @@ docstore()))) { $tmpdir = "{$prefix}tmp/"; @@ -160,12 +160,6 @@ static function docstore_tempdir(Conf $conf = null) { return null; } - /** @return ?non-empty-string - * @deprecated */ - static function docstore_tmpdir(Conf $conf = null) { - return self::docstore_tempdir($conf); - } - /** @param string $pattern * @param bool $extension * @return ?string */ diff --git a/lib/fmt.php b/lib/fmt.php index 1269c456a..5ca97c28d 100644 --- a/lib/fmt.php +++ b/lib/fmt.php @@ -1,6 +1,6 @@ complain("{$fspec} does not expect array"); } @@ -275,7 +277,9 @@ function apply_fmtspec($fspec, $vformat, $value) { $value = "<{$vformat}>{$value}"; } return [$vformat, $value]; - } else if (preg_match('/\A:[-+]?\d*(?:|\.\d+)[difgG]\z/', $fspec)) { + } else if (str_starts_with($fspec, ":plural ")) { + return [$vformat, plural_word($value, substr($fspec, 8))]; + } else if (preg_match('/\A:[-+]?\d*(?:|\.\d+)[difgGxX]\z/', $fspec)) { if (is_numeric($value)) { return [$vformat, sprintf("%" . substr($fspec, 1), $value)]; } else { diff --git a/lib/ftext.php b/lib/ftext.php index a2be164e8..dc18b8bd6 100644 --- a/lib/ftext.php +++ b/lib/ftext.php @@ -1,6 +1,6 @@ */ @@ -17,6 +17,8 @@ class Getopt { private $allmulti = false; /** @var ?bool */ private $otheropt = false; + /** @var ?bool */ + private $dupopt = true; /** @var bool */ private $interleave = false; /** @var ?int */ @@ -152,6 +154,13 @@ function otheropt($otheropt) { return $this; } + /** @param ?bool $dupopt + * @return $this */ + function dupopt($dupopt) { + $this->dupopt = $dupopt; + return $this; + } + /** @param bool $interleave * @return $this */ function interleave($interleave) { @@ -503,7 +512,7 @@ function parse($argv) { throw new CommandLineException("Missing argument for `{$oname}`", $this); } - $poty = $po->argtype; + $poty = $value !== false ? $po->argtype : null; if ($poty === "n" || $poty === "i") { if (!ctype_digit($value) && !preg_match('/\A[-+]\d+\z/', $value)) { throw new CommandLineException("`{$oname}` requires integer", $this); @@ -527,6 +536,9 @@ function parse($argv) { if (!array_key_exists($name, $res)) { $res[$name] = $pot >= self::MARG ? [$value] : $value; } else if ($pot < self::MARG && !$this->allmulti) { + if (!$this->dupopt) { + throw new CommandLineException("`{$oname}` was given multiple times", $this); + } $res[$name] = $value; } else if (is_array($res[$name])) { $res[$name][] = $value; diff --git a/lib/ht.php b/lib/ht.php index bf9a75710..5a59e87be 100644 --- a/lib/ht.php +++ b/lib/ht.php @@ -1,6 +1,6 @@ censor && !$this->preparation->reset_capability) { $capinfo = new TokenInfo($this->conf, TokenInfo::RESETPASSWORD); if (($cdbu = $this->recipient->cdb_user())) { - $capinfo->set_user($cdbu)->set_token_pattern("hcpw1[20]"); + $capinfo->set_cdb_user($cdbu)->set_token_pattern("hcpw1[20]"); } else { $capinfo->set_user($this->recipient)->set_token_pattern("hcpw0[20]"); } diff --git a/lib/mailpreparation.php b/lib/mailpreparation.php index 3d4aa6f05..12dd6e6cc 100644 --- a/lib/mailpreparation.php +++ b/lib/mailpreparation.php @@ -1,6 +1,6 @@ finalized = true; } + /** @return list */ + function invalid_recipient_message_list() { + $mx = []; + foreach ($this->recip as $ru) { + if ($ru->can_receive_mail($this->_self_requested)) { + continue; + } else if (!Contact::is_real_email($ru->preferredEmail ? : $ru->email)) { + $mx["fake"][] = $ru->email; + } else if ($ru->is_disabled()) { + $mx["disabled"][] = $ru->email; + } else if ($ru->is_dormant() && !$this->_self_requested) { + $mx["dormant"][] = $ru->email; + } else if ($ru->is_unconfirmed() && $this->conf->opt("sendEmailUnconfirmed") === false) { + $mx["unconfirmed"][] = $ru->email; + } else { + $mx["other"][] = $ru->email; + } + } + if (empty($mx)) { + return []; + } + $ml = []; + if (isset($mx["disabled"])) { + $ml[] = MessageItem::warning($this->conf->_("<0>Disabled {emails:plural account} {emails:list} cannot receive mail", new FmtArg("emails", $mx["disabled"], 0))); + } + if (isset($mx["dormant"])) { + $ml[] = MessageItem::warning($this->conf->_("<0>{emails:plural Account} {emails:list} {emails:plural has} not activated their {emails:plural account}", new FmtArg("emails", $mx["dormant"], 0))); + } + if (isset($mx["unconfirmed"])) { + $ml[] = MessageItem::warning($this->conf->_("<0>{emails:plural User} {emails:list} {emails:plural has} not yet signed in to their account (this site will not send mail to unconfirmed accounts)", new FmtArg("emails", $mx["unconfirmed"], 0))); + } + if (isset($mx["other"])) { + $ml[] = MessageItem::warning($this->conf->_("<0>Cannot send mail to {emails:plural account} {emails:list}", new FmtArg("emails", $mx["other"], 0))); + } + return $ml; + } + /** @return bool */ function sent() { return $this->sent; @@ -202,27 +239,14 @@ function send() { $headers = $this->headers; // enumerate valid recipients - $vto = $ml = []; + $vto = []; foreach ($this->recip as $ru) { if ($ru->can_receive_mail($this->_self_requested)) { $vto[] = self::recipient_address($ru); - } else if ($ru->is_disabled()) { - $ml[] = MessageItem::error("<0>User {$ru->email} is disabled"); - } else if ($ru->is_dormant() && !$this->_self_requested) { - $ml[] = MessageItem::error("<0>User {$ru->email} has not activated their account"); - } else if ($ru->is_unconfirmed() && $this->conf->opt("sendEmailUnconfirmed") === false) { - $ml[] = MessageItem::error("<0>This site only sends mail to users who have previously signed in"); - } else { - $ml[] = MessageItem::error("<0>User {$ru->email} cannot receive email at this time"); } } if (empty($vto)) { - if (empty($ml)) { - $ml[] = MessageItem::error("<0>No valid recipients"); - } - foreach ($ml as $mi) { - $this->append_item($mi); - } + $this->append_item(MessageItem::error("<0>No valid recipients")); return false; } diff --git a/lib/messageset.php b/lib/messageset.php index afdb92c18..fb16500f1 100644 --- a/lib/messageset.php +++ b/lib/messageset.php @@ -1,6 +1,6 @@ */ function upstream($v, $klass) { $a = []; - $this->add_downstream($this->start_dfs($v), $klass, $a); + $this->add_upstream($this->start_dfs($v), $klass, $a); return $a; } - /** @param MinCostMaxFlow_Node|string $v - * @param ?string $klass - * @return list - * @deprecated */ - function reachable($v, $klass) { - return $this->downstream($v, $klass); - } - /** @param MinCostMaxFlow_Node $v * @param list &$a */ private function topological_sort_visit($v, $klass, &$a) { @@ -400,9 +392,7 @@ private function topological_sort_visit($v, $klass, &$a) { /** @param MinCostMaxFlow_Node|string $v * @return list */ function topological_sort($v, $klass) { - if (is_string($v)) { - $v = $this->vmap[$v]; - } + $v = is_string($v) ? $this->vmap[$v] : $v; if (!$this->has_edges) { $this->initialize_edges(); } @@ -985,48 +975,57 @@ function debug_info($only_flow = false) { * @return string */ private function dimacs_input($flags) { $mincost = ($flags & self::DIMACS_MINCOST) !== 0; - $x = ["p " . ($mincost ? "min" : "max") . " " - . count($this->v) . " " . count($this->e) . "\n"]; + $header = [ + "p " . ($mincost ? "min" : "max") . " " . count($this->v) . " " . count($this->e) . "\n" + ]; + $names = ($flags & self::DIMACS_NAMES) !== 0; foreach ($this->v as $i => $v) { - $v->vindex = $i + 1; + $v->vindex = $names ? $v->name : $i + 1; } if ($mincost && $this->maxflow) { - $x[] = "n {$this->source->vindex} {$this->maxflow}\n"; - $x[] = "n {$this->sink->vindex} -{$this->maxflow}\n"; + $header[] = "n {$this->source->vindex} {$this->maxflow}\n"; + $header[] = "n {$this->sink->vindex} -{$this->maxflow}\n"; } else { - $x[] = "n {$this->source->vindex} s\n"; - $x[] = "n {$this->sink->vindex} t\n"; + $header[] = "n {$this->source->vindex} s\n"; + $header[] = "n {$this->sink->vindex} t\n"; } - foreach ($this->v as $v) { - if ($v !== $this->source && $v !== $this->sink) { + $names = ($flags & self::DIMACS_NAMES) !== 0; + if (!$names) { + foreach ($this->v as $v) { + if ($v === $this->source || $v === $this->sink) { + continue; + } $cmt = "c ninfo {$v->vindex} {$v->name}"; if ($v->klass !== "") { $cmt .= " {$v->klass}"; } - $x[] = "$cmt\n"; + $header[] = "{$cmt}\n"; } } - $names = ($flags & self::DIMACS_NAMES) !== 0; + $x = []; foreach ($this->e as $e) { - $src = $names ? $e->src->name : $e->src->vindex; - $dst = $names ? $e->dst->name : $e->dst->vindex; if ($mincost) { - $x[] = "a $src $dst {$e->mincap} {$e->cap} {$e->cost}\n"; + $x[] = "a {$e->src->vindex} {$e->dst->vindex} {$e->mincap} {$e->cap} {$e->cost}\n"; } else { - $x[] = "a $src $dst {$e->cap}\n"; + $x[] = "a {$e->src->vindex} {$e->dst->vindex} {$e->cap}\n"; } } - return join("", $x); + if ($names) { + natsort($x); + } + return join("", $header) . join("", $x); } - /** @return string */ - function maxflow_dimacs_input() { - return $this->dimacs_input(self::DIMACS_MAXFLOW); + /** @param bool $names + * @return string */ + function maxflow_dimacs_input($names = false) { + return $this->dimacs_input(self::DIMACS_MAXFLOW | ($names ? self::DIMACS_NAMES : 0)); } - /** @return string */ - function mincost_dimacs_input() { - return $this->dimacs_input(self::DIMACS_MINCOST); + /** @param bool $names + * @return string */ + function mincost_dimacs_input($names = false) { + return $this->dimacs_input(self::DIMACS_MINCOST | ($names ? self::DIMACS_NAMES : 0)); } @@ -1034,47 +1033,55 @@ function mincost_dimacs_input() { * @return string */ private function dimacs_output($flags) { $mincost = ($flags & self::DIMACS_MINCOST) !== 0; - $x = ["c p " . ($mincost ? "min" : "max") . " " - . count($this->v) . " " . count($this->e) . "\n"]; + $header = [ + "c p " . ($mincost ? "min" : "max") . " " . count($this->v) . " " . count($this->e) . "\n" + ]; + $names = ($flags & self::DIMACS_NAMES) !== 0; foreach ($this->v as $i => $v) { - $v->vindex = $i + 1; + $v->vindex = $names ? $v->name : $i + 1; } if ($mincost) { - $x[] = "s " . $this->current_cost() . "\n"; - $x[] = "c flow " . $this->current_flow() . "\n"; - $x[] = "c min_epsilon " . $this->epsilon . "\n"; + $header[] = "s " . $this->current_cost() . "\n"; + $header[] = "c flow " . $this->current_flow() . "\n"; + $header[] = "c min_epsilon " . $this->epsilon . "\n"; foreach ($this->v as $v) { - if ($v->price != 0) - $x[] = "c nprice {$v->vindex} {$v->price}\n"; + if ($v->price != 0) { + $header[] = "c nprice {$v->vindex} {$v->price}\n"; + } } } else { - $x[] = "s " . $this->current_flow() . "\n"; + $header[] = "s " . $this->current_flow() . "\n"; } + $x = []; foreach ($this->e as $e) { - if ($e->flow) { - // is this flow ambiguous? - $n = 0; - foreach ($e->src->e as $ee) { - if ($ee->dst === $e->dst) - ++$n; - } - if ($n !== 1) { - $x[] = "c finfo {$e->cap} {$e->cost}\n"; - } - $x[] = "f {$e->src->vindex} {$e->dst->vindex} {$e->flow}\n"; + if (!$e->flow) { + continue; + } + // is this flow ambiguous? + $n = 0; + foreach ($e->src->e as $ee) { + if ($ee->dst === $e->dst) + ++$n; } + $t = $n !== 1 && !$names ? "c finfo {$e->cap} {$e->cost}\n" : ""; + $x[] = "{$t}f {$e->src->vindex} {$e->dst->vindex} {$e->flow}\n"; } - return join("", $x); + if ($names) { + natsort($x); + } + return join("", $header) . join("", $x); } - /** @return string */ - function maxflow_dimacs_output() { - return $this->dimacs_output(self::DIMACS_MAXFLOW); + /** @param bool $names + * @return string */ + function maxflow_dimacs_output($names = false) { + return $this->dimacs_output(self::DIMACS_MAXFLOW | ($names ? self::DIMACS_NAMES : 0)); } - /** @return string */ - function mincost_dimacs_output() { - return $this->dimacs_output(self::DIMACS_MINCOST); + /** @param bool $names + * @return string */ + function mincost_dimacs_output($names = false) { + return $this->dimacs_output(self::DIMACS_MINCOST | ($names ? self::DIMACS_NAMES : 0)); } diff --git a/lib/navigation.php b/lib/navigation.php index d61042fc9..191fed42b 100644 --- a/lib/navigation.php +++ b/lib/navigation.php @@ -1,6 +1,6 @@ host = $server["HTTP_HOST"] ?? $server["SERVER_NAME"] ?? null; + $http_host = $server["HTTP_HOST"] ?? null; + $nav->host = $http_host ?? $server["SERVER_NAME"] ?? null; if ((isset($server["HTTPS"]) && $server["HTTPS"] !== "" && $server["HTTPS"] !== "off") @@ -64,9 +65,10 @@ static function make_server($server) { } $nav->protocol = $x; $x .= $nav->host ? : "localhost"; - if (($port = $server["SERVER_PORT"]) - && $port != $xport - && strpos($x, ":", 6) === false) { + if ($http_host === null // HTTP `Host` header should contain port + && strpos($x, ":", 6) === false + && ($port = $server["SERVER_PORT"]) + && $port != $xport) { $x .= ":" . $port; } $nav->server = $x; @@ -307,16 +309,16 @@ function self() { /** @param bool $downcase_host * @return string */ - function site_absolute($downcase_host = false) { + function base_absolute($downcase_host = false) { $x = $downcase_host ? strtolower($this->server) : $this->server; - return $x . $this->site_path; + return $x . $this->base_path; } /** @param bool $downcase_host * @return string */ - function base_absolute($downcase_host = false) { + function site_absolute($downcase_host = false) { $x = $downcase_host ? strtolower($this->server) : $this->server; - return $x . $this->base_path; + return $x . $this->site_path; } /** @param ?string $url @@ -345,13 +347,23 @@ function siteurl_path($url = null) { } } - /** @param string $url - * @return string */ - function set_siteurl($url) { - if ($url !== "" && $url[strlen($url) - 1] !== "/") { - $url .= "/"; + /** @param string $url */ + function set_site_path_relative($url) { + if ($url === $this->site_path_relative) { + return; + } else if ($url !== "" && $url !== "../" && !preg_match('/\A(\.\.\/)+\z/', $url)) { + $this->base_path_relative = $this->base_path; + $this->site_path_relative = $this->site_path; + return; + } + if ($this->base_path_relative === $this->site_path_relative) { + $this->base_path_relative = $url; + } else if (str_starts_with($this->base_path_relative, "../")) { + $this->base_path_relative = substr($this->base_path_relative, 0, -strlen($this->site_path_relative)) . $url; + } else { + $this->base_path_relative = $this->base_path; } - return ($this->site_path_relative = $url); + $this->site_path_relative = $url; } /** @param string $page diff --git a/lib/qrequest.php b/lib/qrequest.php index 4ee05a778..f3415af06 100644 --- a/lib/qrequest.php +++ b/lib/qrequest.php @@ -1,6 +1,6 @@ $title * @param string $id - * @param array{paperId?:int|string,body_class?:string,action_bar?:string,title_div?:string,subtitle?:string,save_messages?:bool,hide_title?:bool} $extra */ + * @param array{paperId?:int|string,body_class?:string,action_bar?:string,title_div?:string,subtitle?:string,save_messages?:bool,hide_title?:bool,hide_header?:bool} $extra */ function print_header($title, $id, $extra = []) { if (!$this->_conf->_header_printed) { $this->_conf->print_head_tag($this, $title, $extra); @@ -647,7 +647,7 @@ function print_header($title, $id, $extra = []) { function print_footer() { echo '
', // close #p-body '', // close #p-page - '