Skip to content

Commit dc7d3d4

Browse files
authored
maintenance: use enum for matching endpoint in api requests (elabftw#4937)
This way, when we want to list all available endpoints, we can't miss one! * add new ApiEndpoint enum * add a new InvalidEndpointException that will list all available endpoints in the error message to the user * don't use static function for passwordcomplexity enum * add two tests for enums * rename src/enums to src/Enums
1 parent 39c5e2e commit dc7d3d4

38 files changed

+186
-101
lines changed

codeception.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ coverage:
88
- src/classes/*
99
- src/commands/*
1010
- src/controllers/*
11-
- src/enums/*
11+
- src/Enums/*
1212
- src/exceptions/*
1313
- src/factories/*
1414
- src/Import/*

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
"src/exceptions"
8484
],
8585
"Elabftw\\Enums\\": [
86-
"src/enums"
86+
"src/Enums"
8787
],
8888
"Elabftw\\Factories\\": [
8989
"src/factories"
File renamed without changes.

src/Enums/ApiEndpoint.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @author Nicolas CARPi <[email protected]>
4+
* @copyright 2024 Nicolas CARPi
5+
* @see https://www.elabftw.net Official website
6+
* @license AGPL-3.0
7+
* @package elabftw
8+
*/
9+
10+
namespace Elabftw\Enums;
11+
12+
enum ApiEndpoint: string
13+
{
14+
case ApiKeys = 'apikeys';
15+
case Config = 'config';
16+
case Idps = 'idps';
17+
case Info = 'info';
18+
case Experiments = 'experiments';
19+
case Items = 'items';
20+
case ExperimentsTemplates = 'experiments_templates';
21+
case ItemsTypes = 'items_types';
22+
case Event = 'event';
23+
case Events = 'events';
24+
case ExtraFieldsKeys = 'extra_fields_keys';
25+
case FavTags = 'favtags';
26+
case TeamTags = 'team_tags';
27+
case Teams = 'teams';
28+
case Todolist = 'todolist';
29+
case UnfinishedSteps = 'unfinished_steps';
30+
case Users = 'users';
31+
32+
public static function getCases(): array
33+
{
34+
return array_map(fn ($case) => $case->value, ApiEndpoint::cases());
35+
}
36+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/Enums/PasswordComplexity.php

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @author Nicolas CARPi <[email protected]>
4+
* @copyright 2024 Nicolas CARPi
5+
* @see https://www.elabftw.net Official website
6+
* @license AGPL-3.0
7+
* @package elabftw
8+
*/
9+
10+
namespace Elabftw\Enums;
11+
12+
use function array_column;
13+
use function array_combine;
14+
use function array_map;
15+
16+
enum PasswordComplexity: int
17+
{
18+
case None = 0;
19+
case Weak = 10;
20+
case Medium = 20;
21+
case Strong = 30;
22+
23+
public function toHuman(): string
24+
{
25+
return match ($this) {
26+
self::None => _('Minimum password length'),
27+
self::Weak => _('Must have at least one upper and one lower case letter, if your alphabet allows'),
28+
self::Medium => _('Must have at least one upper and one lower case letter, if your alphabet allows, and one digit'),
29+
self::Strong => _('Must have at least one upper and one lower case letter, if your alphabet allows, one special character, and one digit'),
30+
};
31+
}
32+
33+
public function toPattern(): string
34+
{
35+
// we need Lo for unicase/unicameral alphabets like Chinese, Japanese, and Korean
36+
$letters = '(?:(?=.*\p{Ll})(?=.*\p{Lu})|(?=.*\p{Lo}))';
37+
$digits = '(?=.*\d)';
38+
return match ($this) {
39+
self::None => '.*',
40+
self::Weak => "^$letters.*$",
41+
self::Medium => "^$letters$digits.*$",
42+
self::Strong => "^$letters$digits(?=.*[\p{P}\p{S}]).*$",
43+
};
44+
}
45+
46+
/**
47+
* For php, we need to add / as pre+suffix
48+
* @return non-empty-string
49+
*/
50+
public function toPhPattern(): string
51+
{
52+
return '/' . $this->toPattern() . '/u';
53+
}
54+
55+
public static function getAssociativeArray(): array
56+
{
57+
$cases = self::cases();
58+
$values = array_column($cases, 'value');
59+
$descriptions = array_map(function ($case) {
60+
return $case->toHuman();
61+
}, $cases);
62+
63+
return array_combine($values, $descriptions);
64+
}
65+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/controllers/AbstractApiController.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
namespace Elabftw\Controllers;
1111

12+
use Elabftw\Enums\ApiEndpoint;
1213
use Elabftw\Exceptions\ImproperActionException;
14+
use Elabftw\Exceptions\InvalidEndpointException;
1315
use Elabftw\Interfaces\ControllerInterface;
1416
use Elabftw\Models\Users;
1517
use Elabftw\Services\Check;
@@ -28,7 +30,7 @@ abstract class AbstractApiController implements ControllerInterface
2830

2931
protected string $search = '';
3032

31-
protected string $endpoint;
33+
protected ApiEndpoint $endpoint;
3234

3335
public function __construct(protected Users $Users, protected Request $Request, protected bool $canWrite = false)
3436
{
@@ -76,9 +78,9 @@ protected function parseReq(): array
7678
$this->search = trim($this->Request->query->getString('search'));
7779
}
7880

79-
// assign the endpoint (experiments, items, uploads, items_types, status)
80-
// 0 is "", 1 is "api", 2 is "v1"
81-
$this->endpoint = $req[3] ?? 'invalid_endpoint';
81+
// assign the endpoint, see ApiEndpoint enum
82+
// req array: 0 is "", 1 is "api", 2 is "v2"
83+
$this->endpoint = ApiEndpoint::tryFrom((string) $req[3]) ?? throw new InvalidEndpointException();
8284

8385
// assign the id if there is one
8486
if (Check::id((int) ($req[4] ?? 0)) !== false) {

src/controllers/Apiv2Controller.php

+18-18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace Elabftw\Controllers;
1111

1212
use Elabftw\Enums\Action;
13+
use Elabftw\Enums\ApiEndpoint;
1314
use Elabftw\Enums\EntityType;
1415
use Elabftw\Enums\ExportFormat;
1516
use Elabftw\Exceptions\IllegalActionException;
@@ -222,32 +223,31 @@ private function handlePatch(): array
222223
private function getModel(): RestInterface
223224
{
224225
return match ($this->endpoint) {
225-
'apikeys' => new ApiKeys($this->Users, $this->id),
226-
'config' => Config::getConfig(),
227-
'idps' => new Idps($this->id),
228-
'info' => new Info(),
229-
'experiments',
230-
'items',
231-
'experiments_templates',
232-
'items_types' => EntityType::from($this->endpoint)->toInstance($this->Users, $this->id),
226+
ApiEndpoint::ApiKeys => new ApiKeys($this->Users, $this->id),
227+
ApiEndpoint::Config => Config::getConfig(),
228+
ApiEndpoint::Idps => new Idps($this->id),
229+
ApiEndpoint::Info => new Info(),
230+
ApiEndpoint::Experiments,
231+
ApiEndpoint::Items,
232+
ApiEndpoint::ExperimentsTemplates,
233+
ApiEndpoint::ItemsTypes => EntityType::from($this->endpoint->value)->toInstance($this->Users, $this->id),
233234
// for a single event, the id is the id of the event
234-
'event' => new Scheduler(new Items($this->Users), $this->id),
235+
ApiEndpoint::Event => new Scheduler(new Items($this->Users), $this->id),
235236
// otherwise it's the id of the item
236-
'events' => new Scheduler(
237+
ApiEndpoint::Events => new Scheduler(
237238
new Items($this->Users, $this->id),
238239
null,
239240
$this->Request->query->getString('start', Scheduler::EVENT_START),
240241
$this->Request->query->getString('end', Scheduler::EVENT_END),
241242
$this->Request->query->getInt('cat'),
242243
),
243-
'extra_fields_keys' => new ExtraFieldsKeys($this->Users, trim($this->Request->query->getString('q')), $this->Request->query->getInt('limit')),
244-
'favtags' => new FavTags($this->Users, $this->id),
245-
'team_tags' => new TeamTags($this->Users, $this->id),
246-
'teams' => new Teams($this->Users, $this->id),
247-
'todolist' => new Todolist($this->Users->userData['userid'], $this->id),
248-
'unfinished_steps' => new UnfinishedSteps($this->Users, $this->Request->query->get('scope') === 'team'),
249-
'users' => new Users($this->id, $this->Users->team, $this->Users),
250-
default => throw new ImproperActionException('Invalid endpoint: available endpoints: apikeys, config, experiments, info, items, experiments_templates, items_types, event, events, extra_fields_keys, team_tags, teams, todolist, unfinished_steps, users.'),
244+
ApiEndpoint::ExtraFieldsKeys => new ExtraFieldsKeys($this->Users, trim($this->Request->query->getString('q')), $this->Request->query->getInt('limit')),
245+
ApiEndpoint::FavTags => new FavTags($this->Users, $this->id),
246+
ApiEndpoint::TeamTags => new TeamTags($this->Users, $this->id),
247+
ApiEndpoint::Teams => new Teams($this->Users, $this->id),
248+
ApiEndpoint::Todolist => new Todolist($this->Users->userData['userid'], $this->id),
249+
ApiEndpoint::UnfinishedSteps => new UnfinishedSteps($this->Users, $this->Request->query->get('scope') === 'team'),
250+
ApiEndpoint::Users => new Users($this->id, $this->Users->team, $this->Users),
251251
};
252252
}
253253

src/enums/PasswordComplexity.php

-64
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @author Nicolas CARPi <[email protected]>
4+
* @copyright 2024 Nicolas CARPi
5+
* @see https://www.elabftw.net Official website
6+
* @license AGPL-3.0
7+
* @package elabftw
8+
*/
9+
10+
namespace Elabftw\Exceptions;
11+
12+
use Elabftw\Enums\ApiEndpoint;
13+
14+
/**
15+
* For invalid api endpoint
16+
*/
17+
class InvalidEndpointException extends ImproperActionException
18+
{
19+
public function __construct()
20+
{
21+
parent::__construct(sprintf('Invalid endpoint: available endpoints: %s', implode(', ', ApiEndpoint::getCases())));
22+
}
23+
}

src/services/PasswordValidator.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222
class PasswordValidator
2323
{
24-
public function __construct(private int $minLength, private PasswordComplexity $passwordComplexity)
24+
public function __construct(private readonly int $minLength, private readonly PasswordComplexity $passwordComplexity)
2525
{
2626
}
2727

@@ -30,9 +30,9 @@ public function validate(string $password): bool
3030
if (mb_strlen($password) < $this->minLength) {
3131
throw new ImproperActionException(sprintf(_('Password must contain at least %d characters.'), $this->minLength));
3232
}
33-
$pattern = PasswordComplexity::toPhPattern($this->passwordComplexity);
33+
$pattern = $this->passwordComplexity->toPhPattern();
3434
if (((bool) preg_match($pattern, $password)) === false) {
35-
throw new ImproperActionException(sprintf(_('Password does not match requirement: %s'), PasswordComplexity::toHuman($this->passwordComplexity)));
35+
throw new ImproperActionException(sprintf(_('Password does not match requirement: %s'), $this->passwordComplexity->toHuman()));
3636
}
3737
return true;
3838
}

tests/unit/Enums/EnumsTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @author Nicolas CARPi <[email protected]>
4+
* @copyright 2024 Nicolas CARPi
5+
* @see https://www.elabftw.net Official website
6+
* @license AGPL-3.0
7+
* @package elabftw
8+
*/
9+
10+
namespace Elabftw\Enums;
11+
12+
class EnumsTest extends \PHPUnit\Framework\TestCase
13+
{
14+
public function testApiEndpoint(): void
15+
{
16+
$this->assertIsArray(ApiEndpoint::getCases());
17+
}
18+
19+
public function testEntrypoint(): void
20+
{
21+
array_map(fn ($case) => $this->assertStringEndsWith('.php', $case->toPage()), Entrypoint::cases());
22+
}
23+
}

web/admin.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@
120120
'statusArr' => $statusArr,
121121
'experimentsCategoriesArr' => $experimentsCategoriesArr,
122122
'itemsStatusArr' => $itemsStatusArr,
123-
'passwordInputHelp' => PasswordComplexity::toHuman($passwordComplexity),
124-
'passwordInputPattern' => PasswordComplexity::toPattern($passwordComplexity),
123+
'passwordInputHelp' => $passwordComplexity->toHuman(),
124+
'passwordInputPattern' => $passwordComplexity->toPattern(),
125125
'teamGroupsArr' => $teamGroupsArr,
126126
'visibilityArr' => $PermissionsHelper->getAssociativeArray(),
127127
'remoteDirectoryUsersArr' => $remoteDirectoryUsersArr,

web/change-pass.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
$passwordComplexity = PasswordComplexity::from((int) $App->Config->configArr['password_complexity_requirement']);
4343
$renderArr = array(
4444
'key' => $App->Request->query->getAlnum('key'),
45-
'passwordInputHelp' => PasswordComplexity::toHuman($passwordComplexity),
46-
'passwordInputPattern' => PasswordComplexity::toPattern($passwordComplexity),
45+
'passwordInputHelp' => $passwordComplexity->toHuman(),
46+
'passwordInputPattern' => $passwordComplexity->toPattern(),
4747
);
4848
} catch (Exception $e) {
4949
$template = 'error.html';

web/register.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
$template = 'register.html';
5252
$renderArr = array(
5353
'hideTitle' => true,
54-
'passwordInputHelp' => PasswordComplexity::toHuman($passwordComplexity),
55-
'passwordInputPattern' => PasswordComplexity::toPattern($passwordComplexity),
54+
'passwordInputHelp' => $passwordComplexity->toHuman(),
55+
'passwordInputPattern' => $passwordComplexity->toPattern(),
5656
'privacyPolicy' => $App->Config->configArr['privacy_policy'] ?? '',
5757
'teamsArr' => $teamsArr,
5858
);

web/sysconfig.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@
131131
'elabimgVersion' => $elabimgVersion,
132132
'idpsArr' => $idpsArr,
133133
'isSearching' => $isSearching,
134-
'passwordInputHelp' => PasswordComplexity::toHuman($passwordComplexity),
135-
'passwordInputPattern' => PasswordComplexity::toPattern($passwordComplexity),
134+
'passwordInputHelp' => $passwordComplexity->toHuman(),
135+
'passwordInputPattern' => $passwordComplexity->toPattern(),
136136
'phpInfos' => $phpInfos,
137137
'remoteDirectoryUsersArr' => $remoteDirectoryUsersArr,
138138
'samlSecuritySettings' => $samlSecuritySettings,

web/ucp.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@
120120
'metadataGroups' => $metadataGroups,
121121
'allTeamgroupsArr' => $TeamGroups->readGroupsFromUser(),
122122
'notificationsSettings' => $notificationsSettings,
123-
'passwordInputHelp' => PasswordComplexity::toHuman($passwordComplexity),
124-
'passwordInputPattern' => PasswordComplexity::toPattern($passwordComplexity),
123+
'passwordInputHelp' => $passwordComplexity->toHuman(),
124+
'passwordInputPattern' => $passwordComplexity->toPattern(),
125125
'statusArr' => $Status->readAll(),
126126
'teamTagsArr' => $TeamTags->readAll(),
127127
'templatesArr' => $Templates->readAll(),

0 commit comments

Comments
 (0)