From 3c712a70d6f8cbd7b74d1aef94454a1047d110c2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Mar 2023 21:22:25 -0600 Subject: [PATCH 1/7] Add Licenses help page, fixes #4238 --- resources/views/site/help/licenses.blade.php | 17 +++++++++++++++++ routes/web.php | 1 + 2 files changed, 18 insertions(+) create mode 100644 resources/views/site/help/licenses.blade.php diff --git a/resources/views/site/help/licenses.blade.php b/resources/views/site/help/licenses.blade.php new file mode 100644 index 000000000..4ae4cf310 --- /dev/null +++ b/resources/views/site/help/licenses.blade.php @@ -0,0 +1,17 @@ +@extends('site.help.partial.template', ['breadcrumb'=>'Licenses']) + +@section('section') +
+

Licenses

+
+
+ +

+ A Creative Commons license is one of several public copyright licenses that enable the free distribution of an otherwise copyrighted "work". +

+

+ A CC license is used when an author wants to give other people the right to share, use, and build upon a work that the author has created. +

+ +

For more information, please visit creativecommons.org

+@endsection diff --git a/routes/web.php b/routes/web.php index 8d028b745..bd1980b78 100644 --- a/routes/web.php +++ b/routes/web.php @@ -549,6 +549,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy'); Route::view('labs-deprecation', 'site.help.labs-deprecation')->name('help.labs-deprecation'); Route::view('tagging-people', 'site.help.tagging-people')->name('help.tagging-people'); + Route::view('licenses', 'site.help.licenses')->name('help.licenses'); }); Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); Route::get('newsroom/archive', 'NewsroomController@archive'); From 467c9d754ee156d9fc49ab105ff3e23f32ce677f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Mar 2023 21:45:35 -0600 Subject: [PATCH 2/7] Update atom feed, improve cache expiry and fix double encoding bug. Fixes #4121 --- app/Http/Controllers/ProfileController.php | 2 +- app/Jobs/StatusPipeline/StatusDelete.php | 4 +++- app/Jobs/StatusPipeline/StatusEntityLexer.php | 3 +++ resources/views/atom/user.blade.php | 6 ++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 3f6795d5b..d092d349b 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -207,7 +207,7 @@ class ProfileController extends Controller abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); - $data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 86400, function() use($pid, $profile) { + $data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 43200, function() use($pid, $profile) { $items = DB::table('statuses') ->whereProfileId($pid) ->whereVisibility('public') diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index d7f237e20..0a0721648 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -2,7 +2,7 @@ namespace App\Jobs\StatusPipeline; -use DB, Storage; +use DB, Cache, Storage; use App\{ AccountInterstitial, Bookmark, @@ -81,6 +81,8 @@ class StatusDelete implements ShouldQueue } } + Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); + if(config_cache('federation.activitypub.enabled') == true) { return $this->fanoutDelete($status); } else { diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 591a06604..1ee57c559 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -12,6 +12,7 @@ use App\Services\PublicTimelineService; use App\Util\Lexer\Autolink; use App\Util\Lexer\Extractor; use App\Util\Sentiment\Bouncer; +use Cache; use DB; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -166,6 +167,8 @@ class StatusEntityLexer implements ShouldQueue if(config_cache('pixelfed.bouncer.enabled')) { Bouncer::get($status); } + + Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); if( $status->uri == null && $status->scope == 'public' && diff --git a/resources/views/atom/user.blade.php b/resources/views/atom/user.blade.php index b6f7cb4e8..0b7595ddf 100644 --- a/resources/views/atom/user.blade.php +++ b/resources/views/atom/user.blade.php @@ -24,12 +24,14 @@ iteration}}" src="{{ $item['media_attachments'][0]['url'] }}" alt="{{ $item['media_attachments'][0]['description'] }}"> -

{{ $item['content'] }}

+ {{ $item['media_attachments'][0]['description'] }} +

{!! $item['content'] !!}

]]>
+ @if($item['content'] && strlen($item['content'])) {{ $item['content'] }} + @endif @endforeach From 186ba7f018d60c5633b9e1c1e8d7d240a7731078 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Mar 2023 21:51:28 -0600 Subject: [PATCH 3/7] Update email settings, add dangerzone middleware to prompt for password before you can change your email address. Fixes #4101 --- routes/web.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/web.php b/routes/web.php index bd1980b78..de6b2c3fe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -421,8 +421,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::delete('avatar', 'AvatarController@deleteAvatar'); Route::get('password', 'SettingsController@password')->name('settings.password')->middleware('dangerzone'); Route::post('password', 'SettingsController@passwordUpdate')->middleware('dangerzone'); - Route::get('email', 'SettingsController@email')->name('settings.email'); - Route::post('email', 'SettingsController@emailUpdate'); + Route::get('email', 'SettingsController@email')->name('settings.email')->middleware('dangerzone'); + Route::post('email', 'SettingsController@emailUpdate')->middleware('dangerzone'); Route::get('notifications', 'SettingsController@notifications')->name('settings.notifications'); Route::get('privacy', 'SettingsController@privacy')->name('settings.privacy'); Route::post('privacy', 'SettingsController@privacyStore'); From da38b33a24d522a0303d900e6622c9b78767969c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Mar 2023 22:15:56 -0600 Subject: [PATCH 4/7] Remove deprecated routes/methods --- .../Controllers/Api/BaseApiController.php | 113 ------------------ app/Http/Controllers/ApiController.php | 82 +------------ routes/web.php | 6 - 3 files changed, 1 insertion(+), 200 deletions(-) diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 9dd84aa0b..83828a9f3 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -96,89 +96,6 @@ class BaseApiController extends Controller return response()->json($res); } - public function accounts(Request $request, $id) - { - abort_if(!$request->user(), 403); - $profile = Profile::findOrFail($id); - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return response()->json($res); - } - - public function accountFollowers(Request $request, $id) - { - abort_if(!$request->user(), 403); - $profile = Profile::findOrFail($id); - $followers = $profile->followers; - $resource = new Fractal\Resource\Collection($followers, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return response()->json($res); - } - - public function accountFollowing(Request $request, $id) - { - abort_if(!$request->user(), 403); - $profile = Profile::findOrFail($id); - $following = $profile->following; - $resource = new Fractal\Resource\Collection($following, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return response()->json($res); - } - - public function accountStatuses(Request $request, $id) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'only_media' => 'nullable', - 'pinned' => 'nullable', - 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:1', - 'since_id' => 'nullable|integer|min:1', - 'min_id' => 'nullable|integer|min:1', - 'limit' => 'nullable|integer|min:1|max:24' - ]); - $limit = $request->limit ?? 20; - $max_id = $request->max_id ?? false; - $min_id = $request->min_id ?? false; - $since_id = $request->since_id ?? false; - $only_media = $request->only_media ?? false; - $user = Auth::user(); - $account = Profile::whereNull('status')->findOrFail($id); - $statuses = $account->statuses()->getQuery(); - if($only_media == true) { - $statuses = $statuses - ->whereIn('scope', ['public','unlisted']) - ->whereHas('media') - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id'); - } - if($id == $account->id && !$max_id && !$min_id && !$since_id) { - $statuses = $statuses->orderBy('id', 'desc') - ->paginate($limit); - } else if($since_id) { - $statuses = $statuses->where('id', '>', $since_id) - ->orderBy('id', 'DESC') - ->paginate($limit); - } else if($min_id) { - $statuses = $statuses->where('id', '>', $min_id) - ->orderBy('id', 'ASC') - ->paginate($limit); - } else if($max_id) { - $statuses = $statuses->where('id', '<', $max_id) - ->orderBy('id', 'DESC') - ->paginate($limit); - } else { - $statuses = $statuses->whereScope('public')->orderBy('id', 'desc')->paginate($limit); - } - $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return response()->json($res); - } - public function avatarUpdate(Request $request) { abort_if(!$request->user(), 403); @@ -215,21 +132,6 @@ class BaseApiController extends Controller ]); } - public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp) - { - abort(400, 'Endpoint deprecated'); - } - - public function uploadMedia(Request $request) - { - abort(400, 'Endpoint deprecated'); - } - - public function deleteMedia(Request $request) - { - abort(400, 'Endpoint deprecated'); - } - public function verifyCredentials(Request $request) { $user = $request->user(); @@ -242,21 +144,6 @@ class BaseApiController extends Controller return response()->json($res); } - public function drafts(Request $request) - { - $user = $request->user(); - abort_if(!$request->user(), 403); - - $medias = Media::whereUserId($user->id) - ->whereNull('status_id') - ->latest() - ->take(13) - ->get(); - $resource = new Fractal\Resource\Collection($medias, new MediaDraftTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - public function accountLikes(Request $request) { abort_if(!$request->user(), 403); diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php index 0e8313f1e..1cfc820a3 100644 --- a/app/Http/Controllers/ApiController.php +++ b/app/Http/Controllers/ApiController.php @@ -14,16 +14,9 @@ use Auth, Cache; use Illuminate\Support\Facades\Redis; use App\Util\Site\Config; use Illuminate\Http\Request; -use App\Services\SuggestionService; class ApiController extends BaseApiController { - // todo: deprecate and remove - public function hydrateLikes(Request $request) - { - return response()->json([]); - } - public function siteConfiguration(Request $request) { return response()->json(Config::get()); @@ -31,79 +24,6 @@ class ApiController extends BaseApiController public function userRecommendations(Request $request) { - abort_if(!Auth::check(), 403); - abort_if(!config('exp.rec'), 400); - - $id = Auth::user()->profile->id; - - $following = Cache::remember('profile:following:'.$id, now()->addHours(12), function() use ($id) { - return Follower::whereProfileId($id)->pluck('following_id')->toArray(); - }); - array_push($following, $id); - $ids = SuggestionService::get(); - $filters = UserFilter::whereUserId($id) - ->whereFilterableType('App\Profile') - ->whereIn('filter_type', ['mute', 'block']) - ->pluck('filterable_id')->toArray(); - $following = array_merge($following, $filters); - - $key = config('cache.prefix').':api:local:exp:rec:'.$id; - $ttl = (int) Redis::ttl($key); - - if($request->filled('refresh') == true && (290 > $ttl) == true) { - Cache::forget('api:local:exp:rec:'.$id); - } - - $res = Cache::remember('api:local:exp:rec:'.$id, now()->addMinutes(5), function() use($id, $following, $ids) { - return Profile::select( - 'id', - 'username' - ) - ->whereNotIn('id', $following) - ->whereIn('id', $ids) - ->whereIsPrivate(0) - ->whereNull('status') - ->whereNull('domain') - ->inRandomOrder() - ->take(3) - ->get() - ->map(function($item, $key) { - return [ - 'id' => $item->id, - 'avatar' => $item->avatarUrl(), - 'username' => $item->username, - 'message' => 'Recommended for You' - ]; - }); - }); - - return response()->json($res->all()); + return response()->json([]); } - - public function composeLocationSearch(Request $request) - { - abort_if(!Auth::check(), 403); - $this->validate($request, [ - 'q' => 'required|string|max:100' - ]); - $q = filter_var($request->input('q'), FILTER_SANITIZE_STRING); - $hash = hash('sha256', $q); - $key = 'search:location:id:' . $hash; - $places = Cache::remember($key, now()->addMinutes(15), function() use($q) { - $q = '%' . $q . '%'; - return Place::where('name', 'like', $q) - ->take(80) - ->get() - ->map(function($r) { - return [ - 'id' => $r->id, - 'name' => $r->name, - 'country' => $r->country, - 'url' => $r->url() - ]; - }); - }); - return $places; - } - } diff --git a/routes/web.php b/routes/web.php index de6b2c3fe..f43e2d12b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -211,9 +211,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('accounts/{id}', 'PublicApiController@account'); Route::post('avatar/update', 'ApiController@avatarUpdate'); Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis'); - Route::get('likes', 'ApiController@hydrateLikes'); - Route::post('media', 'ApiController@uploadMedia'); - Route::delete('media', 'ApiController@deleteMedia'); Route::get('notifications', 'ApiController@notifications'); Route::get('timelines/public', 'PublicApiController@publicTimelineApi'); Route::get('timelines/home', 'PublicApiController@homeTimelineApi'); @@ -277,7 +274,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('collection/{id}/publish', 'CollectionController@publish'); Route::get('profile/collections/{id}', 'CollectionController@getUserCollections'); - Route::get('compose/location/search', 'ApiController@composeLocationSearch'); Route::post('compose/tag/untagme', 'MediaTagController@untagProfile'); }); @@ -340,8 +336,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/checkpoint', 'AccountController@twoFactorCheckpoint'); Route::post('auth/checkpoint', 'AccountController@twoFactorVerify'); - Route::get('media/preview/{profileId}/{mediaId}/{timestamp}', 'ApiController@showTempMedia')->name('temp-media'); - Route::get('results', 'SearchController@results'); Route::post('visibility', 'StatusController@toggleVisibility'); From 419c0fb0fbed0427567b13706ac15e785689083e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Mar 2023 22:46:22 -0600 Subject: [PATCH 5/7] Updaet InboxPipelines, improve handling of missing signature validation headers --- app/Jobs/InboxPipeline/DeleteWorker.php | 10 + app/Jobs/InboxPipeline/InboxValidator.php | 352 +++++++++++----------- app/Jobs/InboxPipeline/InboxWorker.php | 323 ++++++++++---------- 3 files changed, 357 insertions(+), 328 deletions(-) diff --git a/app/Jobs/InboxPipeline/DeleteWorker.php b/app/Jobs/InboxPipeline/DeleteWorker.php index 05d0138e3..03a20bdd5 100644 --- a/app/Jobs/InboxPipeline/DeleteWorker.php +++ b/app/Jobs/InboxPipeline/DeleteWorker.php @@ -126,6 +126,11 @@ class DeleteWorker implements ShouldQueue return false; } $signatureData = HttpSignature::parseSignatureHeader($signature); + + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return false; + } + $keyId = Helpers::validateUrl($signatureData['keyId']); $id = Helpers::validateUrl($bodyDecoded['id']); $keyDomain = parse_url($keyId, PHP_URL_HOST); @@ -186,6 +191,11 @@ class DeleteWorker implements ShouldQueue return; } $signatureData = HttpSignature::parseSignatureHeader($signature); + + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return; + } + $keyId = Helpers::validateUrl($signatureData['keyId']); $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first(); if(!$actor) { diff --git a/app/Jobs/InboxPipeline/InboxValidator.php b/app/Jobs/InboxPipeline/InboxValidator.php index 8b4d018a4..4017d3acd 100644 --- a/app/Jobs/InboxPipeline/InboxValidator.php +++ b/app/Jobs/InboxPipeline/InboxValidator.php @@ -5,9 +5,9 @@ namespace App\Jobs\InboxPipeline; use Cache; use App\Profile; use App\Util\ActivityPub\{ - Helpers, - HttpSignature, - Inbox + Helpers, + HttpSignature, + Inbox }; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -21,189 +21,199 @@ use Illuminate\Support\Lottery; class InboxValidator implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $username; - protected $headers; - protected $payload; + protected $username; + protected $headers; + protected $payload; - public $timeout = 300; - public $tries = 1; - public $maxExceptions = 1; + public $timeout = 300; + public $tries = 1; + public $maxExceptions = 1; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct($username, $headers, $payload) - { - $this->username = $username; - $this->headers = $headers; - $this->payload = $payload; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct($username, $headers, $payload) + { + $this->username = $username; + $this->headers = $headers; + $this->payload = $payload; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $username = $this->username; - $headers = $this->headers; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $username = $this->username; + $headers = $this->headers; - if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) { - return; - } + if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) { + return; + } - $payload = json_decode($this->payload, true, 8); + $payload = json_decode($this->payload, true, 8); - if(isset($payload['id'])) { - $lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']); - if(Cache::get($lockKey) !== null) { - // Job processed already - return 1; - } - Cache::put($lockKey, 1, 3600); - } + if(isset($payload['id'])) { + $lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']); + if(Cache::get($lockKey) !== null) { + // Job processed already + return 1; + } + Cache::put($lockKey, 1, 3600); + } - $profile = Profile::whereNull('domain')->whereUsername($username)->first(); + $profile = Profile::whereNull('domain')->whereUsername($username)->first(); - if(empty($profile) || empty($headers) || empty($payload)) { - return; - } + if(empty($profile) || empty($headers) || empty($payload)) { + return; + } - if($profile->status != null) { - return; - } + if($profile->status != null) { + return; + } - if($this->verifySignature($headers, $profile, $payload) == true) { - if(isset($payload['type']) && in_array($payload['type'], ['Follow', 'Accept']) ) { - ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('follow'); - } else { - $onQueue = Lottery::odds(1, 12)->winner(fn () => 'high')->loser(fn () => 'inbox')->choose(); - ActivityHandler::dispatch($headers, $profile, $payload)->onQueue($onQueue); - } - return; - } else { - return; - } + if($this->verifySignature($headers, $profile, $payload) == true) { + if(isset($payload['type']) && in_array($payload['type'], ['Follow', 'Accept']) ) { + ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('follow'); + } else { + $onQueue = Lottery::odds(1, 12)->winner(fn () => 'high')->loser(fn () => 'inbox')->choose(); + ActivityHandler::dispatch($headers, $profile, $payload)->onQueue($onQueue); + } + return; + } else { + return; + } - } + } - protected function verifySignature($headers, $profile, $payload) - { - $body = $this->payload; - $bodyDecoded = $payload; - $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; - $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; - if(!$signature) { - return false; - } - if(!$date) { - return false; - } - if(!now()->parse($date)->gt(now()->subDays(1)) || - !now()->parse($date)->lt(now()->addDays(1)) - ) { - return false; - } - if(!isset($bodyDecoded['id'])) { - return false; - } - $signatureData = HttpSignature::parseSignatureHeader($signature); - $keyId = Helpers::validateUrl($signatureData['keyId']); - $id = Helpers::validateUrl($bodyDecoded['id']); - $keyDomain = parse_url($keyId, PHP_URL_HOST); - $idDomain = parse_url($id, PHP_URL_HOST); - if(isset($bodyDecoded['object']) - && is_array($bodyDecoded['object']) - && isset($bodyDecoded['object']['attributedTo']) - ) { - $attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']); - if(is_array($attr)) { - if(isset($attr['id'])) { - $attr = $attr['id']; - } else { - $attr = ""; - } - } - if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) { - return false; - } - } - if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) { - return false; - } - $actor = Profile::whereKeyId($keyId)->first(); - if(!$actor) { - $actorUrl = Helpers::pluckval($bodyDecoded['actor']); - $actor = Helpers::profileFirstOrNew($actorUrl); - } - if(!$actor) { - return false; - } - $pkey = openssl_pkey_get_public($actor->public_key); - if(!$pkey) { - return false; - } - $inboxPath = "/users/{$profile->username}/inbox"; - list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body); - if($verified == 1) { - return true; - } else { - return false; - } - } + protected function verifySignature($headers, $profile, $payload) + { + $body = $this->payload; + $bodyDecoded = $payload; + $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; + $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; + if(!$signature) { + return false; + } + if(!$date) { + return false; + } + if(!now()->parse($date)->gt(now()->subDays(1)) || + !now()->parse($date)->lt(now()->addDays(1)) + ) { + return false; + } + if(!isset($bodyDecoded['id'])) { + return false; + } + $signatureData = HttpSignature::parseSignatureHeader($signature); - protected function blindKeyRotation($headers, $profile, $payload) - { - $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; - $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; - if(!$signature) { - return; - } - if(!$date) { - return; - } - if(!now()->parse($date)->gt(now()->subDays(1)) || - !now()->parse($date)->lt(now()->addDays(1)) - ) { - return; - } - $signatureData = HttpSignature::parseSignatureHeader($signature); - $keyId = Helpers::validateUrl($signatureData['keyId']); - $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first(); - if(!$actor) { - return; - } - if(Helpers::validateUrl($actor->remote_url) == false) { - return; - } + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return false; + } - try { - $res = Http::timeout(20)->withHeaders([ - 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', - ])->get($actor->remote_url); - } catch (ConnectionException $e) { - return false; - } + $keyId = Helpers::validateUrl($signatureData['keyId']); + $id = Helpers::validateUrl($bodyDecoded['id']); + $keyDomain = parse_url($keyId, PHP_URL_HOST); + $idDomain = parse_url($id, PHP_URL_HOST); + if(isset($bodyDecoded['object']) + && is_array($bodyDecoded['object']) + && isset($bodyDecoded['object']['attributedTo']) + ) { + $attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']); + if(is_array($attr)) { + if(isset($attr['id'])) { + $attr = $attr['id']; + } else { + $attr = ""; + } + } + if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) { + return false; + } + } + if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) { + return false; + } + $actor = Profile::whereKeyId($keyId)->first(); + if(!$actor) { + $actorUrl = Helpers::pluckval($bodyDecoded['actor']); + $actor = Helpers::profileFirstOrNew($actorUrl); + } + if(!$actor) { + return false; + } + $pkey = openssl_pkey_get_public($actor->public_key); + if(!$pkey) { + return false; + } + $inboxPath = "/users/{$profile->username}/inbox"; + list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body); + if($verified == 1) { + return true; + } else { + return false; + } + } - if(!$res->ok()) { - return false; - } + protected function blindKeyRotation($headers, $profile, $payload) + { + $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; + $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; + if(!$signature) { + return; + } + if(!$date) { + return; + } + if(!now()->parse($date)->gt(now()->subDays(1)) || + !now()->parse($date)->lt(now()->addDays(1)) + ) { + return; + } + $signatureData = HttpSignature::parseSignatureHeader($signature); - $res = json_decode($res->body(), true, 8); - if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) { - return; - } - if($res['publicKey']['id'] !== $actor->key_id) { - return; - } - $actor->public_key = $res['publicKey']['publicKeyPem']; - $actor->save(); - return $this->verifySignature($headers, $profile, $payload); - } + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return; + } + + $keyId = Helpers::validateUrl($signatureData['keyId']); + $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first(); + if(!$actor) { + return; + } + if(Helpers::validateUrl($actor->remote_url) == false) { + return; + } + + try { + $res = Http::timeout(20)->withHeaders([ + 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', + ])->get($actor->remote_url); + } catch (ConnectionException $e) { + return false; + } + + if(!$res->ok()) { + return false; + } + + $res = json_decode($res->body(), true, 8); + if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) { + return; + } + if($res['publicKey']['id'] !== $actor->key_id) { + return; + } + $actor->public_key = $res['publicKey']['publicKeyPem']; + $actor->save(); + return $this->verifySignature($headers, $profile, $payload); + } } diff --git a/app/Jobs/InboxPipeline/InboxWorker.php b/app/Jobs/InboxPipeline/InboxWorker.php index 305430085..c8508c0fc 100644 --- a/app/Jobs/InboxPipeline/InboxWorker.php +++ b/app/Jobs/InboxPipeline/InboxWorker.php @@ -5,9 +5,9 @@ namespace App\Jobs\InboxPipeline; use Cache; use App\Profile; use App\Util\ActivityPub\{ - Helpers, - HttpSignature, - Inbox + Helpers, + HttpSignature, + Inbox }; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -20,171 +20,180 @@ use Illuminate\Http\Client\ConnectionException; class InboxWorker implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $headers; - protected $payload; + protected $headers; + protected $payload; - public $timeout = 300; - public $tries = 1; - public $maxExceptions = 1; + public $timeout = 300; + public $tries = 1; + public $maxExceptions = 1; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct($headers, $payload) - { - $this->headers = $headers; - $this->payload = $payload; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct($headers, $payload) + { + $this->headers = $headers; + $this->payload = $payload; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $profile = null; - $headers = $this->headers; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = null; + $headers = $this->headers; - if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) { - return; - } + if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) { + return; + } - $payload = json_decode($this->payload, true, 8); + $payload = json_decode($this->payload, true, 8); - if(isset($payload['id'])) { - $lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']); - if(Cache::get($lockKey) !== null) { - // Job processed already - return 1; - } - Cache::put($lockKey, 1, 3600); - } + if(isset($payload['id'])) { + $lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']); + if(Cache::get($lockKey) !== null) { + // Job processed already + return 1; + } + Cache::put($lockKey, 1, 3600); + } - if($this->verifySignature($headers, $payload) == true) { - ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('shared'); - return; - } else { - return; - } - } + if($this->verifySignature($headers, $payload) == true) { + ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('shared'); + return; + } else { + return; + } + } - protected function verifySignature($headers, $payload) - { - $body = $this->payload; - $bodyDecoded = $payload; - $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; - $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; - if(!$signature) { - return false; - } - if(!$date) { - return false; - } - if(!now()->parse($date)->gt(now()->subDays(1)) || - !now()->parse($date)->lt(now()->addDays(1)) - ) { - return false; - } - if(!isset($bodyDecoded['id'])) { - return false; - } - $signatureData = HttpSignature::parseSignatureHeader($signature); - $keyId = Helpers::validateUrl($signatureData['keyId']); - $id = Helpers::validateUrl($bodyDecoded['id']); - $keyDomain = parse_url($keyId, PHP_URL_HOST); - $idDomain = parse_url($id, PHP_URL_HOST); - if(isset($bodyDecoded['object']) - && is_array($bodyDecoded['object']) - && isset($bodyDecoded['object']['attributedTo']) - ) { - $attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']); - if(is_array($attr)) { - if(isset($attr['id'])) { - $attr = $attr['id']; - } else { - $attr = ""; - } - } - if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) { - return false; - } - } - if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) { - return false; - } - $actor = Profile::whereKeyId($keyId)->first(); - if(!$actor) { - $actorUrl = Helpers::pluckval($bodyDecoded['actor']); - $actor = Helpers::profileFirstOrNew($actorUrl); - } - if(!$actor) { - return false; - } - $pkey = openssl_pkey_get_public($actor->public_key); - if(!$pkey) { - return false; - } - $inboxPath = "/f/inbox"; - list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body); - if($verified == 1) { - return true; - } else { - return false; - } - } + protected function verifySignature($headers, $payload) + { + $body = $this->payload; + $bodyDecoded = $payload; + $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; + $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; + if(!$signature) { + return false; + } + if(!$date) { + return false; + } + if(!now()->parse($date)->gt(now()->subDays(1)) || + !now()->parse($date)->lt(now()->addDays(1)) + ) { + return false; + } + if(!isset($bodyDecoded['id'])) { + return false; + } + $signatureData = HttpSignature::parseSignatureHeader($signature); - protected function blindKeyRotation($headers, $payload) - { - $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; - $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; - if(!$signature) { - return; - } - if(!$date) { - return; - } - if(!now()->parse($date)->gt(now()->subDays(1)) || - !now()->parse($date)->lt(now()->addDays(1)) - ) { - return; - } - $signatureData = HttpSignature::parseSignatureHeader($signature); - $keyId = Helpers::validateUrl($signatureData['keyId']); - $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first(); - if(!$actor) { - return; - } - if(Helpers::validateUrl($actor->remote_url) == false) { - return; - } + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return false; + } - try { - $res = Http::timeout(20)->withHeaders([ - 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', - ])->get($actor->remote_url); - } catch (ConnectionException $e) { - return false; - } + $keyId = Helpers::validateUrl($signatureData['keyId']); + $id = Helpers::validateUrl($bodyDecoded['id']); + $keyDomain = parse_url($keyId, PHP_URL_HOST); + $idDomain = parse_url($id, PHP_URL_HOST); + if(isset($bodyDecoded['object']) + && is_array($bodyDecoded['object']) + && isset($bodyDecoded['object']['attributedTo']) + ) { + $attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']); + if(is_array($attr)) { + if(isset($attr['id'])) { + $attr = $attr['id']; + } else { + $attr = ""; + } + } + if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) { + return false; + } + } + if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) { + return false; + } + $actor = Profile::whereKeyId($keyId)->first(); + if(!$actor) { + $actorUrl = Helpers::pluckval($bodyDecoded['actor']); + $actor = Helpers::profileFirstOrNew($actorUrl); + } + if(!$actor) { + return false; + } + $pkey = openssl_pkey_get_public($actor->public_key); + if(!$pkey) { + return false; + } + $inboxPath = "/f/inbox"; + list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body); + if($verified == 1) { + return true; + } else { + return false; + } + } - if(!$res->ok()) { - return false; - } + protected function blindKeyRotation($headers, $payload) + { + $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature']; + $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date']; + if(!$signature) { + return; + } + if(!$date) { + return; + } + if(!now()->parse($date)->gt(now()->subDays(1)) || + !now()->parse($date)->lt(now()->addDays(1)) + ) { + return; + } + $signatureData = HttpSignature::parseSignatureHeader($signature); + if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) { + return; + } - $res = json_decode($res->body(), true, 8); - if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) { - return; - } - if($res['publicKey']['id'] !== $actor->key_id) { - return; - } - $actor->public_key = $res['publicKey']['publicKeyPem']; - $actor->save(); - return $this->verifySignature($headers, $payload); - } + $keyId = Helpers::validateUrl($signatureData['keyId']); + $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first(); + if(!$actor) { + return; + } + if(Helpers::validateUrl($actor->remote_url) == false) { + return; + } + + try { + $res = Http::timeout(20)->withHeaders([ + 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', + ])->get($actor->remote_url); + } catch (ConnectionException $e) { + return false; + } + + if(!$res->ok()) { + return false; + } + + $res = json_decode($res->body(), true, 8); + if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) { + return; + } + if($res['publicKey']['id'] !== $actor->key_id) { + return; + } + $actor->public_key = $res['publicKey']['publicKeyPem']; + $actor->save(); + return $this->verifySignature($headers, $payload); + } } From ecfc0766f81908eb9e7088a14e4e2d7350744447 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 19 Mar 2023 05:29:54 -0600 Subject: [PATCH 6/7] Update admin instances dashboard --- .../Admin/AdminInstanceController.php | 199 ++++-- app/Http/Resources/AdminInstance.php | 3 + app/Instance.php | 2 +- app/Services/InstanceService.php | 14 + ...19_050342_add_notes_to_instances_table.php | 48 ++ .../components/admin/AdminInstances.vue | 628 ++++++++++++++++++ resources/assets/js/admin.js | 5 + .../views/admin/instances/home.blade.php | 211 +----- routes/web.php | 7 + 9 files changed, 852 insertions(+), 265 deletions(-) create mode 100644 database/migrations/2023_03_19_050342_add_notes_to_instances_table.php create mode 100644 resources/assets/components/admin/AdminInstances.vue diff --git a/app/Http/Controllers/Admin/AdminInstanceController.php b/app/Http/Controllers/Admin/AdminInstanceController.php index b24592c31..6a17fdb2a 100644 --- a/app/Http/Controllers/Admin/AdminInstanceController.php +++ b/app/Http/Controllers/Admin/AdminInstanceController.php @@ -8,66 +8,13 @@ use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Validation\Rule; use App\Services\InstanceService; +use App\Http\Resources\AdminInstance; trait AdminInstanceController { - public function instances(Request $request) { - $this->validate($request, [ - - 'filter' => [ - 'nullable', - 'string', - 'min:1', - 'max:20', - Rule::in([ - 'cw', - 'unlisted', - 'banned', - // 'popular', - 'new', - 'all' - ]) - ], - ]); - if($request->has('q') && $request->filled('q')) { - $instances = Instance::where('domain', 'like', '%' . $request->input('q') . '%')->simplePaginate(10); - } else if($request->has('filter') && $request->filled('filter')) { - switch ($request->filter) { - case 'cw': - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereAutoCw(true)->orderByDesc('id')->simplePaginate(10); - break; - case 'unlisted': - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereUnlisted(true)->orderByDesc('id')->simplePaginate(10); - break; - case 'banned': - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereBanned(true)->orderByDesc('id')->simplePaginate(10); - break; - case 'new': - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->latest()->simplePaginate(10); - break; - // case 'popular': - // $popular = Profile::selectRaw('*, count(domain) as count') - // ->whereNotNull('domain') - // ->groupBy('domain') - // ->orderByDesc('count') - // ->take(10) - // ->get() - // ->pluck('domain') - // ->toArray(); - // $instances = Instance::whereIn('domain', $popular)->simplePaginate(10); - // break; - - default: - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10); - break; - } - } else { - $instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10); - } - - return view('admin.instances.home', compact('instances')); + return view('admin.instances.home'); } public function instanceScan(Request $request) @@ -133,4 +80,146 @@ trait AdminInstanceController return response()->json([]); } + + public function getInstancesStatsApi(Request $request) + { + return InstanceService::stats(); + } + + public function getInstancesQueryApi(Request $request) + { + $this->validate($request, [ + 'q' => 'required' + ]); + + $q = $request->input('q'); + + return AdminInstance::collection( + Instance::where('domain', 'like', '%' . $q . '%') + ->orderByDesc('user_count') + ->cursorPaginate(20) + ->withQueryString() + ); + } + + public function getInstancesApi(Request $request) + { + $this->validate($request, [ + 'filter' => [ + 'nullable', + 'string', + 'min:1', + 'max:20', + Rule::in([ + 'cw', + 'unlisted', + 'banned', + 'popular_users', + 'popular_statuses', + 'new', + 'all' + ]) + ], + ]); + $filter = $request->input('filter'); + $query = $request->input('q'); + + return AdminInstance::collection(Instance::when($query, function($q, $qq) use($query) { + return $q->where('domain', 'like', '%' . $query . '%'); + }) + ->when($filter, function($q, $f) use($filter) { + if($filter == 'cw') { return $q->whereAutoCw(true)->orderByDesc('id'); } + if($filter == 'unlisted') { return $q->whereUnlisted(true)->orderByDesc('id'); } + if($filter == 'banned') { return $q->whereBanned(true)->orderByDesc('id'); } + if($filter == 'new') { return $q->orderByDesc('id'); } + if($filter == 'popular_users') { return $q->orderByDesc('user_count'); } + if($filter == 'popular_statuses') { return $q->orderByDesc('status_count'); } + return $q->orderByDesc('id'); + }, function($q) { + return $q->orderByDesc('id'); + }) + ->cursorPaginate(10) + ->withQueryString()); + } + + public function postInstanceUpdateApi(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + 'banned' => 'boolean', + 'auto_cw' => 'boolean', + 'unlisted' => 'boolean', + 'notes' => 'nullable|string|max:500', + ]); + + $id = $request->input('id'); + $instance = Instance::findOrFail($id); + $instance->update($request->only([ + 'banned', + 'auto_cw', + 'unlisted', + 'notes' + ])); + + InstanceService::refresh(); + + return new AdminInstance($instance); + } + + public function postInstanceCreateNewApi(Request $request) + { + $this->validate($request, [ + 'domain' => 'required|string', + 'banned' => 'boolean', + 'auto_cw' => 'boolean', + 'unlisted' => 'boolean', + 'notes' => 'nullable|string|max:500' + ]); + + $domain = $request->input('domain'); + + abort_if(!strpos($domain, '.'), 400, 'Invalid domain'); + abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain'); + + $instance = new Instance; + $instance->domain = $request->input('domain'); + $instance->banned = $request->input('banned'); + $instance->auto_cw = $request->input('auto_cw'); + $instance->unlisted = $request->input('unlisted'); + $instance->manually_added = true; + $instance->notes = $request->input('notes'); + $instance->save(); + + InstanceService::refresh(); + + return new AdminInstance($instance); + } + + public function postInstanceRefreshStatsApi(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + $instance = Instance::findOrFail($request->input('id')); + $instance->user_count = Profile::whereDomain($instance->domain)->count(); + $instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count(); + $instance->save(); + + return new AdminInstance($instance); + } + + public function postInstanceDeleteApi(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + $instance = Instance::findOrFail($request->input('id')); + $instance->delete(); + + InstanceService::refresh(); + + return 200; + } } diff --git a/app/Http/Resources/AdminInstance.php b/app/Http/Resources/AdminInstance.php index 4a52e3481..d534f4681 100644 --- a/app/Http/Resources/AdminInstance.php +++ b/app/Http/Resources/AdminInstance.php @@ -24,6 +24,9 @@ class AdminInstance extends JsonResource 'user_count' => $this->user_count, 'status_count' => $this->status_count, 'last_crawled_at' => $this->last_crawled_at, + 'notes' => $this->notes, + 'base_domain' => $this->base_domain, + 'ban_subdomains' => $this->ban_subdomains, 'actors_last_synced_at' => $this->actors_last_synced_at, 'created_at' => $this->created_at, ]; diff --git a/app/Instance.php b/app/Instance.php index 5541d86c9..6a7b8e6f2 100644 --- a/app/Instance.php +++ b/app/Instance.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class Instance extends Model { - protected $fillable = ['domain']; + protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes']; public function profiles() { diff --git a/app/Services/InstanceService.php b/app/Services/InstanceService.php index b53e8ac00..f044f1b27 100644 --- a/app/Services/InstanceService.php +++ b/app/Services/InstanceService.php @@ -11,6 +11,7 @@ class InstanceService const CACHE_KEY_BANNED_DOMAINS = 'instances:banned:domains'; const CACHE_KEY_UNLISTED_DOMAINS = 'instances:unlisted:domains'; const CACHE_KEY_NSFW_DOMAINS = 'instances:auto_cw:domains'; + const CACHE_KEY_STATS = 'pf:services:instances:stats'; public static function getByDomain($domain) { @@ -52,11 +53,24 @@ class InstanceService }); } + public static function stats() + { + return Cache::remember(self::CACHE_KEY_STATS, 86400, function() { + return [ + 'total_count' => Instance::count(), + 'new_count' => Instance::where('created_at', '>', now()->subDays(14))->count(), + 'banned_count' => Instance::whereBanned(true)->count(), + 'nsfw_count' => Instance::whereAutoCw(true)->count() + ]; + }); + } + public static function refresh() { Cache::forget(self::CACHE_KEY_BANNED_DOMAINS); Cache::forget(self::CACHE_KEY_UNLISTED_DOMAINS); Cache::forget(self::CACHE_KEY_NSFW_DOMAINS); + Cache::forget(self::CACHE_KEY_STATS); self::getBannedDomains(); self::getUnlistedDomains(); diff --git a/database/migrations/2023_03_19_050342_add_notes_to_instances_table.php b/database/migrations/2023_03_19_050342_add_notes_to_instances_table.php new file mode 100644 index 000000000..b259c2e5f --- /dev/null +++ b/database/migrations/2023_03_19_050342_add_notes_to_instances_table.php @@ -0,0 +1,48 @@ +text('notes')->nullable(); + $table->boolean('manually_added')->default(false); + $table->string('base_domain')->nullable(); + $table->boolean('ban_subdomains')->nullable()->index(); + $table->string('ip_address')->nullable(); + $table->boolean('list_limitation')->default(false)->index(); + $table->index('banned'); + $table->index('auto_cw'); + $table->index('unlisted'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('instances', function (Blueprint $table) { + $table->dropColumn('notes'); + $table->dropColumn('manually_added'); + $table->dropColumn('base_domain'); + $table->dropColumn('ban_subdomains'); + $table->dropColumn('ip_address'); + $table->dropColumn('list_limitation'); + $table->dropIndex('instances_banned_index'); + $table->dropIndex('instances_auto_cw_index'); + $table->dropIndex('instances_unlisted_index'); + }); + } +}; diff --git a/resources/assets/components/admin/AdminInstances.vue b/resources/assets/components/admin/AdminInstances.vue new file mode 100644 index 000000000..67bfe9fe6 --- /dev/null +++ b/resources/assets/components/admin/AdminInstances.vue @@ -0,0 +1,628 @@ + + + + + diff --git a/resources/assets/js/admin.js b/resources/assets/js/admin.js index 3e6787403..e38f3e98c 100644 --- a/resources/assets/js/admin.js +++ b/resources/assets/js/admin.js @@ -26,6 +26,11 @@ Vue.component( require('./../components/admin/AdminDirectory.vue').default ); +Vue.component( + 'instances-component', + require('./../components/admin/AdminInstances.vue').default +); + Vue.component( 'hashtag-component', require('./../components/admin/AdminHashtags.vue').default diff --git a/resources/views/admin/instances/home.blade.php b/resources/views/admin/instances/home.blade.php index c8b123da3..373f28fc9 100644 --- a/resources/views/admin/instances/home.blade.php +++ b/resources/views/admin/instances/home.blade.php @@ -1,219 +1,12 @@ @extends('admin.partial.template-full') @section('section') -
-

Instances

-
- All - {{-- Popular --}} - New - CW - Banned - Unlisted -
-
-
-
- -
-
- -
- -
-
- @if($instances->count() == 0 && !request()->has('filter') && !request()->has('q')) -
-

Warning

-

No instances were found.

-
-

Do you want to scan and populate instances from Profiles and Statuses?

-

-

- @csrf - -
-

- @else -
    - @foreach($instances as $instance) -
  • -
    -
    -

    - {{$instance->domain}} -

    -

    - @if($instance->unlisted) - - @endif - @if($instance->auto_cw) - - @endif - @if($instance->banned) - - @endif - Overview - -

    -
    -
    -
  • - @endforeach -
-
- {{$instances->links()}} -
- @endif - - @if(request()->filled('q') && $instances->count() == 0) -

No results found

-

Go back

- @endif - @if(request()->filled('filter') && $instances->count() == 0) -

No results found

-

Go back

- @endif -
+ @endsection @push('scripts') - @endpush diff --git a/routes/web.php b/routes/web.php index f43e2d12b..f83a75788 100644 --- a/routes/web.php +++ b/routes/web.php @@ -113,6 +113,13 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('hashtags/get', 'AdminController@hashtagsGet'); Route::post('hashtags/update', 'AdminController@hashtagsUpdate'); Route::post('hashtags/clear-trending-cache', 'AdminController@hashtagsClearTrendingCache'); + Route::get('instances/get', 'AdminController@getInstancesApi'); + Route::get('instances/stats', 'AdminController@getInstancesStatsApi'); + Route::get('instances/query', 'AdminController@getInstancesQueryApi'); + Route::post('instances/update', 'AdminController@postInstanceUpdateApi'); + Route::post('instances/create', 'AdminController@postInstanceCreateNewApi'); + Route::post('instances/delete', 'AdminController@postInstanceDeleteApi'); + Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi'); }); }); From 00aa5cf30f4f33aab31718455aeddec9293d9bd3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 19 Mar 2023 05:30:58 -0600 Subject: [PATCH 7/7] Update compiled assets --- public/js/admin.js | Bin 92094 -> 114136 bytes public/mix-manifest.json | Bin 5901 -> 5901 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 2940baed0db2004de0017ea5329bc86984193a17..f2cd2496cdc85aa6df3065133a7b33afb26abf19 100644 GIT binary patch delta 12612 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 H@~!^?>!mYF delta 54 zcmV-60LlN@`v$(x1+Y9Wvw=u5(YJR|0sI2DsZ;^QPq!bX0aOvUAGZPD0k{6S0X6}b M&$|KRmzBK%L$Y`na{vGU diff --git a/public/mix-manifest.json b/public/mix-manifest.json index ff5a5e18737fe2f821d3e5baa5b9fa1cf9af0125..bc1ce547d1f79c5fc614528160bae8984a5cd996 100644 GIT binary patch delta 44 zcmeCx>($%P%qnl3n3QZ}VQgw{YG#_6VwP-XX_Ay^l$>m2YGGtxV6u4;YqlT&DYXpC delta 44 zcmeCx>($%P%qpK^o@|n4kYZq*YG7(^U}2t=WSnYZX=-MkWM-P0Y`%FBYqlT&EA$Nt