diff --git a/CHANGELOG.md b/CHANGELOG.md index c787faaee..2b7670c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ - Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a)) - Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c)) - Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8)) +- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7)) +- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06)) +- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9)) +- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4)) +- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595)) +- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f)) +- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843)) +- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6)) +- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13) diff --git a/app/Console/Commands/UserAccountDelete.php b/app/Console/Commands/UserAccountDelete.php new file mode 100644 index 000000000..68fad1e92 --- /dev/null +++ b/app/Console/Commands/UserAccountDelete.php @@ -0,0 +1,123 @@ + strlen($value) > 0 + ? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all() + : [], + ); + + $user = User::withTrashed()->find($id); + + table( + ['Username', 'Name', 'Email', 'Created'], + [[$user->username, $user->name, $user->email, $user->created_at]] + ); + + $confirmed = confirm( + label: 'Do you want to federate this account deletion?', + default: false, + yes: 'Proceed', + no: 'Cancel', + hint: 'This action is irreversible' + ); + + if (! $confirmed) { + $this->error('Aborting...'); + exit; + } + + $profile = Profile::withTrashed()->find($user->profile_id); + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($profile, new DeleteActor()); + $activity = $fractal->createData($resource)->toArray(); + + $audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched']) + ->where('nodeinfo_last_fetched', '>', now()->subHours(12)) + ->distinct() + ->pluck('shared_inbox'); + + $payload = json_encode($activity); + + $client = new Client([ + 'timeout' => 10, + ]); + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ], + ]); + }; + } + }; + + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => 50, + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); + + $promise = $pool->promise(); + + $promise->wait(); + } +} diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 8253e1373..0463e681e 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1664,7 +1664,7 @@ class ApiV1Controller extends Controller ], 'statuses' => [ 'characters_reserved_per_url' => 23, - 'max_characters' => (int) config('pixelfed.max_caption_length'), + 'max_characters' => (int) config_cache('pixelfed.max_caption_length'), 'max_media_attachments' => (int) config('pixelfed.max_album_length'), ], ], @@ -3308,7 +3308,7 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ - 'status' => 'nullable|string', + 'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'), 'in_reply_to_id' => 'nullable', 'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'), 'sensitive' => 'nullable', @@ -4066,7 +4066,7 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id; - $ids = Cache::remember('api:v1.1:discover:accounts:popular', 3600, function () { + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { return DB::table('profiles') ->where('is_private', false) ->whereNull('status') @@ -4075,6 +4075,7 @@ class ApiV1Controller extends Controller ->get(); }); $filters = UserFilterService::filters($pid); + $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); $ids = $ids->map(function ($profile) { return AccountService::get($profile->id, true); }) @@ -4087,6 +4088,9 @@ class ApiV1Controller extends Controller ->filter(function ($profile) use ($pid) { return ! FollowerService::follows($pid, $profile['id'], true); }) + ->filter(function ($profile) use ($asf) { + return ! in_array($profile['id'], $asf); + }) ->filter(function ($profile) use ($filters) { return ! in_array($profile['id'], $filters); }) diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index c34b9d968..6d051866b 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -473,15 +473,15 @@ class ApiV1Dot1Controller extends Controller { return [ 'open' => (bool) config_cache('pixelfed.open_registration'), - 'iara' => config('pixelfed.allow_app_registration') + 'iara' => (bool) config_cache('pixelfed.allow_app_registration'), ]; } public function inAppRegistration(Request $request) { abort_if($request->user(), 404); - abort_unless(config_cache('pixelfed.open_registration'), 404); - abort_unless(config('pixelfed.allow_app_registration'), 404); + abort_unless((bool) config_cache('pixelfed.open_registration'), 404); + abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -609,8 +609,8 @@ class ApiV1Dot1Controller extends Controller public function inAppRegistrationConfirm(Request $request) { abort_if($request->user(), 404); - abort_unless(config_cache('pixelfed.open_registration'), 404); - abort_unless(config('pixelfed.allow_app_registration'), 404); + abort_unless((bool) config_cache('pixelfed.open_registration'), 404); + abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index c585b3b04..ebdc851b8 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -104,7 +104,7 @@ class ApiV2Controller extends Controller 'max_featured_tags' => 0, ], 'statuses' => [ - 'max_characters' => (int) config('pixelfed.max_caption_length'), + 'max_characters' => (int) config_cache('pixelfed.max_caption_length'), 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), 'characters_reserved_per_url' => 23 ], diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 42a5490d0..1dd985723 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -2,23 +2,18 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth; -use DB; -use Cache; - -use App\Comment; use App\Jobs\CommentPipeline\CommentPipeline; use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Util\Lexer\Autolink; -use App\Profile; -use App\Status; -use App\UserFilter; -use League\Fractal; -use App\Transformer\Api\StatusTransformer; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Services\StatusService; +use App\Status; +use App\Transformer\Api\StatusTransformer; +use App\UserFilter; +use App\Util\Lexer\Autolink; +use Auth; +use DB; +use Illuminate\Http\Request; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class CommentController extends Controller { @@ -33,9 +28,9 @@ class CommentController extends Controller abort(403); } $this->validate($request, [ - 'item' => 'required|integer|min:1', - 'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'), - 'sensitive' => 'nullable|boolean' + 'item' => 'required|integer|min:1', + 'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'), + 'sensitive' => 'nullable|boolean', ]); $comment = $request->input('comment'); $statusId = $request->input('item'); @@ -45,7 +40,7 @@ class CommentController extends Controller $profile = $user->profile; $status = Status::findOrFail($statusId); - if($status->comments_disabled == true) { + if ($status->comments_disabled == true) { return; } @@ -55,11 +50,11 @@ class CommentController extends Controller ->whereFilterableId($profile->id) ->exists(); - if($filtered == true) { + if ($filtered == true) { return; } - $reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) { + $reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) { $scope = $profile->is_private == true ? 'private' : 'public'; $autolink = Autolink::create()->autolink($comment); $reply = new Status(); diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index 36bd5a66c..341d56ea8 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -2,59 +2,38 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth, Cache, DB, Storage, URL; -use Carbon\Carbon; -use App\{ - Avatar, - Collection, - CollectionItem, - Hashtag, - Like, - Media, - MediaTag, - Notification, - Profile, - Place, - Status, - UserFilter, - UserSetting -}; -use App\Models\Poll; -use App\Transformer\Api\{ - MediaTransformer, - MediaDraftTransformer, - StatusTransformer, - StatusStatelessTransformer -}; -use League\Fractal; -use App\Util\Media\Filter; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use App\Jobs\AvatarPipeline\AvatarOptimize; +use App\Collection; +use App\CollectionItem; +use App\Hashtag; use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\ImageOptimizePipeline\ImageThumbnail; use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail -}; +use App\Jobs\VideoPipeline\VideoThumbnail; +use App\Media; +use App\MediaTag; +use App\Models\Poll; +use App\Notification; +use App\Profile; use App\Services\AccountService; use App\Services\CollectionService; -use App\Services\NotificationService; -use App\Services\MediaPathService; use App\Services\MediaBlocklistService; +use App\Services\MediaPathService; use App\Services\MediaStorageService; use App\Services\MediaTagService; -use App\Services\StatusService; use App\Services\SnowflakeService; -use Illuminate\Support\Str; -use App\Util\Lexer\Autolink; -use App\Util\Lexer\Extractor; -use App\Util\Media\License; -use Image; use App\Services\UserRoleService; +use App\Status; +use App\Transformer\Api\MediaTransformer; +use App\UserFilter; +use App\Util\Lexer\Autolink; +use App\Util\Media\Filter; +use App\Util\Media\License; +use Auth; +use Cache; +use DB; +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class ComposeController extends Controller { @@ -74,30 +53,30 @@ class ComposeController extends Controller public function mediaUpload(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ 'file.*' => [ 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), ], 'file' => [ 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), ], 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24' + 'filter_class' => 'nullable|alpha_dash|max:24', ]); $user = Auth::user(); $profile = $user->profile; - abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); return $dailyLimit >= 1250; @@ -105,8 +84,8 @@ class ComposeController extends Controller abort_if($limitReached == true, 429); - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + if (config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function () use ($user) { return Media::whereUserId($user->id)->sum('size') / 1000; }); $limit = (int) config_cache('pixelfed.max_account_size'); @@ -144,24 +123,24 @@ class ComposeController extends Controller $media->version = 3; $media->save(); - $preview_url = $media->url() . '?v=' . time(); - $url = $media->url() . '?v=' . time(); + $preview_url = $media->url().'?v='.time(); + $url = $media->url().'?v='.time(); switch ($media->mime) { case 'image/jpeg': case 'image/png': case 'image/webp': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; default: - break; + break; } Cache::forget($limitKey); @@ -169,6 +148,7 @@ class ComposeController extends Controller $res = $this->fractal->createData($resource)->toArray(); $res['preview_url'] = $preview_url; $res['url'] = $url; + return response()->json($res); } @@ -176,21 +156,21 @@ class ComposeController extends Controller { $this->validate($request, [ 'id' => 'required', - 'file' => function() { + 'file' => function () { return [ 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), ]; }, ]); $user = Auth::user(); - abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:media-updates:' . $user->id; + $limitKey = 'compose:rate-limit:media-updates:'.$user->id; $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); return $dailyLimit >= 1500; @@ -202,9 +182,9 @@ class ComposeController extends Controller $id = $request->input('id'); $media = Media::whereUserId($user->id) - ->whereProfileId($user->profile_id) - ->whereNull('status_id') - ->findOrFail($id); + ->whereProfileId($user->profile_id) + ->whereNull('status_id') + ->findOrFail($id); $media->save(); @@ -214,47 +194,48 @@ class ComposeController extends Controller $dir = implode('/', $fragments); $path = $photo->storePubliclyAs($dir, $name); $res = [ - 'url' => $media->url() . '?v=' . time() + 'url' => $media->url().'?v='.time(), ]; ImageOptimize::dispatch($media)->onQueue('mmo'); Cache::forget($limitKey); + return $res; } public function mediaDelete(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'id' => 'required|integer|min:1|exists:media,id' + 'id' => 'required|integer|min:1|exists:media,id', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $media = Media::whereNull('status_id') - ->whereUserId(Auth::id()) - ->findOrFail($request->input('id')); + ->whereUserId(Auth::id()) + ->findOrFail($request->input('id')); MediaStorageService::delete($media, true); return response()->json([ 'msg' => 'Successfully deleted', - 'code' => 200 + 'code' => 200, ]); } public function searchTag(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'q' => 'required|string|min:1|max:50' + 'q' => 'required|string|min:1|max:50', ]); $q = $request->input('q'); - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { return []; } $q = mb_substr($q, 1); @@ -262,7 +243,7 @@ class ComposeController extends Controller $user = $request->user(); - abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $blocked = UserFilter::whereFilterableType('App\Profile') ->whereFilterType('block') @@ -271,34 +252,34 @@ class ComposeController extends Controller $blocked->push($request->user()->profile_id); - $results = Profile::select('id','domain','username') + $results = Profile::select('id', 'domain', 'username') ->whereNotIn('id', $blocked) ->whereNull('domain') - ->where('username','like','%'.$q.'%') + ->where('username', 'like', '%'.$q.'%') ->limit(15) ->get() - ->map(function($r) { + ->map(function ($r) { return [ 'id' => (string) $r->id, 'name' => $r->username, 'privacy' => true, - 'avatar' => $r->avatarUrl() + 'avatar' => $r->avatarUrl(), ]; - }); + }); return $results; } public function searchUntag(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ 'status_id' => 'required', - 'profile_id' => 'required' + 'profile_id' => 'required', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $user = $request->user(); $status_id = $request->input('status_id'); @@ -310,7 +291,7 @@ class ComposeController extends Controller ->whereProfileId($profile_id) ->first(); - if(!$tag) { + if (! $tag) { return []; } Notification::whereItemType('App\MediaTag') @@ -326,37 +307,38 @@ class ComposeController extends Controller public function searchLocation(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'q' => 'required|string|max:100' + 'q' => 'required|string|max:100', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $pid = $request->user()->profile_id; - abort_if(!$pid, 400); + abort_if(! $pid, 400); $q = e($request->input('q')); - $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() { + $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () { $minId = SnowflakeService::byDate(now()->subDays(290)); - if(config('database.default') == 'pgsql') { + if (config('database.default') == 'pgsql') { return Status::selectRaw('id, place_id, count(place_id) as pc') - ->whereNotNull('place_id') - ->where('id', '>', $minId) - ->orderByDesc('pc') - ->groupBy(['place_id', 'id']) - ->limit(400) - ->get() - ->filter(function($post) { - return $post; - }) - ->map(function($place) { - return [ - 'id' => $place->place_id, - 'count' => $place->pc - ]; - }) - ->unique('id') - ->values(); + ->whereNotNull('place_id') + ->where('id', '>', $minId) + ->orderByDesc('pc') + ->groupBy(['place_id', 'id']) + ->limit(400) + ->get() + ->filter(function ($post) { + return $post; + }) + ->map(function ($place) { + return [ + 'id' => $place->place_id, + 'count' => $place->pc, + ]; + }) + ->unique('id') + ->values(); } + return Status::selectRaw('id, place_id, count(place_id) as pc') ->whereNotNull('place_id') ->where('id', '>', $minId) @@ -364,57 +346,58 @@ class ComposeController extends Controller ->orderByDesc('pc') ->limit(400) ->get() - ->filter(function($post) { + ->filter(function ($post) { return $post; }) - ->map(function($place) { + ->map(function ($place) { return [ 'id' => $place->place_id, - 'count' => $place->pc + 'count' => $place->pc, ]; }); }); - $q = '%' . $q . '%'; + $q = '%'.$q.'%'; $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; $places = DB::table('places') - ->where('name', $wildcard, $q) - ->limit((strlen($q) > 5 ? 360 : 30)) - ->get() - ->sortByDesc(function($place, $key) use($popular) { - return $popular->filter(function($p) use($place) { - return $p['id'] == $place->id; - })->map(function($p) use($place) { - return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; - })->values(); - }) - ->map(function($r) { - return [ - 'id' => $r->id, - 'name' => $r->name, - 'country' => $r->country, - 'url' => url('/discover/places/' . $r->id . '/' . $r->slug) - ]; - }) - ->values() - ->all(); + ->where('name', $wildcard, $q) + ->limit((strlen($q) > 5 ? 360 : 30)) + ->get() + ->sortByDesc(function ($place, $key) use ($popular) { + return $popular->filter(function ($p) use ($place) { + return $p['id'] == $place->id; + })->map(function ($p) use ($place) { + return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; + })->values(); + }) + ->map(function ($r) { + return [ + 'id' => $r->id, + 'name' => $r->name, + 'country' => $r->country, + 'url' => url('/discover/places/'.$r->id.'/'.$r->slug), + ]; + }) + ->values() + ->all(); + return $places; } public function searchMentionAutocomplete(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' + 'q' => 'required|string|min:2|max:50', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $q = $request->input('q'); - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { return []; } } @@ -426,32 +409,33 @@ class ComposeController extends Controller $blocked->push($request->user()->profile_id); - $results = Profile::select('id','domain','username') + $results = Profile::select('id', 'domain', 'username') ->whereNotIn('id', $blocked) - ->where('username','like','%'.$q.'%') + ->where('username', 'like', '%'.$q.'%') ->groupBy('id', 'domain') ->limit(15) ->get() - ->map(function($profile) { + ->map(function ($profile) { $username = $profile->domain ? substr($profile->username, 1) : $profile->username; + return [ - 'key' => '@' . str_limit($username, 30), + 'key' => '@'.str_limit($username, 30), 'value' => $username, ]; - }); + }); return $results; } public function searchHashtagAutocomplete(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' + 'q' => 'required|string|min:2|max:50', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $q = $request->input('q'); @@ -461,12 +445,12 @@ class ComposeController extends Controller ->whereIsBanned(false) ->limit(5) ->get() - ->map(function($tag) { + ->map(function ($tag) { return [ - 'key' => '#' . $tag->slug, - 'value' => $tag->slug + 'key' => '#'.$tag->slug, + 'value' => $tag->slug, ]; - }); + }); return $results; } @@ -474,8 +458,8 @@ class ComposeController extends Controller public function store(Request $request) { $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'media.*' => 'required', + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), + 'media.*' => 'required', 'media.*.id' => 'required|integer|min:1', 'media.*.filter_class' => 'nullable|alpha_dash|max:30', 'media.*.license' => 'nullable|string|max:140', @@ -491,14 +475,14 @@ class ComposeController extends Controller // 'optimize_media' => 'nullable' ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - if(config('costar.enabled') == true) { + if (config('costar.enabled') == true) { $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { + if ($blockedKeywords !== null && $request->caption) { $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { + foreach ($keywords as $kw) { + if (Str::contains($request->caption, $kw) == true) { abort(400, 'Invalid object'); } } @@ -508,9 +492,9 @@ class ComposeController extends Controller $user = $request->user(); $profile = $user->profile; - $limitKey = 'compose:rate-limit:store:' . $user->id; + $limitKey = 'compose:rate-limit:store:'.$user->id; $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { $dailyLimit = Status::whereProfileId($user->profile_id) ->whereNull('in_reply_to_id') ->whereNull('reblog_of_id') @@ -534,12 +518,12 @@ class ComposeController extends Controller $tagged = $request->input('tagged'); $optimize_media = (bool) $request->input('optimize_media'); - foreach($medias as $k => $media) { - if($k + 1 > config_cache('pixelfed.max_album_length')) { + foreach ($medias as $k => $media) { + if ($k + 1 > config_cache('pixelfed.max_album_length')) { continue; } $m = Media::findOrFail($media['id']); - if($m->profile_id !== $profile->id || $m->status_id) { + if ($m->profile_id !== $profile->id || $m->status_id) { abort(403, 'Invalid media id'); } $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; @@ -547,7 +531,7 @@ class ComposeController extends Controller $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; - if($cw == true || $profile->cw == true) { + if ($cw == true || $profile->cw == true) { $m->is_nsfw = $cw; $status->is_nsfw = $cw; } @@ -560,19 +544,19 @@ class ComposeController extends Controller $mediaType = StatusController::mimeTypeCheck($mimes); - if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { + if (in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { abort(400, __('exception.compose.invalid.album')); } - if($place && is_array($place)) { + if ($place && is_array($place)) { $status->place_id = $place['id']; } - if($request->filled('comments_disabled')) { + if ($request->filled('comments_disabled')) { $status->comments_disabled = (bool) $request->input('comments_disabled'); } - if($request->filled('spoiler_text') && $cw) { + if ($request->filled('spoiler_text') && $cw) { $status->cw_summary = $request->input('spoiler_text'); } @@ -583,7 +567,7 @@ class ComposeController extends Controller $status->profile_id = $profile->id; $status->save(); - foreach($attachments as $media) { + foreach ($attachments as $media) { $media->status_id = $status->id; $media->save(); } @@ -597,7 +581,7 @@ class ComposeController extends Controller $status->type = $mediaType; $status->save(); - foreach($tagged as $tg) { + foreach ($tagged as $tg) { $mt = new MediaTag; $mt->status_id = $status->id; $mt->media_id = $status->media->first()->id; @@ -612,17 +596,17 @@ class ComposeController extends Controller MediaTagService::sendNotification($mt); } - if($request->filled('collections')) { + if ($request->filled('collections')) { $collections = Collection::whereProfileId($profile->id) ->find($request->input('collections')) - ->each(function($collection) use($status) { + ->each(function ($collection) use ($status) { $count = $collection->items()->count(); CollectionItem::firstOrCreate([ 'collection_id' => $collection->id, 'object_type' => 'App\Status', - 'object_id' => $status->id + 'object_id' => $status->id, ], [ - 'order' => $count + 'order' => $count, ]); CollectionService::addItem( @@ -643,7 +627,7 @@ class ComposeController extends Controller Cache::forget('profile:status_count:'.$profile->id); Cache::forget('status:transformer:media:attachments:'.$status->id); Cache::forget($user->storageUsedKey()); - Cache::forget('profile:embed:' . $status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); Cache::forget($limitKey); return $status->url(); @@ -653,7 +637,7 @@ class ComposeController extends Controller { abort_unless(config('exp.top'), 404); $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), 'cw' => 'nullable|boolean', 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', 'place' => 'nullable', @@ -661,14 +645,14 @@ class ComposeController extends Controller 'tagged' => 'nullable', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - if(config('costar.enabled') == true) { + if (config('costar.enabled') == true) { $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { + if ($blockedKeywords !== null && $request->caption) { $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { + foreach ($keywords as $kw) { + if (Str::contains($request->caption, $kw) == true) { abort(400, 'Invalid object'); } } @@ -683,11 +667,11 @@ class ComposeController extends Controller $cw = $request->input('cw'); $tagged = $request->input('tagged'); - if($place && is_array($place)) { + if ($place && is_array($place)) { $status->place_id = $place['id']; } - if($request->filled('comments_disabled')) { + if ($request->filled('comments_disabled')) { $status->comments_disabled = (bool) $request->input('comments_disabled'); } @@ -707,11 +691,11 @@ class ComposeController extends Controller 'bg_id' => 1, 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', 'length' => strlen($status->caption), - ] + ], ], $entities), JSON_UNESCAPED_SLASHES); $status->save(); - foreach($tagged as $tg) { + foreach ($tagged as $tg) { $mt = new MediaTag; $mt->status_id = $status->id; $mt->media_id = $status->media->first()->id; @@ -726,7 +710,6 @@ class ComposeController extends Controller MediaTagService::sendNotification($mt); } - Cache::forget('user:account:id:'.$profile->user_id); Cache::forget('_api:statuses:recent_9:'.$profile->id); Cache::forget('profile:status_count:'.$profile->id); @@ -737,18 +720,18 @@ class ComposeController extends Controller public function mediaProcessingCheck(Request $request) { $this->validate($request, [ - 'id' => 'required|integer|min:1' + 'id' => 'required|integer|min:1', ]); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $media = Media::whereUserId($request->user()->id) ->whereNull('status_id') ->findOrFail($request->input('id')); - if(config('pixelfed.media_fast_process')) { + if (config('pixelfed.media_fast_process')) { return [ - 'finished' => true + 'finished' => true, ]; } @@ -762,27 +745,27 @@ class ComposeController extends Controller break; default: - # code... + // code... break; } return [ - 'finished' => $finished + 'finished' => $finished, ]; } public function composeSettings(Request $request) { $uid = $request->user()->id; - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); $default = [ 'default_license' => 1, 'media_descriptions' => false, - 'max_altext_length' => config_cache('pixelfed.max_altext_length') + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), ]; $settings = AccountService::settings($uid); - if(isset($settings['other']) && isset($settings['other']['scope'])) { + if (isset($settings['other']) && isset($settings['other']['scope'])) { $s = $settings['compose_settings']; $s['default_scope'] = $settings['other']['scope']; $settings['compose_settings'] = $s; @@ -794,23 +777,22 @@ class ComposeController extends Controller public function createPoll(Request $request) { $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), 'cw' => 'nullable|boolean', 'visibility' => 'required|string|in:public,private', 'comments_disabled' => 'nullable', 'expiry' => 'required|in:60,360,1440,10080', - 'pollOptions' => 'required|array|min:1|max:4' + 'pollOptions' => 'required|array|min:1|max:4', ]); abort(404); abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); - abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); abort_if(Status::whereType('poll') ->whereProfileId($request->user()->profile_id) ->whereCaption($request->input('caption')) ->where('created_at', '>', now()->subDays(2)) - ->exists() - , 422, 'Duplicate detected.'); + ->exists(), 422, 'Duplicate detected.'); $status = new Status; $status->profile_id = $request->user()->profile_id; @@ -827,7 +809,7 @@ class ComposeController extends Controller $poll->profile_id = $status->profile_id; $poll->poll_options = $request->input('pollOptions'); $poll->expires_at = now()->addMinutes($request->input('expiry')); - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { + $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) { return 0; })->toArray(); $poll->save(); diff --git a/app/Http/Controllers/DiscoverController.php b/app/Http/Controllers/DiscoverController.php index 862a16ad0..c9e93eecf 100644 --- a/app/Http/Controllers/DiscoverController.php +++ b/app/Http/Controllers/DiscoverController.php @@ -5,8 +5,11 @@ namespace App\Http\Controllers; use App\Hashtag; use App\Instance; use App\Like; +use App\Services\AccountService; +use App\Services\AdminShadowFilterService; use App\Services\BookmarkService; use App\Services\ConfigCacheService; +use App\Services\FollowerService; use App\Services\HashtagService; use App\Services\LikeService; use App\Services\ReblogService; @@ -377,4 +380,44 @@ class DiscoverController extends Controller return $res; } + + public function discoverAccountsPopular(Request $request) + { + abort_if(! $request->user(), 403); + + $pid = $request->user()->profile_id; + + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { + return DB::table('profiles') + ->where('is_private', false) + ->whereNull('status') + ->orderByDesc('profiles.followers_count') + ->limit(30) + ->get(); + }); + $filters = UserFilterService::filters($pid); + $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); + $ids = $ids->map(function ($profile) { + return AccountService::get($profile->id, true); + }) + ->filter(function ($profile) { + return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked']; + }) + ->filter(function ($profile) use ($pid) { + return $profile['id'] != $pid; + }) + ->filter(function ($profile) use ($pid) { + return ! FollowerService::follows($pid, $profile['id'], true); + }) + ->filter(function ($profile) use ($asf) { + return ! in_array($profile['id'], $asf); + }) + ->filter(function ($profile) use ($filters) { + return ! in_array($profile['id'], $filters); + }) + ->take(16) + ->values(); + + return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES); + } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 26e9e5398..6471ed760 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -2,356 +2,385 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth; -use Cache; -use DB; -use View; use App\AccountInterstitial; use App\Follower; use App\FollowRequest; use App\Profile; -use App\Story; -use App\Status; -use App\User; -use App\UserSetting; -use App\UserFilter; -use League\Fractal; use App\Services\AccountService; use App\Services\FollowerService; use App\Services\StatusService; -use App\Util\Lexer\Nickname; -use App\Util\Webfinger\Webfinger; -use App\Transformer\ActivityPub\ProfileOutbox; +use App\Status; +use App\Story; use App\Transformer\ActivityPub\ProfileTransformer; +use App\User; +use App\UserFilter; +use App\UserSetting; +use Auth; +use Cache; +use Illuminate\Http\Request; +use League\Fractal; +use View; class ProfileController extends Controller { - public function show(Request $request, $username) - { - // redirect authed users to Metro 2.0 - if($request->user()) { - // unless they force static view - if(!$request->has('fs') || $request->input('fs') != '1') { - $pid = AccountService::usernameToId($username); - if($pid) { - return redirect('/i/web/profile/' . $pid); - } - } - } + public function show(Request $request, $username) + { + if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { + $user = $this->getCachedUser($username, true); + abort_if(! $user, 404, 'Not found'); - $user = Profile::whereNull('domain') - ->whereNull('status') - ->whereUsername($username) - ->firstOrFail(); + return $this->showActivityPub($request, $user); + } + // redirect authed users to Metro 2.0 + if ($request->user()) { + // unless they force static view + if (! $request->has('fs') || $request->input('fs') != '1') { + $pid = AccountService::usernameToId($username); + if ($pid) { + return redirect('/i/web/profile/'.$pid); + } + } + } - if($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $user); - } + $user = $this->getCachedUser($username); - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) { - $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + abort_unless($user, 404); - return false; - }); - if($aiCheck) { - return redirect('/login'); - } - return $this->buildProfile($request, $user); - } + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) { + $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } - protected function buildProfile(Request $request, $user) - { - $username = $user->username; - $loggedIn = Auth::check(); - $isPrivate = false; - $isBlocked = false; - if(!$loggedIn) { - $key = 'profile:settings:' . $user->id; - $ttl = now()->addHours(6); - $settings = Cache::remember($key, $ttl, function() use($user) { - return $user->user->settings; - }); + return false; + }); + if ($aiCheck) { + return redirect('/login'); + } - if ($user->is_private == true) { - $profile = null; - return view('profile.private', compact('user')); - } + return $this->buildProfile($request, $user); + } - $owner = false; - $is_following = false; + protected function buildProfile(Request $request, $user) + { + $username = $user->username; + $loggedIn = Auth::check(); + $isPrivate = false; + $isBlocked = false; + if (! $loggedIn) { + $key = 'profile:settings:'.$user->id; + $ttl = now()->addHours(6); + $settings = Cache::remember($key, $ttl, function () use ($user) { + return $user->user->settings; + }); - $profile = $user; - $settings = [ - 'crawlable' => $settings->crawlable, - 'following' => [ - 'count' => $settings->show_profile_following_count, - 'list' => $settings->show_profile_following - ], - 'followers' => [ - 'count' => $settings->show_profile_follower_count, - 'list' => $settings->show_profile_followers - ] - ]; - return view('profile.show', compact('profile', 'settings')); - } else { - $key = 'profile:settings:' . $user->id; - $ttl = now()->addHours(6); - $settings = Cache::remember($key, $ttl, function() use($user) { - return $user->user->settings; - }); + if ($user->is_private == true) { + $profile = null; - if ($user->is_private == true) { - $isPrivate = $this->privateProfileCheck($user, $loggedIn); - } + return view('profile.private', compact('user')); + } - $isBlocked = $this->blockedProfileCheck($user); + $owner = false; + $is_following = false; - $owner = $loggedIn && Auth::id() === $user->user_id; - $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; + $profile = $user; + $settings = [ + 'crawlable' => $settings->crawlable, + 'following' => [ + 'count' => $settings->show_profile_following_count, + 'list' => $settings->show_profile_following, + ], + 'followers' => [ + 'count' => $settings->show_profile_follower_count, + 'list' => $settings->show_profile_followers, + ], + ]; - if ($isPrivate == true || $isBlocked == true) { - $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) - ->whereFollowingId($user->id) - ->exists() : false; - return view('profile.private', compact('user', 'is_following', 'requested')); - } + return view('profile.show', compact('profile', 'settings')); + } else { + $key = 'profile:settings:'.$user->id; + $ttl = now()->addHours(6); + $settings = Cache::remember($key, $ttl, function () use ($user) { + return $user->user->settings; + }); - $is_admin = is_null($user->domain) ? $user->user->is_admin : false; - $profile = $user; - $settings = [ - 'crawlable' => $settings->crawlable, - 'following' => [ - 'count' => $settings->show_profile_following_count, - 'list' => $settings->show_profile_following - ], - 'followers' => [ - 'count' => $settings->show_profile_follower_count, - 'list' => $settings->show_profile_followers - ] - ]; - return view('profile.show', compact('profile', 'settings')); - } - } + if ($user->is_private == true) { + $isPrivate = $this->privateProfileCheck($user, $loggedIn); + } - public function permalinkRedirect(Request $request, $username) - { - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + $isBlocked = $this->blockedProfileCheck($user); - if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $user); - } + $owner = $loggedIn && Auth::id() === $user->user_id; + $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; - return redirect($user->url()); - } + if ($isPrivate == true || $isBlocked == true) { + $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) + ->whereFollowingId($user->id) + ->exists() : false; - protected function privateProfileCheck(Profile $profile, $loggedIn) - { - if (!Auth::check()) { - return true; - } + return view('profile.private', compact('user', 'is_following', 'requested')); + } - $user = Auth::user()->profile; - if($user->id == $profile->id || !$profile->is_private) { - return false; - } + $is_admin = is_null($user->domain) ? $user->user->is_admin : false; + $profile = $user; + $settings = [ + 'crawlable' => $settings->crawlable, + 'following' => [ + 'count' => $settings->show_profile_following_count, + 'list' => $settings->show_profile_following, + ], + 'followers' => [ + 'count' => $settings->show_profile_follower_count, + 'list' => $settings->show_profile_followers, + ], + ]; - $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); - if ($follows == false) { - return true; - } + return view('profile.show', compact('profile', 'settings')); + } + } - return false; - } + protected function getCachedUser($username, $withTrashed = false) + { + $val = str_replace(['_', '.', '-'], '', $username); + if (! ctype_alnum($val)) { + return; + } + $hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username); - public static function accountCheck(Profile $profile) - { - switch ($profile->status) { - case 'disabled': - case 'suspended': - case 'delete': - return view('profile.disabled'); - break; + return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) { + if (! $withTrashed) { + return Profile::whereNull(['domain', 'status']) + ->whereUsername($username) + ->first(); + } else { + return Profile::withTrashed() + ->whereNull('domain') + ->whereUsername($username) + ->first(); + } + }); + } - default: - break; - } - return abort(404); - } + public function permalinkRedirect(Request $request, $username) + { + if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { + $user = $this->getCachedUser($username, true); - protected function blockedProfileCheck(Profile $profile) - { - $pid = Auth::user()->profile->id; - $blocks = UserFilter::whereUserId($profile->id) - ->whereFilterType('block') - ->whereFilterableType('App\Profile') - ->pluck('filterable_id') - ->toArray(); - if (in_array($pid, $blocks)) { - return true; - } + return $this->showActivityPub($request, $user); + } - return false; - } + $user = $this->getCachedUser($username); - public function showActivityPub(Request $request, $user) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if($user->domain, 404); + return redirect($user->url()); + } - return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) { - $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($user, new ProfileTransformer); - $res = $fractal->createData($resource)->toArray(); - return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); - }); - } + protected function privateProfileCheck(Profile $profile, $loggedIn) + { + if (! Auth::check()) { + return true; + } - public function showAtomFeed(Request $request, $user) - { - abort_if(!config('federation.atom.enabled'), 404); + $user = Auth::user()->profile; + if ($user->id == $profile->id || ! $profile->is_private) { + return false; + } - $pid = AccountService::usernameToId($user); + $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); + if ($follows == false) { + return true; + } - abort_if(!$pid, 404); + return false; + } - $profile = AccountService::get($pid, true); + public static function accountCheck(Profile $profile) + { + switch ($profile->status) { + case 'disabled': + case 'suspended': + case 'delete': + return view('profile.disabled'); + break; - abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); + default: + break; + } - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) { - $uid = User::whereProfileId($profile['id'])->first(); - if(!$uid) { - return true; - } - $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + return abort(404); + } - return false; - }); + protected function blockedProfileCheck(Profile $profile) + { + $pid = Auth::user()->profile->id; + $blocks = UserFilter::whereUserId($profile->id) + ->whereFilterType('block') + ->whereFilterableType('App\Profile') + ->pluck('filterable_id') + ->toArray(); + if (in_array($pid, $blocks)) { + return true; + } - abort_if($aiCheck, 404); + return false; + } - $enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) { - $uid = User::whereProfileId($profile['id'])->first(); - if(!$uid) { - return false; - } - $settings = UserSetting::whereUserId($uid->id)->first(); - if(!$settings) { - return false; - } + public function showActivityPub(Request $request, $user) + { + abort_if(! config_cache('federation.activitypub.enabled'), 404); + abort_if(! $user, 404, 'Not found'); + abort_if($user->domain, 404); - return $settings->show_atom; - }); + return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) { + $fractal = new Fractal\Manager(); + $resource = new Fractal\Resource\Item($user, new ProfileTransformer); + $res = $fractal->createData($resource)->toArray(); - abort_if(!$enabled, 404); + return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); + }); + } - $data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) { - $items = Status::whereProfileId($pid) - ->whereScope('public') - ->whereIn('type', ['photo', 'photo:album']) - ->orderByDesc('id') - ->take(10) - ->get() - ->map(function($status) { - return StatusService::get($status->id, true); - }) - ->filter(function($status) { - return $status && - isset($status['account']) && - isset($status['media_attachments']) && - count($status['media_attachments']); - }) - ->values(); - $permalink = config('app.url') . "/users/{$profile['username']}.atom"; - $headers = ['Content-Type' => 'application/atom+xml']; + public function showAtomFeed(Request $request, $user) + { + abort_if(! config('federation.atom.enabled'), 404); - if($items && $items->count()) { - $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); - } + $pid = AccountService::usernameToId($user); - return compact('items', 'permalink', 'headers'); - }); - abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404); - return response() - ->view('atom.user', - [ - 'profile' => $profile, - 'items' => $data['items'], - 'permalink' => $data['permalink'] - ] - ) - ->withHeaders($data['headers']); - } + abort_if(! $pid, 404); - public function meRedirect() - { - abort_if(!Auth::check(), 404); - return redirect(Auth::user()->url()); - } + $profile = AccountService::get($pid, true); - public function embed(Request $request, $username) - { - $res = view('profile.embed-removed'); + abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404); - if(!config('instance.embed.profile')) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 86400, function () use ($profile) { + $uid = User::whereProfileId($profile['id'])->first(); + if (! $uid) { + return true; + } + $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } - if(strlen($username) > 15 || strlen($username) < 2) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + return false; + }); - $profile = Profile::whereUsername($username) - ->whereIsPrivate(false) - ->whereNull('status') - ->whereNull('domain') - ->first(); + abort_if($aiCheck, 404); - if(!$profile) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) { + $uid = User::whereProfileId($profile['id'])->first(); + if (! $uid) { + return false; + } + $settings = UserSetting::whereUserId($uid->id)->first(); + if (! $settings) { + return false; + } - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { - $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + return $settings->show_atom; + }); - return false; - }); + abort_if(! $enabled, 404); - if($aiCheck) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) { + $items = Status::whereProfileId($pid) + ->whereScope('public') + ->whereIn('type', ['photo', 'photo:album']) + ->orderByDesc('id') + ->take(10) + ->get() + ->map(function ($status) { + return StatusService::get($status->id, true); + }) + ->filter(function ($status) { + return $status && + isset($status['account']) && + isset($status['media_attachments']) && + count($status['media_attachments']); + }) + ->values(); + $permalink = config('app.url')."/users/{$profile['username']}.atom"; + $headers = ['Content-Type' => 'application/atom+xml']; - if(AccountService::canEmbed($profile->user_id) == false) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + if ($items && $items->count()) { + $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); + } - $profile = AccountService::get($profile->id); - $res = view('profile.embed', compact('profile')); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + return compact('items', 'permalink', 'headers'); + }); + abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404); - public function stories(Request $request, $username) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - $pid = $profile->id; - $authed = Auth::user()->profile_id; - abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404); - $exists = Story::whereProfileId($pid) - ->whereActive(true) - ->exists(); - abort_unless($exists, 404); - return view('profile.story', compact('pid', 'profile')); - } + return response() + ->view('atom.user', + [ + 'profile' => $profile, + 'items' => $data['items'], + 'permalink' => $data['permalink'], + ] + ) + ->withHeaders($data['headers']); + } + + public function meRedirect() + { + abort_if(! Auth::check(), 404); + + return redirect(Auth::user()->url()); + } + + public function embed(Request $request, $username) + { + $res = view('profile.embed-removed'); + + if (! (bool) config_cache('instance.embed.profile')) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + if (strlen($username) > 15 || strlen($username) < 2) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $profile = $this->getCachedUser($username); + + if (! $profile || $profile->is_private) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) { + $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } + + return false; + }); + + if ($aiCheck) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + if (AccountService::canEmbed($profile->user_id) == false) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $profile = AccountService::get($profile->id); + $res = view('profile.embed', compact('profile')); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + public function stories(Request $request, $username) + { + abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404); + $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + $pid = $profile->id; + $authed = Auth::user()->profile_id; + abort_if($pid != $authed && ! FollowerService::follows($authed, $pid), 404); + $exists = Story::whereProfileId($pid) + ->whereActive(true) + ->exists(); + abort_unless($exists, 404); + + return view('profile.story', compact('pid', 'profile')); + } } diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index 6697bf3c8..6509071e0 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -95,6 +95,8 @@ trait PrivacySettings Cache::forget('pf:acct:settings:hidden-following:' . $pid); Cache::forget('pf:acct-trans:hideFollowing:' . $pid); Cache::forget('pf:acct-trans:hideFollowers:' . $pid); + Cache::forget('pfc:cached-user:wt:' . strtolower($profile->username)); + Cache::forget('pfc:cached-user:wot:' . strtolower($profile->username)); return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!'); } diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index 379b24505..5e205d64d 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -2,166 +2,202 @@ namespace App\Http\Controllers; +use App\Page; +use App\Profile; +use App\Services\FollowerService; +use App\Status; +use App\User; +use App\Util\ActivityPub\Helpers; +use App\Util\Localization\Localization; +use Auth; +use Cache; use Illuminate\Http\Request; use Illuminate\Support\Str; -use App, Auth, Cache, View; -use App\Util\Lexer\PrettyNumber; -use App\{Follower, Page, Profile, Status, User, UserFilter}; -use App\Util\Localization\Localization; -use App\Services\FollowerService; -use App\Util\ActivityPub\Helpers; +use View; class SiteController extends Controller { - public function home(Request $request) - { - if (Auth::check()) { - return $this->homeTimeline($request); - } else { - return $this->homeGuest(); - } - } + public function home(Request $request) + { + if (Auth::check()) { + return $this->homeTimeline($request); + } else { + return $this->homeGuest(); + } + } - public function homeGuest() - { - return view('site.index'); - } + public function homeGuest() + { + return view('site.index'); + } - public function homeTimeline(Request $request) - { - if($request->has('force_old_ui')) { - return view('timeline.home', ['layout' => 'feed']); - } + public function homeTimeline(Request $request) + { + if ($request->has('force_old_ui')) { + return view('timeline.home', ['layout' => 'feed']); + } - return redirect('/i/web'); - } + return redirect('/i/web'); + } - public function changeLocale(Request $request, $locale) - { - // todo: add other locales after pushing new l10n strings - $locales = Localization::languages(); - if(in_array($locale, $locales)) { - if($request->user()) { - $user = $request->user(); - $user->language = $locale; - $user->save(); - } - session()->put('locale', $locale); - } + public function changeLocale(Request $request, $locale) + { + // todo: add other locales after pushing new l10n strings + $locales = Localization::languages(); + if (in_array($locale, $locales)) { + if ($request->user()) { + $user = $request->user(); + $user->language = $locale; + $user->save(); + } + session()->put('locale', $locale); + } - return redirect(route('site.language')); - } + return redirect(route('site.language')); + } - public function about() - { - return Cache::remember('site.about_v2', now()->addMinutes(15), function() { - $user_count = number_format(User::count()); - $post_count = number_format(Status::count()); - $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; - return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); - }); - } + public function about() + { + return Cache::remember('site.about_v2', now()->addMinutes(15), function () { + $user_count = number_format(User::count()); + $post_count = number_format(Status::count()); + $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; - public function language() - { - return view('site.language'); - } + return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); + }); + } - public function communityGuidelines(Request $request) - { - return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() { - $slug = '/site/kb/community-guidelines'; - $page = Page::whereSlug($slug)->whereActive(true)->first(); - return View::make('site.help.community-guidelines')->with(compact('page'))->render(); - }); - } + public function language() + { + return view('site.language'); + } - public function privacy(Request $request) - { - $page = Cache::remember('site:privacy', now()->addDays(120), function() { - $slug = '/site/privacy'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - return View::make('site.privacy')->with(compact('page'))->render(); - } + public function communityGuidelines(Request $request) + { + return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () { + $slug = '/site/kb/community-guidelines'; + $page = Page::whereSlug($slug)->whereActive(true)->first(); - public function terms(Request $request) - { - $page = Cache::remember('site:terms', now()->addDays(120), function() { - $slug = '/site/terms'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - return View::make('site.terms')->with(compact('page'))->render(); - } + return View::make('site.help.community-guidelines')->with(compact('page'))->render(); + }); + } - public function redirectUrl(Request $request) - { - abort_if(!$request->user(), 404); - $this->validate($request, [ - 'url' => 'required|url' - ]); - $url = request()->input('url'); - abort_if(Helpers::validateUrl($url) == false, 404); - return view('site.redirect', compact('url')); - } + public function privacy(Request $request) + { + $page = Cache::remember('site:privacy', now()->addDays(120), function () { + $slug = '/site/privacy'; - public function followIntent(Request $request) - { - $this->validate($request, [ - 'user' => 'string|min:1|max:15|exists:users,username', - ]); - $profile = Profile::whereUsername($request->input('user'))->firstOrFail(); - $user = $request->user(); - abort_if($user && $profile->id == $user->profile_id, 404); - $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; - return view('site.intents.follow', compact('profile', 'user', 'following')); - } + return Page::whereSlug($slug)->whereActive(true)->first(); + }); - public function legacyProfileRedirect(Request $request, $username) - { - $username = Str::contains($username, '@') ? '@' . $username : $username; - if(str_contains($username, '@')) { - $profile = Profile::whereUsername($username) - ->firstOrFail(); + return View::make('site.privacy')->with(compact('page'))->render(); + } - if($profile->domain == null) { - $url = "/$profile->username"; - } else { - $url = "/i/web/profile/_/{$profile->id}"; - } + public function terms(Request $request) + { + $page = Cache::remember('site:terms', now()->addDays(120), function () { + $slug = '/site/terms'; - } else { - $profile = Profile::whereUsername($username) - ->whereNull('domain') - ->firstOrFail(); - $url = "/$profile->username"; - } + return Page::whereSlug($slug)->whereActive(true)->first(); + }); - return redirect($url); - } + return View::make('site.terms')->with(compact('page'))->render(); + } - public function legacyWebfingerRedirect(Request $request, $username, $domain) - { - $un = '@'.$username.'@'.$domain; - $profile = Profile::whereUsername($un) - ->firstOrFail(); + public function redirectUrl(Request $request) + { + abort_if(! $request->user(), 404); + $this->validate($request, [ + 'url' => 'required|url', + ]); + $url = request()->input('url'); + abort_if(Helpers::validateUrl($url) == false, 404); - if($profile->domain == null) { - $url = "/$profile->username"; - } else { - $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); - } + return view('site.redirect', compact('url')); + } - return redirect($url); - } + public function followIntent(Request $request) + { + $this->validate($request, [ + 'user' => 'string|min:1|max:15|exists:users,username', + ]); + $profile = Profile::whereUsername($request->input('user'))->firstOrFail(); + $user = $request->user(); + abort_if($user && $profile->id == $user->profile_id, 404); + $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; - public function legalNotice(Request $request) - { - $page = Cache::remember('site:legal-notice', now()->addDays(120), function() { - $slug = '/site/legal-notice'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - abort_if(!$page, 404); - return View::make('site.legal-notice')->with(compact('page'))->render(); - } + return view('site.intents.follow', compact('profile', 'user', 'following')); + } + + public function legacyProfileRedirect(Request $request, $username) + { + $username = Str::contains($username, '@') ? '@'.$username : $username; + if (str_contains($username, '@')) { + $profile = Profile::whereUsername($username) + ->firstOrFail(); + + if ($profile->domain == null) { + $url = "/$profile->username"; + } else { + $url = "/i/web/profile/_/{$profile->id}"; + } + + } else { + $profile = Profile::whereUsername($username) + ->whereNull('domain') + ->firstOrFail(); + $url = "/$profile->username"; + } + + return redirect($url); + } + + public function legacyWebfingerRedirect(Request $request, $username, $domain) + { + $un = '@'.$username.'@'.$domain; + $profile = Profile::whereUsername($un) + ->firstOrFail(); + + if ($profile->domain == null) { + $url = "/$profile->username"; + } else { + $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); + } + + return redirect($url); + } + + public function legalNotice(Request $request) + { + $page = Cache::remember('site:legal-notice', now()->addDays(120), function () { + $slug = '/site/legal-notice'; + + return Page::whereSlug($slug)->whereActive(true)->first(); + }); + abort_if(! $page, 404); + + return View::make('site.legal-notice')->with(compact('page'))->render(); + } + + public function curatedOnboarding(Request $request) + { + if ($request->user()) { + return redirect('/i/web'); + } + + $regOpen = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg'); + if ($regOpen) { + if ($curOnlyClosed) { + return redirect('/register'); + } + } else { + if (! $curOnboarding) { + return redirect('/'); + } + } + + return view('auth.curated-register.index', ['step' => 1]); + } } diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 873f5eace..4a3b3552d 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -2,458 +2,466 @@ namespace App\Http\Controllers; -use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\AccountInterstitial; use App\Jobs\SharePipeline\SharePipeline; use App\Jobs\SharePipeline\UndoSharePipeline; -use App\AccountInterstitial; -use App\Media; +use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusDelete; use App\Profile; +use App\Services\HashidService; +use App\Services\ReblogService; +use App\Services\StatusService; use App\Status; -use App\StatusArchived; use App\StatusView; -use App\Transformer\ActivityPub\StatusTransformer; use App\Transformer\ActivityPub\Verb\Note; use App\Transformer\ActivityPub\Verb\Question; -use App\User; -use Auth, DB, Cache; +use App\Util\Media\License; +use Auth; +use Cache; +use DB; use Illuminate\Http\Request; use League\Fractal; -use App\Util\Media\Filter; -use Illuminate\Support\Str; -use App\Services\HashidService; -use App\Services\StatusService; -use App\Util\Media\License; -use App\Services\ReblogService; class StatusController extends Controller { - public function show(Request $request, $username, $id) - { - // redirect authed users to Metro 2.0 - if($request->user()) { - // unless they force static view - if(!$request->has('fs') || $request->input('fs') != '1') { - return redirect('/i/web/post/' . $id); - } - } - - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - - if($user->status != null) { - return ProfileController::accountCheck($user); - } - - $status = Status::whereProfileId($user->id) - ->whereNull('reblog_of_id') - ->whereIn('scope', ['public','unlisted', 'private']) - ->findOrFail($id); - - if($status->uri || $status->url) { - $url = $status->uri ?? $status->url; - if(ends_with($url, '/activity')) { - $url = str_replace('/activity', '', $url); - } - return redirect($url); - } - - if($status->visibility == 'private' || $user->is_private) { - if(!Auth::check()) { - abort(404); - } - $pid = Auth::user()->profile; - if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { - abort(404); - } - } - - if($status->type == 'archived') { - if(Auth::user()->profile_id !== $status->profile_id) { - abort(404); - } - } - - if($request->user() && $request->user()->profile_id != $status->profile_id) { - StatusView::firstOrCreate([ - 'status_id' => $status->id, - 'status_profile_id' => $status->profile_id, - 'profile_id' => $request->user()->profile_id - ]); - } - - if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $status); - } - - $template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; - return view($template, compact('user', 'status')); - } - - public function shortcodeRedirect(Request $request, $id) - { - abort(404); - } - - public function showId(int $id) - { - abort(404); - $status = Status::whereNull('reblog_of_id') - ->whereIn('scope', ['public', 'unlisted']) - ->findOrFail($id); - return redirect($status->url()); - } - - public function showEmbed(Request $request, $username, int $id) - { - if(!config('instance.embed.post')) { - $res = view('status.embed-removed'); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - - $profile = Profile::whereNull(['domain','status']) - ->whereIsPrivate(false) - ->whereUsername($username) - ->first(); - - if(!$profile) { - $content = view('status.embed-removed'); - return response($content)->header('X-Frame-Options', 'ALLOWALL'); - } - - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { - $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } - - return false; - }); - - if($aiCheck) { - $res = view('status.embed-removed'); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - $status = Status::whereProfileId($profile->id) - ->whereNull('uri') - ->whereScope('public') - ->whereIsNsfw(false) - ->whereIn('type', ['photo', 'video','photo:album']) - ->find($id); - if(!$status) { - $content = view('status.embed-removed'); - return response($content)->header('X-Frame-Options', 'ALLOWALL'); - } - $showLikes = $request->filled('likes') && $request->likes == true; - $showCaption = $request->filled('caption') && $request->caption !== false; - $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; - $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); - return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - - public function showObject(Request $request, $username, int $id) - { - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - - if($user->status != null) { - return ProfileController::accountCheck($user); - } - - $status = Status::whereProfileId($user->id) - ->whereNotIn('visibility',['draft','direct']) - ->findOrFail($id); - - abort_if($status->uri, 404); - - if($status->visibility == 'private' || $user->is_private) { - if(!Auth::check()) { - abort(403); - } - $pid = Auth::user()->profile; - if($user->followedBy($pid) == false && $user->id !== $pid->id) { - abort(403); - } - } - - return $this->showActivityPub($request, $status); - } - - public function compose() - { - $this->authCheck(); - - return view('status.compose'); - } - - public function store(Request $request) - { - return; - } - - public function delete(Request $request) - { - $this->authCheck(); - - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); - - $status = Status::findOrFail($request->input('item')); - - $user = Auth::user(); - - if($status->profile_id != $user->profile->id && - $user->is_admin == true && - $status->uri == null - ) { - $media = $status->media; - - $ai = new AccountInterstitial; - $ai->user_id = $status->profile->user_id; - $ai->type = 'post.removed'; - $ai->view = 'account.moderation.post.removed'; - $ai->item_type = 'App\Status'; - $ai->item_id = $status->id; - $ai->has_media = (bool) $media->count(); - $ai->blurhash = $media->count() ? $media->first()->blurhash : null; - $ai->meta = json_encode([ - 'caption' => $status->caption, - 'created_at' => $status->created_at, - 'type' => $status->type, - 'url' => $status->url(), - 'is_nsfw' => $status->is_nsfw, - 'scope' => $status->scope, - 'reblog' => $status->reblog_of_id, - 'likes_count' => $status->likes_count, - 'reblogs_count' => $status->reblogs_count, - ]); - $ai->save(); - - $u = $status->profile->user; - $u->has_interstitial = true; - $u->save(); - } - - if($status->in_reply_to_id) { - $parent = Status::find($status->in_reply_to_id); - if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:status_count:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id, true); - Cache::forget('profile:status_count:'.$status->profile_id); - $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); - } - } else if ($status->profile_id == $user->profile_id || $user->is_admin == true) { - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:status_count:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id, true); - Cache::forget('profile:status_count:'.$status->profile_id); - $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); - } - - if($request->wantsJson()) { - return response()->json(['Status successfully deleted.']); - } else { - return redirect($user->url()); - } - } - - public function storeShare(Request $request) - { - $this->authCheck(); - - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); - - $user = Auth::user(); - $profile = $user->profile; - $status = Status::whereScope('public') - ->findOrFail($request->input('item')); - - $count = $status->reblogs_count; - - $exists = Status::whereProfileId(Auth::user()->profile->id) - ->whereReblogOfId($status->id) - ->exists(); - if ($exists == true) { - $shares = Status::whereProfileId(Auth::user()->profile->id) - ->whereReblogOfId($status->id) - ->get(); - foreach ($shares as $share) { - UndoSharePipeline::dispatch($share); - ReblogService::del($profile->id, $status->id); - $count--; - } - } else { - $share = new Status(); - $share->profile_id = $profile->id; - $share->reblog_of_id = $status->id; - $share->in_reply_to_profile_id = $status->profile_id; - $share->type = 'share'; - $share->save(); - $count++; - SharePipeline::dispatch($share); - ReblogService::add($profile->id, $status->id); - } - - Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); - StatusService::del($status->id); - - if ($request->ajax()) { - $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; - } else { - $response = redirect($status->url()); - } - - return $response; - } - - public function showActivityPub(Request $request, $status) - { - $object = $status->type == 'poll' ? new Question() : new Note(); - $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($status, $object); - $res = $fractal->createData($resource)->toArray(); - - return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function edit(Request $request, $username, $id) - { - $this->authCheck(); - $user = Auth::user()->profile; - $status = Status::whereProfileId($user->id) - ->with(['media']) - ->findOrFail($id); - $licenses = License::get(); - return view('status.edit', compact('user', 'status', 'licenses')); - } - - public function editStore(Request $request, $username, $id) - { - $this->authCheck(); - $user = Auth::user()->profile; - $status = Status::whereProfileId($user->id) - ->with(['media']) - ->findOrFail($id); - - $this->validate($request, [ - 'license' => 'nullable|integer|min:1|max:16', - ]); - - $licenseId = $request->input('license'); - - $status->media->each(function($media) use($licenseId) { - $media->license = $licenseId; - $media->save(); - Cache::forget('status:transformer:media:attachments:'.$media->status_id); - }); - - return redirect($status->url()); - } - - protected function authCheck() - { - if (Auth::check() == false) { - abort(403); - } - } - - protected function validateVisibility($visibility) - { - $allowed = ['public', 'unlisted', 'private']; - return in_array($visibility, $allowed) ? $visibility : 'public'; - } - - public static function mimeTypeCheck($mimes) - { - $allowed = explode(',', config_cache('pixelfed.media_types')); - $count = count($mimes); - $photos = 0; - $videos = 0; - foreach($mimes as $mime) { - if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') { - continue; - } - if(str_contains($mime, 'image/')) { - $photos++; - } - if(str_contains($mime, 'video/')) { - $videos++; - } - } - if($photos == 1 && $videos == 0) { - return 'photo'; - } - if($videos == 1 && $photos == 0) { - return 'video'; - } - if($photos > 1 && $videos == 0) { - return 'photo:album'; - } - if($videos > 1 && $photos == 0) { - return 'video:album'; - } - if($photos >= 1 && $videos >= 1) { - return 'photo:video:album'; - } - - return 'text'; - } - - public function toggleVisibility(Request $request) { - $this->authCheck(); - $this->validate($request, [ - 'item' => 'required|string|min:1|max:20', - 'disableComments' => 'required|boolean' - ]); - - $user = Auth::user(); - $id = $request->input('item'); - $state = $request->input('disableComments'); - - $status = Status::findOrFail($id); - - if($status->profile_id != $user->profile->id && $user->is_admin == false) { - abort(403); - } - - $status->comments_disabled = $status->comments_disabled == true ? false : true; - $status->save(); - - return response()->json([200]); - } - - public function storeView(Request $request) - { - abort_if(!$request->user(), 403); - - $views = $request->input('_v'); - $uid = $request->user()->profile_id; - - if(empty($views) || !is_array($views)) { - return response()->json(0); - } - - Cache::forget('profile:home-timeline-cursor:' . $request->user()->id); - - foreach($views as $view) { - if(!isset($view['sid']) || !isset($view['pid'])) { - continue; - } - DB::transaction(function () use($view, $uid) { - StatusView::firstOrCreate([ - 'status_id' => $view['sid'], - 'status_profile_id' => $view['pid'], - 'profile_id' => $uid - ]); - }); - } - - return response()->json(1); - } + public function show(Request $request, $username, $id) + { + // redirect authed users to Metro 2.0 + if ($request->user()) { + // unless they force static view + if (! $request->has('fs') || $request->input('fs') != '1') { + return redirect('/i/web/post/'.$id); + } + } + + $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + + if ($user->status != null) { + return ProfileController::accountCheck($user); + } + + $status = Status::whereProfileId($user->id) + ->whereNull('reblog_of_id') + ->whereIn('scope', ['public', 'unlisted', 'private']) + ->findOrFail($id); + + if ($status->uri || $status->url) { + $url = $status->uri ?? $status->url; + if (ends_with($url, '/activity')) { + $url = str_replace('/activity', '', $url); + } + + return redirect($url); + } + + if ($status->visibility == 'private' || $user->is_private) { + if (! Auth::check()) { + abort(404); + } + $pid = Auth::user()->profile; + if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { + abort(404); + } + } + + if ($status->type == 'archived') { + if (Auth::user()->profile_id !== $status->profile_id) { + abort(404); + } + } + + if ($request->user() && $request->user()->profile_id != $status->profile_id) { + StatusView::firstOrCreate([ + 'status_id' => $status->id, + 'status_profile_id' => $status->profile_id, + 'profile_id' => $request->user()->profile_id, + ]); + } + + if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { + return $this->showActivityPub($request, $status); + } + + $template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; + + return view($template, compact('user', 'status')); + } + + public function shortcodeRedirect(Request $request, $id) + { + $hid = HashidService::decode($id); + abort_if(! $hid, 404); + + return redirect('/i/web/post/'.$hid); + } + + public function showId(int $id) + { + abort(404); + $status = Status::whereNull('reblog_of_id') + ->whereIn('scope', ['public', 'unlisted']) + ->findOrFail($id); + + return redirect($status->url()); + } + + public function showEmbed(Request $request, $username, int $id) + { + if (! (bool) config_cache('instance.embed.post')) { + $res = view('status.embed-removed'); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $profile = Profile::whereNull(['domain', 'status']) + ->whereIsPrivate(false) + ->whereUsername($username) + ->first(); + + if (! $profile) { + $content = view('status.embed-removed'); + + return response($content)->header('X-Frame-Options', 'ALLOWALL'); + } + + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) { + $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } + + return false; + }); + + if ($aiCheck) { + $res = view('status.embed-removed'); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + $status = Status::whereProfileId($profile->id) + ->whereNull('uri') + ->whereScope('public') + ->whereIsNsfw(false) + ->whereIn('type', ['photo', 'video', 'photo:album']) + ->find($id); + if (! $status) { + $content = view('status.embed-removed'); + + return response($content)->header('X-Frame-Options', 'ALLOWALL'); + } + $showLikes = $request->filled('likes') && $request->likes == true; + $showCaption = $request->filled('caption') && $request->caption !== false; + $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; + $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); + + return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + public function showObject(Request $request, $username, int $id) + { + $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + + if ($user->status != null) { + return ProfileController::accountCheck($user); + } + + $status = Status::whereProfileId($user->id) + ->whereNotIn('visibility', ['draft', 'direct']) + ->findOrFail($id); + + abort_if($status->uri, 404); + + if ($status->visibility == 'private' || $user->is_private) { + if (! Auth::check()) { + abort(403); + } + $pid = Auth::user()->profile; + if ($user->followedBy($pid) == false && $user->id !== $pid->id) { + abort(403); + } + } + + return $this->showActivityPub($request, $status); + } + + public function compose() + { + $this->authCheck(); + + return view('status.compose'); + } + + public function store(Request $request) + { + + } + + public function delete(Request $request) + { + $this->authCheck(); + + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); + + $status = Status::findOrFail($request->input('item')); + + $user = Auth::user(); + + if ($status->profile_id != $user->profile->id && + $user->is_admin == true && + $status->uri == null + ) { + $media = $status->media; + + $ai = new AccountInterstitial; + $ai->user_id = $status->profile->user_id; + $ai->type = 'post.removed'; + $ai->view = 'account.moderation.post.removed'; + $ai->item_type = 'App\Status'; + $ai->item_id = $status->id; + $ai->has_media = (bool) $media->count(); + $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + $ai->meta = json_encode([ + 'caption' => $status->caption, + 'created_at' => $status->created_at, + 'type' => $status->type, + 'url' => $status->url(), + 'is_nsfw' => $status->is_nsfw, + 'scope' => $status->scope, + 'reblog' => $status->reblog_of_id, + 'likes_count' => $status->likes_count, + 'reblogs_count' => $status->reblogs_count, + ]); + $ai->save(); + + $u = $status->profile->user; + $u->has_interstitial = true; + $u->save(); + } + + if ($status->in_reply_to_id) { + $parent = Status::find($status->in_reply_to_id); + if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + Cache::forget('profile:status_count:'.$status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); + StatusService::del($status->id, true); + Cache::forget('profile:status_count:'.$status->profile_id); + $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); + } + } elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) { + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + Cache::forget('profile:status_count:'.$status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); + StatusService::del($status->id, true); + Cache::forget('profile:status_count:'.$status->profile_id); + $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); + } + + if ($request->wantsJson()) { + return response()->json(['Status successfully deleted.']); + } else { + return redirect($user->url()); + } + } + + public function storeShare(Request $request) + { + $this->authCheck(); + + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); + + $user = Auth::user(); + $profile = $user->profile; + $status = Status::whereScope('public') + ->findOrFail($request->input('item')); + + $count = $status->reblogs_count; + + $exists = Status::whereProfileId(Auth::user()->profile->id) + ->whereReblogOfId($status->id) + ->exists(); + if ($exists == true) { + $shares = Status::whereProfileId(Auth::user()->profile->id) + ->whereReblogOfId($status->id) + ->get(); + foreach ($shares as $share) { + UndoSharePipeline::dispatch($share); + ReblogService::del($profile->id, $status->id); + $count--; + } + } else { + $share = new Status(); + $share->profile_id = $profile->id; + $share->reblog_of_id = $status->id; + $share->in_reply_to_profile_id = $status->profile_id; + $share->type = 'share'; + $share->save(); + $count++; + SharePipeline::dispatch($share); + ReblogService::add($profile->id, $status->id); + } + + Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); + StatusService::del($status->id); + + if ($request->ajax()) { + $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; + } else { + $response = redirect($status->url()); + } + + return $response; + } + + public function showActivityPub(Request $request, $status) + { + $object = $status->type == 'poll' ? new Question() : new Note(); + $fractal = new Fractal\Manager(); + $resource = new Fractal\Resource\Item($status, $object); + $res = $fractal->createData($resource)->toArray(); + + return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function edit(Request $request, $username, $id) + { + $this->authCheck(); + $user = Auth::user()->profile; + $status = Status::whereProfileId($user->id) + ->with(['media']) + ->findOrFail($id); + $licenses = License::get(); + + return view('status.edit', compact('user', 'status', 'licenses')); + } + + public function editStore(Request $request, $username, $id) + { + $this->authCheck(); + $user = Auth::user()->profile; + $status = Status::whereProfileId($user->id) + ->with(['media']) + ->findOrFail($id); + + $this->validate($request, [ + 'license' => 'nullable|integer|min:1|max:16', + ]); + + $licenseId = $request->input('license'); + + $status->media->each(function ($media) use ($licenseId) { + $media->license = $licenseId; + $media->save(); + Cache::forget('status:transformer:media:attachments:'.$media->status_id); + }); + + return redirect($status->url()); + } + + protected function authCheck() + { + if (Auth::check() == false) { + abort(403); + } + } + + protected function validateVisibility($visibility) + { + $allowed = ['public', 'unlisted', 'private']; + + return in_array($visibility, $allowed) ? $visibility : 'public'; + } + + public static function mimeTypeCheck($mimes) + { + $allowed = explode(',', config_cache('pixelfed.media_types')); + $count = count($mimes); + $photos = 0; + $videos = 0; + foreach ($mimes as $mime) { + if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') { + continue; + } + if (str_contains($mime, 'image/')) { + $photos++; + } + if (str_contains($mime, 'video/')) { + $videos++; + } + } + if ($photos == 1 && $videos == 0) { + return 'photo'; + } + if ($videos == 1 && $photos == 0) { + return 'video'; + } + if ($photos > 1 && $videos == 0) { + return 'photo:album'; + } + if ($videos > 1 && $photos == 0) { + return 'video:album'; + } + if ($photos >= 1 && $videos >= 1) { + return 'photo:video:album'; + } + + return 'text'; + } + + public function toggleVisibility(Request $request) + { + $this->authCheck(); + $this->validate($request, [ + 'item' => 'required|string|min:1|max:20', + 'disableComments' => 'required|boolean', + ]); + + $user = Auth::user(); + $id = $request->input('item'); + $state = $request->input('disableComments'); + + $status = Status::findOrFail($id); + + if ($status->profile_id != $user->profile->id && $user->is_admin == false) { + abort(403); + } + + $status->comments_disabled = $status->comments_disabled == true ? false : true; + $status->save(); + + return response()->json([200]); + } + + public function storeView(Request $request) + { + abort_if(! $request->user(), 403); + + $views = $request->input('_v'); + $uid = $request->user()->profile_id; + + if (empty($views) || ! is_array($views)) { + return response()->json(0); + } + + Cache::forget('profile:home-timeline-cursor:'.$request->user()->id); + + foreach ($views as $view) { + if (! isset($view['sid']) || ! isset($view['pid'])) { + continue; + } + DB::transaction(function () use ($view, $uid) { + StatusView::firstOrCreate([ + 'status_id' => $view['sid'], + 'status_profile_id' => $view['pid'], + 'profile_id' => $uid, + ]); + }); + } + + return response()->json(1); + } } diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index bf34f5dcc..7537830fc 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -75,6 +75,20 @@ class ConfigCacheService 'instance.curated_registration.enabled', 'federation.migration', + + 'pixelfed.max_caption_length', + 'pixelfed.max_bio_length', + 'pixelfed.max_name_length', + 'pixelfed.min_password_length', + 'pixelfed.max_avatar_size', + 'pixelfed.max_altext_length', + 'pixelfed.allow_app_registration', + 'pixelfed.app_registration_rate_limit_attempts', + 'pixelfed.app_registration_rate_limit_decay', + 'pixelfed.app_registration_confirm_rate_limit_attempts', + 'pixelfed.app_registration_confirm_rate_limit_decay', + 'instance.embed.profile', + 'instance.embed.post', // 'system.user_mode' ]; diff --git a/app/Services/HashidService.php b/app/Services/HashidService.php index 914d24321..e12c10599 100644 --- a/app/Services/HashidService.php +++ b/app/Services/HashidService.php @@ -2,54 +2,38 @@ namespace App\Services; -use Cache; +class HashidService +{ + public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; -class HashidService { + public static function encode($id, $minLimit = true) + { + if (! is_numeric($id) || $id > PHP_INT_MAX) { + return null; + } - public const MIN_LIMIT = 15; - public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + $cmap = self::CMAP; + $base = strlen($cmap); + $shortcode = ''; + while ($id) { + $id = ($id - ($r = $id % $base)) / $base; + $shortcode = $cmap[$r].$shortcode; + } - public static function encode($id, $minLimit = true) - { - if(!is_numeric($id) || $id > PHP_INT_MAX) { - return null; - } + return $shortcode; + } - if($minLimit && strlen($id) < self::MIN_LIMIT) { - return null; - } - - $key = "hashids:{$id}"; - return Cache::remember($key, now()->hours(48), function() use($id) { - $cmap = self::CMAP; - $base = strlen($cmap); - $shortcode = ''; - while($id) { - $id = ($id - ($r = $id % $base)) / $base; - $shortcode = $cmap[$r] . $shortcode; - } - return $shortcode; - }); - } - - public static function decode($short) - { - $len = strlen($short); - if($len < 3 || $len > 11) { - return null; - } - $id = 0; - foreach(str_split($short) as $needle) { - $pos = strpos(self::CMAP, $needle); - // if(!$pos) { - // return null; - // } - $id = ($id*64) + $pos; - } - if(strlen($id) < self::MIN_LIMIT) { - return null; - } - return $id; - } + public static function decode($short = false) + { + if (! $short) { + return; + } + $id = 0; + foreach (str_split($short) as $needle) { + $pos = strpos(self::CMAP, $needle); + $id = ($id * 64) + $pos; + } + return $id; + } } diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php index 199768b86..20759ecf4 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -2,105 +2,104 @@ namespace App\Services; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; use App\Status; use App\User; -use App\Services\AccountService; use App\Util\Site\Nodeinfo; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; class LandingService { - public static function get($json = true) - { - $activeMonth = Nodeinfo::activeUsersMonthly(); + public static function get($json = true) + { + $activeMonth = Nodeinfo::activeUsersMonthly(); - $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() { - return User::count(); - }); + $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function () { + return User::count(); + }); - $postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() { - return Status::whereLocal(true)->count(); - }); + $postCount = Cache::remember('api:nodeinfo:statuses', 21600, function () { + return Status::whereLocal(true)->count(); + }); - $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); + $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if (config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); - $openReg = (bool) config_cache('pixelfed.open_registration'); + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function ($rule, $key) { + $id = $key + 1; - $res = [ - 'name' => config_cache('app.name'), - 'url' => config_cache('app.url'), - 'domain' => config('pixelfed.domain.app'), - 'show_directory' => config_cache('instance.landing.show_directory'), - 'show_explore_feed' => config_cache('instance.landing.show_explore'), - 'open_registration' => (bool) $openReg, - 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), - 'version' => config('pixelfed.version'), - 'about' => [ - 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), - 'short_description' => config_cache('app.short_description'), - 'description' => config_cache('app.description'), - ], - 'stats' => [ - 'active_users' => (int) $activeMonth, - 'posts_count' => (int) $postCount, - 'total_users' => (int) $totalUsers - ], - 'contact' => [ - 'account' => $contactAccount, - 'email' => config('instance.email') - ], - 'rules' => $rules, - 'uploader' => [ - 'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024), - 'max_caption_length' => (int) config('pixelfed.max_caption_length'), - 'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), - 'album_limit' => (int) config_cache('pixelfed.max_album_length'), - 'image_quality' => (int) config_cache('pixelfed.image_quality'), - 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), - 'optimize_image' => (bool) config('pixelfed.optimize_image'), - 'optimize_video' => (bool) config('pixelfed.optimize_video'), - 'media_types' => config_cache('pixelfed.media_types'), - ], - 'features' => [ - 'federation' => config_cache('federation.activitypub.enabled'), - 'timelines' => [ - 'local' => true, - 'network' => (bool) config('federation.network_timeline'), - ], - 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), - 'stories' => (bool) config_cache('instance.stories.enabled'), - 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), - ] - ]; + return [ + 'id' => "{$id}", + 'text' => $rule, + ]; + }) + ->toArray() : []; + }); - if($json) { - return json_encode($res); - } + $openReg = (bool) config_cache('pixelfed.open_registration'); - return $res; - } + $res = [ + 'name' => config_cache('app.name'), + 'url' => config_cache('app.url'), + 'domain' => config('pixelfed.domain.app'), + 'show_directory' => config_cache('instance.landing.show_directory'), + 'show_explore_feed' => config_cache('instance.landing.show_explore'), + 'open_registration' => (bool) $openReg, + 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), + 'version' => config('pixelfed.version'), + 'about' => [ + 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), + 'short_description' => config_cache('app.short_description'), + 'description' => config_cache('app.description'), + ], + 'stats' => [ + 'active_users' => (int) $activeMonth, + 'posts_count' => (int) $postCount, + 'total_users' => (int) $totalUsers, + ], + 'contact' => [ + 'account' => $contactAccount, + 'email' => config('instance.email'), + ], + 'rules' => $rules, + 'uploader' => [ + 'max_photo_size' => (int) (config_cache('pixelfed.max_photo_size') * 1024), + 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), + 'album_limit' => (int) config_cache('pixelfed.max_album_length'), + 'image_quality' => (int) config_cache('pixelfed.image_quality'), + 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), + 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'), + 'media_types' => config_cache('pixelfed.media_types'), + ], + 'features' => [ + 'federation' => config_cache('federation.activitypub.enabled'), + 'timelines' => [ + 'local' => true, + 'network' => (bool) config_cache('federation.network_timeline'), + ], + 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), + 'stories' => (bool) config_cache('instance.stories.enabled'), + 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), + ], + ]; + + if ($json) { + return json_encode($res); + } + + return $res; + } } diff --git a/app/Transformer/ActivityPub/ProfileTransformer.php b/app/Transformer/ActivityPub/ProfileTransformer.php index 45d22cd11..96d129bf7 100644 --- a/app/Transformer/ActivityPub/ProfileTransformer.php +++ b/app/Transformer/ActivityPub/ProfileTransformer.php @@ -3,67 +3,80 @@ namespace App\Transformer\ActivityPub; use App\Profile; -use League\Fractal; use App\Services\AccountService; +use League\Fractal; class ProfileTransformer extends Fractal\TransformerAbstract { public function transform(Profile $profile) { $res = [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'toot' => 'http://joinmastodon.org/ns#', - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'alsoKnownAs' => [ - '@id' => 'as:alsoKnownAs', - '@type' => '@id' - ], - 'movedTo' => [ - '@id' => 'as:movedTo', - '@type' => '@id' - ], - 'indexable' => 'toot:indexable', + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'toot' => 'http://joinmastodon.org/ns#', + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'alsoKnownAs' => [ + '@id' => 'as:alsoKnownAs', + '@type' => '@id', + ], + 'movedTo' => [ + '@id' => 'as:movedTo', + '@type' => '@id', + ], + 'indexable' => 'toot:indexable', + 'suspended' => 'toot:suspended', + ], ], - ], - 'id' => $profile->permalink(), - 'type' => 'Person', - 'following' => $profile->permalink('/following'), - 'followers' => $profile->permalink('/followers'), - 'inbox' => $profile->permalink('/inbox'), - 'outbox' => $profile->permalink('/outbox'), - 'preferredUsername' => $profile->username, - 'name' => $profile->name, - 'summary' => $profile->bio, - 'url' => $profile->url(), - 'manuallyApprovesFollowers' => (bool) $profile->is_private, - 'indexable' => (bool) $profile->indexable, - 'published' => $profile->created_at->format('Y-m-d') . 'T00:00:00Z', - 'publicKey' => [ - 'id' => $profile->permalink().'#main-key', - 'owner' => $profile->permalink(), - 'publicKeyPem' => $profile->public_key, - ], - 'icon' => [ - 'type' => 'Image', - 'mediaType' => 'image/jpeg', - 'url' => $profile->avatarUrl(), - ], - 'endpoints' => [ - 'sharedInbox' => config('app.url') . '/f/inbox' - ] - ]; + 'id' => $profile->permalink(), + 'type' => 'Person', + 'following' => $profile->permalink('/following'), + 'followers' => $profile->permalink('/followers'), + 'inbox' => $profile->permalink('/inbox'), + 'outbox' => $profile->permalink('/outbox'), + 'preferredUsername' => $profile->username, + 'name' => $profile->name, + 'summary' => $profile->bio, + 'url' => $profile->url(), + 'manuallyApprovesFollowers' => (bool) $profile->is_private, + 'indexable' => (bool) $profile->indexable, + 'published' => $profile->created_at->format('Y-m-d').'T00:00:00Z', + 'publicKey' => [ + 'id' => $profile->permalink().'#main-key', + 'owner' => $profile->permalink(), + 'publicKeyPem' => $profile->public_key, + ], + 'icon' => [ + 'type' => 'Image', + 'mediaType' => 'image/jpeg', + 'url' => $profile->avatarUrl(), + ], + 'endpoints' => [ + 'sharedInbox' => config('app.url').'/f/inbox', + ], + ]; - if($profile->aliases->count()) { - $res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri); - } + if ($profile->status === 'delete' || $profile->deleted_at != null) { + $res['suspended'] = true; + $res['name'] = ''; + unset($res['icon']); + $res['summary'] = ''; + $res['indexable'] = false; + $res['manuallyApprovesFollowers'] = false; + } else { + if ($profile->aliases->count()) { + $res['alsoKnownAs'] = $profile->aliases->map(fn ($alias) => $alias->uri); + } - if($profile->moved_to_profile_id) { - $res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url']; - } + if ($profile->moved_to_profile_id) { + $movedTo = AccountService::get($profile->moved_to_profile_id); + if ($movedTo && isset($movedTo['url'], $movedTo['id'])) { + $res['movedTo'] = $movedTo['url']; + } + } + } - return $res; + return $res; } } diff --git a/app/Transformer/ActivityPub/Verb/DeleteActor.php b/app/Transformer/ActivityPub/Verb/DeleteActor.php new file mode 100644 index 000000000..5d3fdbc07 --- /dev/null +++ b/app/Transformer/ActivityPub/Verb/DeleteActor.php @@ -0,0 +1,24 @@ + 'https://www.w3.org/ns/activitystreams', + 'id' => $profile->permalink('#delete'), + 'type' => 'Delete', + 'actor' => $profile->permalink(), + 'to' => [ + 'https://www.w3.org/ns/activitystreams#Public' + ], + 'object' => $profile->permalink() + ]; + } + +} diff --git a/app/Util/Site/Config.php b/app/Util/Site/Config.php index e0916591d..02944defe 100644 --- a/app/Util/Site/Config.php +++ b/app/Util/Site/Config.php @@ -5,32 +5,34 @@ namespace App\Util\Site; use Cache; use Illuminate\Support\Str; -class Config { - +class Config +{ const CACHE_KEY = 'api:site:configuration:_v0.8'; - public static function get() { - return Cache::remember(self::CACHE_KEY, 900, function() { + public static function get() + { + return Cache::remember(self::CACHE_KEY, 900, function () { $hls = [ 'enabled' => config('media.hls.enabled'), ]; - if(config('media.hls.enabled')) { + if (config('media.hls.enabled')) { $hls = [ 'enabled' => true, 'debug' => (bool) config('media.hls.debug'), 'p2p' => (bool) config('media.hls.p2p'), 'p2p_debug' => (bool) config('media.hls.p2p_debug'), 'tracker' => config('media.hls.tracker'), - 'ice' => config('media.hls.ice') + 'ice' => config('media.hls.ice'), ]; } + return [ 'version' => config('pixelfed.version'), 'open_registration' => (bool) config_cache('pixelfed.open_registration'), 'uploader' => [ 'max_photo_size' => (int) config('pixelfed.max_photo_size'), - 'max_caption_length' => (int) config('pixelfed.max_caption_length'), - 'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), + 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), 'album_limit' => (int) config_cache('pixelfed.max_album_length'), 'image_quality' => (int) config_cache('pixelfed.image_quality'), @@ -41,12 +43,12 @@ class Config { 'media_types' => config_cache('pixelfed.media_types'), 'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [], - 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit') + 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), ], 'activitypub' => [ 'enabled' => (bool) config_cache('federation.activitypub.enabled'), - 'remote_follow' => config('federation.activitypub.remoteFollow') + 'remote_follow' => config('federation.activitypub.remoteFollow'), ], 'ab' => config('exp'), @@ -54,8 +56,8 @@ class Config { 'site' => [ 'name' => config_cache('app.name'), 'domain' => config('pixelfed.domain.app'), - 'url' => config('app.url'), - 'description' => config_cache('app.short_description') + 'url' => config('app.url'), + 'description' => config_cache('app.short_description'), ], 'account' => [ @@ -63,15 +65,15 @@ class Config { 'max_bio_length' => config('pixelfed.max_bio_length'), 'max_name_length' => config('pixelfed.max_name_length'), 'min_password_length' => config('pixelfed.min_password_length'), - 'max_account_size' => config('pixelfed.max_account_size') + 'max_account_size' => config('pixelfed.max_account_size'), ], 'username' => [ 'remote' => [ 'formats' => config('instance.username.remote.formats'), 'format' => config('instance.username.remote.format'), - 'custom' => config('instance.username.remote.custom') - ] + 'custom' => config('instance.username.remote.custom'), + ], ], 'features' => [ @@ -85,22 +87,29 @@ class Config { 'import' => [ 'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'), 'mastodon' => false, - 'pixelfed' => false + 'pixelfed' => false, ], 'label' => [ 'covid' => [ 'enabled' => (bool) config('instance.label.covid.enabled'), 'org' => config('instance.label.covid.org'), 'url' => config('instance.label.covid.url'), - ] + ], ], - 'hls' => $hls - ] + 'hls' => $hls, + ], ]; }); } - public static function json() { + public static function refresh() + { + Cache::forget(self::CACHE_KEY); + return self::get(); + } + + public static function json() + { return json_encode(self::get(), JSON_FORCE_OBJECT); } } diff --git a/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php b/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php new file mode 100644 index 000000000..a3d69f271 --- /dev/null +++ b/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php @@ -0,0 +1,28 @@ +string('shared_inbox')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instances', function (Blueprint $table) { + $table->dropColumn('shared_inbox'); + }); + } +}; diff --git a/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php b/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php new file mode 100644 index 000000000..ff7eebc69 --- /dev/null +++ b/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php @@ -0,0 +1,32 @@ +domain)->whereNotNull('sharedInbox')->first(); + if($si && $si->sharedInbox) { + $instance->shared_inbox = $si->sharedInbox; + $instance->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + } +}; diff --git a/resources/views/auth/curated-register/concierge_form.blade.php b/resources/views/auth/curated-register/concierge_form.blade.php index b8a1f922c..472003fd5 100644 --- a/resources/views/auth/curated-register/concierge_form.blade.php +++ b/resources/views/auth/curated-register/concierge_form.blade.php @@ -25,7 +25,7 @@

From our Admins:

-

{{ $activity->message }}

+

{{ $activity->message }}

If you don't understand this request, or need additional context you should request clarification from the admin team.

{{--
--}} diff --git a/resources/views/site/help/sharing-media.blade.php b/resources/views/site/help/sharing-media.blade.php index d429f7406..3fe275d39 100644 --- a/resources/views/site/help/sharing-media.blade.php +++ b/resources/views/site/help/sharing-media.blade.php @@ -50,7 +50,7 @@
- During the compose process, you will see the Caption input. Captions are optional and limited to {{config('pixelfed.max_caption_length')}} characters. + During the compose process, you will see the Caption input. Captions are optional and limited to {{config_cache('pixelfed.max_caption_length')}} characters.

diff --git a/routes/web-api.php b/routes/web-api.php index e19c36b6c..4b0595c66 100644 --- a/routes/web-api.php +++ b/routes/web-api.php @@ -115,7 +115,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('discover/admin/features', 'DiscoverController@updateFeatures'); }); - Route::get('discover/accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular'); + Route::get('discover/accounts/popular', 'DiscoverController@discoverAccountsPopular'); Route::post('web/change-language.json', 'SpaController@updateLanguage'); }); diff --git a/routes/web.php b/routes/web.php index 87ac4b564..6768ddb1b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,7 +29,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); - Route::get('auth/sign_up', 'CuratedRegisterController@index')->name('auth.curated-onboarding'); + Route::get('auth/sign_up', 'SiteController@curatedOnboarding')->name('auth.curated-onboarding'); Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge');