Skip to content

Commit

Permalink
Merge branch 'hypernext' into marcel-link-exp
Browse files Browse the repository at this point in the history
* hypernext:
  add users to apiv2 doc
  add query parameters in api doc for experiments and items GET
  add apidocv2 yarn command
  remove dead code, check is handled in readAll
  fix issue with incorrect semantic version compare
  fix event deletion notification preferences not honored
  fix filename download with non ascii characters
  correctly use the image name on download.php. fix #3797
  add more columns to csv scheduler export
  add bound entity title to event output and ui
  make full syntax link on search page appear like a link
  improve ui for ucp change pass
  • Loading branch information
NicolasCARPi committed Sep 17, 2022
2 parents b764973 + 749c578 commit a03cde1
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 66 deletions.
147 changes: 145 additions & 2 deletions apidoc/v2/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ components:
type: string
fullname:
type: string
item_link:
type: integer
item_link_title:
type: string
experiment:
type: integer
experiment_title:
type: string
experiment:
allOf:
- $ref: '#/components/schemas/entity'
Expand Down Expand Up @@ -375,6 +383,32 @@ components:
type: integer
fullname:
type: string
users:
type: object
properties:
userid:
type: integer
firstname:
type: string
lastname:
type: string
email:
type: string
validated:
type: integer
usergroup:
type: integer
archived:
type: integer
last_login:
type: string
fullname:
type: string
orcid:
type: string
auth_service:
type: integer


security:
- token:
Expand Down Expand Up @@ -518,6 +552,41 @@ paths:
tags: ['Experiments']
summary: Read all experiments that are accessible
operationId: read-experiments
# Note: this has to be repeated manually for now
# See https://github.com/OAI/OpenAPI-Specification/issues/445
parameters:
- name: related
in: query
schema:
type: integer
description: |
Look only for entries linked to this item id.
examples:
first:
summary: Look for entries linked to item with id 42.
value: 42
- name: limit
in: query
schema:
type: integer
default: 15
description: |
Limit the number of results.
examples:
first:
summary: Limit number of results to 5.
value: 5
- name: offset
in: query
schema:
type: integer
default: 0
description: |
Skip a number of results. Use with limit to work the pagination.
examples:
first:
summary: Skip 3 first results.
value: 3
responses:
'200':
description: A list of experiments
Expand Down Expand Up @@ -782,6 +851,31 @@ paths:
tags: ['Items']
summary: Read all items that are accessible
operationId: read-items
# start duplicate from GET /experiments (except related)
parameters:
- name: limit
in: query
schema:
type: integer
default: 15
description: |
Limit the number of results.
examples:
first:
summary: Limit number of results to 5.
value: 5
- name: offset
in: query
schema:
type: integer
default: 0
description: |
Skip a number of results. Use with limit to work the pagination.
examples:
first:
summary: Skip 3 first results.
value: 3
# end duplicate from GET /experiments
responses:
'200':
description: A list of items
Expand Down Expand Up @@ -1255,11 +1349,11 @@ paths:
type: integer
responses:
'200':
description: The patched items type.
description: The patched event.
content:
application/json:
schema:
$ref: '#/components/schemas/items_type'
$ref: '#/components/schemas/event'

# EVENT
/event/{id}:
Expand Down Expand Up @@ -1600,3 +1694,52 @@ paths:
type: array
items:
$ref: '#/components/schemas/unfinished_steps'
# USERS
/users:
summary: Display information about users on the instance.
get:
tags: ['Users']
summary: Read users from instance.
description: If Sysadmin all users will be shown. If Admin only the users from the team. Normal users will be shown an error.
operationId: read-users
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/users'
/users/{id}:
summary: Display information about a particular user.
parameters:
- name: id
in: path
description: ID of the user or `me`.
required: true
schema:
oneOf:
- type: number
- type: string
enum: ['me']
examples:
first:
summary: Access our own user.
value: 'me'
second:
summary: Access user with ID 42.
value: 42
get:
tags: ['Users']
summary: Read information of a user.
description: |
Note: it is possible to use "me" instead of the userid to access the user of the API key.
operationId: read-user
responses:
'200':
description: Public properties of a user.
content:
application/json:
schema:
$ref: '#/components/schemas/users'
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"analyzejs": "webpack-bundle-analyzer stats.json web/app/js",
"api": "tests/run.sh api",
"apidoc": "apidoc -c apidoc/v1/apidoc.json -o apidoc/v1/html -i src/controllers",
"apidocv2": "docker run --name swagger --rm -e SWAGGER_JSON=/c/openapi.yaml -v $(pwd)/apidoc/v2/:/c -p 8085:8080 -d swaggerapi/swagger-ui && echo 'APIv2 Doc: http://localhost:8085' && echo 'Run docker stop swagger to remove it.'",
"brotli": "brotli -vf web/assets/*.css web/assets/*.js web/assets/*.svg web/assets/ove/*.css web/assets/ove/*.js",
"zopfli": "zopfli -i1000 web/assets/*.css web/assets/*.js web/assets/*.svg web/assets/ove/*.css web/assets/ove/*.js",
"build": "webpack-cli --progress --config builder.js",
Expand Down
24 changes: 16 additions & 8 deletions src/controllers/DownloadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class DownloadController implements ControllerInterface
// the human-friendly name that we will give to the downloaded file */
private string $realName = 'unnamed_file';

// ascii only
private string $realNameFallback = 'unnamed_file';

private string $filePath;

private string $longName;
Expand All @@ -40,7 +43,8 @@ public function __construct(private Filesystem $fs, string $longName, string $re
$this->longName = Filter::forFilesystem(basename($longName));
// get the first two letters to get the folder
$this->filePath = substr($this->longName, 0, 2) . '/' . $this->longName;
$this->realName = Filter::toAscii($realName ?? '');
$this->realName = $realName ?? $this->realName;
$this->realNameFallback = Filter::toAscii($realName ?? '');
if (empty($this->realName)) {
$this->realName = 'unnamed_file';
}
Expand All @@ -65,7 +69,7 @@ public function getResponse(): Response
}
try {
$fileStream = $this->fs->readStream($this->getFilePath());
} catch (UnableToReadFile $e) {
} catch (UnableToReadFile) {
// display a thumbnail if the real thumbnail cannot be found
$fileStream = fopen(dirname(__DIR__, 2) . '/web/app/img/fallback-thumb.png', 'rb');
if ($fileStream === false) {
Expand Down Expand Up @@ -93,13 +97,17 @@ public function getResponse(): Response
$this->forceDownload = true;
}

$disposition = HeaderUtils::DISPOSITION_INLINE;
// change the diposition to attachment
if ($this->forceDownload) {
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
$this->realName,
);
$Response->headers->set('Content-Disposition', $disposition);
$disposition = HeaderUtils::DISPOSITION_ATTACHMENT;
}
$dispositionHeader = HeaderUtils::makeDisposition(
$disposition,
$this->realName,
$this->realNameFallback,
);
$Response->headers->set('Content-Disposition', $dispositionHeader);

return $Response;
}
Expand All @@ -111,7 +119,7 @@ private function getMimeType(): string
{
try {
return $this->fs->mimeType($this->getFilePath());
} catch (UnableToRetrieveMetadata $e) {
} catch (UnableToRetrieveMetadata) {
return 'application/force-download';
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/models/Notifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ private function getPref(int $category, string $suffix = ''): int
default => throw new ImproperActionException('Invalid category for preferences.'),
};

return $this->users->userData[$pref . $suffix];
// use a new Users() here because userid might have changed (see multiuser create)
return (new Users($this->userid))->userData[$pref . $suffix];
}
}
39 changes: 29 additions & 10 deletions src/models/Scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,21 @@ public function readAll(): array
$end = $this->normalizeDate($this->end, true);

// the title of the event is title + Firstname Lastname of the user who booked it
$sql = "SELECT team_events.title, team_events.id, team_events.start, team_events.end, team_events.userid,
$sql = "SELECT team_events.id, team_events.title AS title_only, team_events.start, team_events.end, team_events.userid,
CONCAT(u.firstname, ' ', u.lastname) AS fullname,
CONCAT('[', items.title, '] ', team_events.title, ' (', u.firstname, ' ', u.lastname, ')') AS title,
items.title AS item_title,
CONCAT('#', items_types.color) AS color,
CONCAT(u.firstname, ' ', u.lastname) AS fullname
team_events.experiment,
experiments.title AS experiment_title,
team_events.item_link,
items_linkt.title AS item_link_title
FROM team_events
LEFT JOIN items ON team_events.item = items.id
LEFT JOIN items_types ON items.category = items_types.id
LEFT JOIN users AS u ON team_events.userid = u.userid
LEFT JOIN experiments ON (team_events.experiment = experiments.id)
LEFT JOIN items ON (team_events.item = items.id)
LEFT JOIN items AS items_linkt ON (team_events.item_link = items_linkt.id)
LEFT JOIN items_types ON (items.category = items_types.id)
LEFT JOIN users AS u ON (team_events.userid = u.userid)
WHERE (team_events.team = :team OR items.team = :team)
AND team_events.start > :start AND team_events.end <= :end";
$req = $this->Db->prepare($sql);
Expand Down Expand Up @@ -174,7 +180,7 @@ public function destroy(): bool
array('event' => $this->readOne(), 'actor' => $this->Items->Users->userData['fullname']),
),
$TeamsHelper->getAllAdminsUserid(),
(int) $this->Items->Users->userData['userid'],
$this->Items->Users->userData['userid'],
);
return $this->Db->execute($req);
}
Expand All @@ -190,11 +196,16 @@ private function read(): array
// the color is used by fullcalendar for the bg color of the event
$sql = "SELECT team_events.*,
CONCAT(team_events.title, ' (', u.firstname, ' ', u.lastname, ') ', COALESCE(experiments.title, '')) AS title,
CONCAT('#', items_types.color) AS color
team_events.title AS title_only,
CONCAT('#', items_types.color) AS color,
experiments.title AS experiment_title,
items_linkt.title AS item_link_title,
items.title AS item_title
FROM team_events
LEFT JOIN items ON team_events.item = items.id
LEFT JOIN items ON (team_events.item = items.id)
LEFT JOIN items AS items_linkt ON (team_events.item_link = items_linkt.id)
LEFT JOIN experiments ON (experiments.id = team_events.experiment)
LEFT JOIN items_types ON items.category = items_types.id
LEFT JOIN items_types ON (items.category = items_types.id)
LEFT JOIN users AS u ON team_events.userid = u.userid
WHERE team_events.item = :item
AND team_events.start > :start AND team_events.end <= :end";
Expand Down Expand Up @@ -229,7 +240,15 @@ private function updateEpoch(string $column, string $epoch): bool

private function readOneEvent(): array
{
$sql = 'SELECT * from team_events WHERE id = :id';
$sql = 'SELECT team_events.id, team_events.team, team_events.item, team_events.start, team_events.end, team_events.title, team_events.userid, team_events.experiment, team_events.item_link,
team_events.title AS title_only,
experiments.title AS experiment_title,
items_linkt.title AS item_link_title
FROM team_events
LEFT JOIN items ON (team_events.item = items.id)
LEFT JOIN experiments ON (experiments.id = team_events.experiment)
LEFT JOIN items AS items_linkt ON (team_events.item_link = items_linkt.id)
WHERE team_events.id = :id';
$req = $this->Db->prepare($sql);
$req->bindParam(':id', $this->id, PDO::PARAM_INT);
$this->Db->execute($req);
Expand Down
22 changes: 17 additions & 5 deletions src/models/Users.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,20 @@ public function readFromQuery(string $query, int $teamId = 0): array
*/
public function readAllFromTeam(): array
{
if ($this->requester->userData['is_admin'] !== 1) {
throw new IllegalActionException('Only admin can read all users from a team.');
}
return $this->readFromQuery('', $this->userData['team']);
}

/**
* This can be called from api and only contains "safe" values
*/
public function readAll(): array
{
$Request = Request::createFromGlobals();
if ($this->requester->userData['is_sysadmin'] === 1) {
return $this->readFromQuery($Request->query->getAlnum('q'));
return $this->readFromQuerySafe($Request->query->getAlnum('q'), 0);
}
if ($this->requester->userData['is_admin'] === 1) {
return $this->readFromQuery($Request->query->getAlnum('q'), $this->requester->userData['team']);
return $this->readFromQuerySafe($Request->query->getAlnum('q'), $this->requester->userData['team']);
}
throw new IllegalActionException('Normal users cannot read other users.');
}
Expand Down Expand Up @@ -341,6 +341,18 @@ public function isAdminOf(int $userid): bool
return $TeamsHelper->isUserInTeam($userid) && $this->userData['is_admin'] === 1;
}

/**
* Remove sensitives values from readFromQuery()
*/
private function readFromQuerySafe(string $query, int $team): array
{
$users = $this->readFromQuery($query, $team);
foreach ($users as &$user) {
unset($user['mfa_secret']);
}
return $users;
}

private function canReadOrExplode(): void
{
if ($this->requester->userid === $this->userid) {
Expand Down
3 changes: 3 additions & 0 deletions src/services/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public static function forFilesystem(string $input): string
return trim($input ?? 'file', '.-_');
}

/**
* This exists because: The filename fallback must only contain ASCII characters. at /elabftw/vendor/symfony/http-foundation/HeaderUtils.php:173
*/
public static function toAscii(string $input): string
{
// mb_convert_encoding will replace invalid characters with ?, but we want _ instead
Expand Down
Loading

0 comments on commit a03cde1

Please sign in to comment.