Aligns with the app's dominant convention (171 static Str:: calls vs
24 Str::of() chains). Uses Str::afterLast() for the repeated
"segment after last slash" pattern, Str::matchAll() where a
Collection return is needed, and native explode()/substr() where a
plain array/string suffices.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aligns with the app's dominant convention (413 now()/today() call
sites vs 12 Carbon::now()). Carbon::parse() calls are untouched since
they parse arbitrary date strings, not current-time access.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
storeStatus() now throws with JSON metadata (checked id/url hosts, expected
rule, and the full activity payload) when status domains mismatch. The Announce
inbox handler catches this, logs the context at debug level, and returns
gracefully instead of surfacing a full production ERROR stack trace.
Image::handleImageTransform derives the output filename from the current
media_path and applies the encoder's output extension. When that differs from
what is already stored (heic/avif -> jpg, or a thumbnail regenerated to a new
extension), the new file landed at a different path and the previous file was
left orphaned in the media directory — the source of the leftover _thumb files
under public/m/_v2.
Capture the path each transform supersedes and delete it after a successful
write (only when the new output path differs, so we never delete what we just
wrote). Remove the stale MediaDeleteLeafCleanupTest whose source change is not
in the tree.
Normalize JSON-LD compacted single attachments (a bare object instead of a
one-item array) in getAttachments(), and route verifyAttachments() through it
so validation and import share one normalization path.
Includes PR #6589's tests plus additional edge-case coverage: list-form
preservation, bare-input normalization, and guards for missing/empty/scalar
attachments.
Helpers::importNoteAttachment unconditionally inserted a new Media row per
attachment, so re-importing a remote status (an Announce racing another
inbox job, a re-fetch, or a duplicate url within one activity) hit the
media_status_id_media_path_unique constraint and crashed the queue job with
a 1062 UniqueConstraintViolationException, dropping the boost/import.
Make createMediaAttachment idempotent on (status_id, media_path): skip when
a row already exists, and catch the unique-constraint violation as a
lost-race no-op, returning null so the caller skips re-dispatching storage.
Adds regression tests (re-import no-op, distinct urls still stored,
concurrent-insert returns null).
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
1. Add FractalService with static item() and collection() helpers
replacing 22 call sites that repeated the 4-line Fractal Manager
+ ArraySerializer boilerplate.
2. Add AccountInterstitial::createFromStatus() factory method
consolidating 4 identical 15-line blocks that create interstitials
with status metadata.
3. Add NotificationService::createNotification() to handle the
repeated pattern of creating, caching, and registering a
notification in the recipient's feed.
4. Add NotificationService::firstOrCreateNotification() for
idempotent notifications (share/boost, mention) that should
only notify once per actor+action+item combination.
Move all Eloquent models from the app/ root directory to app/Models/
for consistency with modern Laravel conventions. The project already had
54 models in App\Models; this migrates the remaining 52 legacy models.
Changes:
- Move 52 model files from app/ to app/Models/
- Update namespace declarations in each model
- Update all ~1000 import references across the codebase
- Add Relation::morphMap() in AppServiceProvider for backward
compatibility with existing polymorphic database records
- Add missing HasSnowflakePrimary imports for models that relied
on same-namespace resolution
Convert all 273 short facade alias imports (e.g. 'use Cache;') to their
fully-qualified class names (e.g. 'use Illuminate\Support\Facades\Cache;')
across 193 files.
This resolves 643 PHPStan 'class.notFound' errors caused by the static
analyzer being unable to resolve global aliases, and aligns with modern
Laravel conventions. It also unblocks removing the aliases array from
config/app.php in a future change.
All 107 tests pass.
Replace all deprecated helper function calls:
- str_slug() → Str::slug()
- starts_with() → str_starts_with()
- ends_with() → str_ends_with()
- array_first() → Arr::first()
- array_last() → Arr::last()
- array_flatten() → Arr::flatten()
Remove laravel/helpers package from composer.json as it is no longer
needed and will not be maintained for Laravel 13.
PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.
Blurhash::generate() allocates one PHP array per pixel of the source. At
roughly 255 bytes per pixel (measured: 224 MB peak for a 720x1280 frame) a
1920x1080 frame approaches half a gigabyte.
Image thumbnails survive this because they are capped at 640x640 in
Image::__construct() *and* run under that constructor's
ini_set('memory_limit', '1024M'). Video thumbnails get neither: FFmpeg saves
them at the source video's resolution, and VideoThumbnail never raises the
limit. So a video whose frame is 1080p or larger exhausts memory_limit.
That is a PHP fatal, not an \Exception, which has three consequences:
- the catch block in VideoThumbnail::handle() does not catch it
- the job never lands in failed_jobs, so nothing reports a problem
- MediaStoragePipeline::dispatch() on the last line of handle() never runs
The video therefore stays on local disk permanently while images beside it
replicate normally. Reported in #2652 (2021-02-13) and diagnosed correctly in
that thread on 2021-11-04.
Two changes:
1. Blurhash::generate() downscales to 128px on the long edge before sampling.
The result is a 4x4-component DCT, so full-resolution sampling adds
essentially nothing: measured against the full-resolution hash, mean
per-channel deviation of the decoded 24x24 preview is ~7.5/255 at a 32px
sample, ~4.5/255 at 64px, ~2.5/255 at 128px, and no better at 256px. Peak
memory for the frame above drops from 224 MB to 6 MB.
This removes the ceiling for every caller rather than moving it, which is
all that raising memory_limit would have done. Existing stored hashes are
not recomputed, so nothing already published changes appearance.
2. VideoThumbnail wraps the blurhash in its own try/catch, so a decorative
step can no longer skip the replication dispatch. Change 1 covers the
fatal; this covers any ordinary exception.
Verified on a live instance with S3 cloud storage: a 1920x1080 video that
previously stranded now generates a blurhash, uploads original and thumbnail
to the bucket, sets cdn_url/thumbnail_url/replicated_at, and removes the local
copies. Existing images re-hash to visually identical previews.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Smartphone photos stored in landscape orientation with an EXIF rotation
tag were being saved to S3 in the wrong orientation. Image.php read the
raw pixel dimensions without first applying the EXIF tag, so portrait
photos (e.g. 4032×3024 with Orientation=6) were classified and resized
as landscape (1920×1080).
Calling orient() immediately after read() physically rotates the image
to match its EXIF orientation tag before any dimension checks or
scaling. This ensures portrait photos remain portrait after processing.
Intervention Image v3 reference:
https://image.intervention.io/v3/modifying/orientation