|
@@ -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);
|
|
}
|
|
}
|
|
}
|
|
}
|