diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 525f6114c..8f29765ee 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -7,6 +7,7 @@ use App\Models\InstanceActor; use App\Page; use App\Profile; use App\Services\AccountService; +use App\Services\AdminSettingsService; use App\Services\ConfigCacheService; use App\User; use App\Util\Site\Config; @@ -14,6 +15,8 @@ use Artisan; use Cache; use DB; use Illuminate\Http\Request; +use App\Services\Internal\BeagleService; +use App\Services\FilesystemService; trait AdminSettingsController { @@ -71,6 +74,7 @@ trait AdminSettingsController 'admin_account_id' => 'nullable', 'regs' => 'required|in:open,filtered,closed', 'account_migration' => 'nullable', + 'rule_delete' => 'sometimes' ]); $orb = false; @@ -310,4 +314,512 @@ trait AdminSettingsController return view('admin.settings.system', compact('sys')); } + + public function settingsApiFetch(Request $request) + { + $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); + $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : []; + $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); + $png = in_array('image/png', $types); + $gif = in_array('image/gif', $types); + $mp4 = in_array('video/mp4', $types); + $webp = in_array('image/webp', $types); + + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); + $accountMigration = (bool) config_cache('federation.migration'); + $autoFollow = config_cache('account.autofollow_usernames'); + if(strlen($autoFollow) > 3) { + $autoFollow = explode(',', $autoFollow); + } + + $res = AdminSettingsService::getAll(); + + return response()->json($res); + } + + public function settingsApiRulesAdd(Request $request) + { + $this->validate($request, [ + 'rule' => 'required|string|min:5|max:1000' + ]); + + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('rule'); + if (! $rules) { + ConfigCacheService::put('app.rules', json_encode([$val])); + } else { + $json = json_decode($rules, true); + $count = count($json); + if($count >= 30) { + return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400); + } + $json[] = $val; + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + return [$val]; + } + + public function settingsApiRulesDelete(Request $request) + { + $this->validate($request, [ + 'rule' => 'required|string', + ]); + + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('rule'); + + if (! $rules) { + return []; + } else { + $json = json_decode($rules, true); + $idx = array_search($val, $json); + if($idx !== false) { + unset($json[$idx]); + $json = array_values($json); + } + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return response()->json($json); + } + + public function settingsApiRulesDeleteAll(Request $request) + { + $rules = ConfigCacheService::get('app.rules'); + + if (! $rules) { + return []; + } else { + ConfigCacheService::put('app.rules', json_encode([])); + } + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return response()->json([]); + } + + public function settingsApiAutofollowDelete(Request $request) + { + $this->validate($request, [ + 'username' => 'required|string', + ]); + + $username = $request->input('username'); + $names = []; + $existing = config_cache('account.autofollow_usernames'); + if($existing) { + $names = explode(',', $existing); + } + + if(in_array($username, $names)) { + $key = array_search($username, $names); + if($key !== false) { + unset($names[$key]); + } + } + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + return response()->json(['accounts' => array_values($names)]); + } + + public function settingsApiAutofollowAdd(Request $request) + { + $this->validate($request, [ + 'username' => 'required|string', + ]); + + $username = $request->input('username'); + $names = []; + $existing = config_cache('account.autofollow_usernames'); + if($existing) { + $names = explode(',', $existing); + } + + $p = Profile::whereUsername($username)->whereNotNull('user_id')->first(); + if (! $p || in_array($p->username, $names)) { + abort(404); + } + array_push($names, strtolower($p->username)); + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + return response()->json(['accounts' => array_values($names)]); + } + + public function settingsApiUpdateType(Request $request, $type) + { + abort_unless(in_array($type, [ + 'posts', + 'platform', + 'home', + 'landing', + 'branding', + 'media', + 'users', + 'storage', + ]), 400); + + switch ($type) { + case 'home': + return $this->settingsApiUpdateHomeType($request); + break; + + case 'landing': + return $this->settingsApiUpdateLandingType($request); + break; + + case 'posts': + return $this->settingsApiUpdatePostsType($request); + break; + + case 'platform': + return $this->settingsApiUpdatePlatformType($request); + break; + + case 'branding': + return $this->settingsApiUpdateBrandingType($request); + break; + + case 'media': + return $this->settingsApiUpdateMediaType($request); + break; + + case 'users': + return $this->settingsApiUpdateUsersType($request); + break; + + case 'storage': + return $this->settingsApiUpdateStorageType($request); + break; + + default: + abort(404); + break; + } + } + + public function settingsApiUpdateHomeType($request) + { + $this->validate($request, [ + 'registration_status' => 'required|in:open,filtered,closed', + 'cloud_storage' => 'required', + 'activitypub_enabled' => 'required', + 'account_migration' => 'required', + 'mobile_apis' => 'required', + 'stories' => 'required', + 'instagram_import' => 'required', + 'autospam_enabled' => 'required', + ]); + + $regStatus = $request->input('registration_status'); + ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open'); + ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered'); + $cloudStorage = $request->boolean('cloud_storage'); + if($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) { + if(!$cloudStorage) { + ConfigCacheService::put('pixelfed.cloud_storage', false); + } else { + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + if(!$cloud_ready) { + return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']); + } else { + ConfigCacheService::put('pixelfed.cloud_storage', true); + } + } + } + ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled')); + ConfigCacheService::put('federation.migration', $request->boolean('account_migration')); + ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis')); + ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories')); + ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import')); + ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled')); + + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + return $request->all(); + } + + public function settingsApiUpdateLandingType($request) + { + $this->validate($request, [ + 'current_admin' => 'required', + 'show_directory' => 'required', + 'show_explore' => 'required', + ]); + + ConfigCacheService::put('instance.admin.pid', $request->input('current_admin')); + ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory')); + ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdateMediaType($request) + { + $this->validate($request, [ + 'image_quality' => 'required|integer|min:1|max:100', + 'max_album_length' => 'required|integer|min:1|max:20', + 'max_photo_size' => 'required|integer|min:100|max:50000', + 'media_types' => 'required', + 'optimize_image' => 'required', + 'optimize_video' => 'required', + ]); + + $mediaTypes = $request->input('media_types'); + $mediaArray = explode(',', $mediaTypes); + foreach ($mediaArray as $mediaType) { + if(!in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) { + return redirect()->back()->withErrors(['media_types' => 'Invalid media type']); + } + } + + ConfigCacheService::put('pixelfed.media_types', $request->input('media_types')); + ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality')); + ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length')); + ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size')); + ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image')); + ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdateBrandingType($request) + { + $this->validate($request, [ + 'name' => 'required', + 'short_description' => 'required', + 'long_description' => 'required', + ]); + + ConfigCacheService::put('app.name', $request->input('name')); + ConfigCacheService::put('app.short_description', $request->input('short_description')); + ConfigCacheService::put('app.description', $request->input('long_description')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdatePostsType($request) + { + $this->validate($request, [ + 'max_caption_length' => 'required|integer|min:5|max:10000', + 'max_altext_length' => 'required|integer|min:5|max:40000', + ]); + + ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length')); + ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length')); + $res = [ + 'max_caption_length' => $request->input('max_caption_length'), + 'max_altext_length' => $request->input('max_altext_length'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + return $res; + } + + public function settingsApiUpdatePlatformType($request) + { + $this->validate($request, [ + 'allow_app_registration' => 'required', + 'app_registration_rate_limit_attempts' => 'required|integer|min:1', + 'app_registration_rate_limit_decay' => 'required|integer|min:1', + 'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1', + 'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1', + 'allow_post_embeds' => 'required', + 'allow_profile_embeds' => 'required', + 'captcha_enabled' => 'required', + 'captcha_on_login' => 'required_if_accepted:captcha_enabled', + 'captcha_on_register' => 'required_if_accepted:captcha_enabled', + 'captcha_secret' => 'required_if_accepted:captcha_enabled', + 'captcha_sitekey' => 'required_if_accepted:captcha_enabled', + 'custom_emoji_enabled' => 'required', + ]); + + ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration')); + ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts')); + ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay')); + ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts')); + ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay')); + ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds')); + ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds')); + ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled')); + $captcha = $request->boolean('captcha_enabled'); + if($captcha) { + $secret = $request->input('captcha_secret'); + $sitekey = $request->input('captcha_sitekey'); + if(config_cache('captcha.secret') !== $secret && strpos('*', $secret) === false) { + ConfigCacheService::put('captcha.secret', $secret); + } + if(config_cache('captcha.sitekey') !== $sitekey && strpos('*', $sitekey) === false) { + ConfigCacheService::put('captcha.sitekey', $sitekey); + } + ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login')); + ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register')); + ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login')); + ConfigCacheService::put('captcha.enabled', true); + } else { + ConfigCacheService::put('captcha.enabled', false); + } + $res = [ + 'allow_app_registration' => $request->boolean('allow_app_registration'), + 'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'), + 'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'), + 'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'), + 'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'), + 'allow_post_embeds' => $request->boolean('allow_post_embeds'), + 'allow_profile_embeds' => $request->boolean('allow_profile_embeds'), + 'captcha_enabled' => $request->boolean('captcha_enabled'), + 'captcha_on_login' => $request->boolean('captcha_on_login'), + 'captcha_on_register' => $request->boolean('captcha_on_register'), + 'captcha_secret' => $request->input('captcha_secret'), + 'captcha_sitekey' => $request->input('captcha_sitekey'), + 'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + return $res; + } + + public function settingsApiUpdateUsersType($request) + { + $this->validate($request, [ + 'require_email_verification' => 'required', + 'enforce_account_limit' => 'required', + 'admin_autofollow' => 'required' + ]); + + ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification')); + ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit')); + ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow')); + $res = [ + 'require_email_verification' => $request->boolean('require_email_verification'), + 'enforce_account_limit' => $request->boolean('enforce_account_limit'), + 'admin_autofollow' => $request->boolean('admin_autofollow'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + return $res; + } + + public function settingsApiUpdateStorageType($request) + { + $this->validate($request, [ + 'primary_disk' => 'required|in:local,cloud', + 'update_disk' => 'sometimes', + 'disk_config' => 'required_if_accepted:update_disk', + 'disk_config.driver' => 'required|in:s3,spaces', + 'disk_config.key' => 'required', + 'disk_config.secret' => 'required', + 'disk_config.region' => 'required', + 'disk_config.bucket' => 'required', + 'disk_config.visibility' => 'required', + 'disk_config.endpoint' => 'required', + 'disk_config.url' => 'nullable', + ]); + + ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud'); + $res = [ + 'primary_disk' => $request->input('primary_disk'), + ]; + if($request->has('update_disk')) { + $res['disk_config'] = $request->input('disk_config'); + $changes = []; + $dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.'; + $key = $request->input('disk_config.key'); + $ckey = null; + $secret = $request->input('disk_config.secret'); + $csecret = null; + $region = $request->input('disk_config.region'); + $bucket = $request->input('disk_config.bucket'); + $visibility = $request->input('disk_config.visibility'); + $url = $request->input('disk_config.url'); + $endpoint = $request->input('disk_config.endpoint'); + if(strpos($key, '*') === false && $key != config_cache($dkey . 'key')) { + array_push($changes, 'key'); + } else { + $ckey = config_cache($dkey . 'key'); + } + if(strpos($secret, '*') === false && $secret != config_cache($dkey . 'secret')) { + array_push($changes, 'secret'); + } else { + $csecret = config_cache($dkey . 'secret'); + } + if($region != config_cache($dkey . 'region')) { + array_push($changes, 'region'); + } + if($bucket != config_cache($dkey . 'bucket')) { + array_push($changes, 'bucket'); + } + if($visibility != config_cache($dkey . 'visibility')) { + array_push($changes, 'visibility'); + } + if($url != config_cache($dkey . 'url')) { + array_push($changes, 'url'); + } + if($endpoint != config_cache($dkey . 'endpoint')) { + array_push($changes, 'endpoint'); + } + + if($changes && count($changes)) { + $isValid = FilesystemService::getVerifyCredentials( + $ckey ?? $key, + $csecret ?? $secret, + $region, + $bucket, + $endpoint, + ); + if(!$isValid) { + return response()->json(['error' => true, 's3_vce' => true, 'message' => "
The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.

Please check all fields and try again.

Any cloud storage configuration changes you made have NOT been saved due to invalid credentials."], 400); + } + } + $res['changes'] = json_encode($changes); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + return $res; + } } diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index 25566dcf6..4abea8b28 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -106,6 +106,24 @@ class ConfigCacheService 'instance.user_filters.max_user_blocks', 'instance.user_filters.max_user_mutes', 'instance.user_filters.max_domain_blocks', + + 'filesystems.disks.s3.key', + 'filesystems.disks.s3.secret', + 'filesystems.disks.s3.region', + 'filesystems.disks.s3.bucket', + 'filesystems.disks.s3.visibility', + 'filesystems.disks.s3.url', + 'filesystems.disks.s3.endpoint', + 'filesystems.disks.s3.use_path_style_endpoint', + + 'filesystems.disks.spaces.key', + 'filesystems.disks.spaces.secret', + 'filesystems.disks.spaces.region', + 'filesystems.disks.spaces.bucket', + 'filesystems.disks.spaces.visibility', + 'filesystems.disks.spaces.url', + 'filesystems.disks.spaces.endpoint', + 'filesystems.disks.spaces.use_path_style_endpoint', // 'system.user_mode' ]; diff --git a/app/Services/FilesystemService.php b/app/Services/FilesystemService.php new file mode 100644 index 000000000..b52f002f4 --- /dev/null +++ b/app/Services/FilesystemService.php @@ -0,0 +1,82 @@ + 'latest', + 'region' => $region, + 'endpoint' => $endpoint, + 'credentials' => [ + 'key' => $key, + 'secret' => $secret, + ] + ]); + + $adapter = new AwsS3V3Adapter( + $client, + $bucket, + ); + + $throw = false; + $filesystem = new Filesystem($adapter); + + $writable = false; + try { + $filesystem->write(self::VERIFY_FILE_NAME, 'ok', []); + $writable = true; + } catch (FilesystemException | UnableToWriteFile $exception) { + $writable = false; + } + + if(!$writable) { + return false; + } + + try { + $response = $filesystem->read(self::VERIFY_FILE_NAME); + if($response === 'ok') { + $writable = true; + $res[] = self::VERIFY_FILE_NAME; + } else { + $writable = false; + } + } catch (FilesystemException | UnableToReadFile $exception) { + $writable = false; + } + + if(in_array(self::VERIFY_FILE_NAME, $res)) { + try { + $filesystem->delete(self::VERIFY_FILE_NAME); + } catch (FilesystemException | UnableToDeleteFile $exception) { + $writable = false; + } + } + + if(!$writable) { + return false; + } + + if(in_array(self::VERIFY_FILE_NAME, $res)) { + return true; + } + + return false; + } +} diff --git a/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php b/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php index b45ad7f80..31b16de1e 100644 --- a/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php +++ b/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php @@ -19,7 +19,7 @@ return new class extends Migration public function up() { ini_set('memory_limit', '-1'); - if(config_cache('pixelfed.cloud_storage') == false) { + if((bool) config_cache('pixelfed.cloud_storage') == false) { return; }