Skip to content

Commit 896f517

Browse files
authored
Add admin check for uploads (#4282)
1 parent b19dfb2 commit 896f517

141 files changed

Lines changed: 3469 additions & 69 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/Actions/Import/FromUrl.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use App\Actions\Photo\Create;
1212
use App\DTO\ImportMode;
13+
use App\Enum\UserUploadTrustLevel;
1314
use App\Exceptions\Handler;
1415
use App\Exceptions\MassImportException;
1516
use App\Image\Files\DownloadedFile;
@@ -18,6 +19,7 @@
1819
use App\Repositories\ConfigManager;
1920
use App\Services\Image\FileExtensionService;
2021
use Illuminate\Support\Collection;
22+
use Illuminate\Support\Facades\Auth;
2123
use Safe\Exceptions\InfoException;
2224
use function Safe\ini_get;
2325
use function Safe\parse_url;
@@ -43,19 +45,30 @@ public function do(array $urls, ?Album $album, int $intended_owner_id): Collecti
4345
$config_manager = resolve(ConfigManager::class);
4446
$result = new Collection();
4547
$exceptions = [];
48+
49+
$user = Auth::user();
50+
if ($user?->may_administrate === true) {
51+
$upload_trust_level = UserUploadTrustLevel::TRUSTED;
52+
} elseif ($user !== null) {
53+
$upload_trust_level = $user->upload_trust_level;
54+
} else {
55+
$upload_trust_level = $config_manager->getValueAsEnum('guest_upload_trust_level', UserUploadTrustLevel::class)
56+
?? UserUploadTrustLevel::CHECK;
57+
}
58+
4659
$create = new Create(
4760
import_mode: new ImportMode(
4861
delete_imported: true,
4962
skip_duplicates: $config_manager->getValueAsBool('skip_duplicates'),
5063
shall_rename_photo_title: $config_manager->getValueAsBool('renamer_photo_title_enabled'),
5164
),
52-
intended_owner_id: $intended_owner_id
65+
intended_owner_id: $intended_owner_id,
66+
upload_trust_level: $upload_trust_level,
5367
);
5468

5569
$file_extension_service = resolve(FileExtensionService::class);
5670
foreach ($urls as $url) {
5771
try {
58-
// Reset the execution timeout for every iteration.
5972
try {
6073
set_time_limit((int) ini_get('max_execution_time'));
6174
} catch (InfoException) {

app/Actions/Photo/Create.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use App\DTO\PhotoCreate\PhotoPartnerDTO;
2525
use App\DTO\PhotoCreate\StandaloneDTO;
2626
use App\DTO\PhotoCreate\VideoPartnerDTO;
27+
use App\Enum\UserUploadTrustLevel;
2728
use App\Exceptions\Internal\LycheeLogicException;
2829
use App\Exceptions\PhotoResyncedException;
2930
use App\Exceptions\PhotoSkippedException;
@@ -44,8 +45,9 @@ class Create
4445
public function __construct(
4546
?ImportMode $import_mode,
4647
int $intended_owner_id,
48+
UserUploadTrustLevel $upload_trust_level,
4749
) {
48-
$this->strategy_parameters = new ImportParam($import_mode, $intended_owner_id);
50+
$this->strategy_parameters = new ImportParam($import_mode, $intended_owner_id, upload_trust_level: $upload_trust_level);
4951
}
5052

5153
/**
@@ -144,6 +146,7 @@ private function handleDuplicate(InitDTO $init_dto): Photo
144146
$pipes[] = Duplicate\SaveIfDirty::class;
145147
}
146148
$pipes[] = Duplicate\ThrowSkipDuplicate::class;
149+
$pipes[] = Duplicate\ThrowUntrustedDuplicate::class;
147150
$pipes[] = Shared\SetHighlighted::class;
148151
$pipes[] = Shared\Save::class;
149152
$pipes[] = Shared\SetParent::class;
@@ -178,6 +181,7 @@ private function handleStandalone(InitDTO $init_dto): Photo
178181
Standalone\PlaceGoogleMotionVideo::class,
179182
Standalone\SetChecksum::class,
180183
Standalone\AutoRenamer::class,
184+
Shared\SetUploadValidated::class,
181185
Shared\Save::class,
182186
Shared\SetParent::class,
183187
Shared\SaveStatistics::class,
@@ -275,6 +279,7 @@ private function handlePhotoLivePartner(InitDTO $init_dto): Photo
275279
Standalone\PlaceGoogleMotionVideo::class,
276280
Standalone\SetChecksum::class,
277281
Standalone\AutoRenamer::class,
282+
Shared\SetUploadValidated::class,
278283
Shared\Save::class,
279284
Shared\SetParent::class,
280285
Standalone\CreateOriginalSizeVariant::class,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo\Pipes\Duplicate;
10+
11+
use App\Contracts\PhotoCreate\DuplicatePipe;
12+
use App\DTO\PhotoCreate\DuplicateDTO;
13+
use App\Exceptions\PhotoSkippedException;
14+
15+
class ThrowUntrustedDuplicate implements DuplicatePipe
16+
{
17+
public function handle(DuplicateDTO $state, \Closure $next): DuplicateDTO
18+
{
19+
if ($state->photo->is_validated) {
20+
return $next($state);
21+
}
22+
23+
throw new PhotoSkippedException('The photo has been skipped, there is already a copy waiting for moderation');
24+
}
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo\Pipes\Shared;
10+
11+
use App\Contracts\PhotoCreate\SharedPipe;
12+
use App\DTO\PhotoCreate\DuplicateDTO;
13+
use App\DTO\PhotoCreate\StandaloneDTO;
14+
use App\Enum\UserUploadTrustLevel;
15+
use App\Repositories\ConfigManager;
16+
17+
/**
18+
* Determines and sets the `is_validated` flag on the photo being created.
19+
*
20+
* Rules (evaluated in order):
21+
* 1. If the trust level was pre-resolved at dispatch time (e.g. from a queued job),
22+
* use it directly — no DB lookup required.
23+
* 2. If there is no authenticated owner (guest upload, intended_owner_id === 0),
24+
* use the `guest_upload_trust_level` config.
25+
* 3. If the intended owner is an admin (`may_administrate`), always validated.
26+
* 4. Otherwise use the owner's `upload_trust_level`.
27+
*
28+
* Trust level mapping:
29+
* - CHECK → not validated (false)
30+
* - MONITOR → validated (true) — behaves as TRUSTED in this iteration
31+
* - TRUSTED → validated (true)
32+
*/
33+
class SetUploadValidated implements SharedPipe
34+
{
35+
public function __construct(
36+
protected readonly ConfigManager $config_manager,
37+
) {
38+
}
39+
40+
public function handle(DuplicateDTO|StandaloneDTO $state, \Closure $next): DuplicateDTO|StandaloneDTO
41+
{
42+
$state->photo->is_validated = $state->upload_trust_level !== UserUploadTrustLevel::CHECK;
43+
44+
return $next($state);
45+
}
46+
}

app/Actions/User/Create.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace App\Actions\User;
1010

11+
use App\Enum\UserUploadTrustLevel;
1112
use App\Exceptions\ConflictingPropertyException;
1213
use App\Exceptions\InvalidPropertyException;
1314
use App\Exceptions\ModelDBException;
@@ -35,6 +36,7 @@ public function do(
3536
bool $may_administrate = false,
3637
?int $quota_kb = null,
3738
?string $note = null,
39+
?UserUploadTrustLevel $upload_trust_level = null,
3840
): User {
3941
if (User::query()->where('username', '=', $username)->count() !== 0) {
4042
throw new ConflictingPropertyException('Username already exists');
@@ -52,6 +54,9 @@ public function do(
5254
$user->password = Hash::make($password);
5355
$user->quota_kb = $quota_kb;
5456
$user->note = $note;
57+
$user->upload_trust_level = $upload_trust_level
58+
?? $this->config_manager->getValueAsEnum('default_user_trust_level', UserUploadTrustLevel::class)
59+
?? UserUploadTrustLevel::TRUSTED;
5560
$user->save();
5661

5762
return $user;

app/Actions/User/Save.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace App\Actions\User;
1010

11+
use App\Enum\UserUploadTrustLevel;
1112
use App\Exceptions\ConflictingPropertyException;
1213
use App\Exceptions\InvalidPropertyException;
1314
use App\Exceptions\ModelDBException;
@@ -42,6 +43,7 @@ public function do(User $user,
4243
bool $may_administrate = false,
4344
?int $quota_kb = null,
4445
?string $note = null,
46+
?UserUploadTrustLevel $upload_trust_level = null,
4547
): void {
4648
if (User::query()
4749
->where('username', '=', $username)
@@ -62,6 +64,9 @@ public function do(User $user,
6264
$user->may_administrate = $may_administrate;
6365
$user->note = $note;
6466
$user->quota_kb = $quota_kb;
67+
if ($upload_trust_level !== null) {
68+
$user->upload_trust_level = $upload_trust_level;
69+
}
6570
if ($password !== null && $password !== '') {
6671
$user->password = Hash::make($password);
6772
}

app/Contracts/Http/Requests/RequestAttribute.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class RequestAttribute
100100
public const MAY_UPLOAD_ATTRIBUTE = 'may_upload';
101101
public const MAY_EDIT_OWN_SETTINGS_ATTRIBUTE = 'may_edit_own_settings';
102102
public const MAY_ADMINISTRATE = 'may_administrate';
103+
public const UPLOAD_TRUST_LEVEL_ATTRIBUTE = 'upload_trust_level';
103104
public const SHARED_ALBUMS_VISIBILITY_ATTRIBUTE = 'shared_albums_visibility';
104105

105106
/**

app/DTO/ImportDTO.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use App\Actions\Album\Create as AlbumCreate;
1212
use App\Actions\Photo\Create as PhotoCreate;
13+
use App\Enum\UserUploadTrustLevel;
1314
use App\Jobs\ImportImageJob;
1415
use App\Jobs\RecomputeAlbumSizeJob;
1516
use App\Jobs\RecomputeAlbumStatsJob;
@@ -39,7 +40,7 @@ public function __construct(
3940
public readonly bool $should_execute_jobs = false,
4041
) {
4142
$this->album_create = new AlbumCreate($intended_owner_id);
42-
$this->photo_create = new PhotoCreate($import_mode, $intended_owner_id);
43+
$this->photo_create = new PhotoCreate($import_mode, $intended_owner_id, upload_trust_level: UserUploadTrustLevel::TRUSTED);
4344
$this->album_renamer = new AlbumRenamer($intended_owner_id);
4445
$this->photo_renamer = new PhotoRenamer($intended_owner_id);
4546
}

app/DTO/ImportParam.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88

99
namespace App\DTO;
1010

11+
use App\Enum\UserUploadTrustLevel;
12+
use App\Exceptions\Internal\LycheeLogicException;
1113
use App\Metadata\Extractor;
1214
use App\Models\Album;
1315

1416
final class ImportParam
1517
{
18+
public UserUploadTrustLevel $upload_trust_level;
19+
1620
/**
1721
* @param ImportMode $import_mode
1822
* @param int $intended_owner_id indicates the intended owner of the image
@@ -30,6 +34,8 @@ public function __construct(
3034
public bool $is_highlighted = false,
3135
public Extractor|null $exif_info = null,
3236
public ?bool $apply_watermark = null,
37+
?UserUploadTrustLevel $upload_trust_level = null,
3338
) {
39+
$this->upload_trust_level = $upload_trust_level ?? throw new LycheeLogicException('Upload trust level must be provided');
3440
}
3541
}

app/DTO/PhotoCreate/InitDTO.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Contracts\Models\AbstractAlbum;
1212
use App\DTO\ImportMode;
1313
use App\DTO\ImportParam;
14+
use App\Enum\UserUploadTrustLevel;
1415
use App\Image\Files\NativeLocalFile;
1516
use App\Metadata\Extractor;
1617
use App\Models\Photo;
@@ -23,6 +24,9 @@ class InitDTO
2324
// Indicates the intended owner of the image.
2425
public readonly int $intended_owner_id;
2526

27+
// Pre-resolved upload trust level (set when session context is available at dispatch time).
28+
public readonly UserUploadTrustLevel $upload_trust_level;
29+
2630
// Indicates whether the new photo shall be highlighted.
2731
public bool $is_highlighted = false;
2832

@@ -60,6 +64,7 @@ public function __construct(
6064
$this->source_file = $source_file;
6165
$this->import_mode = $parameters->import_mode;
6266
$this->intended_owner_id = $parameters->intended_owner_id;
67+
$this->upload_trust_level = $parameters->upload_trust_level;
6368
$this->is_highlighted = $parameters->is_highlighted;
6469
$this->exif_info = $parameters->exif_info;
6570
$this->apply_watermark = $parameters->apply_watermark;

0 commit comments

Comments
 (0)