diff --git a/devel/manual/components.md b/devel/manual/components.md index 533d79d88..83ecc3c7f 100644 --- a/devel/manual/components.md +++ b/devel/manual/components.md @@ -11,27 +11,27 @@ The components for each feature are defined by a JSON configuration file in `etc/`, and then modified by JSON objects and files listed in an `$Opt` setting. -| Feature | Default configuration file | `$Opt` setting | Match? | User `allow_if`? | -| ----------------------------------------- | ----------------------------- | ------------------------- | ------ | ----------- | -| Request handling and page rendering | `etc/pages.json` | `$Opt["pages"]` | | ✓ | -| API endpoints | `etc/apifunctions.json` | `$Opt["apiFunctions"]` | | ✓ | -| Formula functions | `etc/formulafunctions.json` | `$Opt["formulaFunctions"]` | | ✓ | -| Submission field types | `etc/optiontypes.json` | `$Opt["optionTypes"]` | | | -| Sample submission fields | `etc/submissionfieldlibrary.json` | `$Opt["submissionFieldLibraries"]` | | | -| Assignment types for bulk assignment | `etc/assignmentparsers.json` | `$Opt["assignmentParsers"]` | | ✓ | -| Autoassigners | `etc/autoassigners.json` | `$Opt["autoassigners"]` | | ✓ | -| Help topics | `etc/helptopics.json` | `$Opt["helpTopics"]` | | ✓ | -| Search actions | `etc/listactions.json` | `$Opt["listActions"]` | | ✓ | -| Mail keywords | `etc/mailkeywords.json` | `$Opt["mailKeywords"]` | ✓ | ✓ | -| Mail templates | `etc/mailtemplates.json` | `$Opt["mailTemplates"]` | | ✓ | -| Search columns | `etc/papercolumns.json` | `$Opt["paperColumns"]` | ✓ | ✓ | -| Profile page topics | `etc/profilegroups.json` | `$Opt["profileGroups"]` | | ✓ | -| Sample review fields | `etc/reviewfieldlibrary.json` | `$Opt["reviewFieldLibraries"]` | | ✓ | -| Review field types | `etc/reviewfieldtypes.json` | `$Opt["reviewFieldTypes"]` | | | -| Search keywords | `etc/searchkeywords.json` | `$Opt["searchKeywords"]` | ✓ | ✓ | -| Setting topics and rendering | `etc/settinggroups.json` | `$Opt["settingGroups"]` | | ✓ | -| Settings | `etc/settinginfo.json` | `$Opt["settingInfo"]` | | ✓ | -| OAuth/OpenID authentication types | None | `$Opt["oAuthTypes"]` | | | +| Feature | Default configuration file | `$Opt` setting | Match? | User `allow_if`? | Nested? | +|:---------------------------------------|:------------------------------|:--------------------------|---|---|---| +| [Request handling and page rendering](./pages.md) | `etc/pages.json` | `$Opt["pages"]` | | ✓ | ✓ | +| API endpoints | `etc/apifunctions.json` | `$Opt["apiFunctions"]` | | ✓ | | +| Formula functions | `etc/formulafunctions.json` | `$Opt["formulaFunctions"]` | | ✓ | | +| Submission field types | `etc/optiontypes.json` | `$Opt["optionTypes"]` | | | | +| Sample submission fields | `etc/submissionfieldlibrary.json` | `$Opt["submissionFieldLibraries"]` | | | | +| Assignment types for bulk assignment | `etc/assignmentparsers.json` | `$Opt["assignmentParsers"]` | | ✓ | | +| Autoassigners | `etc/autoassigners.json` | `$Opt["autoassigners"]` | | ✓ | | +| Help topics | `etc/helptopics.json` | `$Opt["helpTopics"]` | | ✓ | ✓ | +| Search actions | `etc/listactions.json` | `$Opt["listActions"]` | | ✓ | | +| Mail keywords | `etc/mailkeywords.json` | `$Opt["mailKeywords"]` | ✓ | ✓ | | +| Mail templates | `etc/mailtemplates.json` | `$Opt["mailTemplates"]` | | ✓ | | +| Search columns | `etc/papercolumns.json` | `$Opt["paperColumns"]` | ✓ | ✓ | | +| Profile page topics | `etc/profilegroups.json` | `$Opt["profileGroups"]` | | ✓ | ✓ | +| Sample review fields | `etc/reviewfieldlibrary.json` | `$Opt["reviewFieldLibraries"]` | | ✓ | | +| Review field types | `etc/reviewfieldtypes.json` | `$Opt["reviewFieldTypes"]` | | | | +| Search keywords | `etc/searchkeywords.json` | `$Opt["searchKeywords"]` | ✓ | ✓ | | +| Setting topics and rendering | `etc/settinggroups.json` | `$Opt["settingGroups"]` | | ✓ | ✓ | +| Settings | `etc/settinginfo.json` | `$Opt["settingInfo"]` | | ✓ | | +| OAuth/OpenID authentication types | None | `$Opt["oAuthTypes"]` | | | | ## Component construction @@ -107,3 +107,33 @@ returned. The merge process works as follows. 4. The highest-priority remaining component, if any, is the result of the merge process. + +## Listing components + +In some cases, such as rendering pages, HotCRP needs to list all components, +or all components that match a given prefix. This list is usually produced by +ordering the relevant components according to their `order` properties. +`order` defaults to 0; ties are broken by the `name` property and and/or by +source order. + +## Aliases + +When a component fragment has an `alias` property, HotCRP replaces that +fragment by the result of looking up the component named `alias`. This can be +useful to provide synonyms; for example, the search keyword component fragment +`{"name": "administrator", "alias": "admin"}` allows users to search for +`administrator:` as an alias for `admin:`. Aliasing may be preferred to +copying a component definition because changes in the source component are +automatically incorporated in the alias. + +An alias may point to another alias, but HotCRP will only follow alias chains +to a limited depth before giving up. + +## Nested components + +Certain component types, such as pages and help topics, support nesting. A +name may contain one or more slashes. The component is considered a member of +the **group** defined by everything up to the last slash; for instance, +components `a/b`, `a/c`, and `a/b/c` are members of groups `a`, `a`, and +`a/b`, respectively. If present, the component’s `group` property overrides +the group value derived from the component name. diff --git a/devel/manual/pages.md b/devel/manual/pages.md new file mode 100644 index 000000000..512fbee8d --- /dev/null +++ b/devel/manual/pages.md @@ -0,0 +1,97 @@ +# HotCRP page construction + +HotCRP uses [components][] to handle requests and render pages. You can add +new pages to HotCRP or selectively override existing pages by extending +`$Opt["pages"]`. + +## Page flow + +When a request arrives, HotCRP parses the requested URL into the *site path*, +which the base URL for this conference; the *page*, which is the name of the +page to render; and the *path*, which is anything following the page. For +example: + + https://yoursite.edu/hotcrpprefix/paper/3/edit + -------------- ******* + SITE PATH ===== PATH + PAGE + +The site path may end with a user identifier, such as `/u/0/`, that’s present +when a single browser logs in to multiple accounts. + +HotCRP looks up the page component named `PAGE`. If the page component does +not exist, or if its name starts with two underscores, HotCRP reports a +page-not-found error. If the user is disabled, then HotCRP checks the page +component’s `allow_disabled` property and returns an error to the user unless +`allow_disabled` is true. + +Otherwise, the page component will have a `group` member, which defaults to +`PAGE` but need not equal it (for instance, if one page is an alias for +another). The value of this `group` member is called the **page group**. + +## Processing the request + +HotCRP next obtains a list of the page group’s components, including (1) the +page component itself, and then (2) any components that are direct members of +that group, ordered by their `order` properties. + +In the first stage, HotCRP scans the component list and calls any allowed +`request_function`s. + +A `request_function` defines a PHP callback that processes the request before +page rendering starts. Parameters to `request_function` are (1) the viewing +user (a `Contact` object), (2) the request being processed (a `Qrequest` +object), (3) the set of page components (a `ComponentSet` object), and (4) the +page component itself (a generic object). `request_function` may have the +format `CLASSNAME::FUNCTIONNAME` or `*CLASSNAME::FUNCTIONNAME`. In the first +form, HotCRP calls the named function statically. In the second form (with +`*`), HotCRP constructs an object of type `CLASSNAME` (constructor parameters +`Contact, Qrequest, ComponentSet`) and then calls the relevant `FUNCTIONNAME` +on that object. All calls with the same `*CLASSNAME` will use the same object. + +A component’s `request_function` may be blocked by the `allow_request_if` as +well as the `allow_if` property. For instance, `"allow_request_if": +"req.clearbug"` allows the `request_function` only if the request has a +`clearbug` parameter. + +If a `request_function` explicitly returns `false` (as opposed to `null`, or +by simply falling off the end), then HotCRP quits calling `request_function`s +and terminates request processing. A `request_function` may also throw a value +of type `Redirection` to force HotCRP to redirect the user’s browser to +another URI. + +## Rendering the result + +Assuming the `request_function`s do not throw a redirection, HotCRP next +re-scans the component list and prints the corresponding components. + +To print a component, HotCRP checks for: + +1. A `print_function` property. If present, this calls the corresponding PHP + callback, using the same syntax and arguments as `request_function`, above. + +2. Otherwise, an `html_content` property. If present, this is copied to the output. + +3. In either case, if the component has a `print_members` property, HotCRP + next prints the members of that group. + +A `print_function` may cancel further rendering by returning explicit `false`, +or by throwing a `Redirection` or `PageCompletion` exception. + +Many HotCRP pages do not have separate `request_function`s, instead handling +all request parsing as part of the first `print_function` for a page. + +## Shorthand + +Page components may be defined using an array shorthand. The notation `[NAME, +ORDER, PRINT_FUNCTION]` or `[NAME, ORDER, PRINT_FUNCTION, PRIORITY]` is the +same as an with the corresponding properties: + +``` +{ + "name": NAME, "order": ORDER, "print_function": PRINT_FUNCTION [, "priority": PRIORITY] +} +``` + + +[components]: ./components.md diff --git a/index.php b/index.php index 7f76951b8..b793b334a 100644 --- a/index.php +++ b/index.php @@ -6,20 +6,29 @@ /** @param Contact $user * @param Qrequest $qreq - * @param string $group + * @param object $pagej * @param ComponentSet $pc */ -function handle_request_components($user, $qreq, $group, $pc) { +function handle_request_components($user, $qreq, $pagej, $pc) { $pc->add_xt_checker([$qreq, "xt_allow"]); $reqgj = []; + $nfound = 0; $not_allowed = false; - foreach ($pc->members($group, "request_function") as $gj) { + if (isset($pagej->request_function)) { + ++$nfound; + if ($pc->allowed($gj->allow_request_if ?? null, $gj)) { + $reqgj[] = $gj; + } + } + foreach ($pc->members($pagej->group, "request_function") as $gj) { + ++$nfound; if ($pc->allowed($gj->allow_request_if ?? null, $gj)) { $reqgj[] = $gj; - } else { - $not_allowed = true; } } - if ($not_allowed && $qreq->is_post() && !$qreq->valid_token()) { + if ($nfound > 0 + && $nfound < count($reqgj) + && $qreq->is_post() + && !$qreq->valid_token()) { $user->conf->error_msg($user->conf->_i("badpost")); } foreach ($reqgj as $gj) { @@ -46,7 +55,7 @@ function handle_request($nav) { Multiconference::fail($qreq, 403, ["link" => true], $user->conf->_i("account_disabled")); } else { $pc->set_root($pagej->group); - handle_request_components($user, $qreq, $pagej->group, $pc); + handle_request_components($user, $qreq, $pagej, $pc); $pc->print_group($pagej->group, true); } } catch (Redirection $redir) {