From 853a729f762b2209d438c0d2bd5f5bf94b88572a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Mar 2024 05:00:56 -0700 Subject: [PATCH] Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup --- app/Http/Controllers/ProfileController.php | 605 +++++++++++---------- 1 file changed, 317 insertions(+), 288 deletions(-) diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 26e9e5398..5354475e3 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 (! config('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')); + } }