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 media storage migration commands read/parsed the .env file directly to
check and flip PF_ENABLE_CLOUD. In containerized deploys there is no .env on
disk (config is injected via env vars), so updateEnvFile() threw
'file_get_contents(.env): Failed to open stream' and the scheduled command
exited 1.
- Check the live setting via config_cache('pixelfed.cloud_storage') like the
rest of the app, instead of parsing .env.
- Make the .env write best-effort in ManagesMediaStorageEnv: skip gracefully
when the file is missing or read-only, and still apply the runtime + DB
config-cache updates (the load-bearing changes on a hot server).
- Apply the same fix to the sibling unstable:MediaMoveStorageCloudToLocal.
- Add a regression test covering the no-.env container scenario.
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.
The status- and account-deletion jobs loaded whole collections with
->get() and then looped, running a per-row Notification lookup inside
each iteration.
- StatusDelete / RemoteStatusDelete: resolve associated DirectMessage and
MediaTag ids, fetch their notifications in a single whereIn query,
clear each via cursor (NotificationService::del must run per row for
cache/redis cleanup), then bulk delete the DMs and media tags.
- DeleteAccountPipeline / DeleteRemoteProfilePipeline: stream Story and
Collection deletions with cursor() instead of loading every row into
memory. Per-row file unlink and item deletes are preserved.
Adds StatusDeleteCleanupTest covering DM + notification cleanup, media
tag + notification cleanup, and the no-associations 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.
RemoteAuthService::getVerifyCredentials, getFollowing and getToken made
outbound HTTP requests to a user-controlled remote instance during the
Mastodon login flow with no timeout, no retry and no exception handling.
A slow or hostile instance could hang the request or surface an uncaught
exception.
Wrap all three in timeout(20)->retry(3, 750) with try/catch that returns
false on failure, matching the existing pattern in isDomainCompatible().
Callers already treat a falsy return as a failure; add the missing guard
at the one verify_credentials call site that accessed the result array
without checking it first.
Adds RemoteAuthServiceTest covering connection failure, server error and
success paths.
DirectMessageController@read fetched every matching DirectMessage and
saved each one individually in a loop, issuing one UPDATE per row. On an
active thread this is N queries.
Pluck the matching ids and perform a single bulk update, preserving the
existing response (the list of affected message ids) and updated_at
behaviour.
Adds regression tests covering the marked-read ids, the status_id lower
bound, and sender isolation.
AvatarController@store and BaseApiController@avatarUpdate wrapped the
upload flow in an empty catch(\Exception) block and returned a success
response even when the upload or save failed.
Log the exception and return a real error response (500 JSON for the
API endpoint, a redirect with validation errors for the web endpoint).
Adds regression tests covering the failure path, the success path, and
non-image rejection.