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

Loading…
Cancel
Save