Update CustomFilterController, improve case-insentive handling, mastoAPI compatibility and custom config limits

pull/5928/head
Daniel Supernault 5 months ago
parent a16a4ddbd0
commit c4a96da019
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -8,9 +8,13 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class CustomFilterController extends Controller
{
// const ACTIVE_TYPES = ['home', 'public', 'tags', 'notifications', 'thread', 'profile', 'groups'];
const ACTIVE_TYPES = ['home', 'public', 'tags'];
public function index(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
@ -91,15 +95,18 @@ class CustomFilterController extends Controller
$validatedData = $request->validate([
'title' => 'required|string|max:100',
'context' => 'required|array',
'context.*' => 'string|in:home,notifications,public,thread,account,tags,groups',
'context.*' => [
'string',
Rule::in(self::ACTIVE_TYPES),
],
'filter_action' => 'string|in:warn,hide,blur',
'expires_in' => 'nullable|integer|min:0|max:63072000',
'keywords_attributes' => 'required|array|min:1|max:'.CustomFilter::MAX_KEYWORDS_PER_FILTER,
'keywords_attributes' => 'required|array|min:1|max:'.CustomFilter::getMaxKeywordsPerFilter(),
'keywords_attributes.*.keyword' => [
'required',
'string',
'min:1',
'max:'.CustomFilter::MAX_KEYWORD_LEN,
'max:'.CustomFilter::getMaxKeywordLength(),
'regex:/^[\p{L}\p{N}\p{Zs}\p{P}\p{M}]+$/u',
function ($attribute, $value, $fail) {
if (preg_match('/(.)\1{20,}/', $value)) {
@ -109,12 +116,22 @@ class CustomFilterController extends Controller
],
'keywords_attributes.*.whole_word' => 'boolean',
]);
$profile_id = $request->user()->profile_id;
$userFilterCount = CustomFilter::where('profile_id', $profile_id)->count();
$maxFiltersPerUser = CustomFilter::getMaxFiltersPerUser();
if (! $request->user()->is_admin && $userFilterCount >= $maxFiltersPerUser) {
return response()->json([
'error' => 'Filter limit exceeded',
'message' => 'You can only have '.$maxFiltersPerUser.' filters at a time.',
], 422);
}
$rateKey = 'filters_created:'.$request->user()->id;
$maxFiltersPerHour = CustomFilter::MAX_PER_HOUR;
$maxFiltersPerHour = CustomFilter::getMaxCreatePerHour();
$currentCount = Cache::get($rateKey, 0);
if ($currentCount >= $maxFiltersPerHour) {
if (! $request->user()->is_admin && $currentCount >= $maxFiltersPerHour) {
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'You can only create '.$maxFiltersPerHour.' filters per hour.',
@ -124,10 +141,9 @@ class CustomFilterController extends Controller
DB::beginTransaction();
try {
$profile_id = $request->user()->profile_id;
$requestedKeywords = array_map(function ($item) {
return $item['keyword'];
return mb_strtolower(trim($item['keyword']));
}, $validatedData['keywords_attributes']);
$existingKeywords = DB::table('custom_filter_keywords')
@ -144,16 +160,6 @@ class CustomFilterController extends Controller
], 422);
}
$userFilterCount = CustomFilter::where('profile_id', $profile_id)->count();
$maxFiltersPerUser = CustomFilter::MAX_LIMIT;
if ($userFilterCount >= $maxFiltersPerUser) {
return response()->json([
'error' => 'Filter limit exceeded',
'message' => 'You can only have '.$maxFiltersPerUser.' filters at a time.',
], 422);
}
$expiresAt = null;
if (isset($validatedData['expires_in']) && $validatedData['expires_in'] > 0) {
$expiresAt = now()->addSeconds($validatedData['expires_in']);
@ -253,22 +259,42 @@ class CustomFilterController extends Controller
abort_unless($request->user()->tokenCan('write'), 403);
$filter = CustomFilter::findOrFail($id);
$pid = $request->user()->profile_id;
if ($filter->profile_id !== $pid) {
return response()->json(['error' => 'This action is unauthorized'], 401);
}
Gate::authorize('update', $filter);
$validatedData = $request->validate([
'title' => 'string|max:100',
'context' => 'array|max:10',
'context.*' => 'string|in:home,notifications,public,thread,account,tags,groups',
'context.*' => [
'string',
Rule::in(self::ACTIVE_TYPES),
],
'filter_action' => 'string|in:warn,hide,blur',
'expires_in' => 'nullable|integer|min:0|max:63072000',
'keywords_attributes' => 'required|array|min:1|max:'.CustomFilter::MAX_KEYWORDS_PER_FILTER,
'keywords_attributes' => [
'required',
'array',
'min:1',
function ($attribute, $value, $fail) {
$activeKeywords = collect($value)->filter(function ($keyword) {
return ! isset($keyword['_destroy']) || $keyword['_destroy'] !== true;
})->count();
if ($activeKeywords > CustomFilter::getMaxKeywordsPerFilter()) {
$fail('You may not have more than '.CustomFilter::getMaxKeywordsPerFilter().' active keywords.');
}
},
],
'keywords_attributes.*.id' => 'nullable|integer|exists:custom_filter_keywords,id',
'keywords_attributes.*.keyword' => [
'required_without:keywords_attributes.*.id',
'string',
'min:1',
'max:'.CustomFilter::MAX_KEYWORD_LEN,
'max:'.CustomFilter::getMaxKeywordLength(),
'regex:/^[\p{L}\p{N}\p{Zs}\p{P}\p{M}]+$/u',
function ($attribute, $value, $fail) {
if (preg_match('/(.)\1{20,}/', $value)) {
@ -281,10 +307,10 @@ class CustomFilterController extends Controller
]);
$rateKey = 'filters_updated:'.$request->user()->id;
$maxUpdatesPerHour = CustomFilter::MAX_UPDATES_PER_HOUR;
$maxUpdatesPerHour = CustomFilter::getMaxUpdatesPerHour();
$currentCount = Cache::get($rateKey, 0);
if ($currentCount >= $maxUpdatesPerHour) {
if (! $request->user()->is_admin && $currentCount >= $maxUpdatesPerHour) {
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'You can only update filters '.$maxUpdatesPerHour.' times per hour.',
@ -294,12 +320,18 @@ class CustomFilterController extends Controller
DB::beginTransaction();
try {
$pid = $request->user()->profile_id;
$keywordIds = collect($validatedData['keywords_attributes'])->pluck('id')->filter()->toArray();
if (count($keywordIds) && ! CustomFilterKeyword::whereCustomFilterId($filter->id)->whereIn('id', $keywordIds)->count()) {
return response()->json([
'error' => 'Record not found',
], 404);
}
$requestedKeywords = [];
foreach ($validatedData['keywords_attributes'] as $item) {
if (isset($item['keyword']) && (! isset($item['_destroy']) || ! $item['_destroy'])) {
$requestedKeywords[] = $item['keyword'];
$requestedKeywords[] = mb_strtolower(trim($item['keyword']));
}
}
@ -307,8 +339,8 @@ class CustomFilterController extends Controller
$existingKeywords = DB::table('custom_filter_keywords')
->join('custom_filters', 'custom_filter_keywords.custom_filter_id', '=', 'custom_filters.id')
->where('custom_filters.profile_id', $pid)
->where('custom_filter_keywords.custom_filter_id', '!=', $id)
->whereIn('custom_filter_keywords.keyword', $requestedKeywords)
->where('custom_filter_keywords.custom_filter_id', '!=', $id)
->pluck('custom_filter_keywords.keyword')
->toArray();
@ -349,7 +381,7 @@ class CustomFilterController extends Controller
foreach ($validatedData['keywords_attributes'] as $keywordData) {
// Case 1: Explicit deletion with _destroy flag
if (isset($keywordData['id']) && isset($keywordData['_destroy']) && $keywordData['_destroy']) {
if (isset($keywordData['id']) && isset($keywordData['_destroy']) && (bool) $keywordData['_destroy']) {
// Verify this ID belongs to this filter before deletion
$kwf = CustomFilterKeyword::where('custom_filter_id', $filter->id)
->where('id', $keywordData['id'])
@ -372,6 +404,13 @@ class CustomFilterController extends Controller
->where('id', $keywordData['id'])
->first();
if (! isset($keywordData['_destroy']) && $filter->keywords()->pluck('id')->search($keywordData['id']) === false) {
return response()->json([
'error' => 'Duplicate keywords found',
'message' => 'The following keywords already exist: '.$keywordData['keyword'],
], 422);
}
if ($keyword) {
$updateData = [];
@ -394,7 +433,7 @@ class CustomFilterController extends Controller
elseif (isset($keywordData['keyword'])) {
// Check if we're about to exceed the keyword limit
$existingKeywordCount = $filter->keywords()->count();
$maxKeywordsPerFilter = CustomFilter::MAX_KEYWORDS_PER_FILTER;
$maxKeywordsPerFilter = CustomFilter::getMaxKeywordsPerFilter();
if ($existingKeywordCount >= $maxKeywordsPerFilter) {
return response()->json([
@ -403,6 +442,11 @@ class CustomFilterController extends Controller
], 422);
}
// Skip existing case-insensitive keywords
if ($filter->keywords()->pluck('keyword')->search(mb_strtolower(trim($keywordData['keyword']))) !== false) {
continue;
}
$filter->keywords()->create([
'keyword' => trim($keywordData['keyword']),
'whole_word' => (bool) ($keywordData['whole_word'] ?? true),
@ -416,7 +460,7 @@ class CustomFilterController extends Controller
Cache::put($rateKey, 1, 3600);
}
Cache::forget("filters:v3:{$request->user()->profile_id}");
Cache::forget("filters:v3:{$pid}");
DB::commit();
@ -464,6 +508,6 @@ class CustomFilterController extends Controller
Gate::authorize('delete', $filter);
$filter->delete();
return response()->json([], 200);
return response()->json((object) [], 200);
}
}

Loading…
Cancel
Save