├── images ├── _.gif ├── edit.png ├── pdf.png ├── pdff.png ├── pdfx.png ├── txt.png ├── bendurt.png ├── buzzer.mp3 ├── check.png ├── cross.png ├── edit48.png ├── generic.png ├── info45.png ├── pdf24.png ├── pdff24.png ├── pdffx.png ├── pdffx24.png ├── pdfx24.png ├── txt24.png ├── view48.png ├── viewas.png ├── assign48.png ├── comment48.png ├── extagsset.png ├── extracker.png ├── generic24.png ├── genericf.png ├── review24.png ├── review48.png ├── exassignone.png ├── extagcolors.png ├── extagseditkw.png ├── extagssearch.png ├── genericf24.png ├── postscript.png ├── postscript24.png ├── postscriptf.png ├── stophand45.png ├── exsearchaction.png ├── extagvotehover.png ├── pageresultsex.png ├── postscriptf24.png ├── quicksearchex.png └── .htaccess ├── etc ├── sample.pdf ├── .htaccess ├── capabilityhandlers.json ├── reviewfieldtypes.json └── autoassigners.json ├── test ├── sample50pg.pdf ├── ldap │ ├── lldap_data │ │ └── users.db │ ├── compose.yaml │ └── README.md ├── .htaccess ├── test04.php ├── test03.php ├── test07.php ├── test09.php ├── test01.php ├── test05.php ├── oauth │ ├── composer.json │ ├── db.json │ ├── README.md │ └── private.key ├── test06.php ├── t_invariants.php ├── cdb-options.php ├── check.sh ├── test02.php ├── test08.php ├── review0.txt ├── t_events.php ├── options.php ├── t_userapi.php ├── t_ht.php ├── t_mailer.php ├── t_s3.php ├── t_xtcheck.php └── review1A.txt ├── .gitattributes ├── scripts └── .htaccess ├── api.php ├── doc.php ├── log.php ├── stylesheets └── .htaccess ├── assign.php ├── buzzer.php ├── graph.php ├── help.php ├── mail.php ├── oauth.php ├── paper.php ├── review.php ├── search.php ├── signin.php ├── users.php ├── offline.php ├── profile.php ├── settings.php ├── signout.php ├── authorize.php ├── autoassign.php ├── bulkassign.php ├── cacheable.php ├── deadlines.php ├── manageemail.php ├── newaccount.php ├── reviewprefs.php ├── scorechart.php ├── checkupdates.php ├── manualassign.php ├── mergeaccounts.php ├── resetpassword.php ├── conf └── .htaccess ├── conflictassign.php ├── forgotpassword.php ├── lib ├── .htaccess ├── backupdb.sh ├── restoredb.sh ├── jsonexception.php ├── subprocess.php ├── collatorshim.php ├── memoryqsession.php ├── gmpshim.php ├── filer.php └── phpqsession.php ├── src ├── .htaccess ├── settings │ ├── s_shepherds.php │ ├── s_users.php │ ├── s_rf.php │ ├── s_preference.php │ ├── s_sf.php │ ├── s_basics.php │ ├── s_finalversions.php │ └── s_messages.php ├── formulas │ ├── f_now.php │ ├── f_pdfsize.php │ ├── f_timefield.php │ ├── f_decision.php │ ├── f_submittedat.php │ ├── f_reviewer.php │ ├── f_reviewwordcount.php │ ├── f_optionvalue.php │ ├── f_pagecount.php │ ├── f_realnumberoption.php │ ├── f_conflict.php │ ├── f_optionpresent.php │ ├── f_reviewround.php │ ├── f_revtype.php │ ├── f_topicscore.php │ ├── f_pref.php │ ├── f_topic.php │ └── f_reviewermatch.php ├── search │ ├── st_topic.php │ ├── st_editfinal.php │ ├── st_optiontext.php │ ├── st_documentname.php │ ├── st_optionpresent.php │ ├── st_cmtafter.php │ ├── st_badge.php │ ├── st_realnumberoption.php │ ├── st_perm.php │ ├── st_optionvalue.php │ ├── st_paperpc.php │ ├── st_reconflict.php │ ├── st_paperstatus.php │ ├── st_documentcount.php │ ├── st_sclass.php │ ├── st_decision.php │ ├── st_phase.php │ └── st_color.php ├── pages │ └── p_graph_procrastination.php ├── capabilities │ └── cap_manageemail.php ├── listactions │ ├── la_getpcassignments.php │ ├── la_get.php │ ├── la_getlead.php │ ├── la_decide.php │ ├── la_mail.php │ └── la_getjsonrqc.php ├── papercolumns │ ├── pc_timestamp.php │ ├── pc_paperidorder.php │ ├── pc_desirability.php │ ├── pc_topics.php │ ├── pc_commenters.php │ ├── pc_pcconflicts.php │ ├── pc_administrator.php │ ├── pc_lead.php │ ├── pc_preferencelist.php │ ├── pc_reviewdelegation.php │ └── pc_shepherd.php ├── notificationinfo.php ├── api │ ├── api_follow.php │ ├── api_graphdata.php │ ├── api_job.php │ ├── api_decision.php │ ├── api_events.php │ ├── api_formatcheck.php │ ├── api_assign.php │ └── api_paperpc.php ├── assigners │ └── a_error.php ├── tagmessagereport.php ├── reviewfields │ └── rf_text.php ├── help │ ├── h_scoresort.php │ └── h_votetags.php ├── options │ ├── o_checkboxes.php │ ├── o_title.php │ └── o_topics.php ├── searchoperator.php ├── autoassigners │ ├── aa_prefconflict.php │ └── aa_clear.php ├── apihelpers.php ├── fieldchangeset.php ├── textformat.php └── logentryfilter.php ├── batch ├── .htaccess ├── fileinfo.php └── cli │ └── cli_test.php ├── .phan └── stubs │ └── polyfills.phan_php ├── devel ├── d3 │ ├── index.js │ ├── package.json │ └── rollup.config.mjs ├── hotcrp.vim ├── apidoc │ ├── subadmin.md │ ├── redocly.yaml │ ├── tags.md │ └── search.md ├── hotcrp-daemonize.c └── manual │ ├── index.md │ └── css.md ├── .gitignore ├── .user.ini ├── .eslintrc.json ├── package.json ├── LICENSE ├── .htaccess └── .github └── workflows └── tests.yml /images/_.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/_.gif -------------------------------------------------------------------------------- /etc/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/etc/sample.pdf -------------------------------------------------------------------------------- /images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/edit.png -------------------------------------------------------------------------------- /images/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdf.png -------------------------------------------------------------------------------- /images/pdff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdff.png -------------------------------------------------------------------------------- /images/pdfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdfx.png -------------------------------------------------------------------------------- /images/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/txt.png -------------------------------------------------------------------------------- /images/bendurt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/bendurt.png -------------------------------------------------------------------------------- /images/buzzer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/buzzer.mp3 -------------------------------------------------------------------------------- /images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/check.png -------------------------------------------------------------------------------- /images/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/cross.png -------------------------------------------------------------------------------- /images/edit48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/edit48.png -------------------------------------------------------------------------------- /images/generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/generic.png -------------------------------------------------------------------------------- /images/info45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/info45.png -------------------------------------------------------------------------------- /images/pdf24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdf24.png -------------------------------------------------------------------------------- /images/pdff24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdff24.png -------------------------------------------------------------------------------- /images/pdffx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdffx.png -------------------------------------------------------------------------------- /images/pdffx24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdffx24.png -------------------------------------------------------------------------------- /images/pdfx24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pdfx24.png -------------------------------------------------------------------------------- /images/txt24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/txt24.png -------------------------------------------------------------------------------- /images/view48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/view48.png -------------------------------------------------------------------------------- /images/viewas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/viewas.png -------------------------------------------------------------------------------- /images/assign48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/assign48.png -------------------------------------------------------------------------------- /images/comment48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/comment48.png -------------------------------------------------------------------------------- /images/extagsset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extagsset.png -------------------------------------------------------------------------------- /images/extracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extracker.png -------------------------------------------------------------------------------- /images/generic24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/generic24.png -------------------------------------------------------------------------------- /images/genericf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/genericf.png -------------------------------------------------------------------------------- /images/review24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/review24.png -------------------------------------------------------------------------------- /images/review48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/review48.png -------------------------------------------------------------------------------- /test/sample50pg.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/test/sample50pg.pdf -------------------------------------------------------------------------------- /images/exassignone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/exassignone.png -------------------------------------------------------------------------------- /images/extagcolors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extagcolors.png -------------------------------------------------------------------------------- /images/extagseditkw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extagseditkw.png -------------------------------------------------------------------------------- /images/extagssearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extagssearch.png -------------------------------------------------------------------------------- /images/genericf24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/genericf24.png -------------------------------------------------------------------------------- /images/postscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/postscript.png -------------------------------------------------------------------------------- /images/postscript24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/postscript24.png -------------------------------------------------------------------------------- /images/postscriptf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/postscriptf.png -------------------------------------------------------------------------------- /images/stophand45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/stophand45.png -------------------------------------------------------------------------------- /images/exsearchaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/exsearchaction.png -------------------------------------------------------------------------------- /images/extagvotehover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/extagvotehover.png -------------------------------------------------------------------------------- /images/pageresultsex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/pageresultsex.png -------------------------------------------------------------------------------- /images/postscriptf24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/postscriptf24.png -------------------------------------------------------------------------------- /images/quicksearchex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/images/quicksearchex.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /scripts/*.min.js -diff 2 | /scripts/*.map -diff 3 | /test/ldap/lldap_data/*.db -diff 4 | -------------------------------------------------------------------------------- /test/ldap/lldap_data/users.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/hotcrp/master/test/ldap/lldap_data/users.db -------------------------------------------------------------------------------- /images/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | ExpiresActive On 3 | ExpiresDefault "access plus 10 years" 4 | 5 | FileETag none 6 | -------------------------------------------------------------------------------- /scripts/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | ExpiresActive On 3 | ExpiresDefault "access plus 10 years" 4 | 5 | FileETag none 6 | -------------------------------------------------------------------------------- /api.php: -------------------------------------------------------------------------------- 1 | 2 | ExpiresActive On 3 | ExpiresDefault "access plus 10 years" 4 | 5 | FileETag none 6 | -------------------------------------------------------------------------------- /assign.php: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /conflictassign.php: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /forgotpassword.php: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /test/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /batch/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /.phan/stubs/polyfills.phan_php: -------------------------------------------------------------------------------- 1 | /dev/null; then LIBDIR=./ 7 | else LIBDIR=`echo "$0" | sed 's,^\(.*/\)[^/]*$,\1,'`; fi 8 | exec php ${LIBDIR}../batch/backupdb.php "$@" 9 | -------------------------------------------------------------------------------- /lib/restoredb.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ## restoredb.sh -- HotCRP script to restore from backup 3 | ## Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | # Now relies on PHP. 6 | if ! expr "$0" : '.*[/]' >/dev/null; then LIBDIR=./ 7 | else LIBDIR=`echo "$0" | sed 's,^\(.*/\)[^/]*$,\1,'`; fi 8 | exec php ${LIBDIR}../batch/backupdb.php -r "$@" 9 | -------------------------------------------------------------------------------- /devel/hotcrp.vim: -------------------------------------------------------------------------------- 1 | " Vim syntax file 2 | " Language: HotCRP Offline Review Forms 3 | 4 | if exists("b:current_syntax") 5 | finish 6 | endif 7 | 8 | 9 | " Matches 10 | syn match hotCrpSec "^==+==.*$" 11 | syn match hotCrpSubSec "^==-==.*$" 12 | 13 | let b:current_syntax = "hotcrp" 14 | 15 | hi def link hotCrpSec Comment 16 | hi def link hotCrpSubSec Constant 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /Code/options.inc 4 | /composer.json 5 | /composer.lock 6 | /conf 7 | /devel/d3/d3.js 8 | /devel/d3/d3-hotcrp.min.js 9 | /devel/d3/node_modules 10 | /devel/hotcrp-daemonize 11 | /docs 12 | /filestore 13 | /logs 14 | /node_modules 15 | /package-lock.json 16 | /test/oauth/vendor 17 | /test/oauth/auths.json 18 | /test/oauth/composer.lock 19 | /vendor 20 | -------------------------------------------------------------------------------- /src/settings/s_shepherds.php: -------------------------------------------------------------------------------- 1 | decisions page 3 | // Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | class Shepherds_SettingParser extends SettingParser { 6 | static function print_visibility(SettingValues $sv) { 7 | $sv->print_checkbox("shepherd_visibility", "Show shepherd names to authors"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test05.php: -------------------------------------------------------------------------------- 1 | = upload_max_filesize. 5 | ;upload_max_filesize = 15M 6 | ;post_max_size = 20M 7 | 8 | ; Some pages involve a lot of post variables. 9 | ;max_input_vars = 4096 10 | 11 | ; A large memory_limit helps when sending very large zipped files. 12 | memory_limit = 128M 13 | -------------------------------------------------------------------------------- /test/test06.php: -------------------------------------------------------------------------------- 1 | set_format($ff->kwdef->is_time ? Fexpr::FTIME : Fexpr::FDATE); 8 | } 9 | function compile(FormulaCompiler $state) { 10 | return $state->_add_now(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/ldap/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | lldap: 3 | image: lldap/lldap:stable 4 | ports: 5 | # LDAP 6 | - "17169:3890" 7 | # Web front end 8 | - "17170:17170" 9 | volumes: 10 | - "./lldap_data:/data" 11 | environment: 12 | - LLDAP_JWT_SECRET=ybke34;H~a0sb6iM9RTZ&I|~l,9qsF21 13 | - LLDAP_KEY_SEED=Lg57%.J*EG42YwYq}_}g@cFEoJ^E_F=r 14 | - LLDAP_KEY_FILE= 15 | - LLDAP_LDAP_USER_PASS=aequee0Oe1ee1A 16 | -------------------------------------------------------------------------------- /src/formulas/f_pdfsize.php: -------------------------------------------------------------------------------- 1 | queryOptions["pdfSize"] = true; 8 | $prow = $state->_prow(); 9 | return "(\$contact->can_view_pdf({$prow}) ? (int) {$prow}->primary_document_size() : null)"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/t_invariants.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 12 | } 13 | 14 | function test_invariants() { 15 | xassert(ConfInvariants::test_all($this->conf)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/search/st_topic.php: -------------------------------------------------------------------------------- 1 | conf->option_by_id(PaperOption::TOPICSID); 8 | $sword->set_compar_word($sword->word); 9 | return Option_SearchTerm::parse_option($sword, $srch, $opt); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "jquery": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": "latest" 9 | }, 10 | "globals": { 11 | "Set": "readonly", 12 | "WeakMap": "readonly", 13 | "Promise": "readonly" 14 | }, 15 | "rules": { 16 | "no-empty": 0, 17 | "no-constant-condition": ["error", {"checkLoops": false}] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /devel/d3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-hotcrp", 3 | "version": "0.0.8", 4 | "scripts": { 5 | "prepare": "rollup -c" 6 | }, 7 | "devDependencies": { 8 | "@rollup/plugin-json": "6", 9 | "@rollup/plugin-node-resolve": "15", 10 | "@rollup/plugin-terser": "^0.4.0", 11 | "rollup": "3", 12 | "d3-array": "3", 13 | "d3-axis": "3", 14 | "d3-quadtree": "3", 15 | "d3-scale": "4", 16 | "d3-selection": "3", 17 | "d3-shape": "3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "eslint": "^8.21.0" 4 | }, 5 | "name": "hotcrp", 6 | "description": "HotCRP Conference Review Software", 7 | "version": "3.0.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/kohler/hotcrp.git" 11 | }, 12 | "author": "Eddie Kohler", 13 | "bugs": { 14 | "url": "https://github.com/kohler/hotcrp/issues" 15 | }, 16 | "homepage": "https://github.com/kohler/hotcrp#readme" 17 | } 18 | -------------------------------------------------------------------------------- /lib/jsonexception.php: -------------------------------------------------------------------------------- 1 | field = $field; 10 | } 11 | function compile(FormulaCompiler $state) { 12 | return "((int) " . $state->_prow() . "->" . $this->field . ")"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/formulas/f_decision.php: -------------------------------------------------------------------------------- 1 | set_format(Fexpr::FDECISION); 8 | } 9 | function viewable_by(Contact $user) { 10 | return $user->can_view_some_decision(); 11 | } 12 | function compile(FormulaCompiler $state) { 13 | return $state->_add_decision(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/cdb-options.php: -------------------------------------------------------------------------------- 1 | set_format($ff->kwdef->is_time ? Fexpr::FTIME : Fexpr::FDATE); 9 | } 10 | function compile(FormulaCompiler $state) { 11 | return '(' . $state->_prow() . '->submitted_at() ? : null)'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/p_graph_procrastination.php: -------------------------------------------------------------------------------- 1 | json()), 12 | ") });\n"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/settings/s_users.php: -------------------------------------------------------------------------------- 1 | users page 3 | // Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | class Users_SettingRenderer { 6 | static function print(SettingValues $sv) { 7 | echo '

"new", "role" => "pc"]), '" class="btn">Create PC accounts · ', 8 | "Select a user’s name to edit a profile.

\n"; 9 | $pl = new ContactList($sv->user, false); 10 | echo $pl->table_html("pcadminx", $sv->conf->hoturl("users", "t=pcadmin")); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/capabilities/cap_manageemail.php: -------------------------------------------------------------------------------- 1 | conf, TokenInfo::MANAGEEMAIL)) 10 | ->set_user_id($viewer->contactId) 11 | ->set_invalid_after(3600 /* 1 hour */) 12 | ->set_expires_after(7200 /* 2 hours */) 13 | ->set_token_pattern("hcme_[16]"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/oauth/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "clients": [ 3 | { 4 | "client_id": "hotcrp-oauth-test", 5 | "client_secret": "Dudfield", 6 | "name": "HotCRP OAuth Test", 7 | "redirect_uri": "http://localhost:8080/testconf/oauth" 8 | } 9 | ], 10 | "users": [ 11 | { 12 | "email": "fran@hotcrp-oauth.org", 13 | "email_verified": true, 14 | "given_name": "Fran", 15 | "family_name": "Framer", 16 | "name": "Fran Framer" 17 | }, 18 | { 19 | "email": "paula@hotcrp-oauth.org", 20 | "email_verified": true, 21 | "given_name": "Paula", 22 | "family_name": "Books", 23 | "name": "Paula Books" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/listactions/la_getpcassignments.php: -------------------------------------------------------------------------------- 1 | is_manager(); 8 | } 9 | function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { 10 | list($header, $items) = ListAction::pcassignments_csv_data($user, $ssel->selection()); 11 | return $user->conf->make_csvg("pcassignments")->select($header)->append($items); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/check.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | all=false 4 | if [ "$1" = "-a" -o "$1" = "--all" ]; then all=true; shift; fi 5 | 6 | a=0 7 | runcheck () { 8 | echo $1: 9 | php -d error_reporting=E_ALL "$@" 10 | z=$? 11 | echo 12 | if [ "$a" = 0 ]; then a=$z; fi 13 | } 14 | 15 | runcheck test/test01.php "$@" 16 | runcheck test/test02.php "$@" 17 | runcheck test/test03.php "$@" 18 | runcheck test/test04.php "$@" 19 | runcheck test/test05.php "$@" 20 | runcheck test/test06.php "$@" 21 | runcheck test/test07.php "$@" 22 | if $all; then 23 | runcheck test/test08.php "$@" 24 | fi 25 | runcheck test/test09.php "$@" 26 | 27 | exit $a 28 | -------------------------------------------------------------------------------- /devel/d3/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import json from "@rollup/plugin-json"; 2 | import nodeResolve from "@rollup/plugin-node-resolve"; 3 | import terser from "@rollup/plugin-terser"; 4 | 5 | export default [{ 6 | input: "index.js", 7 | plugins: [ 8 | nodeResolve(), 9 | json(), 10 | terser({ 11 | mangle: { 12 | reserved: [ 13 | "InternMap", 14 | "InternSet" 15 | ] 16 | } 17 | }) 18 | ], 19 | output: { 20 | file: "d3-hotcrp.min.js", 21 | name: "d3", 22 | format: "umd", 23 | indent: false, 24 | extend: true 25 | } 26 | }]; 27 | -------------------------------------------------------------------------------- /test/test02.php: -------------------------------------------------------------------------------- 1 | getfn, 12 | ["class" => "want-focus js-submit-action-info-get w-small-selector ignore-diff"]) 13 | . $pl->action_submit("get", ["formmethod" => "get", "class" => "can-submit-all"]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/formulas/f_reviewer.php: -------------------------------------------------------------------------------- 1 | set_format(Fexpr::FREVIEWER); 9 | } 10 | function inferred_index() { 11 | return Fexpr::IDX_REVIEW; 12 | } 13 | function viewable_by(Contact $user) { 14 | return $user->can_view_some_review_identity(); 15 | } 16 | function compile(FormulaCompiler $state) { 17 | $state->queryOptions["reviewSignatures"] = true; 18 | return $state->review_identity_loop_cid(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /devel/apidoc/subadmin.md: -------------------------------------------------------------------------------- 1 | # post /assign 2 | 3 | > Assignments 4 | 5 | 6 | # get /{p}/decision 7 | 8 | > Retrieve submission decision 9 | 10 | 11 | # post /{p}/decision 12 | 13 | > Change submission decision 14 | 15 | 16 | # get /{p}/lead 17 | 18 | > Retrieve submission discussion lead 19 | 20 | 21 | # post /{p}/lead 22 | 23 | > Change submission discussion lead 24 | 25 | 26 | # get /{p}/manager 27 | 28 | > Retrieve submission administrator 29 | 30 | 31 | # post /{p}/manager 32 | 33 | > Change submission administrator 34 | 35 | 36 | # post /{p}/reviewround 37 | 38 | > Change review round 39 | 40 | 41 | # get /{p}/shepherd 42 | 43 | > Retrieve submission shepherd 44 | 45 | 46 | # post /{p}/shepherd 47 | 48 | > Change submission shepherd 49 | -------------------------------------------------------------------------------- /devel/hotcrp-daemonize.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char** argv) { 9 | DIR* dir = opendir("/dev/fd"); 10 | struct dirent* de; 11 | while (dir && (de = readdir(dir))) { 12 | if (!isdigit((unsigned char) de->d_name[0])) { 13 | continue; 14 | } 15 | char* ends; 16 | unsigned long u = strtoul(de->d_name, &ends, 10); 17 | if (*ends == 0 && (int) u != dirfd(dir) && (int) u > 2) { 18 | close((int) u); 19 | } 20 | } 21 | closedir(dir); 22 | if (fork() > 0) { 23 | exit(0); 24 | } 25 | setsid(); 26 | execvp(argv[1], argv + 1); 27 | exit(127); 28 | } 29 | -------------------------------------------------------------------------------- /test/test08.php: -------------------------------------------------------------------------------- 1 | timeSubmitted, 0) <=> max($b->timeSubmitted, 0); 11 | } 12 | function content_empty(PaperList $pl, PaperInfo $row) { 13 | return $row->timeSubmitted <= 0; 14 | } 15 | function content(PaperList $pl, PaperInfo $row) { 16 | if ($row->timeSubmitted > 0) { 17 | return $row->conf->unparse_time_log($row->timeSubmitted); 18 | } 19 | return ""; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devel/apidoc/redocly.yaml: -------------------------------------------------------------------------------- 1 | apis: 2 | core@v1: 3 | root: ../openapi.json 4 | rules: 5 | operation-4xx-response: off 6 | operation-operationId: off 7 | no-empty-servers: off 8 | operation-summary: off 9 | tag-description: off 10 | info-license: off 11 | 12 | extends: 13 | - recommended 14 | 15 | theme: 16 | openapi: 17 | disableSearch: true 18 | expandResponses: 200,201 19 | theme: 20 | sidebar: 21 | backgroundColor: 'ivory' 22 | rightPanel: 23 | textColor: 'ivory' 24 | backgroundColor: '#001036' 25 | typography: 26 | fontFamily: 'sans-serif' 27 | links: 28 | color: '#0090cc' 29 | headings: 30 | fontFamily: 'sans-serif' 31 | code: 32 | fontFamily: 'SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace' 33 | -------------------------------------------------------------------------------- /test/review0.txt: -------------------------------------------------------------------------------- 1 | ==+== ===================================================================== 2 | ==+== Begin Review #0 3 | 4 | ==+== Paper #0 5 | 6 | ==+== Review Readiness 7 | ==-== Enter "Ready" if the review is ready for others to see: 8 | 9 | Ready 10 | 11 | ==+== A. Overall merit 12 | ==-== Choices: 13 | ==-== 1. Reject 14 | ==-== 2. Weak reject 15 | ==-== 3. Weak accept 16 | ==-== No entry 17 | ==-== Enter your choice: 18 | 19 | (Your choice here) 20 | 21 | ==+== B. Reviewer expertise 22 | ==-== Choices: 23 | ==-== 1. No familiarity 24 | ==-== 2. Some familiarity 25 | ==-== 3. Knowledgeable 26 | ==-== 4. Expert 27 | ==-== Enter the number of your choice: 28 | 29 | (Your choice here) 30 | 31 | ==+== C. Paper summary 32 | 33 | 34 | 35 | ==+== D. Comments for authors 36 | 37 | ==+== Scratchpad (for unsaved private notes) 38 | 39 | ==+== End Review 40 | -------------------------------------------------------------------------------- /test/t_events.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 12 | } 13 | 14 | function test_events() { 15 | $u_mgbaker = $this->conf->checked_user_by_email("mgbaker@cs.stanford.edu"); 16 | $evs = new PaperEvents($u_mgbaker); 17 | xassert_gt(count($evs->events(Conf::$now, 10)), 0); 18 | 19 | $u_diot = $this->conf->checked_user_by_email("ojuelegba@gmail.com"); 20 | $evs = new PaperEvents($u_diot); 21 | foreach ($evs->events(Conf::$now, 10) as $x) { 22 | error_log(Conf::$now . " " . json_encode($x)); 23 | } 24 | xassert_eqq(count($evs->events(Conf::$now, 10)), 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/oauth/README.md: -------------------------------------------------------------------------------- 1 | HotCRP OAuth test server 2 | ======================== 3 | 4 | This server, built using https://github.com/thephpleague/oauth2-server and 5 | https://github.com/Nyholm/psr7, can be used to test HotCRP’s OAuth support. 6 | 7 | 8 | Usage 9 | ----- 10 | 11 | 1. Install required libraries with `composer install` 12 | 13 | 2. Run the server with `php -S localhost:19382 oauth-provider.php` 14 | 15 | 3. Configure HotCRP to access the server by setting `$Opt["oAuthProviders"]` 16 | in `conf/options.php`: 17 | 18 | ```php 19 | $Opt["oAuthProviders"][] = [ 20 | "name" => "local", 21 | "client_id" => "hotcrp-oauth-test", 22 | "client_secret" => "Dudfield", 23 | "auth_uri" => "http://localhost:19382/auth", 24 | "token_uri" => "http://localhost:19382/token", 25 | "redirect_uri" => "http://localhost:8080/testconf/oauth", 26 | "button_html" => "Sign in with local OAuth" 27 | ]; 28 | ``` 29 | -------------------------------------------------------------------------------- /test/options.php: -------------------------------------------------------------------------------- 1 | user = $user; 23 | $this->flags = $flags; 24 | } 25 | 26 | /** @param int $flags 27 | * @return bool */ 28 | function has($flags) { 29 | return ($this->flags & $flags) === $flags; 30 | } 31 | 32 | /** @return bool */ 33 | function sent() { 34 | return ($this->flags & self::SENT) !== 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/api/api_follow.php: -------------------------------------------------------------------------------- 1 | u ?? $qreq->reviewer, $user, $prow); 9 | $following = friendly_boolean($qreq->following); 10 | if ($following === null) { 11 | return JsonResult::make_parameter_error("following", "Expected boolean"); 12 | } 13 | $bits = Contact::WATCH_REVIEW_EXPLICIT | ($following ? Contact::WATCH_REVIEW : 0); 14 | $user->conf->qe("insert into PaperWatch set paperId=?, contactId=?, watch=? on duplicate key update watch=(watch&~?)|?", 15 | $prow->paperId, $reviewer->contactId, $bits, 16 | Contact::WATCH_REVIEW_EXPLICIT | Contact::WATCH_REVIEW, $bits); 17 | return ["ok" => true, "following" => $following]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/formulas/f_reviewwordcount.php: -------------------------------------------------------------------------------- 1 | is_reviewer(); 14 | } 15 | function compile(FormulaCompiler $state) { 16 | if ($state->index_type !== Fexpr::IDX_MY 17 | && VIEWSCORE_REVIEWER <= $state->user->permissive_view_score_bound()) { 18 | return "null"; 19 | } 20 | $state->_ensure_review_word_counts(); 21 | $rrow = $state->_rrow(); 22 | $rrow_vsb = $state->_rrow_view_score_bound(true); 23 | return "(" . VIEWSCORE_AUTHORDEC . " > {$rrow_vsb} ? {$rrow}->reviewWordCount : null)"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/api/api_graphdata.php: -------------------------------------------------------------------------------- 1 | x)) { 8 | return JsonResult::make_missing_error("x"); 9 | } 10 | $fg = new FormulaGraph($user, $qreq->gtype ? : "scatter", $qreq->x, $qreq->y); 11 | if ($qreq->xorder) { 12 | $fg->set_xorder($qreq->xorder); 13 | } 14 | 15 | list($queries, $styles) = FormulaGraph::parse_queries($qreq); 16 | for ($i = 0; $i < count($queries); ++$i) { 17 | $fg->add_query($queries[$i], $styles[$i], isset($qreq->q1) ? "q$i" : "q"); 18 | } 19 | 20 | if (!$fg->has_error()) { 21 | return ["ok" => true] + $fg->graph_json(); 22 | } else { 23 | return new JsonResult(["ok" => false, "message_list" => $fg->message_list()]); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/formulas/f_optionvalue.php: -------------------------------------------------------------------------------- 1 | option = $option; 11 | } 12 | function paper_options(&$oids) { 13 | $oids[$this->option->id] = true; 14 | } 15 | function viewable_by(Contact $user) { 16 | return $user->can_view_some_option($this->option); 17 | } 18 | function compile(FormulaCompiler $state) { 19 | $id = $this->option->id; 20 | $oval = "\$optvalue" . ($id < 0 ? "m" . -$id : $id); 21 | if ($state->check_gvar($oval)) { 22 | $ovv = $state->_add_option_value($this->option); 23 | $state->gstmt[] = "{$oval} = {$ovv} ? {$ovv}->value : null;"; 24 | } 25 | return $oval; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/subprocess.php: -------------------------------------------------------------------------------- 1 | $args 16 | * @return string */ 17 | static function shell_quote_args($args) { 18 | $s = []; 19 | foreach ($args as $word) { 20 | $s[] = self::shell_quote_light($word); 21 | } 22 | return join(" ", $s); 23 | } 24 | 25 | /** @param list $args 26 | * @return list|string */ 27 | static function args_to_command($args) { 28 | if (PHP_VERSION_ID < 70400) { 29 | return self::shell_quote_args($args); 30 | } 31 | return $args; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/settings/s_rf.php: -------------------------------------------------------------------------------- 1 | */ 16 | public $values; 17 | public $ids; 18 | public $start; 19 | public $flip; 20 | public $scheme; 21 | 22 | // internal 23 | public $presence; 24 | /** @var list */ 25 | public $xvalues; 26 | /** @var bool */ 27 | public $existed = true; 28 | /** @var bool */ 29 | public $deleted = false; 30 | } 31 | 32 | class RfValue_Setting { 33 | public $id; 34 | public $name; 35 | public $order; 36 | public $symbol; 37 | 38 | // internal 39 | public $old_value; 40 | /** @var bool */ 41 | public $deleted = false; 42 | } 43 | -------------------------------------------------------------------------------- /src/papercolumns/pc_paperidorder.php: -------------------------------------------------------------------------------- 1 | "__numericorder" . (++self::$type_uid), "sort" => true]); 12 | $this->order_term = $order_term; 13 | } 14 | function compare(PaperInfo $a, PaperInfo $b, PaperList $pl) { 15 | $ap = $this->order_term->index_of($a->paperId); 16 | $bp = $this->order_term->index_of($b->paperId); 17 | if ($ap !== false && $bp !== false) { 18 | return $ap <=> $bp; 19 | } else if ($ap !== false || $bp !== false) { 20 | return $ap === false ? 1 : -1; 21 | } 22 | return $a->paperId <=> $b->paperId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/settings/s_preference.php: -------------------------------------------------------------------------------- 1 | name === "preference_min") { 8 | $v = $sv->newv($si); 9 | if ($v < -1000000) { 10 | $sv->error_at($si->name, "<0>Minimum preference must be at least -1000000"); 11 | } else if ($v > 0) { 12 | $sv->error_at($si->name, "<0>Minimum preference cannot be greater than 0"); 13 | } 14 | } else if ($si->name === "preference_max") { 15 | $v = $sv->newv($si); 16 | if ($v > 1000000) { 17 | $sv->error_at($si->name, "<0>Maximum preference must be at most 1000000"); 18 | } else if ($v < 0) { 19 | $sv->error_at($si->name, "<0>Maximum preference cannot be less than 0"); 20 | } 21 | } 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/search/st_editfinal.php: -------------------------------------------------------------------------------- 1 | negate(); 14 | } else { 15 | $srch->lwarning($sword, "<0>Only “editfinal:yes” and “editfinal:no” are allowed"); 16 | return new False_SearchTerm; 17 | } 18 | } 19 | function sqlexpr(SearchQueryInfo $sqi) { 20 | return "(Paper.outcome>0)"; 21 | } 22 | function test(PaperInfo $row, $xinfo) { 23 | return $row->author_edit_state() === 2; 24 | } 25 | function about() { 26 | return self::ABOUT_PAPER; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/formulas/f_pagecount.php: -------------------------------------------------------------------------------- 1 | */ 9 | static public $checkers = []; 10 | function __construct(FormulaCall $ff, Formula $formula) { 11 | parent::__construct("pagecount"); 12 | $this->chkindex = 0; 13 | while ($this->chkindex < count(self::$checkers) 14 | && self::$checkers[$this->chkindex]->conf !== $formula->conf) { 15 | ++$this->chkindex; 16 | } 17 | if ($this->chkindex === count(self::$checkers)) { 18 | self::$checkers[] = new CheckFormat($formula->conf, CheckFormat::RUN_IF_NECESSARY_TIMEOUT); 19 | } 20 | } 21 | function compile(FormulaCompiler $state) { 22 | $doc = $state->_add_primary_document(); 23 | return "({$doc} ? {$doc}->npages(PageCount_Fexpr::\$checkers[{$this->chkindex}]) : null)"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/formulas/f_realnumberoption.php: -------------------------------------------------------------------------------- 1 | option = $option; 11 | } 12 | function paper_options(&$oids) { 13 | $oids[$this->option->id] = true; 14 | } 15 | function viewable_by(Contact $user) { 16 | return $user->can_view_some_option($this->option); 17 | } 18 | function compile(FormulaCompiler $state) { 19 | $id = $this->option->id; 20 | $oval = "\$optvalue" . ($id < 0 ? "m" . -$id : $id); 21 | if ($state->check_gvar($oval)) { 22 | $ovv = $state->_add_option_value($this->option); 23 | $state->gstmt[] = "{$oval} = {$ovv} && {$ovv}->value !== null ? floatval({$ovv}->data()) : null;"; 24 | } 25 | return $oval; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/formulas/f_conflict.php: -------------------------------------------------------------------------------- 1 | ispc = is_object($ispc) ? $ispc->kwdef->is_pc : $ispc; 9 | $this->set_format(Fexpr::FBOOL); 10 | } 11 | function inferred_index() { 12 | return Fexpr::IDX_PC; 13 | } 14 | function compile(FormulaCompiler $state) { 15 | // XXX the actual search is different 16 | $idx = $state->loop_cid(); 17 | if ($state->index_type === Fexpr::IDX_MY) { 18 | $rt = $state->_prow() . "->has_conflict($idx)"; 19 | } else { 20 | $rt = "((" . $state->_add_conflict_types() . "[" . $idx . "] ?? 0) > " 21 | . CONFLICT_MAXUNCONFLICTED . ")"; 22 | if ($this->ispc) { 23 | $rt = "(" . $state->_add_pc() . "[" . $idx . "] ?? false ? $rt : null)"; 24 | } 25 | } 26 | return $rt; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/formulas/f_optionpresent.php: -------------------------------------------------------------------------------- 1 | option = $option; 11 | $this->set_format(Fexpr::FBOOL); 12 | } 13 | function paper_options(&$oids) { 14 | $oids[$this->option->id] = true; 15 | } 16 | function viewable_by(Contact $user) { 17 | return $user->can_view_some_option($this->option); 18 | } 19 | function compile(FormulaCompiler $state) { 20 | $id = $this->option->id; 21 | $ovp = "\$optpresent" . ($id < 0 ? "m" . -$id : $id); 22 | if ($state->check_gvar($ovp)) { 23 | $ovv = $state->_add_option_value($this->option); 24 | $state->gstmt[] = "{$ovp} = {$ovv} && {$ovv}->option->value_present({$ovv});"; 25 | } 26 | return $ovp; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assigners/a_error.php: -------------------------------------------------------------------------------- 1 | iswarning = $aj->name === "warning"; 10 | } 11 | function paper_universe($req, AssignmentState $state) { 12 | return "none"; 13 | } 14 | function allow_paper(PaperInfo $prow, AssignmentState $state) { 15 | return true; 16 | } 17 | function allow_user(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) { 18 | return true; 19 | } 20 | function apply(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) { 21 | $m = $req["message"] ?? ($this->iswarning ? "Warning" : "Error"); 22 | if (!Ftext::is_ftext($m)) { 23 | $m = "<0>{$m}"; 24 | } 25 | $state->msg_near($state->landmark(), $m, $this->iswarning ? 1 : 2); 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/papercolumns/pc_desirability.php: -------------------------------------------------------------------------------- 1 | user->is_manager()) 11 | return false; 12 | if ($visible) 13 | $pl->qopts["allReviewerPreference"] = true; 14 | return true; 15 | } 16 | function compare(PaperInfo $a, PaperInfo $b, PaperList $pl) { 17 | return $a->desirability() <=> $b->desirability(); 18 | } 19 | function content(PaperList $pl, PaperInfo $row) { 20 | $d = $row->desirability(); 21 | return $d < 0 ? "−" /*U+2122*/ . (-$d) : (string) $d; 22 | } 23 | function text(PaperList $pl, PaperInfo $row) { 24 | return (string) $row->desirability(); 25 | } 26 | function json(PaperList $pl, PaperInfo $row) { 27 | return $row->desirability(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/tagmessagereport.php: -------------------------------------------------------------------------------- 1 | */ 11 | public $message_list; 12 | /** @var ?list */ 13 | public $tags; 14 | /** @var ?list */ 15 | public $tags_conflicted; 16 | /** @var ?string */ 17 | public $tags_edit_text; 18 | /** @var ?string */ 19 | public $tags_view_html; 20 | /** @var ?string */ 21 | public $tag_decoration_html; 22 | /** @var ?string */ 23 | public $color_classes; 24 | /** @var ?string */ 25 | public $color_classes_conflicted; 26 | /** @var ?string */ 27 | public $status_html; 28 | 29 | #[\ReturnTypeWillChange] 30 | function jsonSerialize() { 31 | $r = []; 32 | foreach (get_object_vars($this) as $k => $v) { 33 | if ($v !== null) 34 | $r[$k] = $v; 35 | } 36 | return $r; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/t_userapi.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 15 | $this->user = $conf->root_user(); 16 | } 17 | 18 | function test_disable() { 19 | $user = $this->conf->checked_user_by_email("marina@poema.ru"); 20 | xassert_eqq($user->is_disabled(), false); 21 | 22 | $j = call_api("=account", $this->user, ["u" => "marina@poema.ru", "disable" => true], null); 23 | xassert($j->ok); 24 | $user = $this->conf->checked_user_by_email("marina@poema.ru"); 25 | xassert_eqq($user->is_disabled(), true); 26 | 27 | $j = call_api("=account", $this->user, ["u" => "marina@poema.ru", "enable" => true], null); 28 | xassert($j->ok); 29 | $user = $this->conf->checked_user_by_email("marina@poema.ru"); 30 | xassert_eqq($user->is_disabled(), false); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/formulas/f_reviewround.php: -------------------------------------------------------------------------------- 1 | set_format(Fexpr::FROUND); 8 | } 9 | function inferred_index() { 10 | return Fexpr::IDX_REVIEW; 11 | } 12 | function viewable_by(Contact $user) { 13 | return $user->is_reviewer(); 14 | } 15 | function compile(FormulaCompiler $state) { 16 | $rrow = $state->_rrow(); 17 | if ($state->index_type === Fexpr::IDX_MY) { 18 | return $state->define_gvar("myrevround", "{$rrow} ? {$rrow}->reviewRound : null"); 19 | } 20 | $view_score = $state->user->permissive_view_score_bound(); 21 | if (VIEWSCORE_REVIEWER <= $view_score) { 22 | return "null"; 23 | } 24 | $state->queryOptions["reviewSignatures"] = true; 25 | $rrow_vsb = $state->_rrow_view_score_bound(false); 26 | return "(" . VIEWSCORE_REVIEWER . " > {$rrow_vsb} ? {$rrow}->reviewRound : null)"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/formulas/f_revtype.php: -------------------------------------------------------------------------------- 1 | set_format(Fexpr::FREVTYPE); 9 | } 10 | function inferred_index() { 11 | return Fexpr::IDX_REVIEW; 12 | } 13 | function viewable_by(Contact $user) { 14 | return $user->is_reviewer(); 15 | } 16 | function compile(FormulaCompiler $state) { 17 | if ($state->index_type === Fexpr::IDX_MY) { 18 | $rt = $state->define_gvar("myrevtype", $state->_prow() . "->review_type(\$contact)"); 19 | } else { 20 | $view_score = $state->user->permissive_view_score_bound(); 21 | if (VIEWSCORE_REVIEWER <= $view_score) { 22 | return "null"; 23 | } 24 | $state->queryOptions["reviewSignatures"] = true; 25 | $rrow = $state->_rrow(); 26 | return "({$rrow} ? {$rrow}->reviewType : null)"; 27 | } 28 | return $rt; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/listactions/la_getlead.php: -------------------------------------------------------------------------------- 1 | type = $fj->type; 9 | } 10 | function allow(Contact $user, Qrequest $qreq) { 11 | return $user->isPC; 12 | } 13 | function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { 14 | $key = $this->type . "ContactId"; 15 | $can_view = "can_view_" . $this->type; 16 | $texts = []; 17 | foreach ($ssel->paper_set($user) as $row) { 18 | if ($row->$key && $user->$can_view($row, true)) { 19 | $name = $user->conf->user_by_id($row->$key, USER_SLICE); 20 | $texts[] = [$row->paperId, $row->title, $name->firstName, $name->lastName, $name->email]; 21 | } 22 | } 23 | return $user->conf->make_csvg($this->type . "s") 24 | ->select(["paper", "title", "given_name", "family_name", "{$this->type}email"]) 25 | ->append($texts); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/reviewfields/rf_text.php: -------------------------------------------------------------------------------- 1 | */ 6 | class Text_ReviewFieldSearch extends ReviewFieldSearch { 7 | /** @var int */ 8 | public $op; 9 | /** @var TextPregexes */ 10 | public $preg; 11 | 12 | /** @param Text_ReviewField $rf 13 | * @param int $op 14 | * @param TextPregexes $preg */ 15 | function __construct($rf, $op, $preg) { 16 | parent::__construct($rf); 17 | $this->op = $op; 18 | $this->preg = $preg; 19 | } 20 | 21 | function sqlexpr() { 22 | return "tfields is not null"; 23 | } 24 | 25 | function test_value($rrow, $fv) { 26 | $match = $fv !== null 27 | && $fv !== "" 28 | && $rrow->field_match_pregexes($this->preg, $this->rf->order); 29 | if (!$match) { 30 | if (($this->op & CountMatcher::RELALL) !== 0 && $fv !== null) { 31 | $this->finished = -1; 32 | } 33 | return false; 34 | } 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/api/api_job.php: -------------------------------------------------------------------------------- 1 | job ?? "")) === "") { 9 | return JsonResult::make_missing_error("job"); 10 | } else if (strlen($jobid) < 24 11 | || !preg_match('/\A\w+\z/', $jobid)) { 12 | return JsonResult::make_parameter_error("job"); 13 | } 14 | 15 | try { 16 | $tok = Job_Capability::find($jobid, $user->conf); 17 | } catch (CommandLineException $ex) { 18 | $tok = null; 19 | } 20 | if (!$tok) { 21 | return JsonResult::make_not_found_error("job"); 22 | } 23 | 24 | $ok = $tok->is_active(); 25 | // XXX is it meaningfully safer to treat inactive tokens as not found? 26 | $answer = ["ok" => $ok] + (array) $tok->data(); 27 | $answer["ok"] = $ok; 28 | $answer["update_at"] = $answer["update_at"] ?? $tok->timeUsed; 29 | return new JsonResult($ok ? 200 : 409, $answer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/t_ht.php: -------------------------------------------------------------------------------- 1 | '); 9 | xassert_eqq(Ht::select("x", [ 10 | ["optgroup", "a"], 11 | "b", 12 | "c" 13 | ], 1), 14 | ''); 15 | xassert_eqq(Ht::select("x", [ 16 | 1 => ["optgroup" => "a", "label" => "One"], 17 | 2 => ["optgroup" => "a", "label" => "Two"], 18 | 3 => ["label" => "Three"] 19 | ], 2), 20 | ''); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/formulas/f_topicscore.php: -------------------------------------------------------------------------------- 1 | isPC; 17 | } 18 | function compile(FormulaCompiler $state) { 19 | $state->queryOptions["topics"] = true; 20 | $prow = $state->_prow(); 21 | if ($state->index_type === Fexpr::IDX_MY) { 22 | return $state->define_gvar("mytopicscore", "{$prow}->topic_interest_score(\$contact)"); 23 | } else if ($state->user->can_view_pc()) { 24 | return "{$prow}->topic_interest_score(" . $state->loop_cid(true) . ")"; 25 | } else { 26 | return "(" . $state->loop_cid() . " == " . $state->user->contactId 27 | . " ? {$prow}->topic_interest_score(" . $state->loop_cid() . ")" 28 | . " : null)"; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/settings/s_sf.php: -------------------------------------------------------------------------------- 1 | */ 39 | public $xvalues; 40 | } 41 | 42 | class SfValue_Setting { 43 | public $id; 44 | public $name; 45 | public $order; 46 | 47 | // internal 48 | public $old_value; 49 | /** @var bool */ 50 | public $deleted = false; 51 | } 52 | -------------------------------------------------------------------------------- /test/t_mailer.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 12 | } 13 | 14 | function run_send_template(MailRecipients $mr, $template, $qreq = []) { 15 | if (!($qreq instanceof Qrequest)) { 16 | $qreq = (new Qrequest("POST", $qreq))->set_user($mr->user)->approve_token(); 17 | } 18 | ob_start(); 19 | try { 20 | $ms = new MailSender($mr, $qreq, 2); 21 | $ms->set_template($template); 22 | $ms->set_no_print(true)->set_send_all(true); 23 | $ms->prepare_sending_mailid(); 24 | $ms->run(); 25 | } catch (PageCompletion $unused) { 26 | } 27 | ob_end_clean(); 28 | } 29 | 30 | function test_send() { 31 | MailChecker::clear(); 32 | $user = $this->conf->checked_user_by_email("chair@_.com"); 33 | $mr = (new MailRecipients($user))->set_recipients("au")->set_paper_ids([13, 14, 15, 16]); 34 | $this->run_send_template($mr, "@authors"); 35 | MailChecker::check_db("t_mailer-send-1"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /etc/capabilityhandlers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "match": "0([1-9][0-9]*)a\\S+", 4 | "apply_function": "AuthorView_Capability::apply_old_author_view", 5 | "$comment": "XXX backward compat" 6 | }, 7 | { 8 | "match": "hcav_?(\\d+)[a-zA-Z]+", "type": 4, 9 | "apply_function": "AuthorView_Capability::apply_author_view" 10 | }, 11 | { 12 | "match": "([1-9][0-9]*)ra(\\S+)", 13 | "apply_function": "ReviewAccept_Capability::apply_old_review_acceptor", 14 | "$comment": "XXX backward compat" 15 | }, 16 | { 17 | "match": "hcra_?([1-9][0-9]*)[a-zA-Z]+", "type": 5, 18 | "apply_function": "ReviewAccept_Capability::apply_review_acceptor" 19 | }, 20 | { 21 | "match": "ra([1-9][0-9]*)([a-zA-Z]+)", 22 | "apply_function": "ReviewAccept_Capability::apply_old_review_acceptor" 23 | }, 24 | { 25 | "match": "kiosk-([a-zA-Z0-9]+)", 26 | "apply_function": "MeetingTracker::apply_kiosk_capability", 27 | "$comment": "XXX backward compat" 28 | }, 29 | { 30 | "match": "hckk_?([a-zA-Z0-9]+)", 31 | "apply_function": "MeetingTracker::apply_kiosk_capability" 32 | }, 33 | { 34 | "type": 3, "cleanup_function": "Upload_API::cleanup" 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /devel/apidoc/tags.md: -------------------------------------------------------------------------------- 1 | # get /{p}/tags 2 | 3 | > Retrieve submission tags 4 | 5 | * response_schema tag_response 6 | 7 | 8 | # post /{p}/tags 9 | 10 | > Change submission tags 11 | 12 | * response_schema tag_response 13 | * response_schema search_response 14 | 15 | 16 | # post /assigntags 17 | 18 | > Change several tags 19 | 20 | * param =tagassignment string:Comma-separated list of paper IDs and tag assignments 21 | * param ?=search search_parameter_specification 22 | * response_schema search_response 23 | 24 | 25 | # get /alltags 26 | 27 | > Retrieve all visible tags 28 | 29 | * response tags tag_list 30 | 31 | 32 | # get /taganno 33 | 34 | > Retrieve tag annotations 35 | 36 | * param tag tag 37 | * param ?search search_parameter_specification 38 | * response tag tag 39 | * response editable boolean 40 | * response anno [tag_annotation] 41 | * response_schema search_response 42 | 43 | 44 | # post /taganno 45 | 46 | > Change tag annotations 47 | 48 | * param +anno [tag_annotation] 49 | * response tag tag 50 | * response editable boolean 51 | * response anno [tag_annotation] 52 | * response_schema search_response 53 | 54 | 55 | # get /{p}/tagmessages 56 | 57 | > Retrieve tag edit messages 58 | 59 | 60 | # get /{p}/votereport 61 | 62 | > Retrieve vote analysis 63 | 64 | * param tag tag 65 | -------------------------------------------------------------------------------- /src/api/api_decision.php: -------------------------------------------------------------------------------- 1 | conf->decision_set(); 8 | if ($qreq->method() !== "GET") { 9 | $aset = (new AssignmentSet($user))->set_override_conflicts(true); 10 | $aset->enable_papers($prow); 11 | if (is_numeric($qreq->decision) && $decset->contains(+$qreq->decision)) { 12 | $qreq->decision = $decset->get(+$qreq->decision)->name; 13 | } 14 | $aset->parse("paper,action,decision\n{$prow->paperId},decision," . CsvGenerator::quote($qreq->decision)); 15 | if (!$aset->execute()) { 16 | return $aset->json_result(); 17 | } 18 | $prow->load_decision(); 19 | } 20 | $dec = $prow->viewable_decision($user); 21 | $jr = new JsonResult([ 22 | "ok" => true, 23 | "decision" => $dec->id, 24 | "decision_html" => $dec->name_as(5) 25 | ]); 26 | if ($user->can_set_decision($prow)) { 27 | $jr->content["editable"] = true; 28 | } 29 | return $jr; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/t_s3.php: -------------------------------------------------------------------------------- 1 | opt("testS3Key")) 10 | || !($s3s = $conf->opt("testS3Secret")) 11 | || !in_array($tester, $conf->opt("testS3Testers") ?? [])) { 12 | return null; 13 | } 14 | $s3r = $conf->opt("testS3Region"); 15 | $s3b = $conf->opt("testS3Bucket") ?? ("hotcrptest-" . strtolower(encode_token(random_bytes(8)))); 16 | return S3Client::make([ 17 | "key" => $s3k, "secret" => $s3s, "region" => $s3r, 18 | "bucket" => $s3b 19 | ]); 20 | } 21 | 22 | /** @param S3Client|array{?string,?string,?string,?string} $s3i 23 | * @return array{?string,?string,?string,?string} */ 24 | static function install_s3_options(Conf $conf, $s3i) { 25 | $r = []; 26 | foreach (["s3_bucket", "s3_key", "s3_secret", "s3_region"] as $i => $k) { 27 | $v = $s3i instanceof S3Client ? $s3i->$k : $s3i[$i]; 28 | $r[] = $conf->opt($k); 29 | $conf->set_opt($k, $v); 30 | } 31 | return $r; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/search/st_optiontext.php: -------------------------------------------------------------------------------- 1 | match = $match; 14 | } 15 | function debug_json() { 16 | return [ 17 | "type" => $this->type, 18 | "option" => $this->option->search_keyword(), 19 | "match" => $this->match 20 | ]; 21 | } 22 | function test(PaperInfo $row, $xinfo) { 23 | if ($this->user->can_view_option($row, $this->option) 24 | && ($ov = $row->option($this->option)) 25 | && ($ov->data() ?? "") !== "") { 26 | $this->pregexes = $this->pregexes ?? Text::star_text_pregexes($this->match); 27 | return Text::match_pregexes($this->pregexes, (string) $ov->data(), null); 28 | } else { 29 | return false; 30 | } 31 | } 32 | function about() { 33 | return self::ABOUT_PAPER; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/t_xtcheck.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 14 | } 15 | 16 | static function check() { 17 | ++self::$nchecks; 18 | return true; 19 | } 20 | 21 | function test_xt_check() { 22 | $xtp = new XtParams($this->conf, null); 23 | xassert($xtp->check("allow")); 24 | xassert(!$xtp->check("deny")); 25 | xassert($xtp->check("!deny")); 26 | xassert(!$xtp->check("! allow")); 27 | xassert($xtp->check("!!allow")); 28 | xassert($xtp->check("!!!deny")); 29 | xassert($xtp->check("allow || deny")); 30 | xassert(!$xtp->check("allow && deny")); 31 | xassert($xtp->check("!(allow && deny)")); 32 | xassert($xtp->check("!(allow && deny)")); 33 | xassert($xtp->check("!opt.sendEmail")); 34 | xassert(!$xtp->check("opt.sendEmail && XtCheck_Tester::check && allow")); 35 | xassert_eqq(self::$nchecks, 0); 36 | xassert($xtp->check("XtCheck_Tester::check && allow")); 37 | xassert_eqq(self::$nchecks, 1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/ldap/README.md: -------------------------------------------------------------------------------- 1 | HotCRP LDAP test server 2 | ======================= 3 | 4 | This directory contains a configuration for a [Light 5 | LDAP](https://github.com/lldap/lldap) server that can be used to test HotCRP’s 6 | LDAP support. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | 1. Run `docker compose up` in this directory. 13 | 14 | 2. (Optional) Create users using the LLDAP admin interface. 15 | 16 | HotCRP ships with an LLDAP SQLite database whose users are listed below in 17 | “Default users.” If you want to change these users or create other ones, 18 | sign in to the LLDAP administration server at `http://localhost:17170` 19 | using username `admin` and password `aequee0Oe1ee1A` . 20 | 21 | 3. Configure HotCRP to use the running LLDAP server for authentication by 22 | setting `$Opt["ldapLogin"]` in `conf/options.php`: 23 | 24 | ```php 25 | $Opt["ldapLogin"] = "ldap://localhost:17169/ uid=*,ou=people,dc=hotcrp,dc=org"; 26 | ``` 27 | 28 | 4. Sign in to HotCRP as one of the LDAP users. 29 | 30 | 31 | Default users 32 | ------------- 33 | 34 | | User | Name | Mail | Password | 35 | |----------|--------------|------------------------|----------------| 36 | | fran | Fran Framer | fran@hotcrp-ldap.org | raec3ohL5u | 37 | | paula | Paula Books | paula@hotcrp-ldap.org | gi1eiluoCh | 38 | -------------------------------------------------------------------------------- /src/api/api_events.php: -------------------------------------------------------------------------------- 1 | is_reviewer()) { 11 | return JsonResult::make_permission_error(); 12 | } 13 | $from = $qreq->from; 14 | if (!$from || !ctype_digit($from)) { 15 | $from = Conf::$now; 16 | } 17 | $when = $from; 18 | $rf = $user->conf->review_form(); 19 | $events = new PaperEvents($user); 20 | $rows = []; 21 | $more = false; 22 | foreach ($events->events($when, 11) as $xr) { 23 | if (count($rows) == 10) { 24 | $more = true; 25 | } else { 26 | if ($xr->crow) { 27 | $rows[] = $xr->crow->unparse_flow_entry($user); 28 | } else { 29 | $rows[] = $rf->unparse_flow_entry($xr->prow, $xr->rrow, $user); 30 | } 31 | $when = $xr->eventTime; 32 | } 33 | } 34 | return new JsonResult([ 35 | "ok" => true, "from" => (int) $from, "to" => (int) $when - 1, 36 | "rows" => $rows, "more" => $more 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/papercolumns/pc_topics.php: -------------------------------------------------------------------------------- 1 | conf->has_topics()) { 13 | return false; 14 | } 15 | if ($visible) { 16 | $pl->qopts["topics"] = 1; 17 | } 18 | // only managers can see other users’ topic interests 19 | $this->interest_contact = $pl->reviewer_user(); 20 | if ($this->interest_contact->contactId !== $pl->user->contactId 21 | && !$pl->user->is_manager()) { 22 | $this->interest_contact = null; 23 | } 24 | return true; 25 | } 26 | function content_empty(PaperList $pl, PaperInfo $row) { 27 | return $row->topicIds === ""; 28 | } 29 | function content(PaperList $pl, PaperInfo $row) { 30 | return $pl->conf->topic_set()->unparse_list_html($row->topic_list(), $this->interest_contact ? $this->interest_contact->topic_interest_map() : null); 31 | } 32 | function text(PaperList $pl, PaperInfo $row) { 33 | return $row->unparse_topics_text(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/search/st_documentname.php: -------------------------------------------------------------------------------- 1 | want = $want; 17 | $this->match = $match; 18 | } 19 | function debug_json() { 20 | return [$this->type, $this->option->search_keyword(), $this->match]; 21 | } 22 | function test(PaperInfo $row, $xinfo) { 23 | if ($this->user->can_view_option($row, $this->option) 24 | && ($ov = $row->option($this->option))) { 25 | $this->pregexes = $this->pregexes ?? Text::star_text_pregexes($this->match); 26 | foreach ($ov->document_set() as $d) { 27 | $m = $this->pregexes->match($d->filename, null); 28 | if ($m === $this->want) 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | function about() { 35 | return self::ABOUT_PAPER; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /batch/fileinfo.php: -------------------------------------------------------------------------------- 1 | run()); 8 | } 9 | 10 | class Fileinfo_Batch { 11 | /** @var list */ 12 | public $files = []; 13 | 14 | /** @return int */ 15 | function run() { 16 | if (empty($this->files)) { 17 | $this->files[] = "-"; 18 | } 19 | foreach ($this->files as $file) { 20 | if ($file === "-") { 21 | $content = stream_get_contents(STDIN); 22 | } else { 23 | $content = file_get_contents($file); 24 | } 25 | fwrite(STDOUT, sprintf("%-39s %s\n", $file, json_encode(Mimetype::content_info($content)))); 26 | } 27 | return 0; 28 | } 29 | 30 | /** @param list $argv 31 | * @return Fileinfo_Batch */ 32 | static function make_args($argv) { 33 | $arg = (new Getopt)->long( 34 | "help,h !" 35 | )->description("Report HotCRP-derived file info. 36 | Usage: php batch/fileinfo.php FILES...") 37 | ->helpopt("help") 38 | ->parse($argv); 39 | $fib = new Fileinfo_Batch(); 40 | $fib->files = $arg["_"]; 41 | return $fib; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/search/st_optionpresent.php: -------------------------------------------------------------------------------- 1 | $this->type, 12 | "option" => $this->option->search_keyword() 13 | ]; 14 | } 15 | function is_sqlexpr_precise() { 16 | return $this->option->always_visible() 17 | && $this->option->is_value_present_trivial(); 18 | } 19 | function test(PaperInfo $row, $xinfo) { 20 | return $this->user->can_view_option($row, $this->option) 21 | && ($ov = $row->option($this->option)) 22 | && $this->option->value_present($ov); 23 | } 24 | function script_expression(PaperInfo $row, $about) { 25 | if (($about & self::ABOUT_PAPER) === 0) { 26 | return parent::script_expression($row, $about); 27 | } else if ($this->user->can_view_option($row, $this->option)) { 28 | return $this->option->present_script_expression(); 29 | } else { 30 | return false; 31 | } 32 | } 33 | function about() { 34 | return self::ABOUT_PAPER; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/api/api_formatcheck.php: -------------------------------------------------------------------------------- 1 | doc, $user); 9 | } catch (Exception $unused) { 10 | return JsonResult::make_error(404, "<0>Document not found"); 11 | } 12 | if (($whynot = $docreq->perm_view_document($user))) { 13 | return JsonResult::make_message_list(isset($whynot["permission"]) ? 403 : 404, $whynot->message_list()); 14 | } 15 | if (!($doc = $docreq->prow->document($docreq->dtype, $docreq->docid, true))) { 16 | return JsonResult::make_error(404, "<0>Document not found"); 17 | } 18 | $runflag = friendly_boolean($qreq->soft) ? CheckFormat::RUN_IF_NECESSARY : CheckFormat::RUN_ALWAYS; 19 | $cf = new CheckFormat($user->conf, $runflag); 20 | $cf->check_document($doc); 21 | $ms = $cf->document_messages($doc); 22 | return [ 23 | "ok" => $cf->check_ok(), 24 | "docid" => $doc->paperStorageId, 25 | "npages" => $cf->npages, 26 | "nwords" => $cf->nwords, 27 | "problem_fields" => $cf->problem_fields(), 28 | "has_error" => $cf->has_error(), 29 | "message_list" => $ms->message_list() 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/review1A.txt: -------------------------------------------------------------------------------- 1 | ==+== Testconf I Paper Review Form 2 | ==-== DO NOT CHANGE LINES THAT START WITH "==+==" UNLESS DIRECTED! 3 | ==-== For further guidance, or to upload this file when you are done, go to: 4 | ==-== http://hotcrp.lcdf.org/test/offline 5 | 6 | ==+== ===================================================================== 7 | ==+== Begin Review #1A 8 | ==+== Reviewer: Mary Baker 9 | ==-== Updated 8 Aug 2017 6:46:19pm EDT 10 | 11 | ==+== Paper #1 12 | ==-== Title: Scalable Timers for Soft State Protocols 13 | 14 | ==+== Review Readiness 15 | ==-== Enter "Ready" if the review is ready for others to see: 16 | 17 | Ready 18 | 19 | ==+== A. Overall merit 20 | ==-== Choices: 21 | ==-== 1. Reject 22 | ==-== 2. Weak reject 23 | ==-== 3. Weak accept 24 | ==-== 4. Accept 25 | ==-== 5. Strong accept 26 | ==-== Enter the number of your choice: 27 | 28 | 4 29 | 30 | ==+== B. Reviewer expertise 31 | ==-== Choices: 32 | ==-== 1. No familiarity 33 | ==-== 2. Some familiarity 34 | ==-== 3. Knowledgeable 35 | ==-== 4. Expert 36 | ==-== Enter the number of your choice: 37 | 38 | 2 39 | 40 | ==+== C. Paper summary 41 | 42 | 43 | 44 | ==+== D. Comments for authors 45 | 46 | 47 | 48 | ==+== E. Comments for PC 49 | ==-== Hidden from authors. 50 | 51 | 52 | 53 | 54 | This is a test of leading whitespace 55 | 56 | It should be preserved 57 | And defended 58 | 59 | ==+== Scratchpad (for unsaved private notes) 60 | 61 | ==+== End Review 62 | -------------------------------------------------------------------------------- /src/help/h_scoresort.php: -------------------------------------------------------------------------------- 1 | Some paper search results include columns with score graphs. Click on a score 9 | column heading to sort the paper list using that score. Search > View 10 | options changes how scores are sorted. There are five choices:

11 | 12 |
13 | 14 |
Counts (default)
15 | 16 |
Sort by the number of highest scores, then the number of second-highest 17 | scores, then the number of third-highest scores, and so on. To sort a paper 18 | with fewer reviews than others, HotCRP adds phantom reviews with scores just 19 | below the paper’s lowest real score. Also known as Minshall score.
20 | 21 |
Average
22 |
Sort by the average (mean) score.
23 | 24 |
Median
25 |
Sort by the median score.
26 | 27 |
Variance
28 |
Sort by the variance in scores.
29 | 30 |
Max − min
31 |
Sort by the difference between the largest and smallest scores (a good 32 | measure of differences of opinion).
33 | 34 |
My score
35 |
Sort by your score. In the score graphs, your score is highlighted with a 36 | darker colored square.
37 | 38 |
"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/settings/s_basics.php: -------------------------------------------------------------------------------- 1 | info page 3 | // Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | class Basics_SettingParser extends SettingParser { 6 | static function print_names(SettingValues $sv) { 7 | $sv->print_entry_group("conference_abbreviation", null, [ 8 | "hint" => "Examples: “HotOS XIV”, “NSDI '14”" 9 | ]); 10 | $sv->print_entry_group("conference_name", null, [ 11 | "hint" => "Example: “14th Workshop on Hot Topics in Operating Systems”" 12 | ]); 13 | $sv->print_entry_group("conference_url", null, [ 14 | "hint" => "Example: “https://yourconference.org/”" 15 | ]); 16 | } 17 | 18 | static function print_email(SettingValues $sv) { 19 | $sv->print_entry_group("email_default_cc", null); 20 | $sv->print_entry_group("email_default_reply_to", null); 21 | } 22 | 23 | function apply_req(Si $si, SettingValues $sv) { 24 | if (($v = $sv->base_parse_req($si)) !== null 25 | && $sv->update($si->name, $v) 26 | && $sv->conf->contactdb()) { 27 | $sv->register_cleanup_function("update_shortName", function () use ($sv) { 28 | $conf = $sv->conf; 29 | Dbl::ql($conf->contactdb(), "update Conferences set shortName=?, longName=? where dbName=?", $conf->short_name, $conf->long_name, $conf->dbname); 30 | }); 31 | } 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/listactions/la_decide.php: -------------------------------------------------------------------------------- 1 | can_set_some_decision(); 8 | } 9 | static function render(PaperList $pl, Qrequest $qreq) { 10 | $opts = []; 11 | foreach ($pl->conf->decision_set() as $dec) { 12 | $opts[$dec->id] = $dec->name_as(5); 13 | } 14 | return ["Set to  " 15 | . Ht::select("decision", $opts, "", ["class" => "want-focus js-submit-action-info-decide"]) 16 | . $pl->action_submit("decide")]; 17 | } 18 | function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { 19 | $aset = (new AssignmentSet($user))->set_override_conflicts(true); 20 | $did = $qreq->decision; 21 | if (is_numeric($did) 22 | && ($dec = $user->conf->decision_set()->get(+$did))) { 23 | $did = $dec->name; 24 | } 25 | $aset->parse("paper,action,decision\n" . join(" ", $ssel->selection()) . ",decision," . CsvGenerator::quote($did)); 26 | if ($aset->execute()) { 27 | return new Redirection($user->conf->selfurl($qreq, ["atab" => "decide", "decision" => $qreq->decision], Conf::HOTURL_RAW | Conf::HOTURL_REDIRECTABLE)); 28 | } else { 29 | $user->conf->feedback_msg($aset->message_list()); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/options/o_checkboxes.php: -------------------------------------------------------------------------------- 1 | assign_values($args->values ?? [], $args->ids ?? null); 13 | $this->compact = true; 14 | } 15 | 16 | 17 | function jsonSerialize() { 18 | $j = parent::jsonSerialize(); 19 | $j->values = $this->values(); 20 | if ($this->is_ids_nontrivial()) { 21 | $j->ids = $this->ids(); 22 | } 23 | return $j; 24 | } 25 | 26 | function export_setting() { 27 | $sfs = parent::export_setting(); 28 | $this->unparse_values_setting($sfs); 29 | return $sfs; 30 | } 31 | 32 | /** @return TopicSet */ 33 | function topic_set() { 34 | return $this->values_topic_set(); 35 | } 36 | 37 | 38 | function search_examples(Contact $viewer, $context) { 39 | $a = [$this->has_search_example()]; 40 | if (($q = $this->value_search_keyword(2))) { 41 | $a[] = new SearchExample( 42 | $this, $this->search_keyword() . ":{value}", 43 | "<0>submission’s {title} field has value ‘{value}’", 44 | new FmtArg("value", $this->values[1]) 45 | ); 46 | } 47 | return $a; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/searchoperator.php: -------------------------------------------------------------------------------- 1 | type = $type; 35 | $this->subtype = $subtype; 36 | $this->precedence = $precedence; 37 | $this->flags = $flags; 38 | } 39 | 40 | /** @return bool */ 41 | function unary() { 42 | return ($this->flags & self::F_UNARY) !== 0; 43 | } 44 | 45 | /** @param string $subtype 46 | * @return SearchOperator */ 47 | function make_subtype($subtype) { 48 | assert(($this->flags & self::F_ALLOW_SUBTYPE) !== 0); 49 | return new SearchOperator($this->type, $this->precedence, ($this->flags & ~self::F_ALLOW_SUBTYPE) | self::F_SUBTYPE, $subtype); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/autoassigners/aa_prefconflict.php: -------------------------------------------------------------------------------- 1 | $pcids 7 | * @param list $papersel */ 8 | function __construct(Contact $user, $pcids, $papersel) { 9 | parent::__construct($user, $pcids, $papersel); 10 | $this->set_assignment_action("conflict"); 11 | } 12 | 13 | /** @param bool $exists_submitted 14 | * @return Dbl_Result */ 15 | static function query_result(Conf $conf, $exists_submitted) { 16 | $qsuffix = $exists_submitted ? " and P.timeSubmitted>0 limit 1" : ""; 17 | return $conf->ql_raw("select PRP.paperId, PRP.contactId, PRP.preference 18 | from PaperReviewPreference PRP 19 | join ContactInfo c on (c.contactId=PRP.contactId and c.roles!=0 and (c.roles&" . Contact::ROLE_PC . ")!=0) 20 | join Paper P on (P.paperId=PRP.paperId) 21 | left join PaperConflict PC on (PC.paperId=PRP.paperId and PC.contactId=PRP.contactId) 22 | where PRP.preference<=-100 and coalesce(PC.conflictType,0)<=" . CONFLICT_MAXUNCONFLICTED . " 23 | and P.timeWithdrawn<=0" . $qsuffix); 24 | } 25 | 26 | function run() { 27 | $result = self::query_result($this->conf, false); 28 | while (($row = $result->fetch_row())) { 29 | $this->assign1((int) $row[1], (int) $row[0]); 30 | } 31 | Dbl::free($result); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2022 Eddie Kohler 2 | Copyright (c) 2006-2008 Regents of the University of California 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | The names and trademarks of copyright holders may not be used in 15 | advertising or publicity pertaining to the software without specific 16 | prior permission. Title to copyright in this software and any associated 17 | documentation will at all times remain with the copyright holders. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | 27 | HotCRP was originally derived from Dirk Grunwald's CRP (Copyright (c) 28 | 2002-2005 Dirk Grunwald et al., distributed under an "MIT license"). 29 | -------------------------------------------------------------------------------- /src/apihelpers.php: -------------------------------------------------------------------------------- 1 | contactId > 0 && $text === (string) $viewer->contactId) 14 | || ($viewer->has_email() && strcasecmp($text, $viewer->email) === 0)) { 15 | return $viewer; 16 | } 17 | if (ctype_digit($text)) { 18 | $u = $viewer->conf->user_by_id(intval($text), USER_SLICE); 19 | } else { 20 | $u = $viewer->conf->user_by_email($text, USER_SLICE); 21 | } 22 | if ($u) { 23 | return $u; 24 | } else if ($viewer->isPC) { 25 | JsonResult::make_not_found_error($field, "<0>User not found")->complete(); 26 | } else { 27 | JsonResult::make_permission_error()->complete(); 28 | } 29 | } 30 | 31 | /** @param ?string $text 32 | * @param ?PaperInfo $prow 33 | * @return Contact */ 34 | static function parse_reviewer_for($text, Contact $viewer, $prow) { 35 | $u = self::parse_user($text, $viewer); 36 | if ($u->contactId === $viewer->contactId 37 | || ($prow ? $viewer->can_administer($prow) : $viewer->privChair)) { 38 | return $u; 39 | } else { 40 | JsonResult::make_permission_error()->complete(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/listactions/la_mail.php: -------------------------------------------------------------------------------- 1 | template = $uf->mail_template ?? null; 10 | $this->recipients = $uf->recipients ?? null; 11 | } 12 | function allow(Contact $user, Qrequest $qreq) { 13 | return $user->is_manager() && $qreq->page() !== "reviewprefs"; 14 | } 15 | static function render(PaperList $pl, Qrequest $qreq, ComponentSet $gex) { 16 | $sel_opt = ListAction::members_selector_options($gex, "mail"); 17 | if (!empty($sel_opt)) { 18 | return Ht::select("mailfn", $sel_opt, $qreq->mailfn, 19 | ["class" => "want-focus js-submit-action-info-mail ignore-diff"]) 20 | . $pl->action_submit("mail", ["class" => "can-submit-all", "formmethod" => "get"]); 21 | } else { 22 | return null; 23 | } 24 | } 25 | function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { 26 | $args = []; 27 | if ($ssel->equals_search(new PaperSearch($user, $qreq))) { 28 | $args["q"] = $qreq->q; 29 | $args["plimit"] = 1; 30 | } else { 31 | $args["p"] = join(" ", $ssel->selection()); 32 | } 33 | $args["t"] = $qreq->t; 34 | $args["template"] = $this->template; 35 | $args["to"] = $this->recipients; 36 | return new Redirection($user->conf->hoturl("mail", $args)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /etc/reviewfieldtypes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dropdown", "title": "Dropdown", "order": 300, 4 | "properties": {"values_text": true, "scheme": true, "required": true}, 5 | "id_prefix": "s", 6 | "conversions": [{"from": "radio"}] 7 | }, 8 | { 9 | "name": "radio", "title": "Radio buttons", "order": 200, 10 | "properties": {"values_text": true, "scheme": true, "required": true}, 11 | "id_prefix": "s", 12 | "conversions": [{"from": "dropdown"}] 13 | }, 14 | { 15 | "name": "text", "title": "Text", "order": 500, 16 | "sample": {"value": "Text entry"}, 17 | "id_prefix": "t" 18 | }, 19 | { 20 | "name": "checkbox", "title": "Checkbox", "order": 600, 21 | "properties": {"scheme": true, "required": true, "checkbox": true}, 22 | "id_prefix": "s", 23 | "conversions": [ 24 | {"to": "radio", "setting_function": "Checkbox_ReviewField::convert_to_score_setting"}, 25 | {"to": "dropdown", "setting_function": "Checkbox_ReviewField::convert_to_score_setting"}, 26 | {"from": "radio", "allow_function": "Checkbox_ReviewField::allow_convert_from_score", "setting_function": "Checkbox_ReviewField::convert_from_score_setting"}, 27 | {"from": "dropdown", "allow_function": "Checkbox_ReviewField::allow_convert_from_score", "setting_function": "Checkbox_ReviewField::convert_from_score_setting"} 28 | ] 29 | }, 30 | { 31 | "name": "checkboxes", "title": "Checkboxes", "order": 700, 32 | "properties": {"values_text": true, "scheme": true, "required": true}, 33 | "id_prefix": "s" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /src/papercolumns/pc_commenters.php: -------------------------------------------------------------------------------- 1 | viewable_comments($pl->user); 11 | } 12 | function content(PaperList $pl, PaperInfo $row) { 13 | $crows = $row->viewable_comments($pl->user); 14 | $cnames = array_map(function ($cx) use ($pl) { 15 | $n = $t = $cx[0]->unparse_commenter_html($pl->user); 16 | if (($tags = $cx[0]->viewable_tags($pl->user)) 17 | && ($color = $cx[0]->conf->tags()->color_classes($tags))) { 18 | $t = "{$n}"; 19 | } 20 | if ($cx[1] > 1) { 21 | $t .= " ({$cx[1]})"; 22 | } 23 | return $t . $cx[2]; 24 | }, CommentInfo::group_by_identity($crows, $pl->user, true)); 25 | return join(" ", $cnames); 26 | } 27 | function text(PaperList $pl, PaperInfo $row) { 28 | $crows = $row->viewable_comments($pl->user); 29 | $cnames = array_map(function ($cx) use ($pl) { 30 | $t = $cx[0]->unparse_commenter_text($pl->user); 31 | if ($cx[1] > 1) { 32 | $t .= " ({$cx[1]})"; 33 | } 34 | return $t . $cx[2]; 35 | }, CommentInfo::group_by_identity($crows, $pl->user, false)); 36 | return join(" ", $cnames); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/fieldchangeset.php: -------------------------------------------------------------------------------- 1 | */ 7 | public $_m = []; 8 | 9 | const ABSENT = 0; 10 | const UNCHANGED = 1; 11 | const CHANGED = 2; 12 | 13 | /** @param ?string $s 14 | * @return $this */ 15 | function mark_unchanged($s) { 16 | $this->apply($s, self::UNCHANGED); 17 | return $this; 18 | } 19 | 20 | /** @param ?string $s 21 | * @return $this */ 22 | function mark_changed($s) { 23 | $this->apply($s, self::CHANGED); 24 | return $this; 25 | } 26 | 27 | /** @param string $src 28 | * @param string $dst 29 | * @return $this */ 30 | function mark_synonym($src, $dst) { 31 | $this->_m[$src] = $this->_m[$dst] = 32 | ($this->_m[$src] ?? 0) | ($this->_m[$dst] ?? 0); 33 | return $this; 34 | } 35 | 36 | /** @param ?string $s 37 | * @param 1|2 $bit */ 38 | private function apply($s, $bit) { 39 | foreach (explode(" ", $s ?? "") as $word) { 40 | if ($word !== "") { 41 | $this->_m[$word] = ($this->_m[$word] ?? 0) | $bit; 42 | if (($colon = strpos($word, ":")) !== false) { 43 | $px = substr($word, 0, $colon); 44 | $this->_m[$px] = ($this->_m[$px] ?? 0) | $bit; 45 | } 46 | } 47 | } 48 | } 49 | 50 | /** @param string $key 51 | * @return 0|1|2|3 */ 52 | function test($key) { 53 | return $this->_m[$key] ?? 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/papercolumns/pc_pcconflicts.php: -------------------------------------------------------------------------------- 1 | user->can_view_some_conflicts()) { 11 | return false; 12 | } 13 | if ($visible) { 14 | $pl->qopts["allConflictType"] = 1; 15 | } 16 | return true; 17 | } 18 | function content_empty(PaperList $pl, PaperInfo $row) { 19 | return !$pl->user->can_view_conflicts($row); 20 | } 21 | function content(PaperList $pl, PaperInfo $row) { 22 | $y = []; 23 | $pcm = $row->conf->pc_members(); 24 | foreach ($row->conflict_types() as $uid => $ctype) { 25 | if (!($pc = $pcm[$uid] ?? null) 26 | || !Conflict::is_conflicted($ctype)) { 27 | continue; 28 | } 29 | $y[$pc->pc_index] = $pl->user->reviewer_html_for($pc); 30 | } 31 | ksort($y); 32 | return join(", ", $y); 33 | } 34 | function text(PaperList $pl, PaperInfo $row) { 35 | $y = []; 36 | $pcm = $row->conf->pc_members(); 37 | foreach ($row->conflict_types() as $uid => $ctype) { 38 | if (!($pc = $pcm[$uid] ?? null) 39 | || !Conflict::is_conflicted($ctype)) { 40 | continue; 41 | } 42 | $y[$pc->pc_index] = $pl->user->reviewer_text_for($pc); 43 | } 44 | ksort($y); 45 | return join("; ", $y); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/textformat.php: -------------------------------------------------------------------------------- 1 | format = $format; 21 | foreach ($settings as $k => $v) { 22 | $this->$k = $v; 23 | } 24 | } 25 | function description_text() { 26 | if ($this->description_text === null 27 | && $this->description !== null) { 28 | $this->description_text = Text::html_to_text($this->description); 29 | } 30 | return (string) $this->description_text; 31 | } 32 | function description_preview_html() { 33 | $d = []; 34 | if ((string) $this->description !== "") { 35 | $d[] = $this->description; 36 | } else if ((string) $this->description_text !== "") { 37 | $d[] = htmlspecialchars($this->description_text); 38 | } 39 | if ($this->has_preview) { 40 | $d[] = ''; 42 | } 43 | if ($d) { 44 | return '
' 45 | . join(' · ', $d) . '
'; 46 | } else { 47 | return ""; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/search/st_cmtafter.php: -------------------------------------------------------------------------------- 1 | user = $user; 17 | $this->cm = $cm; 18 | $this->time = $time; 19 | } 20 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 21 | if (!preg_match('/\A(.++)(|(?:[!=<>]=?|≥|≤|≠)\d+)\z/', $word, $m) 22 | || ($t = $srch->conf->parse_time($m[1])) === false) { 23 | $srch->lwarning($sword, "<0>Expected ‘cmtafter:DATE’"); 24 | return new False_SearchTerm; 25 | } 26 | $cm = new CountMatcher($m[2] !== "" ? $m[2] : ">0"); 27 | return new CmtAfter_SearchTerm($srch->user, $cm, $t); 28 | } 29 | function sqlexpr(SearchQueryInfo $sqi) { 30 | $sqi->add_comment_signature_columns(); 31 | if ($this->cm->test(0)) { 32 | return "true"; 33 | } 34 | return "exists(select * from PaperComment where paperId=Paper.paperId and timeModified>={$this->time})"; 35 | } 36 | function test(PaperInfo $row, $xinfo) { 37 | $n = 0; 38 | foreach ($row->viewable_comment_skeletons($this->user, true) as $crow) { 39 | if ($crow->mtime($this->user) >= $this->time) 40 | ++$n; 41 | } 42 | return $this->cm->test($n); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/search/st_badge.php: -------------------------------------------------------------------------------- 1 | user = $user; 15 | $this->word = $word; 16 | } 17 | function sqlexpr(SearchQueryInfo $sqi) { 18 | return 'exists (select * from PaperTag where paperId=Paper.paperId)'; 19 | } 20 | function test(PaperInfo $row, $xinfo) { 21 | $tags = $row->viewable_tags($this->user); 22 | foreach ($row->conf->tags()->badges($tags) as $tb) { 23 | if ($this->word === "any" || $this->word === $tb[1]) 24 | return true; 25 | } 26 | return false; 27 | } 28 | function debug_json() { 29 | return ["type" => $this->type, "style" => $this->word]; 30 | } 31 | function about() { 32 | return self::ABOUT_PAPER; 33 | } 34 | 35 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 36 | $word = strtolower($word); 37 | if ($word === "any" || $word === "none") { 38 | return (new Badge_SearchTerm($srch->user, "any"))->negate_if($word === "none"); 39 | } else if (($ks = $srch->conf->tags()->known_badge($word))) { 40 | return new Badge_SearchTerm($srch->user, $ks->style); 41 | } else { 42 | $srch->lwarning($sword, "<0>Badge color ‘{$word}’ not found"); 43 | return new False_SearchTerm; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/search/st_realnumberoption.php: -------------------------------------------------------------------------------- 1 | compar = $relation; 15 | $this->value = $value; 16 | } 17 | function debug_json() { 18 | return [$this->type, $this->option->search_keyword()]; 19 | } 20 | function test(PaperInfo $row, $xinfo) { 21 | return $this->user->can_view_option($row, $this->option) 22 | && ($ov = $row->option($this->option)) 23 | && $ov->value !== null 24 | && CountMatcher::compare(floatval($ov->data()), $this->compar, $this->value); 25 | } 26 | function script_expression(PaperInfo $row, $about) { 27 | if (($about & self::ABOUT_PAPER) === 0) { 28 | return parent::script_expression($row, $about); 29 | } else if ($this->user->can_view_option($row, $this->option)) { 30 | if (($se = $this->option->value_script_expression())) { 31 | return ["type" => "compar", "child" => [$se, $this->value], "compar" => CountMatcher::unparse_relation($this->compar)]; 32 | } else { 33 | return null; 34 | } 35 | } else { 36 | return false; 37 | } 38 | } 39 | function about() { 40 | return self::ABOUT_PAPER; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/oauth/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA5Q6puFDxKPiLjAkq4m4Y0hrmdM4rtYkdCiKbhtx7QHOxvdVU 3 | L9FC2TKduyQPIbPQtUO9h2N+gngLdpnszdD0Pkv6bMUu4OocJaR3TvXivverhtoc 4 | KJDWy77hKf2cAT+1AAz7TCP8RadKnGNzK1IFUigkIEQogoRiSqAaA1b5KXRsbRV/ 5 | JsC5kf+z11ZkrBAL53phITtoc0i+SYa3NMMUINCFfjorrxzV2YswIvm9maY7Kyus 6 | tsrxbRt/74i6pQFlyaH3K9UP+KIrfK/Rl+2HdbG+XgDiWGL6HSyHw1ybx+RxZCkX 7 | 69vhyGDBe3tNjZSO876bVkHCJr5h6POCC+PDJwIDAQABAoIBAQCKFXDTIFiBbnQR 8 | k2U640wrPPQ47iEDawkKlxpTDo9up1A7NGNwACLgdNcJfg9xLclfvNqAx8X4OQ4Q 9 | DXLoEFNtSrhI4gYEqJ0XRDJ4c1qh7QSGYu4etlIGuadbfPuS9SjUQv8rQ3ZNNzCP 10 | XpSLRQLYKEK/ANe69ruaaTHFWaUTC2xrWtM0lSkgLIcdIicaIfEhyqdlFYQcoHqQ 11 | z2KwCDtOOv3pkJ31RWM0PpKray/wXmFlaI8lrGf7+HGPQYyXdd+XxtEeTGuCh5oZ 12 | 4L9pOc9c1ejMUZik4UeAk+V6XrzNlOBl8/W99RxelOK82ZxQDeSCdAV/phL+15gJ 13 | Ki6kZN2hAoGBAPJFA0bseptMYDfZagAF9AHAY6KNcrBqedD82ud863rETXWnrMws 14 | 8ghXVanoJCT9cUqFnO3lwwA0HEqZnECRatiQJjF8ZA3gqUtv2os3ocg3Jt8xrShx 15 | wLGETcj5mi1kFrPrn973VwVHYMX+IhIyOPhLUP7R4Ndy35b2Drg3U1JLAoGBAPIJ 16 | 9Vr9E8XasnzI3ALNtOncDihahgWlt4cKng1AzdueqvMxt9tp/TwEDP6O0z3fD/FF 17 | JyTys9M0y5gWlX6NziCxG+suvp+OolqpklF7h1Sfhbqw4t6V/PHwRa5x0MuraAw+ 18 | 4+oSYoefL+xyC7OBcbGPNKsIpR4yhMi3AcRRoCkVAoGBAJSnngQl1HF4Is4CHNWY 19 | 0YlFmJ1Ed6wiGU8P5+4Eq6Tv0Kux0AiUR4qwtAKGS69ax+o3I/yhb86vKvDnYoYH 20 | 9Gyfvp+8uNP/F0IPhyTHZQCqPrLTE3HuopMKIISCC4VwlbGekcFJOV8m1g2HCzbp 21 | FCXeaPuCopjwhptlrdCBOiITAoGBAMzsL4ak5NvMSPgrm1LoVTb28CmsUvJvFw7H 22 | p39zEZfTI8uZmZ+0ggoRJ+tSg3lL5ZSRxw2aSzQT7BhNbq7iYtX8/bVGM3Cl88Gs 23 | 9kv0uWSlVzT0VHC+LpWsp2KFzJDUA9jyWkcw36kR1yJqgIuvmdIKfD4eqKYDgbbq 24 | cx2DOoXtAoGAN8DDBd4uUSWrJTSXE+i0OGPMmjh1Upx9R7ycFsv5d2CzAiyWnJ73 25 | 3uhl/VL7pSPFpljh4daA7T+bnCVs1yKQ5G8efXWlEJJoogzIgQU37c+T8NIvkfd3 26 | uXt6dejxKvWNHK/ydrUGxu3XJjpLJ7djohmC83TDa/asPKo1ZbL0stg= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # Apache settings for HotCRP 2 | 3 | # These directives limit how large a paper can be uploaded. 4 | # post_max_size should be >= upload_max_filesize. 5 | #php_value upload_max_filesize 15M 6 | #php_value post_max_size 20M 7 | 8 | # Some pages involve a lot of post variables. 9 | #php_value max_input_vars 4096 10 | 11 | # A large memory_limit helps when sending very large zipped files. 12 | php_value memory_limit 128M 13 | 14 | # Default to UTF-8 (most scripts will override this with ). 15 | AddDefaultCharset UTF-8 16 | 17 | # Use index.php for directory access. 18 | DirectoryIndex index.php 19 | 20 | # Prevent access to SCM directory, logs, test, README, regardless of case. 21 | RedirectMatch 403 ^.*/(\..*|[Rr][Ee][Aa][Dd][Mm][Ee].*|[Ff][Ii][Ll][Ee][Ss][Tt][Oo][Rr][Ee]|[Dd][Oo][Cc][Ss]|[Cc][Oo][Nn][Ff]|[Cc][Oo][Dd][Ee]|[Ll][Oo][Gg][Ss])($|/.*$) 22 | 23 | # Don't use MultiViews, which can conflict with mod_rewrite suffixless URLs. 24 | Options -MultiViews 25 | 26 | # Add .php to suffixless URLs. 27 | 28 | RewriteEngine on 29 | RewriteBase / 30 | RewriteCond %{REQUEST_FILENAME}.php -f 31 | RewriteCond %{REQUEST_URI} ^(.*)$ 32 | RewriteRule ^[^/]*$ %1.php [L,NE] 33 | RewriteCond %{REQUEST_FILENAME}.php -f 34 | RewriteCond %{REQUEST_URI},,$1,, ^(.*)(.*,,)\2$ 35 | RewriteRule ^[^/]*(/.*)$ %1.php$1 [L,NE] 36 | 37 | 38 | # Uncomment this line to ONLY grant access via https. Requires mod_ssl. 39 | # 40 | # SSLRequireSSL 41 | 42 | # HTTP Authentication: To ask the server to authenticate users, 43 | # uncomment these lines and set $Opt["httpAuthLogin"] in 44 | # conf/options.php. The $Opt["httpAuthLogin"] value should correspond 45 | # to your AuthType and AuthName (AuthName is the "realm"). 46 | # 47 | # AuthType Basic 48 | # AuthName "HotCRP" 49 | # AuthUserFile FILENAME 50 | # Require valid-user 51 | -------------------------------------------------------------------------------- /src/search/st_perm.php: -------------------------------------------------------------------------------- 1 | user = $user; 14 | $this->perm = $perm; 15 | } 16 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 17 | if (strcasecmp($word, "author-edit") === 0 18 | || strcasecmp($word, "author-write") === 0) { 19 | return new Perm_SearchTerm($srch->user, "author-write"); 20 | } else if (strcasecmp($word, "author-edit-final") === 0 21 | || strcasecmp($word, "author-write-final") === 0) { 22 | return new Perm_SearchTerm($srch->user, "author-write-final"); 23 | } else { 24 | $srch->lwarning($sword, "<0>Permission not found"); 25 | return new False_SearchTerm; 26 | } 27 | } 28 | function sqlexpr(SearchQueryInfo $sqi) { 29 | if ($this->perm === "author-write-final") { 30 | return "(Paper.timeWithdrawn<=0 and Paper.outcome>0)"; 31 | } else { 32 | return "(Paper.timeWithdrawn<=0)"; 33 | } 34 | } 35 | function test(PaperInfo $row, $xinfo) { 36 | if ($this->perm === "author-write") { 37 | return $row->author_edit_state() !== 0; 38 | } else if ($this->perm === "author-write-final") { 39 | return $row->author_edit_state() === 2 40 | && $this->user->can_view_decision($row); 41 | } else { 42 | return false; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/papercolumns/pc_administrator.php: -------------------------------------------------------------------------------- 1 | user->can_view_manager(null)) { 16 | return false; 17 | } 18 | $pl->conf->pc_set(); // prepare cache 19 | $this->nameflags = $this->user_view_option_name_flags($pl->conf); 20 | return true; 21 | } 22 | static private function cid(PaperList $pl, PaperInfo $row) { 23 | if ($row->managerContactId && $pl->user->can_view_manager($row)) { 24 | return $row->managerContactId; 25 | } else { 26 | return 0; 27 | } 28 | } 29 | function sort_name() { 30 | return $this->sort_name_with_options("format"); 31 | } 32 | function compare(PaperInfo $a, PaperInfo $b, PaperList $pl) { 33 | $ianno = $this->nameflags & NAME_L ? Contact::SORTSPEC_LAST : Contact::SORTSPEC_FIRST; 34 | return $pl->user_compare(self::cid($pl, $a), self::cid($pl, $b), $ianno); 35 | } 36 | function content_empty(PaperList $pl, PaperInfo $row) { 37 | return !self::cid($pl, $row); 38 | } 39 | function content(PaperList $pl, PaperInfo $row) { 40 | return $pl->user_content($row->managerContactId, $row, $this->nameflags); 41 | } 42 | function text(PaperList $pl, PaperInfo $row) { 43 | return $pl->user_text($row->managerContactId, $this->nameflags); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/search/st_optionvalue.php: -------------------------------------------------------------------------------- 1 | compar = $relation; 15 | $this->value = $value; 16 | } 17 | function debug_json() { 18 | return [ 19 | "type" => $this->type, 20 | "option" => $this->option->search_keyword(), 21 | "compar" => CountMatcher::unparse_relation($this->compar), 22 | "value" => $this->value 23 | ]; 24 | } 25 | function test(PaperInfo $row, $xinfo) { 26 | return $this->user->can_view_option($row, $this->option) 27 | && ($ov = $row->option($this->option)) 28 | && $ov->value !== null 29 | && CountMatcher::compare($ov->value, $this->compar, $this->value); 30 | } 31 | function script_expression(PaperInfo $row, $about) { 32 | if (($about & self::ABOUT_PAPER) === 0) { 33 | return parent::script_expression($row, $about); 34 | } else if (!$this->user->can_view_option($row, $this->option)) { 35 | return false; 36 | } 37 | if (($se = $this->option->value_script_expression())) { 38 | return ["type" => "compar", "child" => [$se, $this->value], "compar" => CountMatcher::unparse_relation($this->compar)]; 39 | } 40 | return null; 41 | } 42 | function about() { 43 | return self::ABOUT_PAPER; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/settings/s_finalversions.php: -------------------------------------------------------------------------------- 1 | final versions page 3 | // Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | class FinalVersions_SettingParser extends SettingParser { 6 | static function print(SettingValues $sv) { 7 | if ($sv->oldv("final_soft") === $sv->oldv("final_done")) { 8 | $sv->set_oldv("final_soft", null); 9 | } 10 | echo '
'; 11 | $sv->print_checkbox('final_open', 'Collect final versions of accepted submissions', ["class" => "uich js-foldup", "group_class" => "form-g", "group_open" => true]); 12 | echo '
'; 13 | $sv->print_entry_group("final_soft", "Deadline", ["horizontal" => true]); 14 | $sv->print_entry_group("final_done", "Hard deadline", ["horizontal" => true]); 15 | $sv->print_entry_group("final_grace", "Grace period", ["horizontal" => true]); 16 | echo '
'; 17 | $sv->print_message_minor("final_edit_message", "Instructions"); 18 | Banal_SettingParser::print("final", $sv); 19 | echo "
\n\n"; 20 | } 21 | 22 | static function crosscheck(SettingValues $sv) { 23 | if ($sv->has_interest("final_open") 24 | && $sv->oldv("final_open") 25 | && ($sv->oldv("final_soft") || $sv->oldv("final_done")) 26 | && (!$sv->oldv("final_done") || $sv->oldv("final_done") > Conf::$now) 27 | && !$sv->conf->time_all_author_view_decision()) { 28 | $sv->warning_at(null, "<5>The system is set to collect final versions, but authors cannot submit final versions until they can see decisions. You may want to update the " . $sv->setting_link("“Can authors see decisions” setting", "decision_visibility_author") . "."); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/collatorshim.php: -------------------------------------------------------------------------------- 1 | setAttribute(self::STRENGTH, $strength); // which does nothing 23 | } 24 | /** @param int $name 25 | * @param int $value */ 26 | function setAttribute($name, $value) { 27 | if ($name === self::NUMERIC_COLLATION) { 28 | $this->numeric = $value; 29 | } 30 | } 31 | /** @param string $a 32 | * @param string $b 33 | * @return -1|0|1 */ 34 | function compare($a, $b) { 35 | if ($this->numeric) { 36 | return strnatcasecmp($a, $b); 37 | } else { 38 | return strcasecmp($a, $b); 39 | } 40 | } 41 | /** @param list &$v */ 42 | function sort(&$v) { 43 | if ($this->numeric) { 44 | sort($v, SORT_NATURAL | SORT_FLAG_CASE); 45 | } else { 46 | sort($v, SORT_FLAG_CASE); 47 | } 48 | } 49 | /** @param array &$v */ 50 | function asort(&$v) { 51 | if ($this->numeric) { 52 | asort($v, SORT_NATURAL | SORT_FLAG_CASE); 53 | } else { 54 | asort($v, SORT_FLAG_CASE); 55 | } 56 | } 57 | } 58 | 59 | if (!class_exists("Collator", false)) { 60 | class_alias("CollatorShim", "Collator"); 61 | } 62 | -------------------------------------------------------------------------------- /lib/memoryqsession.php: -------------------------------------------------------------------------------- 1 | */ 7 | private $a; 8 | 9 | /** @param ?string $sid 10 | * @param array $a */ 11 | function __construct($sid = null, $a = []) { 12 | $this->sid = $sid ?? "sess_" . base64_encode(random_bytes(15)); 13 | $this->sopen = true; 14 | $this->a = $a; 15 | } 16 | 17 | function all() { 18 | return $this->a; 19 | } 20 | 21 | function clear() { 22 | assert($this->sopen); 23 | $this->a = []; 24 | } 25 | 26 | function has($key) { 27 | return $this->sopen && isset($this->a[$key]); 28 | } 29 | 30 | function get($key) { 31 | return $this->sopen ? $this->a[$key] ?? null : null; 32 | } 33 | 34 | function set($key, $value) { 35 | assert($this->sopen); 36 | $this->a[$key] = $value; 37 | } 38 | 39 | function unset($key) { 40 | assert($this->sopen); 41 | unset($this->a[$key]); 42 | } 43 | 44 | function has2($key1, $key2) { 45 | return $this->sopen && isset($this->a[$key1][$key2]); 46 | } 47 | 48 | function get2($key1, $key2) { 49 | return $this->sopen ? $this->a[$key1][$key2] ?? null : null; 50 | } 51 | 52 | function set2($key1, $key2, $value) { 53 | assert($this->sopen); 54 | $this->a[$key1][$key2] = $value; 55 | } 56 | 57 | function unset2($key1, $key2) { 58 | assert($this->sopen); 59 | if (isset($this->a[$key1])) { 60 | unset($this->a[$key1][$key2]); 61 | if (empty($this->a[$key1])) { 62 | unset($this->a[$key1]); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/autoassigners/aa_clear.php: -------------------------------------------------------------------------------- 1 | $pcids 10 | * @param list $papersel 11 | * @param array $subreq 12 | * @param object $gj */ 13 | function __construct(Contact $user, $pcids, $papersel, $subreq, $gj) { 14 | parent::__construct($user, $pcids, $papersel); 15 | $t = $gj->type ?? $subreq["type"] ?? null; 16 | if (in_array($t, ["conflict", "lead", "shepherd"], true)) { 17 | $this->type = $t; 18 | } else if (is_string($t) && ($x = ReviewInfo::parse_type($t, true))) { 19 | $this->type = $x; 20 | } else { 21 | $this->error_at("type", "<0>Expected review type, ‘conflict’, ‘lead’, or ‘shepherd’"); 22 | } 23 | } 24 | 25 | function run() { 26 | if (is_int($this->type)) { 27 | $q = "select paperId, contactId from PaperReview where reviewType=" . $this->type; 28 | $action = "noreview"; 29 | } else if ($this->type === "conflict") { 30 | $q = "select paperId, contactId from PaperConflict where conflictType>" . CONFLICT_MAXUNCONFLICTED . " and conflictType<" . CONFLICT_AUTHOR; 31 | $action = "noconflict"; 32 | } else { 33 | $q = "select paperId, {$this->type}ContactId from Paper where {$this->type}ContactId!=0"; 34 | $action = "no" . $this->type; 35 | } 36 | $this->set_assignment_action($action); 37 | $result = $this->conf->qe_raw($q); 38 | while (($row = $result->fetch_row())) { 39 | $this->assign1((int) $row[1], (int) $row[0]); 40 | } 41 | Dbl::free($result); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/papercolumns/pc_lead.php: -------------------------------------------------------------------------------- 1 | override = PaperColumn::OVERRIDE_IFEMPTY; 11 | } 12 | function view_option_schema() { 13 | return self::user_view_option_schema(); 14 | } 15 | function prepare(PaperList $pl, $visible) { 16 | if (!$pl->user->can_view_lead(null) 17 | || (!$pl->conf->has_any_lead_or_shepherd() && !$visible)) { 18 | return false; 19 | } 20 | $pl->conf->pc_set(); // prepare cache 21 | $this->nameflags = $this->user_view_option_name_flags($pl->conf); 22 | return true; 23 | } 24 | static private function cid(PaperList $pl, PaperInfo $row) { 25 | if ($row->leadContactId > 0 && $pl->user->can_view_lead($row)) { 26 | return $row->leadContactId; 27 | } else { 28 | return 0; 29 | } 30 | } 31 | function sort_name() { 32 | return $this->sort_name_with_options("format"); 33 | } 34 | function compare(PaperInfo $a, PaperInfo $b, PaperList $pl) { 35 | $ianno = $this->nameflags & NAME_L ? Contact::SORTSPEC_LAST : Contact::SORTSPEC_FIRST; 36 | return $pl->user_compare(self::cid($pl, $a), self::cid($pl, $b), $ianno); 37 | } 38 | function content_empty(PaperList $pl, PaperInfo $row) { 39 | return !self::cid($pl, $row); 40 | } 41 | function content(PaperList $pl, PaperInfo $row) { 42 | return $pl->user_content($row->leadContactId, $row, $this->nameflags); 43 | } 44 | function text(PaperList $pl, PaperInfo $row) { 45 | return $pl->user_text($row->leadContactId, $this->nameflags); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/search/st_paperpc.php: -------------------------------------------------------------------------------- 1 | user = $user; 15 | $this->kind = $kind; 16 | $this->fieldname = $kind . "ContactId"; 17 | $this->match = $match; 18 | } 19 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 20 | if (($word === "any" || $word === "" || $word === "yes") && !$sword->quoted) { 21 | $match = "!=0"; 22 | } else if (($word === "none" || $word === "no") && !$sword->quoted) { 23 | $match = "=0"; 24 | } else { 25 | $match = $srch->matching_uids($word, $sword->quoted, true); 26 | } 27 | // XXX what about track admin privilege? 28 | $qt = [new PaperPC_SearchTerm($srch->user, $sword->kwdef->pcfield, $match)]; 29 | if ($sword->kwdef->pcfield === "manager" 30 | && $word === "me" 31 | && !$sword->quoted 32 | && $srch->user->privChair) { 33 | $qt[] = new PaperPC_SearchTerm($srch->user, $qt[0]->kind, "=0"); 34 | } 35 | return $qt; 36 | } 37 | function sqlexpr(SearchQueryInfo $sqi) { 38 | $sqi->add_column($this->fieldname, "Paper.{$this->fieldname}"); 39 | return "(Paper.{$this->fieldname}" . CountMatcher::sqlexpr_using($this->match) . ")"; 40 | } 41 | function test(PaperInfo $row, $xinfo) { 42 | $can_view = "can_view_{$this->kind}"; 43 | return $this->user->$can_view($row) 44 | && CountMatcher::compare_using($row->{$this->fieldname}, $this->match); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/gmpshim.php: -------------------------------------------------------------------------------- 1 | = 8 ? 6 : 5); 7 | const GMPSHIM_INT_SIZE = 1 << GMPSHIM_INT_SHIFT; 8 | 9 | class GMPShim { 10 | static function init($v) { 11 | return [(int) $v]; 12 | } 13 | 14 | static function clrbit(&$a, $index) { 15 | assert($index >= 0); 16 | $i = $index >> GMPSHIM_INT_SHIFT; 17 | if ($i < count($a)) { 18 | $j = $index & (GMPSHIM_INT_SIZE - 1); 19 | $a[$i] &= ~(1 << $j); 20 | } 21 | } 22 | 23 | static function setbit(&$a, $index, $bit_on = true) { 24 | assert($index >= 0); 25 | $i = $index >> GMPSHIM_INT_SHIFT; 26 | if ($bit_on || $i < count($a)) { 27 | while ($i >= count($a)) { 28 | $a[] = 0; 29 | } 30 | $j = $index & (GMPSHIM_INT_SIZE - 1); 31 | if ($bit_on) { 32 | $a[$i] |= 1 << $j; 33 | } else { 34 | $a[$i] &= ~(1 << $j); 35 | } 36 | } 37 | } 38 | 39 | static function testbit($a, $index) { 40 | assert($index >= 0); 41 | $i = $index >> GMPSHIM_INT_SHIFT; 42 | $j = $index & (GMPSHIM_INT_SIZE - 1); 43 | return $i < count($a) && ($a[$i] & (1 << $j)) !== 0; 44 | } 45 | 46 | static function scan1($a, $start) { 47 | assert($start >= 0); 48 | $i = $start >> GMPSHIM_INT_SHIFT; 49 | $j = $start & (GMPSHIM_INT_SIZE - 1); 50 | while ($i < count($a)) { 51 | $v = $a[$i]; 52 | while ($j < GMPSHIM_INT_SIZE) { 53 | if ($v & (1 << $j)) { 54 | return ($i << GMPSHIM_INT_SHIFT) | $j; 55 | } 56 | ++$j; 57 | } 58 | $j = 0; 59 | ++$i; 60 | } 61 | return -1; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/settings/s_messages.php: -------------------------------------------------------------------------------- 1 | messages page 3 | // Copyright (c) 2006-2022 Eddie Kohler; see LICENSE. 4 | 5 | class Messages_SettingParser extends SettingParser { 6 | function default_value(Si $si, SettingValues $sv) { 7 | if ($si->name === "preference_instructions") { 8 | $targ = new FmtArg("topics", !!$sv->oldv("has_topics")); 9 | $t = $sv->conf->fmt()->default_translation("revprefdescription", $targ); 10 | return Ftext::as(5, $t); 11 | } 12 | return null; 13 | } 14 | function apply_req(Si $si, SettingValues $sv) { 15 | if ($si->name === "submission_terms" || $si->name === "review_terms") { 16 | if (($v = $sv->base_parse_req($si)) !== null) { 17 | $sv->save("{$si->name}_exist", $v !== "" ? 1 : 0); 18 | } 19 | } 20 | return false; 21 | } 22 | static function print_submissions(SettingValues $sv) { 23 | $sv->print_message("home_message", "Home page message"); 24 | $sv->print_message("submission_terms", "Clickthrough submission terms", 25 | "

Users must “accept” these terms to edit a submission. Use HTML and include a headline, such as “<h2>Submission terms</h2>”.

"); 26 | $sv->print_message("submission_edit_message", "Submission message", 27 | "

This message will appear on submission editing pages.

"); 28 | } 29 | static function print_reviews(SettingValues $sv) { 30 | $sv->print_message("review_terms", "Clickthrough reviewing terms", 31 | "

Users must “accept” these terms to edit a review. Use HTML and include a headline, such as “<h2>Submission terms</h2>”.

"); 32 | $sv->print_message("conflict_description", "Definition of conflict of interest"); 33 | $sv->print_message("preference_instructions", "Review preference instructions"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/search/st_reconflict.php: -------------------------------------------------------------------------------- 1 | add_range((int) $m[1], (int) $m[2]); 12 | } else { 13 | $st->add_range((int) $m[1], (int) $m[1]); 14 | } 15 | $xword = $m[3]; 16 | } 17 | if ($xword !== "" || $st->is_empty()) { 18 | $srch->lwarning($sword, "<0>List of paper numbers expected"); 19 | return new False_SearchTerm; 20 | } 21 | 22 | $old_overrides = $srch->user->add_overrides(Contact::OVERRIDE_CONFLICT); 23 | $cids = []; 24 | foreach ($srch->user->paper_set([ 25 | "paperId" => $st, 26 | "reviewSignatures" => true, 27 | "finalized" => $srch->limit_term()->is_submitted() 28 | ]) as $prow) { 29 | if ($srch->user->can_view_paper($prow)) { 30 | foreach ($prow->all_reviews() as $rrow) { 31 | if ($rrow->reviewToken === 0 32 | && $srch->user->can_view_review_identity($prow, $rrow)) { 33 | $cids[$rrow->contactId] = true; 34 | } 35 | } 36 | } 37 | } 38 | $srch->user->set_overrides($old_overrides); 39 | 40 | if (!empty($cids)) { 41 | return new Conflict_SearchTerm($srch->user, new ContactCountMatcher(">0", array_keys($cids)), false); 42 | } else { 43 | $srch->lwarning($sword, "<0>No visible reviewers"); 44 | return new False_SearchTerm; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/formulas/f_pref.php: -------------------------------------------------------------------------------- 1 | kwdef->is_expertise : $ff; 10 | parent::__construct($is_expertise ? "prefexp" : "pref"); 11 | $this->is_expertise = $is_expertise; 12 | $this->set_format($is_expertise ? Fexpr::FPREFEXPERTISE : Fexpr::FNUMERIC); 13 | if (is_object($ff) && $ff->modifier) { 14 | $this->cids = $ff->modifier; 15 | } 16 | } 17 | static function parse_modifier(FormulaCall $ff, $arg, Formula $formula) { 18 | if ($ff->modifier !== false || str_starts_with($arg, ".")) { 19 | return false; 20 | } 21 | if (str_starts_with($arg, ":")) { 22 | $arg = substr($arg, 1); 23 | } 24 | $csm = ContactSearch::make_pc($arg, $formula->user); 25 | if (!$csm->has_error()) { 26 | $ff->modifier = $csm->user_ids(); 27 | return true; 28 | } 29 | return false; 30 | } 31 | function inferred_index() { 32 | return Fexpr::IDX_PC; 33 | } 34 | function viewable_by(Contact $user) { 35 | return $user->isPC; 36 | } 37 | function compile(FormulaCompiler $state) { 38 | if (!$state->user->is_reviewer()) { 39 | return "null"; 40 | } 41 | $state->queryOptions["allReviewerPreference"] = true; 42 | $pref = $state->_add_preferences(); 43 | $cid = $state->loop_cid(!$this->cids); 44 | $condition = "isset({$pref}[{$cid}])"; 45 | if ($this->cids) { 46 | $condition .= " && in_array({$cid}, [" . join(",", $this->cids) . "], true)"; 47 | } 48 | $property = $this->is_expertise ? "expertise" : "preference"; 49 | return "({$condition} ? {$pref}[{$cid}]->{$property} : null)"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/search/st_paperstatus.php: -------------------------------------------------------------------------------- 1 | match = $match; 11 | } 12 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 13 | $fval = PaperSearch::status_field_matcher($srch->conf, $word, $sword->quoted); 14 | if (is_array($fval[1]) && empty($fval[1])) { 15 | $srch->lwarning($sword, "<0>Submission status ‘{$word}’ not found"); 16 | $fval[1][] = -10000000; 17 | } 18 | if ($fval[0] === "outcome") { 19 | return new Decision_SearchTerm($srch->user, $fval[1]); 20 | } else { 21 | if ($srch->limit_term()->is_submitted() 22 | && ($fval[0] !== "timeSubmitted" || $fval[1] !== ">0")) { 23 | $srch->lwarning($sword, "<0>Matches nothing because this search is limited to completed submissions"); 24 | } 25 | return new PaperStatus_SearchTerm($fval); 26 | } 27 | } 28 | function sqlexpr(SearchQueryInfo $sqi) { 29 | $q = []; 30 | for ($i = 0; $i < count($this->match); $i += 2) { 31 | $sqi->add_column($this->match[$i], "Paper." . $this->match[$i]); 32 | $q[] = "Paper." . $this->match[$i] . CountMatcher::sqlexpr_using($this->match[$i+1]); 33 | } 34 | return self::andjoin_sqlexpr($q); 35 | } 36 | function is_sqlexpr_precise() { 37 | return true; 38 | } 39 | function test(PaperInfo $row, $xinfo) { 40 | for ($i = 0; $i < count($this->match); $i += 2) { 41 | if (!CountMatcher::compare_using($row->{$this->match[$i]}, $this->match[$i+1])) 42 | return false; 43 | } 44 | return true; 45 | } 46 | function about() { 47 | return self::ABOUT_PAPER; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/filer.php: -------------------------------------------------------------------------------- 1 | curlh, CURLOPT_CUSTOMREQUEST, "GET"); 14 | curl_setopt($clib->curlh, CURLOPT_URL, "{$clib->site}/whoami"); 15 | if (!$clib->exec_api(null) 16 | || !($clib->content_json->ok ?? false)) { 17 | return 1; 18 | } 19 | if (!$clib->quiet) { 20 | if ($this->email || $this->roles) { 21 | $t = $clib->content_json->email ?? null; 22 | if (!is_string($t)) { 23 | $t = "__unknown__"; 24 | } 25 | if ($this->roles && isset($clib->content_json->roles)) { 26 | $t .= " [" . join(" ", $clib->content_json->roles) . "]"; 27 | } 28 | } else { 29 | $t = "Success"; 30 | } 31 | $clib->set_output("{$t}\n"); 32 | } 33 | return 0; 34 | } 35 | 36 | /** @return Test_CLIBatch */ 37 | static function make_arg(Hotcrapi_Batch $clib, Getopt $getopt, $arg) { 38 | $tb = new Test_CLIBatch; 39 | $tb->email = isset($arg["email"]); 40 | $tb->roles = isset($arg["roles"]); 41 | return $tb; 42 | } 43 | 44 | static function register(Hotcrapi_Batch $clib, Getopt $getopt) { 45 | $getopt->subcommand_description( 46 | "test", 47 | "Test API connection 48 | Usage: php batch/hotcrapi.php test" 49 | )->long( 50 | "j,json !test Output JSON", 51 | "email !test Output associated email", 52 | "roles !test Output associated email and roles" 53 | ); 54 | $clib->register_command("test", "Test_CLIBatch"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /devel/manual/css.md: -------------------------------------------------------------------------------- 1 | # HotCRP CSS 2 | 3 | This page documents aspects of HotCRP styles not obvious from the style file 4 | itself. 5 | 6 | ## `z-index` 7 | 8 | Page-level stacking contexts 9 | 10 | * `#p-tracker`: 8 11 | * `#p-page`: 0 12 | * `#p-footer`: 0 13 | 14 | Values meaningful within page stacking contexts, especially `#p-page` 15 | 16 | Generic values 17 | 18 | * `.modal.transparent`: 10 19 | * `#h-actas`, `.dropmenu-container`: 12 (must be > `.modal.transparent`) 20 | * `.modal`: 14 21 | * `.modal-dialog`: 16 (must be > `.modal`) 22 | * `.bubble`: 20 23 | 24 | * `body.page #p-header`: 3 (to occlude `.pslcard-nav`) 25 | * `.pspcard`: 3 (to occlude `.pslcard-nav`) 26 | * `.pslcard-nav`: (0) 27 | * `.pslcard-home`: -1 28 | * `.home-sidebar`: 1 29 | * `button:hover`, `button:focus`, etc.: 1 30 | * `.longtext-fader`: 1 31 | * `.longtext-expander`: 2 32 | * `.overlong-content`: 1 33 | * `.overlong-collapsed > .overlong-content`: 0 34 | * `.overlong-collapsed > .overlong-divider > .overlong-mark`: 2 35 | * `.cmtcard.edit.popout`: 4 36 | 37 | ## `id` 38 | 39 | * Any `id` starting with `[a-z][-_]` is reserved for HotCRP use 40 | * Paper and review fields cannot follow that pattern 41 | * Paper and review fields also must not match JSON keys used for papers 42 | and reviews 43 | * `id^=t-` defines the page type; it is only set on the `` element 44 | * `id^=p-` is for page-level elements 45 | * `#p-tracker` (optional) 46 | * `#p-page` 47 | * `#p-header` 48 | * `#p-body` 49 | * `#p-footer` 50 | * `id^=i-` is for icons 51 | * `id^=f-` is for forms 52 | * `id^=h-` is for header elements 53 | * `#h-actas` 54 | * `#h-site` 55 | * `#h-page` 56 | * `#h-right` 57 | * `#h-deadline` 58 | * `#h-messages` 59 | * `#h-usermenu` 60 | * `#h-usermenubutton` 61 | * `id^=n-` is for navigation elements (quicklinks) 62 | * `#n-next` 63 | * `#n-prev` 64 | * `#n-search` 65 | * `#n-list` 66 | * `id^=k-` is for inputs and for programmatically assigned IDs, e.g., elements 67 | that need IDs for reference by `label` 68 | -------------------------------------------------------------------------------- /devel/apidoc/search.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | These endpoints perform searches on submissions. 4 | 5 | 6 | # get /search 7 | 8 | > Retrieve search results 9 | 10 | Return IDs of submissions that match a search. 11 | 12 | Pass the search query in the `q` parameter. The list of matching IDs is 13 | returned in the `ids` response property. 14 | 15 | The `t`, `qt`, `reviewer`, `sort`, and `scoresort` parameters can also affect 16 | the search. `t` defines the collection of submissions to search. `t=viewable` 17 | checks all submissions the user can view; the default collection is often 18 | narrower (a typical default is `t=s`, which searches complete submissions). 19 | 20 | The `groups` response property is an array of annotations that apply to the 21 | search, and is returned for `THEN` searches, `LEGEND` searches, and searches 22 | on tags with annotations. Each annotation contains a position `pos`, and may 23 | also have a `legend`, a `search`, an `annoid`, and other properties. `pos` is 24 | an integer index into the `ids` array; it ranges from 0 to the number of items 25 | in that array. Annotations with a given `pos` should appear *before* the paper 26 | at that index in the `ids` array. For instance, this response might be 27 | returned for the search `10-12 THEN 15-18`: 28 | 29 | ```json 30 | { 31 | "ok": true, 32 | "message_list": [], 33 | "ids": [10, 12, 18], 34 | "groups": [ 35 | { 36 | "pos": 0, 37 | "legend": "10-12", 38 | "search": "10-12" 39 | }, 40 | { 41 | "pos": 2, 42 | "legend": "15-18", 43 | "search": "15-18" 44 | } 45 | ] 46 | } 47 | ``` 48 | 49 | * response_schema search_response 50 | 51 | 52 | # get /fieldhtml 53 | 54 | > Retrieve search results as field HTML 55 | 56 | 57 | # get /fieldtext 58 | 59 | > Retrieve list field text 60 | 61 | 62 | # get /searchactions 63 | 64 | > Retrieve available search actions 65 | 66 | 67 | # get /searchaction 68 | 69 | > Perform search action 70 | 71 | 72 | # post /searchaction 73 | 74 | > Perform search action 75 | -------------------------------------------------------------------------------- /src/search/st_documentcount.php: -------------------------------------------------------------------------------- 1 | compar = CountMatcher::parse_relation($compar); 15 | $this->value = $value; 16 | } 17 | function debug_json() { 18 | return [$this->type, $this->option->search_keyword(), CountMatcher::unparse_relation($this->compar), $this->value]; 19 | } 20 | function sqlexpr(SearchQueryInfo $sqi) { 21 | $sqi->add_options_columns(); 22 | return CountMatcher::compare(0, $this->compar, $this->value) ? "true" : parent::sqlexpr($sqi); 23 | } 24 | function test(PaperInfo $row, $xinfo) { 25 | if ($this->user->can_view_option($row, $this->option) 26 | && ($ov = $row->option($this->option))) { 27 | $n = count($this->option->value_dids($ov)); 28 | } else { 29 | $n = 0; 30 | } 31 | return CountMatcher::compare($n, $this->compar, $this->value); 32 | } 33 | function script_expression(PaperInfo $row, $about) { 34 | if (($about & self::ABOUT_PAPER) !== 0) { 35 | return parent::script_expression($row, $about); 36 | } else if ($this->user->can_view_option($row, $this->option)) { 37 | return [ 38 | "type" => "compar", 39 | "child" => [$this->option->present_script_expression(), $this->value], 40 | "compar" => CountMatcher::unparse_relation($this->compar) 41 | ]; 42 | } else { 43 | return CountMatcher::compare(0, $this->compar, $this->value); 44 | } 45 | } 46 | function about() { 47 | return self::ABOUT_PAPER; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/formulas/f_topic.php: -------------------------------------------------------------------------------- 1 | */ 7 | private $match; 8 | function __construct(FormulaCall $ff, Formula $formula) { 9 | parent::__construct($ff); 10 | if ($ff->modifier === null || $ff->modifier === [-1]) { 11 | $this->match = true; 12 | $this->set_format(Fexpr::FNUMERIC); 13 | } else { 14 | $this->match = $ff->modifier; 15 | if (count($this->match) <= 1 || $this->match[0] === 0) { 16 | $this->set_format(Fexpr::FBOOL); 17 | } 18 | } 19 | } 20 | static function parse_modifier(FormulaCall $ff, $arg, Formula $formula) { 21 | if ($ff->modifier !== null || str_starts_with($arg, ".")) { 22 | return false; 23 | } 24 | if (str_starts_with($arg, ":")) { 25 | $arg = substr($arg, 1); 26 | } 27 | $ff->modifier = $formula->conf->topic_set()->find_all(SearchWord::unquote($arg)); 28 | // XXX warn if no match 29 | return true; 30 | } 31 | function paper_options(&$oids) { 32 | $oids[PaperOption::TOPICSID] = true; 33 | } 34 | function compile(FormulaCompiler $state) { 35 | $state->queryOptions["topics"] = true; 36 | $texpr = $state->_prow() . "->topic_list()"; 37 | if ($this->match === true) { 38 | return "count({$texpr})"; 39 | } else if ($this->match === []) { 40 | return "false"; 41 | } 42 | $none = $this->match[0] === 0; 43 | $ts = $none ? array_slice($this->match, 1) : $this->match; 44 | if ($ts === []) { 45 | return "empty({$texpr})"; 46 | } else if (count($ts) === 1) { 47 | return ($none ? "!" : "") . "in_array({$ts[0]}, {$texpr}, true)"; 48 | } else { 49 | return ($none ? "empty" : "count") . "(array_intersect({$texpr}," . json_encode($ts) . "))"; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/phpqsession.php: -------------------------------------------------------------------------------- 1 | = 20 9 | && strlen($sid) <= 128 10 | && (ctype_alnum($sid) || preg_match('/\A[-,0-9A-Za-z]+\z/', $sid))) { 11 | session_id($sid); 12 | } 13 | session_start(); 14 | $this->set_start_sid(session_id()); 15 | } 16 | 17 | function new_sid() { 18 | return session_create_id(); 19 | } 20 | 21 | function commit() { 22 | if ($this->sopen) { 23 | session_commit(); 24 | $this->sopen = false; 25 | } 26 | } 27 | 28 | function all() { 29 | return $_SESSION; 30 | } 31 | 32 | function clear() { 33 | assert($this->sopen); 34 | $_SESSION = []; 35 | } 36 | 37 | function has($key) { 38 | return $this->sopen && isset($_SESSION[$key]); 39 | } 40 | 41 | function get($key) { 42 | return $this->sopen ? $_SESSION[$key] ?? null : null; 43 | } 44 | 45 | function set($key, $value) { 46 | assert($this->sopen); 47 | $_SESSION[$key] = $value; 48 | } 49 | 50 | function unset($key) { 51 | assert($this->sopen); 52 | unset($_SESSION[$key]); 53 | } 54 | 55 | function has2($key1, $key2) { 56 | return $this->sopen && isset($_SESSION[$key1][$key2]); 57 | } 58 | 59 | function get2($key1, $key2) { 60 | return $this->sopen ? $_SESSION[$key1][$key2] ?? null : null; 61 | } 62 | 63 | function set2($key1, $key2, $value) { 64 | assert($this->sopen); 65 | $_SESSION[$key1][$key2] = $value; 66 | } 67 | 68 | function unset2($key1, $key2) { 69 | assert($this->sopen); 70 | if (isset($_SESSION[$key1])) { 71 | unset($_SESSION[$key1][$key2]); 72 | if (empty($_SESSION[$key1])) { 73 | unset($_SESSION[$key1]); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/help/h_votetags.php: -------------------------------------------------------------------------------- 1 | example_tag(TagInfo::TF_ALLOTMENT) ?? "vote"; 8 | echo "

Some conferences have PC members vote for papers. In 9 | allotment voting, each PC member is assigned a vote allotment to 10 | distribute among unconflicted papers; a PC member might assign one vote to one 11 | submission and five to another. In approval voting, each PC member 12 | can vote for as many papers as they like. The PC’s aggregated vote totals 13 | might help determine which papers to discuss.

14 | 15 |

HotCRP supports voting through ", $hth->help_link("tags", "tags"), ". 16 | The chair can ", $hth->setting_link("define a set of voting tags", "tag_vote_allotment"), 17 | " and allotments" . $hth->tag_settings_having_note(TagInfo::TF_ALLOTMENT) . ". 18 | Votes are represented as twiddle tags, and the vote total is automatically 19 | computed and shown in the public tag.

20 | 21 |

For example, an administrator might define an allotment voting tag 22 | “#". $votetag . "” with an allotment of 10 votes. 23 | To assign two votes to a submission, a PC member can either enter that vote 24 | into a text box on the submission page, or directly tag that submission with 25 | “#~". $votetag . "#2”. 26 | As other PC members add their votes with their own “#~vote” tags, the system 27 | updates the main “#vote” tag to reflect the total. 28 | (An error is reported when PC members exceed their allotment.)

29 | 30 |

To see papers’ vote counts in a list, search for ", 31 | $hth->search_link("show:#{$votetag}"), 32 | ". To list the papers with votes, sorted by vote count (most votes first), 33 | search for ", $hth->search_link("rorder:#{$votetag}"), " or ", 34 | $hth->search_link("rorder:#{$votetag} show:#{$votetag}"), ".

35 | 36 |

Hover to learn how the PC voted:

37 | 38 |

" . Ht::img("extagvotehover.png", "[Hovering over a voting tag]", ["width" => 390, "height" => 46]) . "

"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/search/st_sclass.php: -------------------------------------------------------------------------------- 1 | sr = $sr; 16 | $this->negate = $negate; 17 | } 18 | 19 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 20 | $tagger = new Tagger($srch->user); 21 | $tag = $tagger->check($word, Tagger::ALLOWRESERVED | Tagger::NOPRIVATE | Tagger::NOCHAIR); 22 | if ($tag === false) { 23 | $srch->lwarning($sword, $tagger->error_ftext(true)); 24 | return new False_SearchTerm; 25 | } 26 | 27 | if (strcasecmp($tag, "any") === 0) { 28 | return new Sclass_SearchTerm($srch->conf->unnamed_submission_round(), true); 29 | } else if (($sr = $srch->conf->submission_round_by_tag($tag, true))) { 30 | return new Sclass_SearchTerm($sr, false); 31 | } else { 32 | $srch->lwarning($sword, $srch->conf->_("<0>{Submission} class ‘{}’ not found", $tag)); 33 | return new False_SearchTerm; 34 | } 35 | } 36 | function sqlexpr(SearchQueryInfo $sqi) { 37 | if (!$this->sr->unnamed) { 38 | return Dbl::format_query($sqi->srch->conf->dblink, 39 | "exists (select * from PaperTag where paperId=Paper.paperId and tag=?)", 40 | $this->sr->tag); 41 | } else { 42 | return "true"; 43 | } 44 | } 45 | function test(PaperInfo $row, $xinfo) { 46 | return ($row->submission_round() === $this->sr) !== $this->negate; 47 | } 48 | function debug_json() { 49 | return ["type" => $this->type, "sclass" => $this->negate ? "any" : $this->sr->tag]; 50 | } 51 | function about() { 52 | return self::ABOUT_PAPER; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/search/st_decision.php: -------------------------------------------------------------------------------- 1 | */ 9 | private $match; 10 | 11 | /** @param string|list $match */ 12 | function __construct(Contact $user, $match) { 13 | parent::__construct("decision"); 14 | $this->user = $user; 15 | $this->match = $match; 16 | } 17 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 18 | $dec = $srch->conf->decision_set()->matchexpr($word); 19 | if (is_array($dec) && empty($dec)) { 20 | $srch->lwarning($sword, "<0>Decision not found"); 21 | $dec[] = -10000000; 22 | } 23 | return new Decision_SearchTerm($srch->user, $dec); 24 | } 25 | /** @return string|list */ 26 | function matchexpr() { 27 | return $this->match; 28 | } 29 | function sqlexpr(SearchQueryInfo $sqi) { 30 | $f = ["Paper.outcome" . CountMatcher::sqlexpr_using($this->match)]; 31 | if (CountMatcher::compare_using(0, $this->match) 32 | && !$this->user->allow_administer_all()) { 33 | $f[] = "Paper.outcome=0"; 34 | } 35 | return "(" . join(" or ", $f) . ")"; 36 | } 37 | function test(PaperInfo $row, $xinfo) { 38 | $d = $this->user->can_view_decision($row) ? $row->outcome : 0; 39 | return CountMatcher::compare_using($d, $this->match); 40 | } 41 | function about() { 42 | return self::ABOUT_PAPER; 43 | } 44 | function drag_assigners(Contact $user) { 45 | $ds = $user->conf->decision_set()->filter_using($this->match); 46 | if (count($ds) !== 1 || !$user->can_set_some_decision()) { 47 | return null; 48 | } 49 | return [ 50 | ["action" => "decision", "decision" => $ds[0]->name, "ondrag" => "enter"], 51 | ["action" => "decision", "decision" => "none", "ondrag" => "leave"] 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/papercolumns/pc_preferencelist.php: -------------------------------------------------------------------------------- 1 | topics = !!($cj->topics ?? false); 11 | $this->override = PaperColumn::OVERRIDE_IFEMPTY_LINK; 12 | } 13 | function view_option_schema() { 14 | return ["topics", "topic/topics"]; 15 | } 16 | function prepare(PaperList $pl, $visible) { 17 | if (!$pl->user->is_manager()) { 18 | return false; 19 | } 20 | $this->topics = ($this->view_option("topics") ?? $this->topics) 21 | && $pl->conf->has_topics(); 22 | if ($visible) { 23 | $pl->qopts["allReviewerPreference"] = true; 24 | if ($this->topics) { 25 | $pl->qopts["topics"] = true; 26 | } 27 | $pl->conf->stash_hotcrp_pc($pl->user); 28 | } 29 | return true; 30 | } 31 | function content_empty(PaperList $pl, PaperInfo $row) { 32 | return !$pl->user->can_administer($row); 33 | } 34 | function content(PaperList $pl, PaperInfo $row) { 35 | $ts = []; 36 | if ($this->topics || $row->preferences()) { 37 | foreach ($row->conf->pc_members() as $pcid => $pc) { 38 | $pf = $row->preference($pc); 39 | if ($pf->exists()) { 40 | $ts[] = "{$pcid}P{$pf->preference}" . unparse_expertise($pf->expertise); 41 | } else if ($this->topics && ($tv = $row->topic_interest_score($pc))) { 42 | $ts[] = "{$pcid}T{$tv}"; 43 | } 44 | } 45 | } 46 | $pl->row_attr["data-allpref"] = join(" ", $ts); 47 | if (!empty($ts)) { 48 | $t = 'Loading'; 49 | $pl->need_render = true; 50 | return $t; 51 | } else { 52 | return ''; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/listactions/la_getjsonrqc.php: -------------------------------------------------------------------------------- 1 | is_manager(); 8 | } 9 | function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { 10 | $old_overrides = $user->add_overrides(Contact::OVERRIDE_CONFLICT); 11 | $results = ["hotcrp_version" => HOTCRP_VERSION]; 12 | if (($git_data = Conf::git_status())) { 13 | $results["hotcrp_commit"] = $git_data[0]; 14 | } 15 | $rf = $user->conf->review_form(); 16 | $fj = []; 17 | foreach ($rf->bound_viewable_fields(VIEWSCORE_REVIEWERONLY) as $f) { 18 | $fj[$f->uid()] = $f->export_json(ReviewField::UJ_EXPORT); 19 | } 20 | $results["reviewform"] = $fj; 21 | $pj = []; 22 | $pex = new PaperExport($user); 23 | $pex->set_include_permissions(false); 24 | $pex->set_override_ratings(true); 25 | foreach ($ssel->paper_set($user, ["topics" => true, "options" => true]) as $prow) { 26 | if ($user->allow_administer($prow)) { 27 | $pj[] = $j = $pex->paper_json($prow); 28 | $prow->ensure_full_reviews(); 29 | foreach ($prow->viewable_reviews_as_display($user) as $rrow) { 30 | if ($rrow->reviewSubmitted) { 31 | $j->reviews[] = $pex->review_json($prow, $rrow); 32 | } 33 | } 34 | } else { 35 | $pj[] = (object) ["pid" => $prow->paperId, "error" => "You don’t have permission to administer this paper."]; 36 | } 37 | } 38 | $user->set_overrides($old_overrides); 39 | $results["papers"] = $pj; 40 | $dopt = new Downloader; 41 | $dopt->set_attachment(true); 42 | $dopt->set_filename($user->conf->download_prefix . "rqc.json"); 43 | $dopt->set_mimetype(Mimetype::JSON_UTF8_TYPE); 44 | $dopt->set_content(json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"); 45 | return $dopt; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/options/o_title.php: -------------------------------------------------------------------------------- 1 | prow->title !== "") { 11 | $ov->set_value_data([1], [$ov->prow->title]); 12 | } 13 | } 14 | function value_present(PaperValue $ov) { 15 | return $ov->value 16 | && (strlen($ov->data()) > 6 17 | || !preg_match('/\A(?:|N\/?A|TB[AD])\z/i', $ov->data())); 18 | } 19 | function value_export_json(PaperValue $ov, PaperExport $pex) { 20 | return (string) $ov->data(); 21 | } 22 | function value_save(PaperValue $ov, PaperStatus $ps) { 23 | if ($ov->equals($ov->prow->base_option($this->id))) { 24 | return true; 25 | } 26 | $ps->change_at($this); 27 | $ov->prow->set_prop("title", $ov->data()); 28 | return true; 29 | } 30 | /** @return ?PaperValue */ 31 | private function check_value(?PaperValue $ov) { 32 | if ($ov && $ov->data() !== null && strlen($ov->data()) > 511) { 33 | $ov->estop("<0>Field too long"); 34 | } 35 | return $ov; 36 | } 37 | function parse_qreq(PaperInfo $prow, Qrequest $qreq) { 38 | return $this->check_value($this->parse_json_string($prow, $qreq->title, PaperOption::PARSE_STRING_CONVERT | PaperOption::PARSE_STRING_SIMPLIFY)); 39 | } 40 | function parse_json(PaperInfo $prow, $j) { 41 | return $this->check_value($this->parse_json_string($prow, $j, PaperOption::PARSE_STRING_SIMPLIFY)); 42 | } 43 | function print_web_edit(PaperTable $pt, $ov, $reqov) { 44 | $this->print_web_edit_text($pt, $ov, $reqov, ["no_format_description" => true, "rows" => 1]); 45 | } 46 | function render(FieldRender $fr, PaperValue $ov) { 47 | $fr->value = $ov->prow->title ? : "[No title]"; 48 | $fr->value_format = $ov->prow->title_format(); 49 | } 50 | function present_script_expression() { 51 | return ["type" => "text_present", "formid" => $this->formid]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/papercolumns/pc_reviewdelegation.php: -------------------------------------------------------------------------------- 1 | user->isPC) { 13 | return false; 14 | } 15 | $pl->qopts["reviewSignatures"] = true; 16 | $this->requester = $pl->reviewer_user(); 17 | return true; 18 | } 19 | function content(PaperList $pl, PaperInfo $row) { 20 | $rx = []; 21 | $row->ensure_reviewer_names(); 22 | $old_overrides = $pl->user->add_overrides(Contact::OVERRIDE_CONFLICT); 23 | foreach ($row->reviews_as_display() as $rrow) { 24 | if ($rrow->reviewType !== REVIEW_EXTERNAL 25 | || $rrow->requestedBy !== $this->requester->contactId 26 | || !$pl->user->can_view_review_assignment($row, $rrow)) { 27 | continue; 28 | } 29 | if ($pl->user->can_view_review_identity($row, $rrow)) { 30 | $t = $pl->user->reviewer_html_for($rrow); 31 | } else { 32 | $t = "review"; 33 | } 34 | $ranal = $pl->make_review_analysis($rrow, $row); 35 | $d = $rrow->status_description(); 36 | if ($rrow->reviewOrdinal) { 37 | $d = rtrim("#" . $rrow->unparse_ordinal_id() . " " . $d); 38 | } 39 | $d = $ranal->wrap_link($d, "noq nw"); 40 | if ($rrow->reviewStatus < ReviewInfo::RS_DELIVERED) { 41 | if ($rrow->reviewNeedsSubmit >= 0) { 42 | $d = '' . $d . ''; 43 | } 44 | $pl->mark_has("need_review"); 45 | } else if ($rrow->reviewStatus === ReviewInfo::RS_DELIVERED) { 46 | $d = '' . $d . ''; 47 | } 48 | $rx[] = $t . ', ' . $d; 49 | } 50 | $pl->user->set_overrides($old_overrides); 51 | return join('; ', $rx); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/logentryfilter.php: -------------------------------------------------------------------------------- 1 | */ 9 | private $pidset; 10 | /** @var bool */ 11 | private $want; 12 | /** @var ?array */ 13 | private $includes; 14 | 15 | /** @param array $pidset 16 | * @param bool $want */ 17 | function __construct(Contact $user, $pidset, $want, $includes) { 18 | $this->user = $user; 19 | $this->pidset = $pidset; 20 | $this->want = $want; 21 | $this->includes = $includes; 22 | } 23 | 24 | private function test_pidset($row, $pidset, $want, $includes) { 25 | if ($row->paperId) { 26 | return isset($pidset[$row->paperId]) === $want 27 | && (!$includes || isset($includes[$row->paperId])); 28 | } 29 | if (!preg_match('/\A(.*) \(papers ([\d, ]+)\)?\z/', $row->action, $m)) { 30 | return $this->user->privChair; 31 | } 32 | preg_match_all('/\d+/', $m[2], $mm); 33 | $pids = []; 34 | $included = !$includes; 35 | foreach ($mm[0] as $pid) { 36 | if (isset($pidset[$pid]) === $want) { 37 | $pids[] = $pid; 38 | $included = $included || isset($includes[$pid]); 39 | } 40 | } 41 | if (empty($pids) || !$included) { 42 | return false; 43 | } else if (count($pids) === 1) { 44 | $row->action = $m[1]; 45 | $row->paperId = (int) $pids[0]; 46 | } else { 47 | $row->action = $m[1] . " (papers " . join(", ", $pids) . ")"; 48 | } 49 | return true; 50 | } 51 | 52 | /** @param LogEntry $row 53 | * @return bool */ 54 | function __invoke($row) { 55 | if ($this->user->hidden_papers !== null 56 | && !$this->test_pidset($row, $this->user->hidden_papers, false, null)) { 57 | return false; 58 | } else if ($row->contactId === $this->user->contactId) { 59 | return true; 60 | } else { 61 | return $this->test_pidset($row, $this->pidset, $this->want, $this->includes); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/formulas/f_reviewermatch.php: -------------------------------------------------------------------------------- 1 | user = $user; 19 | $this->set_format(Fexpr::FBOOL); 20 | $this->arg = $arg; 21 | $this->istag = $arg[0] === "#" || ($arg[0] !== "\"" && $user->conf->pc_tag_exists($arg)); 22 | $flags = ContactSearch::F_USER; 23 | if ($user->can_view_user_tags()) { 24 | $flags |= ContactSearch::F_TAG; 25 | } 26 | if ($arg[0] === "\"") { 27 | $flags |= ContactSearch::F_QUOTED; 28 | $arg = str_replace("\"", "", $arg); 29 | } 30 | $this->csearch = new ContactSearch($flags, $arg, $user); 31 | } 32 | function inferred_index() { 33 | return self::IDX_REVIEW; 34 | } 35 | function viewable_by(Contact $user) { 36 | return $user->can_view_some_review_identity(); 37 | } 38 | function compile(FormulaCompiler $state) { 39 | assert($state->user === $this->user); 40 | // NB the following case also catches attempts to view a non-viewable 41 | // user tag (the csearch will return nothing). 42 | if ($this->csearch->is_empty()) { 43 | return "null"; 44 | } 45 | $state->queryOptions["reviewSignatures"] = true; 46 | $t = $state->review_identity_loop_cid(); 47 | return "({$t} !== null ? array_search({$t}, [" . join(", ", $this->csearch->user_ids()) . "]) !== false : null)"; 48 | } 49 | function matches_at_most_once() { 50 | return count($this->csearch->user_ids()) <= 1; 51 | } 52 | #[\ReturnTypeWillChange] 53 | function jsonSerialize() { 54 | $x = parent::jsonSerialize(); 55 | $x["match"] = $this->arg; 56 | if ($this->csearch->is_empty()) { 57 | $x["empty"] = true; 58 | } 59 | return $x; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/api_assign.php: -------------------------------------------------------------------------------- 1 | assignments)) { 8 | return JsonResult::make_missing_error("assignments"); 9 | } 10 | $a = json_decode($qreq->assignments); 11 | if (!is_array($a)) { 12 | return JsonResult::make_parameter_error("assignments"); 13 | } 14 | 15 | $aset = (new AssignmentSet($user))->set_override_conflicts(true); 16 | if ($prow) { 17 | $aset->enable_papers($prow); 18 | } 19 | $aset->parse(CsvParser::make_json($a)); 20 | $aset->execute(); 21 | $jr = $aset->json_result(); 22 | 23 | if ($jr["ok"] && $qreq->search) { 24 | Search_API::apply_search($jr, $user, $qreq, $qreq->search); 25 | // include tag information 26 | if (($p = self::assigned_paper_info($user, $aset))) { 27 | $jr["p"] = $p; 28 | } 29 | } 30 | return $jr; 31 | } 32 | 33 | static function assigned_paper_info(Contact $user, AssignmentSet $assigner) { 34 | $pids = []; 35 | foreach ($assigner->assignments() as $ai) { 36 | if ($ai instanceof Tag_Assigner) { 37 | $pids[$ai->pid] = ($pids[$ai->pid] ?? 0) | 1; 38 | } else if ($ai instanceof Decision_Assigner 39 | || $ai instanceof Status_Assigner) { 40 | $pids[$ai->pid] = ($pids[$ai->pid] ?? 0) | 2; 41 | } 42 | } 43 | $p = []; 44 | foreach ($user->paper_set(["paperId" => array_keys($pids)]) as $pr) { 45 | $p[$pr->paperId] = $tmr = new TagMessageReport; 46 | $pr->add_tag_info_json($tmr, $user); 47 | if (($pids[$pr->paperId] & 2) !== 0) { 48 | list($class, $name) = $pr->status_class_and_name($user); 49 | if ($class !== "ps-submitted") { 50 | $tmr->status_html = "" . htmlspecialchars($name) . ""; 51 | } else { 52 | $tmr->status_html = ""; 53 | } 54 | } 55 | } 56 | return $p; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/search/st_phase.php: -------------------------------------------------------------------------------- 1 | user = !$user || $user->is_root_user() ? null : $user; 15 | $this->phase = $phase; 16 | } 17 | static function parse($word, SearchWord $sword, PaperSearch $srch) { 18 | if (strcasecmp($word, "final") === 0) { 19 | return new Phase_SearchTerm($srch->user, PaperInfo::PHASE_FINAL); 20 | } else if (strcasecmp($word, "review") === 0) { 21 | return new Phase_SearchTerm($srch->user, PaperInfo::PHASE_REVIEW); 22 | } else { 23 | $srch->lwarning($sword, "<0>Only “phase:review” and “phase:final” are allowed"); 24 | return new False_SearchTerm; 25 | } 26 | } 27 | function sqlexpr(SearchQueryInfo $sqi) { 28 | return $this->phase === PaperInfo::PHASE_FINAL ? "(Paper.timeWithdrawn<=0 and Paper.outcome>0)" : "true"; 29 | } 30 | function test(PaperInfo $row, $xinfo) { 31 | return $row->visible_phase($this->user) === $this->phase; 32 | } 33 | function about() { 34 | return self::ABOUT_PAPER; 35 | } 36 | 37 | /** @return ?int */ 38 | static function term_phase(SearchTerm $st) { 39 | return $st->visit(function ($t, ...$vals) { 40 | if (empty($vals)) { 41 | return $t instanceof Phase_SearchTerm ? $t->phase : null; 42 | } else if ($t instanceof And_SearchTerm) { 43 | foreach ($vals as $v) { 44 | if ($v !== null) 45 | return $v; 46 | } 47 | return null; 48 | } else if ($t instanceof Not_SearchTerm) { 49 | return null; 50 | } else { 51 | $x = $vals[0]; 52 | foreach ($vals as $v) { 53 | if ($v !== $x) 54 | return null; 55 | } 56 | return $x; 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/papercolumns/pc_shepherd.php: -------------------------------------------------------------------------------- 1 | override = PaperColumn::OVERRIDE_IFEMPTY; 13 | } 14 | function view_option_schema() { 15 | return self::user_view_option_schema(); 16 | } 17 | function prepare(PaperList $pl, $visible) { 18 | if (!$pl->user->can_view_shepherd(null) 19 | || (!$pl->conf->has_any_lead_or_shepherd() && !$visible)) { 20 | return false; 21 | } 22 | $pl->conf->pc_set(); // prepare cache 23 | $this->nameflags = $this->user_view_option_name_flags($pl->conf); 24 | return true; 25 | } 26 | static private function cid(PaperList $pl, PaperInfo $row) { 27 | if ($row->shepherdContactId > 0 && $pl->user->can_view_shepherd($row)) { 28 | return $row->shepherdContactId; 29 | } else { 30 | return 0; 31 | } 32 | } 33 | function reset(PaperList $pl) { 34 | if (!$this->was_reset && $pl->conf->setting("extrev_shepherd")) { 35 | foreach ($pl->rowset() as $row) { 36 | if ($row->shepherdContactId > 0) 37 | $pl->conf->prefetch_user_by_id($row->shepherdContactId); 38 | } 39 | } 40 | } 41 | function sort_name() { 42 | return $this->sort_name_with_options("format"); 43 | } 44 | function compare(PaperInfo $a, PaperInfo $b, PaperList $pl) { 45 | $ianno = $this->nameflags & NAME_L ? Contact::SORTSPEC_LAST : Contact::SORTSPEC_FIRST; 46 | return $pl->user_compare(self::cid($pl, $a), self::cid($pl, $b), $ianno); 47 | } 48 | function content_empty(PaperList $pl, PaperInfo $row) { 49 | return !self::cid($pl, $row); 50 | } 51 | function content(PaperList $pl, PaperInfo $row) { 52 | return $pl->user_content($row->shepherdContactId, $row, $this->nameflags); 53 | } 54 | function text(PaperList $pl, PaperInfo $row) { 55 | return $pl->user_text($row->shepherdContactId, $this->nameflags); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/api_paperpc.php: -------------------------------------------------------------------------------- 1 | method() === "POST" && isset($qreq->$type)) { 8 | $aset = new AssignmentSet($user); 9 | $aset->enable_papers($prow); 10 | $aset->parse("paper,action,user\n{$prow->paperId},{$type}," . CsvGenerator::quote($qreq->$type)); 11 | if (!$aset->execute()) { 12 | return $aset->json_result(); 13 | } 14 | $cid = $user->conf->fetch_ivalue("select {$type}ContactId from Paper where paperId=?", $prow->paperId); 15 | } else { 16 | $k = "can_view_{$type}"; 17 | if (!$user->$k($prow)) { 18 | return JsonResult::make_permission_error(); 19 | } 20 | $k = "{$type}ContactId"; 21 | $cid = $prow->$k; 22 | } 23 | $pcu = $cid ? $user->conf->user_by_id($cid, USER_SLICE) : null; 24 | $j = [ 25 | "ok" => true, 26 | $type => $pcu ? $pcu->email : "none", 27 | "{$type}_html" => $pcu ? $user->name_html_for($pcu) : "None" 28 | ]; 29 | if ($user->can_view_user_tags()) { 30 | $j["color_classes"] = $pcu ? $pcu->viewable_color_classes($user) : ""; 31 | } 32 | return $j; 33 | } 34 | 35 | static function lead_api(Contact $user, Qrequest $qreq, PaperInfo $prow) { 36 | return self::run($user, $qreq, $prow, "lead"); 37 | } 38 | 39 | static function shepherd_api(Contact $user, Qrequest $qreq, PaperInfo $prow) { 40 | return self::run($user, $qreq, $prow, "shepherd"); 41 | } 42 | 43 | static function manager_api(Contact $user, Qrequest $qreq, PaperInfo $prow) { 44 | return self::run($user, $qreq, $prow, "manager"); 45 | } 46 | 47 | static function pc_api(Contact $user, Qrequest $qreq) { 48 | if (!$user->can_view_pc()) { 49 | return JsonResult::make_permission_error(); 50 | } 51 | $jr = new JsonResult($user->conf->hotcrp_pc_json($user, $qreq->ui ? Conf::PCJM_UI : Conf::PCJM_DEFAULT)); 52 | if ($qreq->ui) { 53 | $jr->set_pretty_print(false); 54 | } 55 | return $jr; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /etc/autoassigners.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "prefconflict", 4 | "function": "+PrefConflict_Autoassigner" 5 | }, 6 | { 7 | "name": "clear", 8 | "function": "+Clear_Autoassigner", 9 | "parameters": ["type"] 10 | }, 11 | { 12 | "name": "lead", 13 | "function": "+PaperPC_Autoassigner", 14 | "parameters": ["?score", "?allow_incomplete {bool}", "?method", "?balance", "?max_load", "?max_load_tag"] 15 | }, 16 | { 17 | "name": "shepherd", 18 | "function": "+PaperPC_Autoassigner", 19 | "parameters": ["?score", "?allow_incomplete {bool}", "?method", "?balance", "?max_load", "?max_load_tag"] 20 | }, 21 | { 22 | "name": "__review_parameters", 23 | "parameters": [ 24 | "?rtype Review type", 25 | "?round Review round", 26 | "?method", 27 | "?balance", 28 | "?load", 29 | "?max_load", 30 | "?max_load_tag", 31 | "?gadget", 32 | "?assignment_cost", 33 | "?preference_cost", 34 | "?expertise_x_cost", 35 | "?expertise_y_cost" 36 | ] 37 | }, 38 | { 39 | "name": "review", 40 | "function": "+Review_Autoassigner", 41 | "parameters": [ 42 | "count {n} Number of reviews to assign per paper", 43 | "$__review_parameters" 44 | ] 45 | }, 46 | { 47 | "name": "review_ensure", 48 | "function": "+Review_Autoassigner", 49 | "parameters": [ 50 | "count {n} Number of reviews to ensure per paper", 51 | "$__review_parameters" 52 | ] 53 | }, 54 | { 55 | "name": "review_adjust", 56 | "function": "+Review_Autoassigner", 57 | "parameters": [ 58 | "count {n} Number of reviews to ensure per paper", 59 | "$__review_parameters" 60 | ] 61 | }, 62 | { 63 | "name": "review_per_pc", 64 | "function": "+Review_Autoassigner", 65 | "parameters": [ 66 | "count {n} Number of reviews to assign per PC member", 67 | "$__review_parameters" 68 | ] 69 | }, 70 | { 71 | "name": "discussion_order", 72 | "function": "+DiscussionOrder_Autoassigner", 73 | "parameters": ["tag {tag} Destination tag"] 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /src/options/o_topics.php: -------------------------------------------------------------------------------- 1 | refresh_topic_set(); 9 | } 10 | 11 | function refresh_topic_set() { 12 | $ts = $this->topic_set(); 13 | $empty = $ts->count() === 0 && !$ts->auto_add(); 14 | $this->override_exists_condition($empty ? false : null); 15 | } 16 | 17 | function topic_set() { 18 | return $this->conf->topic_set(); 19 | } 20 | 21 | function interests($user) { 22 | return $user ? $user->topic_interest_map() : []; 23 | } 24 | 25 | function value_force(PaperValue $ov) { 26 | if ($this->id === PaperOption::TOPICSID) { 27 | $vs = $ov->prow->topic_list(); 28 | $ov->set_value_data($vs, array_fill(0, count($vs), null)); 29 | } 30 | } 31 | 32 | private function _store_new_values(PaperValue $ov, PaperStatus $ps) { 33 | $this->topic_set()->commit_auto_add(); 34 | $vs = $ov->value_list(); 35 | $newvs = $ov->anno("new_values"); 36 | '@phan-var list $newvs'; 37 | foreach ($newvs as $tk) { 38 | if (($tid = $this->topic_set()->find_exact($tk)) !== null) { 39 | $vs[] = $tid; 40 | } 41 | } 42 | $this->topic_set()->sort($vs); // to reduce unnecessary diffs 43 | $ov->set_value_data($vs, array_fill(0, count($vs), null)); 44 | $ov->set_anno("new_values", null); 45 | } 46 | 47 | function value_save(PaperValue $ov, PaperStatus $ps) { 48 | if (!$ov->anno("new_values") 49 | && $ov->equals($ov->prow->base_option($this->id))) { 50 | return true; 51 | } 52 | if ($ov->anno("new_values")) { 53 | if (!$ps->save_status_prepared()) { 54 | $ps->request_resave($this); 55 | $ps->change_at($this); 56 | } else { 57 | $this->_store_new_values($ov, $ps); 58 | } 59 | } 60 | $ps->change_at($this); 61 | if ($this->id === PaperOption::TOPICSID) { 62 | $ov->prow->set_prop("topicIds", join(",", $ov->value_list())); 63 | } 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/search/st_color.php: -------------------------------------------------------------------------------- 1 | user = $user; 15 | $this->word = $word; 16 | } 17 | function sqlexpr(SearchQueryInfo $sqi) { 18 | return 'exists (select * from PaperTag where paperId=Paper.paperId)'; 19 | } 20 | function test(PaperInfo $row, $xinfo) { 21 | $tags = $row->viewable_tags($this->user); 22 | $styles = $row->conf->tags()->styles($tags, 0, true); 23 | return !empty($styles) 24 | && ($this->word === "any" || in_array($this->word, $styles, true)); 25 | } 26 | function debug_json() { 27 | return ["type" => $this->type, "style" => $this->word]; 28 | } 29 | function about() { 30 | return self::ABOUT_PAPER; 31 | } 32 | 33 | static function parse_style($word, SearchWord $sword, PaperSearch $srch) { 34 | $word = strtolower($word); 35 | if ($word === "any" || $word === "none") { 36 | return (new Color_SearchTerm($srch->user, "any"))->negate_if($word === "none"); 37 | } else if ($word === "color") { 38 | return new Color_SearchTerm($srch->user, "tagbg"); 39 | } else if (($ks = $srch->conf->tags()->known_style($word))) { 40 | return new Color_SearchTerm($srch->user, "tag-" . $ks->style); 41 | } else { 42 | $srch->lwarning($sword, "<0>Unknown style ‘{$word}’"); 43 | return new False_SearchTerm; 44 | } 45 | } 46 | static function parse_color($word, SearchWord $sword, PaperSearch $srch) { 47 | $word = strtolower($word); 48 | if ($word === "any" || $word === "color" || $word === "none") { 49 | return (new Color_SearchTerm($srch->user, "tagbg"))->negate_if($word === "none"); 50 | } else if (($ks = $srch->conf->tags()->known_style($word))) { 51 | return new Color_SearchTerm($srch->user, $ks->style); 52 | } else { 53 | $srch->lwarning($sword, "<0>Unknown color ‘{$word}’"); 54 | return new False_SearchTerm; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master, github ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Start mysql 22 | run: | 23 | (echo '[mysqld]'; echo 'default-authentication-plugin=mysql_native_password') | sudo sh -c "cat > /etc/mysql/conf.d/nativepassword.cnf" 24 | sudo systemctl start mysql 25 | mysql -V 26 | 27 | - name: Prepare the application 28 | run: | 29 | sudo lib/createdb.sh -u root -proot -c test/options.php --batch 30 | sudo lib/createdb.sh -u root -proot -c test/cdb-options.php --no-dbuser --batch 31 | 32 | - name: Set up PHP 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php-versions }} 36 | extensions: mbstring, intl, mysqlnd 37 | 38 | - name: Install poppler 39 | run: | 40 | sudo apt-get --fix-broken install 41 | sudo apt-get install poppler-utils || ( sudo apt-get update && sudo apt-get install poppler-utils ) 42 | 43 | - name: Run tests 44 | run: sh test/check.sh --all 45 | 46 | build-22: 47 | runs-on: ubuntu-22.04 48 | 49 | strategy: 50 | matrix: 51 | php-versions: ['7.3', '8.0'] 52 | 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v4 56 | 57 | - name: Start mysql 58 | run: | 59 | (echo '[mysqld]'; echo 'default-authentication-plugin=mysql_native_password') | sudo sh -c "cat > /etc/mysql/conf.d/nativepassword.cnf" 60 | sudo /etc/init.d/mysql start 61 | mysql -V 62 | 63 | - name: Prepare the application 64 | run: | 65 | sudo lib/createdb.sh -u root -proot -c test/options.php --batch 66 | sudo lib/createdb.sh -u root -proot -c test/cdb-options.php --no-dbuser --batch 67 | 68 | - name: Set up PHP 69 | uses: shivammathur/setup-php@v2 70 | with: 71 | php-version: ${{ matrix.php-versions }} 72 | extensions: mbstring, intl, mysqlnd 73 | 74 | - name: Install poppler 75 | run: | 76 | sudo apt-get update 77 | sudo apt-get --fix-missing install poppler-utils 78 | 79 | - name: Run tests 80 | run: sh test/check.sh --all 81 | --------------------------------------------------------------------------------