The migration failed on MySQL with a 1062 duplicate-entry error: recollating
name/slug to utf8mb4_unicode_520_ci makes previously-distinct values collide
on the unique indexes, so the ALTER TABLE was rejected.
Merge colliding rows first, comparing values under the target collation. For
each collision group the lowest id is kept, references in status_hashtags,
hashtag_follows, hashtag_related and discover_category_hashtags are repointed
to it (UPDATE IGNORE + cleanup), and the losing rows are deleted before the
collation is applied. Adds a regression test for the merge path.
MySQL/MariaDB's utf8mb4_unicode_ci collation treats all characters outside
the Basic Multilingual Plane as equal, conflating distinct same-length
hashtags (e.g. Shavian vs cuneiform) on the unique name/slug indexes.
Migrate the hashtags name/slug columns to utf8mb4_unicode_520_ci, which
differentiates supplementary-plane characters. Improvements over the
original PR:
- Use an explicit ALTER ... MODIFY, since Laravel's fluent ->change() emits
no collation change on MySQL/MariaDB and silently no-ops.
- Match both 'mysql' and 'mariadb' drivers (Laravel 11+ reports MariaDB as a
distinct driver, so a mysql-only check would skip the fix on MariaDB).
- Provide an accurate, reversible down() and preserve NOT NULL + unique keys.
Add feature tests covering the no-op path on non-MySQL drivers and, on
MySQL/MariaDB, that distinct BMP-outside hashtags coexist while same-slug
and case-insensitive dedup still work.
Cover source-of-truth resync of likes/boosts/comments, dry-run, no-op on
correct data, --type restriction, argument validation, and bulk --all
mode. Includes regression tests for the two reporting bugs: the summary
now lists only drifted metrics, and a null reply_count renders as 0.
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).
Cold-migrate existing media from an old S3 bucket to the current cloud
bucket, one media row at a time (like MigrateLocalS3MediaURL):
- Source = --sourceDisk (default s3-old, reads AWS_OLD_*); destination = the
current cloud disk (config filesystems.cloud). No .env editing: operators
point AWS_* at the new bucket first (restarting workers as usual) so new
uploads/downloads land on the new bucket, then run this to backfill old data.
- Copies media (+thumbnail) source->destination, verifies by size and by
sha256 of the freshly-written destination object (against original_sha256),
rewrites cdn_url/optimized_url/thumbnail_url to the destination host, and
GCs the source objects (unless --keep-source). Busts caches.
- Only touches rows whose cdn_url still points at the source host; idempotent.
- --sourceDisk / --limit / --dry-run / --force.
- Adds the s3-old disk (AWS_OLD_*) to config/filesystems.php and feature tests.
Add admin:MediaMoveStorageLocalToCloud and admin:MediaMoveStorageCloudToLocal:
- Copy media (+thumbnail) between local and cloud disks, verify by size (and
sha256 against original_sha256 when present) before deleting the source.
- Integrated GC: delete the verified source copy (local on upload, cloud on
download), set version=4 / reset to 3, and bust MediaService/StatusService
caches. --keep-local / --keep-cloud opt out.
- Manage PF_ENABLE_CLOUD in .env AND the live runtime + config cache so new
uploads route to the correct backend mid-migration on a hot server. Uses the
installer's atomic .env writer (shared ManagesMediaStorageEnv trait).
- --limit / --dry-run / --force.
Replaces media:migrate2cloud (CloudMediaMigrate) and media:s3gc
(MediaS3GarbageCollector); scheduler now runs MediaMoveStorageLocalToCloud
hourly for straggler upload + GC. Keeps media:fix-nonlocal-driver.
Adds feature tests (download+GC, --keep-cloud, dry-run, env-flag flip both
directions, unknown-disk guard).
config_cache() falls through to config() when instance.enable_cc is off
(ENABLE_CONFIG_CACHE=false, as in CI/.env.testing), so ConfigCacheService::put()
alone did not toggle pixelfed.cloud_storage and the command's cloud-enabled
guard aborted with exit 1. Set the underlying config value too (both in
beforeEach and the local-storage refusal test).
Rename the command (and test) to admin:MigrateLocalS3MediaURL to reflect its
scope: rewriting stale S3/cloud media URLs only. Remove avatar handling and
the --avatars option; the command now focuses solely on status media
(cdn_url, thumbnail_url, optimized_url).
Rebuilds stale local media URLs (cdn_url, thumbnail_url, optimized_url) and
avatar cdn_urls from their storage paths via the configured cloud disk.
- Default target host comes from the configured cloud disk (AWS_URL);
requires confirmation (or --force) and can be overridden with --newDomain.
- Optional --oldDomain filters to a single old backend host; by default all
stale hosts are rewritten.
- Refuses to run when PF_ENABLE_CLOUD is false (local storage) and, when
auto-detecting, refuses a target equal to the app domain — so local-storage
instances are never rewritten.
- Single status id / post URL, --all, --avatars; --dry-run; busts
MediaService/StatusService caches for affected statuses.
- Removes the superseded media:cloud-url-rewrite command.
- Adds feature tests covering rewrite/skip/dry-run/oldDomain/newDomain/
remote-skip/local-storage-refusal.
Bulk --all reconciliation previously scanned both local and remote profiles
implicitly. Now --all requires an explicit --scope of local, remote, or
both. --active stays local-only and rejects a non-local --scope. Adds the
BelongsTo return type to Profile::user() so the whereHas('user') scope
filter passes Larastan, and adds tests for scope requirement/validation and
local/remote filtering.
- Rename command signature fix:profilecounts -> admin:fixProfileCounts.
- --active is now its own bulk mode (recently-active local accounts),
mutually exclusive with --all and a single id.
- Add --type=followers|following|statuses to restrict reconciliation to a
single metric (validated).
- Update/extend tests for the new name, --type restriction and invalid-type
rejection.
- Add --force flag to fix:profilecounts for unattended runs and schedule
'fix:profilecounts --all --force' weekly (Sun 03:37) as a safety-net
reconcile. Kept as a low-frequency full scan rather than a new event-driven
dirty-set; it only writes profiles that actually drifted.
- Add Feature tests for AccountStatService recompute helpers and
reconcileProfileCounts (media-type status_count semantics, follower/
following counts, drift/no-drift/no-write, metric restriction, missing
profile) plus fix:profilecounts command behavior (silent-when-synced,
dry-run makes no changes).
Caching an Eloquent model in a Cache::remember closure could deserialize
into a __PHP_Incomplete_Class on read, throwing 'attempt to access a
property on an incomplete object' and returning a 500. This surfaced on
guest profile pages (ProfileController::buildProfile reading
$user->user->settings) and affected several other latent call sites.
Changes:
- ProfileController: cache a plain settings array instead of the
UserSetting model; fall back to defaults when the settings row is missing
- StoryService::getById: fetch a live model instead of caching it
- InstanceService::getByDomain, CustomEmoji::scan: cache arrays
- Site/MobileController: cache Page data as an array via a shared
ManagesCachedPages trait; update blade views to array access
- Add public-route smoke/regression tests covering the cache-read path
Pure rename of the App\Rules\PixelfedUsername validation rule to
App\Rules\ValidUsername for a clearer, more idiomatic name. Updates
the class, filename, test, and all 8 controller call sites. No
behavior change.
Replace 7 duplicated inline username validation closures across 6
controllers (ApiV1Dot1, RemoteAuth, CuratedRegister, AdminInvite x2,
AppRegister, Auth/Register) with the existing PixelfedUsername rule.
Add the 'must contain at least one alphabetical character' check to
the rule so all call sites share consistent, stricter validation.
Add PixelfedUsernameTest covering all validation branches.
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.
Add ActivityPubDeliveryService::pool() using Laravel's Http::pool() to
consolidate the duplicated delivery pattern found across 10 jobs.
Updated jobs:
- StatusActivityPubDeliver
- StatusDelete
- StatusLocalUpdateActivityPubDeliverPipeline
- FanoutDeletePipeline
- SharePipeline
- UndoSharePipeline
- StoryFanout
- StoryExpire
- StoryDelete
- ProfileMigrationDeliverMoveActivityPipeline
The shared method accepts a Profile (sender), audience (inbox URLs),
and activity (payload array), handling signing, user-agent, timeout,
and concurrency in one place. No direct Guzzle usage remains in
app/Jobs/.
Ensures that existing database records using the old App\Status,
App\Profile, etc. morph types correctly resolve to the new
App\Models\ classes via Relation::morphMap().
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
AuthScope/RequestUserTest: 22 tests verifying all controllers and
middleware that were migrated from Auth::user() to $request->user().
CI: update to checkout@v7, cache@v6
Security/AdminAccessTest: verifies non-admin users are blocked from
all admin web routes (dashboard, users, reports, settings, instances,
curated onboarding) and admin API endpoints.
Security/ApiScopeSecurityTest: verifies read-only tokens cannot write
(follow, favourite, delete, mute, block), write tokens can read+write,
cross-user access is denied, and private statuses are protected.
BUG FOUND: CheckForAnyScope middleware (referenced in v1/admin routes)
was removed in Passport 13. All /api/v1/admin/* routes throw
BindingResolutionException. 6 tests skipped pending fix.
PHP's str_ends_with() only accepts a string needle, not an array.
The username validation was passing an array of extensions which
caused a TypeError on every registration attempt.
Replace with a loop over a configurable array of disallowed extensions,
making it easy to add new entries.
Also updates RegisterTest to properly test the registration flow
including the RT anti-bot token and age verification fields.
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.
These deprecated helpers will throw 'undefined function' errors at
runtime since laravel/helpers was removed. Replace with Str::random(),
Str::limit(), and Str::slug() respectively.
* Added current title as value for input so that the current value remains stored by default
* Added parameter 'show_legal_notice_link' => (bool) config_cache('instance.has_legal_notice'),
* Added conditional display of a link to legal notice if the page is active
* Added key 'legalNotice'
* feat translate story
* translate auth
- register
- login
* add remove follow
* Update ApiV1Controller.php
Co-Authored-By: Mathieu <385764+Casmo@users.noreply.github.com>
* New translations web.php (Chinese Simplified)
[ci skip]
* Added current title as value for input so that the current value remains stored by default
* Added parameter 'show_legal_notice_link' => (bool) config_cache('instance.has_legal_notice'),
* Added conditional display of a link to legal notice if the page is active
* Added key 'legalNotice'
* add missing key
* add missing keys
* New translations web.php (Portuguese, Brazilian)
[ci skip]
* New translations web.php (Turkish)
[ci skip]
* New translations web.php (Italian)
[ci skip]
* translate custom filter
* New translations web.php (Italian)
[ci skip]
* use configured alt text length limit when uploading multiple photos
* in notifications sidebar, show popover on shared posts too, not just liked posts
* use case insensitive search when tagging accounts
* New translations web.php (Portuguese, Brazilian)
[ci skip]
* Generic OIDC Support
* Everything should be configurable by env variables
* Basic request tests
* Fixes for items highlighted by review.ai
* Consider using `hash_equals()` instead of `==` when comparing the state values to prevent timing attacks:
`abort_unless(hash_equals($request->input('state'), $request->session()->pull('oauth2state')), 400, 'invalid
state');`
* For better data integrity, consider adding a foreign key constraint to the user_id column: `$table-
>foreign('user_id')->references('id')->on('users')->onDelete('cascade');`
* Does the OIDC provider guarantee that the username field exists in the userInfo data? Consider adding a
null check or fallback: `$userInfoData[config('remote-auth.oidc.field_username')] ?? null`
* field isnt accessTokenResourceOwnerId but responseResourceOwnerId
* New translations web.php (Dutch)
[ci skip]
* Fix components
* Update LandingService and Config util to properly support the legal_notice setting
* Update footer to use legalNotice i18n
* Update i18n
* Update sidebar with gap padding for footer links
* Update compiled assets
* Update i18n json
* Update OIDC config with comments, and disable tests as we dont have db tests configured
* Update remove_from_followers api endpoint
* Update i18n
* Update compiled assets
* Update changelog
* New supported formats, Preserve ICC Color Profiles, libvips support
Update image pipeline to handle avif, heic and webp and preserve ICC color profiles and added libvips support.
* Fix tests
* Update CHANGELOG.md
---------
Co-authored-by: Samy Elshamy <elshamy@coderbutze.de>
Co-authored-by: Felipe Mateus <eu@felipemateus.com>
Co-authored-by: Mathieu <385764+Casmo@users.noreply.github.com>
Co-authored-by: Mackenzie Morgan <macoafi@gmail.com>
Co-authored-by: Gavin Mogan <git@gavinmogan.com>