Skip to content

Commit 202c483

Browse files
authored
Get user pictures from API (#14963)
* get user pictures from API * better self-map check * fix missing path requirements * add tests * test new picture field
1 parent c9169ef commit 202c483

File tree

5 files changed

+211
-3
lines changed

5 files changed

+211
-3
lines changed

src/Api/HL/Controller/AdministrationController.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ public static function getRawKnownSchemas(): array
148148
'description' => 'Password confirmation',
149149
'x-writeonly' => true,
150150
],
151+
'picture' => [
152+
'type' => Doc\Schema::TYPE_STRING,
153+
'x-mapped-from' => 'picture',
154+
'x-mapper' => static function ($v) {
155+
global $CFG_GLPI;
156+
$path = \Toolbox::getPictureUrl($v, false);
157+
if (!empty($path)) {
158+
return $path;
159+
}
160+
return $CFG_GLPI["root_doc"] . '/pics/picture.png';
161+
}
162+
]
151163
]
152164
],
153165
'Group' => [
@@ -472,6 +484,40 @@ public function getMyEmail(Request $request): Response
472484
return self::getNotFoundErrorResponse();
473485
}
474486

487+
/**
488+
* Get the specified user picture as a Response
489+
* @param string $username The username of the user. Used in Content-Disposition header.
490+
* @param string|null $picture_path The path to the picture from the user's "picture" field.
491+
* @return Response A response with the picture as binary content (or the placeholder user picture if the user has no picture).
492+
*/
493+
private function getUserPictureResponse(string $username, ?string $picture_path): Response
494+
{
495+
if ($picture_path !== null) {
496+
$picture_path = GLPI_PICTURE_DIR . '/' . $picture_path;
497+
} else {
498+
$picture_path = 'pics/picture.png';
499+
}
500+
return \Toolbox::sendFile($picture_path, $username, null, false, true);
501+
}
502+
503+
#[Route(path: '/User/me/Picture', methods: ['GET'])]
504+
#[Doc\Route(
505+
description: 'Get the picture for the current user'
506+
)]
507+
public function getMyPicture(Request $request): Response
508+
{
509+
global $DB;
510+
$it = $DB->request([
511+
'SELECT' => ['name', 'picture'],
512+
'FROM' => User::getTable(),
513+
'WHERE' => [
514+
'id' => $this->getMyUserID(),
515+
],
516+
]);
517+
$data = $it->current();
518+
return $this->getUserPictureResponse($data['name'], $data['picture']);
519+
}
520+
475521
#[Route(path: '/User', methods: ['POST'])]
476522
#[Doc\Route(description: 'Create a new user', parameters: [
477523
[
@@ -510,6 +556,42 @@ public function getUserByUsername(Request $request): Response
510556
return Search::getOneBySchema($this->getKnownSchema('User'), $request->getAttributes(), $request->getParameters(), 'username');
511557
}
512558

559+
#[Route(path: '/User/{id}/Picture', methods: ['GET'], requirements: ['id' => '\d+'])]
560+
#[Doc\Route(
561+
description: 'Get the picture for the current user'
562+
)]
563+
public function getUserPictureByID(Request $request): Response
564+
{
565+
global $DB;
566+
$it = $DB->request([
567+
'SELECT' => ['name', 'picture'],
568+
'FROM' => User::getTable(),
569+
'WHERE' => [
570+
'id' => $request->getAttribute('id'),
571+
],
572+
]);
573+
$data = $it->current();
574+
return $this->getUserPictureResponse($data['name'], $data['picture']);
575+
}
576+
577+
#[Route(path: '/User/username/{username}/Picture', methods: ['GET'], requirements: ['username' => '[a-zA-Z0-9_]+'])]
578+
#[Doc\Route(
579+
description: 'Get the picture for the current user'
580+
)]
581+
public function getUserPictureByUsername(Request $request): Response
582+
{
583+
global $DB;
584+
$it = $DB->request([
585+
'SELECT' => ['name', 'picture'],
586+
'FROM' => User::getTable(),
587+
'WHERE' => [
588+
'name' => $request->getAttribute('username'),
589+
],
590+
]);
591+
$data = $it->current();
592+
return $this->getUserPictureResponse($data['name'], $data['picture']);
593+
}
594+
513595
#[Route(path: '/User/{id}', methods: ['PATCH'], requirements: ['id' => '\d+'])]
514596
#[Doc\Route(
515597
description: 'Update a user by ID',

src/Api/HL/Search.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,10 @@ private function hydrateRecords(array $records): array
437437

438438
if ($fkey === 'id') {
439439
$props_to_use = array_filter($this->flattened_properties, static function ($prop_params, $prop_name) {
440+
$prop_field = $prop_params['x-field'] ?? $prop_name;
441+
$mapped_from_other = isset($prop_params['x-mapped-from']) && $prop_params['x-mapped-from'] !== $prop_field;
440442
// We aren't handling joins or mapped fields here
441-
return !str_contains($prop_name, '.') && !isset($prop_params['x-mapped-from']);
443+
return !str_contains($prop_name, '.') && !$mapped_from_other;
442444
}, ARRAY_FILTER_USE_BOTH);
443445
$criteria['FROM'] = "$table AS " . $DB::quoteName('_');
444446
if ($this->union_search_mode) {

src/Toolbox.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2859,7 +2859,7 @@ public static function stripTags(string $str): string
28592859
*
28602860
* @since 9.5.0
28612861
*/
2862-
public static function savePicture($src, $uniq_prefix = '')
2862+
public static function savePicture($src, $uniq_prefix = '', $keep_src = false)
28632863
{
28642864

28652865
if (!Document::isImage($src)) {
@@ -2884,7 +2884,11 @@ public static function savePicture($src, $uniq_prefix = '')
28842884
return false;
28852885
}
28862886

2887-
if (!rename($src, $dest)) {
2887+
if (!$keep_src) {
2888+
if (!rename($src, $dest)) {
2889+
return false;
2890+
}
2891+
} else if (!copy($src, $dest)) {
28882892
return false;
28892893
}
28902894

tests/GLPITestCase.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public function beforeTestMethod($method)
6161
// By default, no session, not connected
6262
$this->resetSession();
6363

64+
// By default, there shouldn't be any pictures in the test files
65+
$this->resetPictures();
66+
6467
// Ensure cache is clear
6568
global $GLPI_CACHE;
6669
$GLPI_CACHE->clear();
@@ -103,6 +106,32 @@ public function afterTestMethod($method)
103106
}
104107
}
105108

109+
protected function resetPictures()
110+
{
111+
// Delete contents of test files/_pictures
112+
$dir = GLPI_PICTURE_DIR;
113+
if (!str_contains($dir, '/tests/files/_pictures')) {
114+
throw new \RuntimeException('Invalid picture dir: ' . $dir);
115+
}
116+
// Delete nested folders and files in dir
117+
$fn_delete = function ($dir, $parent) use (&$fn_delete) {
118+
$files = glob($dir . '/*') ?? [];
119+
foreach ($files as $file) {
120+
if (is_dir($file)) {
121+
$fn_delete($file, $parent);
122+
} else {
123+
unlink($file);
124+
}
125+
}
126+
if ($dir !== $parent) {
127+
rmdir($dir);
128+
}
129+
};
130+
if (file_exists($dir) && is_dir($dir)) {
131+
$fn_delete($dir, $dir);
132+
}
133+
}
134+
106135
/**
107136
* Call a private method, and get its return value.
108137
*

tests/functional/Glpi/Api/HL/Controller/AdministrationController.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,97 @@ public function testGetMySpecificEmail()
368368
});
369369
}
370370

371+
private function addCustomUserPicture(int $user_id, string $picture_path)
372+
{
373+
global $DB;
374+
$picture_path = \Toolbox::savePicture($picture_path, '', true);
375+
$this->variable($picture_path)->isNotFalse();
376+
$DB->update('glpi_users', [
377+
'id' => $user_id,
378+
'picture' => $picture_path
379+
], [
380+
'id' => $user_id
381+
]);
382+
}
383+
384+
public function testGetMyPicture()
385+
{
386+
$this->login();
387+
$this->api->call(new Request('GET', '/Administration/User/me/picture'), function ($call) {
388+
/** @var \HLAPICallAsserter $call */
389+
$call->response
390+
->isOK()
391+
->content(function ($content) {
392+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/pics/picture.png'));
393+
});
394+
});
395+
$this->addCustomUserPicture($_SESSION['glpiID'], GLPI_ROOT . '/tests/fixtures/uploads/foo.png');
396+
397+
$this->api->call(new Request('GET', '/Administration/User/me'), function ($call) {
398+
/** @var \HLAPICallAsserter $call */
399+
$call->response
400+
->isOK()
401+
->jsonContent(function ($content) {
402+
$this->string($content['picture'])->contains('/front/document.send.php');
403+
});
404+
});
405+
406+
$this->api->call(new Request('GET', '/Administration/User/me/picture'), function ($call) {
407+
/** @var \HLAPICallAsserter $call */
408+
$call->response
409+
->isOK()
410+
->content(function ($content) {
411+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/tests/fixtures/uploads/foo.png'));
412+
});
413+
});
414+
}
415+
416+
public function testGetUserPictureByID()
417+
{
418+
$this->login();
419+
$this->api->call(new Request('GET', '/Administration/User/' . $_SESSION['glpiID'] . '/Picture'), function ($call) {
420+
/** @var \HLAPICallAsserter $call */
421+
$call->response
422+
->isOK()
423+
->content(function ($content) {
424+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/pics/picture.png'));
425+
});
426+
});
427+
$this->addCustomUserPicture($_SESSION['glpiID'], GLPI_ROOT . '/tests/fixtures/uploads/foo.png');
428+
429+
$this->api->call(new Request('GET', '/Administration/User/' . $_SESSION['glpiID'] . '/Picture'), function ($call) {
430+
/** @var \HLAPICallAsserter $call */
431+
$call->response
432+
->isOK()
433+
->content(function ($content) {
434+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/tests/fixtures/uploads/foo.png'));
435+
});
436+
});
437+
}
438+
439+
public function testGetUserPictureByUsername()
440+
{
441+
$this->login();
442+
$this->api->call(new Request('GET', '/Administration/User/username/' . $_SESSION['glpiname'] . '/Picture'), function ($call) {
443+
/** @var \HLAPICallAsserter $call */
444+
$call->response
445+
->isOK()
446+
->content(function ($content) {
447+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/pics/picture.png'));
448+
});
449+
});
450+
$this->addCustomUserPicture($_SESSION['glpiID'], GLPI_ROOT . '/tests/fixtures/uploads/foo.png');
451+
452+
$this->api->call(new Request('GET', '/Administration/User/username/' . $_SESSION['glpiname'] . '/Picture'), function ($call) {
453+
/** @var \HLAPICallAsserter $call */
454+
$call->response
455+
->isOK()
456+
->content(function ($content) {
457+
$this->string($content)->isIdenticalTo(file_get_contents(GLPI_ROOT . '/tests/fixtures/uploads/foo.png'));
458+
});
459+
});
460+
}
461+
371462
public function testCreateUpdateDeleteUser()
372463
{
373464
$this->login();

0 commit comments

Comments
 (0)