cropPhoto() used coverDown(1080, 1920), which only downscales, so an
undersized crop was stored at its original size (e.g. 110x196). Remote
servers then dropped the image story for not matching expected story
dimensions while videos federated. Use cover() so small crops are scaled
up to fill the canvas.
Note: the complementary remote-side validator relaxation (StoryFetch
validateImageFile) is tracked separately and not included here.
apiV1Add passed an absolute path to FFMpeg::open(), which laravel-ffmpeg
resolves relative to the local disk root, producing a doubled path and an
ffprobe 'Unable to probe / No such file or directory' failure.
Open the file via the disk instead: FFMpeg::fromDisk('local')->open($path)
for local storage, and an on-demand local disk rooted at the temp dir for
cloud storage.
Adds feature tests that pin the disk-relative path resolution.
publishNext() throws ValidationException::withMessages() for invalid
story overlays (bad text/hashtag/mention/url/type), but the enclosing
catch (\Exception $e) caught it too, since ValidationException extends
Exception. Clients got a generic 500 "Failed to create story" instead
of the intended 422 with field-level errors, and the log was flooded
with ordinary user-input validation failures logged as server errors.
Catch ValidationException separately, roll back, and rethrow it so
Laravel's exception handler renders the normal 422 response.
store() and update() open a manual DB::beginTransaction() but had early
return paths (duplicate keyword, keyword not found, per-filter keyword
limit) that hit neither DB::commit() nor the catch block's rollback,
leaving the transaction open past the end of the request. Add the
missing DB::rollBack() call on each early-exit branch, matching the
manual-transaction pattern used elsewhere in the app.
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>
Replace Auth::check() with $request->user() !== null in confirmEmail(),
which already has the request in scope, and drop the now-unused Auth
facade import. Resolves the 2 remaining project-wide larastan errors.
users.storage_used only ever grew: uploads incremented it but no deletion
path decremented it, so users hit the account size limit even when their
real media usage was well below it.
- Decrement storage_used in MediaDeletePipeline when media is removed
- Add UserStorageService::increaseStorageUsed / decrementStorageUsed as the
fast, symmetric hot-path counter updates (floor-based, clamped at zero)
- Refactor the 6 upload call sites to use increaseStorageUsed instead of
duplicated inline writes (also fixes ceil/floor drift vs the reconciler)
- Add (user_id, size) covering index so per-user SUM(size) is not a full
table scan (INPLACE/LOCK=NONE, skipped on sqlite)
- Add user:storage:recalculate command to repair affected accounts, with a
daily --stale=168 scheduled reconciler to correct any drift
- Add regression tests for the pipeline and UserStorageService
Swap the custom 'rt' register token anti-spam mechanism for
spatie/laravel-honeypot on the registration and parental-controls
invite flows.
- Add spatie/laravel-honeypot and publish config/honeypot.php
- Remove getRegisterToken() and the rt validation rule from RegisterController
- Replace the rt hidden field with the @honeypot directive in both forms
- Attach ProtectAgainstSpam middleware to POST /register and the
parental-controls invite register route
- Update RegisterTest to disable honeypot for the valid registration case
An automated return-type pass aliased the view contract as ViewContract
to avoid clashing with the imported View facade, but left four methods
(curatedOnboarding, language, redirectUrl, followIntent) typed against
the facade instead of the contract. That threw a TypeError on
/auth/sign_up.
Convert the four View::make() calls to the view() helper, drop the
facade import, and use a single Illuminate\Contracts\View\View import
for all return types.
Applies the ::class conversion from pixelfed-staging PR #9 (patch 1/21),
formatted with Pint (short imported ::class form). Excludes the
ModelNamespaceMigrationTest namespace assertions, which intentionally
compare against literal namespace strings.
- Relocate translation files from resources/lang to lang/ via git mv
- Update PHP references to use the lang_path() helper
- Update crowdin.yml source/translation paths
- Update phpstan.neon translationDirectories
The Cache::remember('profile:following:'.$pid, ...) block that plucks
following_id and appends the caller's own id was copy-pasted across four
call sites, with inconsistent TTLs (1440 minutes vs 1209600 seconds).
Add FollowerService::getFollowingIds($pid), which owns the cache key that
add()/remove() already invalidate, and use it from InternalApiController,
PublicApiController, ApiV1Controller and HashtagUnfollowPipeline. Removes
the now-unused Follower/Cache imports left behind.
Adds a test covering the followed-ids-plus-self result and the
follows-nobody case.
CollectionController::index and StoryComposeController::createPoll had no
route mapping and simply returned $request->all(). Both are unreachable
debug leftovers; remove them. The live poll route maps to
ComposeController::createPoll, which is unaffected.
Two query patterns were copy-pasted across several call sites:
- The 'users who blocked me, plus myself' list used to filter profile
search (UserFilter::whereFilterableId($pid)->pluck('user_id')->push($pid))
appeared in ComposeController (x2) and DirectMessageController. Extracted
to UserFilterService::searchExcludedProfileIds(). Note this is the
inverse of blocks() (who I blocked), so it is a distinct method.
- CustomEmoji duplicate detection (groupBy('shortcode')->havingRaw(
'count(*) > 1')) appeared three times in AdminController. Extracted to a
CustomEmoji::duplicateShortcodes() query scope.
Adds tests for both. No behaviour change.
SeasonalController::getData computed the average posts/likes per profile
by grouping in SQL, then pulling every grouped row into a collection and
calling ->pluck('count')->avg() in PHP. This loaded one row per profile
into memory just to average.
Wrap the grouped per-profile counts in a subquery and let the database
compute AVG(count), returning a single value. Also drops the invalid
SELECT * with GROUP BY (ONLY_FULL_GROUP_BY) by selecting count(*) only.
Adds a test verifying the average-of-per-profile-counts and its
exclusions (remote, wrong type, out-of-range date), plus the empty case.