Skip to content

Commit

Permalink
Start publishing search API
Browse files Browse the repository at this point in the history
  • Loading branch information
kohler committed Dec 9, 2024
1 parent 4046ccf commit 74fb3c4
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 61 deletions.
13 changes: 7 additions & 6 deletions devel/apidoc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ issue](https://github.com/kohler/hotcrp/issues). We also welcome [pull
requests](https://github.com/kohler/hotcrp/pulls).


## Basics
## Overview

HotCRP parameters are generally provided using [form
encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST),
Expand All @@ -22,8 +22,8 @@ for requests that include uploaded files.

The common `p` parameter defines a submission ID. It can appear either in the
query string or immediately following `api/` in the query path:
`api/comment?p=1` and `api/1/comment` are the same API call. `p` is a decimal
number greater than 0, but some API calls accept `p=new` when defining a new
`api/comment?p=1` and `api/1/comment` are the same API call. `p` is a positive
decimal integer, but some API calls accept `p=new` when defining a new
submission.

Responses are formatted as JSON. Every response has an `ok` property; `ok` is
Expand All @@ -36,9 +36,10 @@ deleting a comment uses a `delete=1` parameter for a `POST` request, rather
than a `DELETE` request.


## Authentication
### Authentication

External applications should authenticate to HotCRP’s API using bearer tokens.
Obtain an API token using Account settings > Developer.
External applications should authenticate to HotCRP’s API using bearer tokens
(an `Authorization: bearer` HTTP header). Obtain an API token using Account
settings > Developer.

HotCRP Javascript makes API calls using session cookies for authentication.
5 changes: 4 additions & 1 deletion devel/apidoc/openapi-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@
},
"tag_list": {
"type": "array",
"description": "List of tag names",
"items": {
"$ref": "#/components/schemas/tag"
},
Expand All @@ -537,6 +538,7 @@
},
"tag_value_list": {
"type": "array",
"description": "List of tag names with values",
"items": {
"$ref": "#/components/schemas/tag_value"
},
Expand Down Expand Up @@ -606,7 +608,8 @@
"properties": {
"pos": {
"type": "integer",
"minimum": 0
"minimum": 0,
"description": "Position of annotation in associated submission list"
},
"annoid": {
"type": "integer"
Expand Down
72 changes: 72 additions & 0 deletions devel/apidoc/search.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
# Search

These endpoints perform searches on submissions.


# get /search

> Retrieve search results
Use this endpoint to return the list of submission IDs corresponding to a
search.

Pass the search query in the `q` parameter. The list of matching IDs is
returned in the `ids` response property.

The `t`, `qt`, `reviewer`, `sort`, and `scoresort` parameters can also affect
the search. `t` defines the collection of submissions to search. `t=viewable`
checks all submissions the user can view; the default collection is often
narrower (a typical default is `t=s`, which searches complete submissions).

The `groups` response property is an array of annotations that apply to the
search, and is returned for `THEN` searches, `LEGEND` searches, and searches
on tags with annotations. Each annotation contains a position `pos`, and may
also have a `legend`, a `search`, an `annoid`, and other properties. `pos` is
an integer index into the `ids` array; it ranges from 0 to the number of items
in that array. Annotations with a given `pos` should appear *before* the paper
at that index in the `ids` array. For instance, this response might be
returned for the search `10-12 THEN 15-18`:

```json
{
"ok": true,
"message_list": [],
"ids": [10, 12, 18],
"groups": [
{
"pos": 0,
"legend": "10-12",
"search": "10-12"
},
{
"pos": 2,
"legend": "15-18",
"search": "15-18"
}
]
}
```

* response_schema search_response


# get /fieldhtml

> Retrieve search results as field HTML

# get /fieldtext

> Retrieve list field text

# get /searchactions

> Retrieve available search actions

# get /searchaction

> Perform search action

# post /searchaction

> Perform search action
12 changes: 6 additions & 6 deletions devel/apidoc/submissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ can see them.

> Create or modify submission
This endpoint modifies a specified submission. The `p` parameter determines
the submission ID. Setting `p=new` will create a new submission; the response
will contain the chosen submission ID.
This endpoint modifies the submission specified by the `p` parameter. Setting
`p=new` will create a new submission; the response will contain the chosen
submission ID.

Modifications are specified using a JSON object. There are three ways to
provide that JSON, depending on the content-type of the request:

1. As a request body with content-type `application/json`.
2. As a file named `data.json` in a ZIP archive. The request body has
content-type `application/zip`.
1. As a JSON request body with content-type `application/json`.
2. In a ZIP archive request body with content-type `application/zip`, as a
file named `data.json`.
3. As a parameter named `json` in a normal `application/x-www-form-urlencoded`
or `multipart/form-data` body.

Expand Down
12 changes: 8 additions & 4 deletions etc/apiexpansions.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,16 @@
"tags": ["Search"], "order": 1
},
{
"name": "searchaction", "get": true, "merge": true,
"tags": ["Search"]
"name": "fieldtext", "get": true, "merge": true,
"tags": ["Search"], "order": 2
},
{
"name": "fieldtext", "get": true, "merge": true,
"tags": ["Search"]
"name": "searchactions", "get": true, "merge": true,
"tags": ["Search"], "order": 3
},
{
"name": "searchaction", "get": true, "merge": true,
"tags": ["Search"], "order": 4
},
{
"name": "graphdata", "get": true, "merge": true,
Expand Down
9 changes: 7 additions & 2 deletions etc/apifunctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,18 @@
{
"name": "search", "get": true,
"function": "Search_API::search",
"parameters": "q ?t ?qt ?sort ?report ?scoresort ?reviewer",
"parameters": "q ?t ?qt ?sort ?scoresort ?reviewer ?report ?warn_missing",
"response": "ids groups hotlist search_params"
},
{
"name": "searchactions", "get": true,
"function": "Search_API::searchactions",
"response": "actions"
},
{
"name": "searchaction", "get": true,
"function": "Search_API::searchaction",
"parameters": "?action q ?t ?qt ?sort ?report ?scoresort ?reviewer ?pap[]"
"parameters": "?action q ?t ?qt ?sort ?report ?scoresort ?reviewer ?p"
},
{
"name": "searchcompletion", "get": true,
Expand Down
86 changes: 44 additions & 42 deletions src/api/api_search.php
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
<?php
// api_search.php -- HotCRP search-related API calls
// Copyright (c) 2008-2022 Eddie Kohler; see LICENSE.
// Copyright (c) 2008-2024 Eddie Kohler; see LICENSE.

class Search_API {
/** @return JsonResult|PaperList */
static function make_list(Contact $user, Qrequest $qreq) {
$q = $qreq->q;
if (isset($q)) {
$q = trim($q);
if ($q === "(All)") {
$q = "";
/** @return JsonResult|PaperSearch */
static private function make_search(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
$sq = PaperSearch::qreq_subset($qreq);
if (!isset($sq["q"])) {
if ($prow) {
$sq["q"] = (string) $prow->paperId;
$sq["t"] = "viewable";
} else if (isset($qreq->qa) || isset($qreq->qo) || isset($qreq->qx)) {
$sq["q"] = PaperSearch::canonical_query((string) $qreq->qa, (string) $qreq->qo, (string) $qreq->qx, $qreq->qt, $user->conf);
} else {
return JsonResult::make_missing_error("q");
}
} else if (isset($qreq->qa) || isset($qreq->qo) || isset($qreq->qx)) {
$q = PaperSearch::canonical_query((string) $qreq->qa, (string) $qreq->qo, (string) $qreq->qx, $qreq->qt, $user->conf);
} else {
return JsonResult::make_missing_error("q");
}
$search = new PaperSearch($user, $sq);
if (friendly_boolean($qreq->warn_missing)) {
$search->set_warn_missing(true);
}
return $search;
}

$search = new PaperSearch($user, [
"t" => $qreq->t ?? "",
"q" => $q,
"qt" => $qreq->qt,
"reviewer" => $qreq->reviewer,
"sort" => $qreq->sort,
"scoresort" => $qreq->scoresort
]);
/** @return JsonResult|PaperList */
static private function make_list(Contact $user, Qrequest $qreq) {
$search = self::make_search($user, $qreq, null);
if ($search instanceof JsonResult) {
return $search;
}
$pl = new PaperList($qreq->report ? : "pl", $search, ["sort" => true], $qreq);
$pl->apply_view_report_default();
$pl->apply_view_session($qreq);
if (friendly_boolean($qreq->session) !== false) {
$pl->apply_view_session($qreq);
}
return $pl;
}

Expand All @@ -40,6 +46,7 @@ static function search(Contact $user, Qrequest $qreq) {
$ih = $pl->ids_and_groups();
return new JsonResult([
"ok" => true,
"message_list" => $pl->search->message_list(),
"ids" => $ih[0],
"groups" => $ih[1],
"hotlist" => $pl->session_list_object()->info_string(),
Expand Down Expand Up @@ -76,17 +83,13 @@ static function fieldhtml(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
if ($qreq->f === null) {
return JsonResult::make_missing_error("f");
}
if (!isset($qreq->q) && $prow) {
$qreq->t = $prow->timeSubmitted > 0 ? "s" : "all";
$qreq->q = $prow->paperId;
} else if (!isset($qreq->q)) {
$qreq->q = "";
$search = self::make_search($user, $qreq, $prow);
if ($search instanceof JsonResult) {
return $search;
}

$search = new PaperSearch($user, $qreq);
$pl = new PaperList("empty", $search);
if (isset($qreq->aufull)) {
$pl->set_view("aufull", (bool) $qreq->aufull, PaperList::VIEWORIGIN_SESSION);
if (($aufull = friendly_boolean($qreq->aufull)) !== null) {
$pl->set_view("aufull", $aufull, PaperList::VIEWORIGIN_SESSION);
}
$pl->parse_view($qreq->f, PaperList::VIEWORIGIN_MAX);
$response = $pl->table_html_json();
Expand All @@ -95,7 +98,11 @@ static function fieldhtml(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
"ok" => !empty($response["fields"]),
"message_list" => $pl->message_set()->message_list()
] + $response;
if ($j["ok"] && $qreq->session && $qreq->valid_token() && !$qreq->is_head()) {
if ($j["ok"]
&& $qreq->session
&& $qreq->valid_token()
&& !$qreq->is_head()
&& friendly_boolean($qreq->session) === null) {
Session_API::change_session($qreq, $qreq->session);
}
return $j;
Expand All @@ -105,17 +112,14 @@ static function fieldtext(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
if ($qreq->f === null) {
return JsonResult::make_missing_error("f");
}

if (!isset($qreq->q) && $prow) {
$qreq->t = $prow->timeSubmitted > 0 ? "s" : "all";
$qreq->q = $prow->paperId;
} else if (!isset($qreq->q)) {
$qreq->q = "";
$search = self::make_search($user, $qreq, $prow);
if ($search instanceof JsonResult) {
return $search;
}
$search = new PaperSearch($user, $qreq);
$pl = new PaperList("empty", $search);
$pl->parse_view($qreq->f, PaperList::VIEWORIGIN_MAX);
$response = $pl->text_json();

return [
"ok" => !empty($response),
"message_list" => $pl->message_set()->message_list(),
Expand All @@ -124,9 +128,7 @@ static function fieldtext(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
}

static function searchaction(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
if ($qreq->is_get() && ($qreq->action ?? "") === "") {
return self::searchaction_list($user);
} else if (($qreq->action ?? "") === "") {
if (($qreq->action ?? "") === "") {
return JsonResult::make_missing_error("action");
}
$qreq->p = $qreq->p ?? "all";
Expand All @@ -138,7 +140,7 @@ static function searchaction(Contact $user, Qrequest $qreq, ?PaperInfo $prow) {
return ListAction::resolve_document($action, $qreq);
}

static function searchaction_list(Contact $user) {
static function searchactions(Contact $user) {
$fjs = [];
$cs = ListAction::components($user);
foreach ($cs->members("") as $rf) {
Expand Down
6 changes: 6 additions & 0 deletions src/papersearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ class PaperSearch extends MessageSet {
];


/** @param Qrequest $qreq
* @return array<string,mixed> */
static function qreq_subset($qreq) {
return $qreq->subset_as_array("q", "t", "qt", "reviewer", "sort", "scoresort");
}

// NB: `$options` can come from an unsanitized user request.
/** @param string|array|Qrequest $options */
function __construct(Contact $user, $options) {
Expand Down

0 comments on commit 74fb3c4

Please sign in to comment.