Skip to content

Commit 36e5a0f

Browse files
committed
Merge branch 'master' into copilot/add-admin-check-for-public-uploads
2 parents 3ef055c + 27ee517 commit 36e5a0f

26 files changed

Lines changed: 885 additions & 195 deletions

.trivyignore

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,9 @@
77
# to maintain our own fork of Frankenphp and keep it up to date with the latest security patches, which
88
# is not sustainable for us.
99

10-
# True positive but we expect Lychee to be run behind a reverse proxy that is taking care of the cryptography and TLS configuration.
11-
# CVSS 7.6
12-
CVE-2026-25793
13-
14-
# Gnu lib C vulnerbility that is used by the underlying image of frankenphp.
15-
CVE-2026-0861
16-
17-
# gRPC-Go is the Go language implementation of gRPC. CVSS 9.1
18-
CVE-2026-33186
19-
20-
# Step CA is an online certificate authority for secure, automated certificate management for DevOps. Versions 0.30.0-rc6 and below do not safeguard against unauthenticated certificate issuance through the SCEP UpdateReq. CVSS 10
21-
CVE-2026-30836
22-
23-
2410
# This CVE is stupid and disputed.
2511
# The "vulnerability" is that php-jwt accepts short HMAC keys without validation.
2612
# This is not a library bug — key management is the caller's responsibility.
2713
# PHP's own hash_hmac() and openssl_sign() behave identically and have no CVEs for this.
2814
# NVD agrees — hence the Disputed tag and no score from NIST.
29-
CVE-2025-45769
30-
31-
# We do not use JWT on the GOLANG side (frankenphp), so this CVE does not apply to us.
32-
CVE-2026-34986
15+
CVE-2025-45769

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ RUN npm run build
5656
# ============================================================================
5757
# Stage 3: Production FrankenPHP Image
5858
# ============================================================================
59-
FROM dunglas/frankenphp:php8.5-trixie@sha256:93dcc4f16e01f0bc8e9d752bb19559cba4a23c14c9fd7ab825538fb432cd91ed
59+
FROM dunglas/frankenphp:php8.5-trixie@sha256:d82c11b5f9c96862130ee02c3f4f2513b81b7de5f29275593c526fa385fe9952
6060

6161
ARG USER=appuser
6262

app/Actions/Photo/Create.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ private function handleStandalone(InitDTO $init_dto): Photo
181181
Standalone\EncodePlaceholder::class,
182182
Standalone\ReplaceOriginalWithBackup::class,
183183
Shared\UploadSizeVariantsToS3::class,
184+
Shared\GeodecodeLocation::class,
184185
Shared\ExtractColourPalette::class,
185186
Shared\NotifyAlbums::class,
186187
];
@@ -277,6 +278,7 @@ private function handlePhotoLivePartner(InitDTO $init_dto): Photo
277278
Standalone\EncodePlaceholder::class,
278279
Standalone\ReplaceOriginalWithBackup::class,
279280
Shared\UploadSizeVariantsToS3::class,
281+
Shared\GeodecodeLocation::class,
280282
Shared\ExtractColourPalette::class,
281283
];
282284
$stand_alone_dto = $this->executePipeOnDTO($stand_alone_pipes, $stand_alone_dto);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\PhotoDTO;
12+
use App\Contracts\PhotoCreate\PhotoPipe;
13+
use App\Jobs\GeodecodeLocationJob;
14+
use App\Repositories\ConfigManager;
15+
use Illuminate\Support\Facades\Log;
16+
17+
/**
18+
* Dispatch an asynchronous job to reverse-geocode the GPS coordinates of a
19+
* photo and populate its location field.
20+
*
21+
* The actual HTTP call to Nominatim is intentionally deferred so that photo
22+
* uploads are not slowed down by the external network request.
23+
*/
24+
class GeodecodeLocation implements PhotoPipe
25+
{
26+
public function __construct(
27+
protected readonly ConfigManager $config_manager,
28+
) {
29+
}
30+
31+
public function handle(PhotoDTO $state, \Closure $next): PhotoDTO
32+
{
33+
if (!$this->config_manager->getValueAsBool('location_decoding')) {
34+
return $next($state);
35+
}
36+
37+
$photo = $state->getPhoto();
38+
39+
if ($photo->latitude === null || $photo->longitude === null) {
40+
return $next($state);
41+
}
42+
43+
// @codeCoverageIgnoreStart
44+
// This is already tested directly in the GeodecodeLocationJobTest.
45+
try {
46+
GeodecodeLocationJob::dispatch($photo);
47+
} catch (\Exception $e) {
48+
// Fail silently and continue.
49+
Log::error('Failed to dispatch GeodecodeLocationJob: ' . $e->getMessage());
50+
}
51+
52+
return $next($state);
53+
// @codeCoverageIgnoreEnd
54+
}
55+
}

app/Enum/AspectRatioCSSType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ enum AspectRatioCSSType: string
2020
case aspect3by2 = 'aspect-3x2';
2121
case aspect1by1 = 'aspect-square';
2222
case aspect2by3 = 'aspect-2x3';
23-
case aspect1byx9 = 'aspect-video';
23+
case aspect16by9 = 'aspect-video';
2424
}

app/Enum/AspectRatioType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum AspectRatioType: string
2020
case aspect1by1 = '1/1';
2121
case aspect2by3 = '2/3';
2222
case aspect4by5 = '4/5';
23-
case aspect1byx9 = '16/9';
23+
case aspect16by9 = '16/9';
2424

2525
public function css(): AspectRatioCSSType
2626
{
@@ -30,7 +30,7 @@ public function css(): AspectRatioCSSType
3030
self::aspect3by2 => AspectRatioCSSType::aspect3by2,
3131
self::aspect1by1 => AspectRatioCSSType::aspect1by1,
3232
self::aspect2by3 => AspectRatioCSSType::aspect2by3,
33-
self::aspect1byx9 => AspectRatioCSSType::aspect1byx9,
33+
self::aspect16by9 => AspectRatioCSSType::aspect16by9,
3434
};
3535
}
3636
}

app/Enum/MapProviders.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
*/
1515
enum MapProviders: string
1616
{
17-
case Wikimedia = 'Wikimedia';
1817
case OpenStreetMapOrg = 'OpenStreetMap.org';
1918
case OpenStreetMapDe = 'OpenStreetMap.de';
2019
case OpenStreetMapFr = 'OpenStreetMap.fr';
@@ -23,7 +22,6 @@ enum MapProviders: string
2322
public function getLayer(): string
2423
{
2524
return match ($this) {
26-
self::Wikimedia => 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png',
2725
self::OpenStreetMapOrg => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
2826
self::OpenStreetMapDe => 'https://tile.openstreetmap.de/{z}/{x}/{y}.png ',
2927
self::OpenStreetMapFr => 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png ',
@@ -34,7 +32,6 @@ public function getLayer(): string
3432
public function getAtributionHtml(): string
3533
{
3634
return match ($this) {
37-
self::Wikimedia => '<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia</a>',
3835
self::OpenStreetMapOrg => '&copy; <a href="https://openstreetmap.org/copyright">' . __('gallery.map.osm_contributors') . '</a>',
3936
self::OpenStreetMapDe => '&copy; <a href="https://openstreetmap.org/copyright">' . __('gallery.map.osm_contributors') . '</a>',
4037
self::OpenStreetMapFr => '&copy; <a href="https://openstreetmap.org/copyright">' . __('gallery.map.osm_contributors') . '</a>',

app/Jobs/GeodecodeLocationJob.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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\Jobs;
10+
11+
use App\Enum\JobStatus;
12+
use App\Metadata\Extractor;
13+
use App\Metadata\Geodecoder;
14+
use App\Models\JobHistory;
15+
use App\Models\Photo;
16+
use Illuminate\Bus\Queueable;
17+
use Illuminate\Contracts\Queue\ShouldQueue;
18+
use Illuminate\Foundation\Bus\Dispatchable;
19+
use Illuminate\Queue\InteractsWithQueue;
20+
use Illuminate\Queue\Middleware\RateLimited;
21+
use Illuminate\Queue\SerializesModels;
22+
use Illuminate\Support\Facades\Log;
23+
use Illuminate\Support\Str;
24+
25+
/**
26+
* Asynchronously reverse-geocodes the GPS coordinates of a photo and
27+
* stores the resolved location string on the photo record.
28+
*/
29+
class GeodecodeLocationJob implements ShouldQueue
30+
{
31+
use HasFailedTrait;
32+
use Dispatchable;
33+
use InteractsWithQueue;
34+
use Queueable;
35+
use SerializesModels;
36+
37+
protected JobHistory $history;
38+
public string $photo_id;
39+
40+
/**
41+
* Create a new job instance.
42+
*/
43+
public function __construct(Photo $photo)
44+
{
45+
$this->photo_id = $photo->id;
46+
47+
$this->history = new JobHistory();
48+
$this->history->owner_id = $photo->owner_id;
49+
$this->history->job = Str::limit(sprintf('Geodecode location for %s [%s].', $photo->title, $this->photo_id), 200);
50+
$this->history->status = JobStatus::READY;
51+
52+
$this->history->save();
53+
}
54+
55+
/**
56+
* Get the middleware the job should pass through.
57+
*
58+
* @return array
59+
*/
60+
public function middleware()
61+
{
62+
return [new RateLimited('geo-queue')];
63+
}
64+
65+
/**
66+
* Execute the job.
67+
*/
68+
public function handle(): void
69+
{
70+
Log::channel('jobs')->info("Starting geodecode location job for photo ID {$this->photo_id}.");
71+
$this->history->status = JobStatus::STARTED;
72+
$this->history->save();
73+
74+
$photo = Photo::findOrFail($this->photo_id);
75+
76+
if ($photo->latitude === null || $photo->longitude === null) {
77+
$this->history->status = JobStatus::SUCCESS;
78+
$this->history->save();
79+
80+
return;
81+
}
82+
83+
$cached_provider = Geodecoder::getGeocoderProvider();
84+
$location = Geodecoder::decodeLocation_core($photo->latitude, $photo->longitude, $cached_provider);
85+
86+
if ($location !== null) {
87+
$location = substr($location, 0, Extractor::MAX_LOCATION_STRING_LENGTH);
88+
}
89+
90+
Photo::query()
91+
->where('id', '=', $this->photo_id)
92+
->where(function ($query): void {
93+
$query->whereNull('location')->orWhere('location', '=', '');
94+
})
95+
->update(['location' => $location]);
96+
97+
$this->history->status = JobStatus::SUCCESS;
98+
$this->history->save();
99+
}
100+
}

app/Metadata/Extractor.php

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

99
namespace App\Metadata;
1010

11-
use App\Exceptions\ExternalComponentFailedException;
1211
use App\Exceptions\ExternalComponentMissingException;
1312
use App\Exceptions\Handler;
1413
use App\Exceptions\MediaFileOperationException;
@@ -23,7 +22,6 @@
2322
use PHPExif\Reader\PhpExifReaderException;
2423
use PHPExif\Reader\Reader;
2524
use Safe\DateTime;
26-
use Safe\Exceptions\StringsException;
2725

2826
/**
2927
* Collects normalized EXIF info about an image/video.
@@ -473,20 +471,6 @@ public static function createFromFile(NativeLocalFile $file, int $file_last_modi
473471
$metadata->shutter .= self::SUFFIX_SEC_UNIT;
474472
}
475473

476-
// Decode location data, it can be longer than is acceptable for DB that's the reason for substr
477-
// but only if return value is not null (= function has been disabled)
478-
try {
479-
$metadata->location = Geodecoder::decodeLocation($metadata->latitude, $metadata->longitude);
480-
if ($metadata->location !== null) {
481-
$metadata->location = substr($metadata->location, 0, self::MAX_LOCATION_STRING_LENGTH);
482-
}
483-
// @codeCoverageIgnoreStart
484-
} catch (ExternalComponentFailedException|StringsException $e) {
485-
Handler::reportSafely($e);
486-
$metadata->location = null;
487-
}
488-
// @codeCoverageIgnoreEnd
489-
490474
return $metadata;
491475
}
492476
}

app/Metadata/Geodecoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static function getGeocoderProvider(): ProviderCache
3939
$config_manager = app(ConfigManager::class);
4040
try {
4141
$stack = HandlerStack::create();
42-
$stack->push(RateLimiterMiddleware::perSecond(1));
42+
$stack->push(RateLimiterMiddleware::perSecond(config('features.location_decoding_requests_per_second', 1)));
4343

4444
$http_client = new \GuzzleHttp\Client([
4545
'handler' => $stack,

0 commit comments

Comments
 (0)