diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 744c52a6..ea05c076 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Http\Requests\Comment\StoreCommentRequest; +use App\Http\Requests\StoreCommentRequest; use App\Http\Resources\CommentResource; use App\Http\Resources\UserResource; use App\Services\CommentService; diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php index 1cfdbba8..935c6ddf 100644 --- a/app/Http/Controllers/ComputerScienceResourceController.php +++ b/app/Http/Controllers/ComputerScienceResourceController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers; use App\Events\TagFrequencyChanged; -use App\Http\Requests\ComputerScienceResource\StoreResourceRequest; +use App\Http\Requests\StoreResourceRequest; use App\Models\ComputerScienceResource; use App\Models\NewsPost; use App\Models\ResourceEdits; diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 43b70f18..363b26bd 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers; use App\Events\TagFrequencyChanged; -use App\Http\Requests\ResourceEdit\StoreResourceEdit; +use App\Http\Requests\StoreResourceEdit; use App\Models\ComputerScienceResource; use App\Models\ResourceEdits; use App\Services\DataNormalizationService; diff --git a/app/Http/Controllers/ResourceReviewController.php b/app/Http/Controllers/ResourceReviewController.php index ddf63396..ffb03c8f 100644 --- a/app/Http/Controllers/ResourceReviewController.php +++ b/app/Http/Controllers/ResourceReviewController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Http\Requests\ResourceReview\StoreResourceReview; +use App\Http\Requests\StoreResourceReview; use App\Models\ComputerScienceResource; use App\Models\ResourceReview; use Illuminate\Support\Facades\Auth; diff --git a/app/Http/Requests/Comment/StoreCommentRequest.php b/app/Http/Requests/StoreCommentRequest.php similarity index 96% rename from app/Http/Requests/Comment/StoreCommentRequest.php rename to app/Http/Requests/StoreCommentRequest.php index 57ba2c47..90e9e01b 100644 --- a/app/Http/Requests/Comment/StoreCommentRequest.php +++ b/app/Http/Requests/StoreCommentRequest.php @@ -1,6 +1,6 @@ ['nullable', 'string', 'url:http,https', 'max:255'], 'proposed_changes.difficulty' => ['nullable', 'string', Rule::in(config('computerScienceResource.difficulties'))], 'proposed_changes.pricing' => ['nullable', 'string', Rule::in(config('computerScienceResource.pricings'))], - 'proposed_changes.topic_tags' => ['nullable', 'array', 'min:2'], - 'proposed_changes.topic_tags.*' => ['required', 'distinct', 'string', 'max:50'], 'proposed_changes.image_file' => ['nullable', 'image', 'max:500'], // 500 kilobytes + 'proposed_changes.topic_tags' => ['nullable', 'array', 'min:2'], + 'proposed_changes.topic_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], 'proposed_changes.general_tags' => ['nullable', 'array'], - 'proposed_changes.general_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'proposed_changes.general_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], 'proposed_changes.programming_language_tags' => ['nullable', 'array'], - 'proposed_changes.programming_language_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'proposed_changes.programming_language_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], ]; } } diff --git a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php b/app/Http/Requests/StoreResourceRequest.php similarity index 87% rename from app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php rename to app/Http/Requests/StoreResourceRequest.php index 699a1b05..0c2d35c5 100644 --- a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php +++ b/app/Http/Requests/StoreResourceRequest.php @@ -1,6 +1,6 @@ ['required', 'string', Rule::in(config('computerScienceResource.difficulties'))], 'pricing' => ['required', 'string', Rule::in(config('computerScienceResource.pricings'))], 'topic_tags' => ['required', 'array', 'min:2'], - 'topic_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'topic_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], // Optional, can just be omitted 'image_file' => ['nullable', 'image', 'max:500'], // 500 kiloBytes 'general_tags' => ['array'], - 'general_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'general_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], 'programming_language_tags' => ['array'], - 'programming_language_tags.*' => ['required', 'distinct', 'string', 'max:50'], + 'programming_language_tags.*' => ['required', 'distinct', 'string', 'max:50', 'regex:'.config('computerScienceResource.tags_regex')], ]; } } diff --git a/app/Http/Requests/ResourceReview/StoreResourceReview.php b/app/Http/Requests/StoreResourceReview.php similarity index 97% rename from app/Http/Requests/ResourceReview/StoreResourceReview.php rename to app/Http/Requests/StoreResourceReview.php index aa141b58..6966a860 100644 --- a/app/Http/Requests/ResourceReview/StoreResourceReview.php +++ b/app/Http/Requests/StoreResourceReview.php @@ -1,6 +1,6 @@ ['book', 'podcast', 'youtube_channel', 'blog', 'website', 'organization', 'service', 'bootcamp', 'newsletter', 'workshop', 'course', 'forum', 'mobile_app', 'desktop_app', 'magazine'], 'difficulties' => ['any', 'beginner', 'industry_simple', 'industry_standard', 'industry_professional', 'academic'], 'pricings' => ['free', 'paid', 'freemium', 'premium'], + 'tags_regex' => '/^[a-z0-9-]+$/', ]; diff --git a/database/factories/ComputerScienceResourceFactory.php b/database/factories/ComputerScienceResourceFactory.php index 6bd041f8..c992a03e 100644 --- a/database/factories/ComputerScienceResourceFactory.php +++ b/database/factories/ComputerScienceResourceFactory.php @@ -56,11 +56,30 @@ public function configure(): Factory return $this->afterCreating(function (ComputerScienceResource $resource) { $fakerTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', fake()->word(), fake()->word()]; - $resource->topic_tags = $this->topicTags ?? fake()->randomElements($fakerTags, fake()->numberBetween(3, count($fakerTags))); - $resource->programming_language_tags = $this->programmingLanguageTags ?? fake()->randomElements($fakerTags); - $resource->general_tags = $this->generalTags ?? fake()->randomElements($fakerTags); + // Sanitize all tags before assigning + $topicTags = $this->topicTags ?? fake()->randomElements($fakerTags, fake()->numberBetween(3, count($fakerTags))); + $topicTags = array_map([$this, 'sanitizeTag'], $topicTags); + + $programmingLanguageTags = $this->programmingLanguageTags ?? fake()->randomElements($fakerTags); + $programmingLanguageTags = array_map([$this, 'sanitizeTag'], $programmingLanguageTags); + + $generalTags = $this->generalTags ?? fake()->randomElements($fakerTags); + $generalTags = array_map([$this, 'sanitizeTag'], $generalTags); + + $resource->topic_tags = $topicTags; + $resource->programming_language_tags = $programmingLanguageTags; + $resource->general_tags = $generalTags; TagFrequencyChanged::dispatch(null, $resource->tagCounter()); }); } + + private function sanitizeTag(string $tag): string + { + // Lowercase, replace spaces with -, remove invalid chars + $tag = strtolower(str_replace(' ', '-', $tag)); + + // Remove any character not a-z, 0-9, or hyphen + return preg_replace('/[^a-z0-9-]/', '', $tag); + } } diff --git a/database/factories/ResourceEditsFactory.php b/database/factories/ResourceEditsFactory.php index 7302e009..2687d964 100644 --- a/database/factories/ResourceEditsFactory.php +++ b/database/factories/ResourceEditsFactory.php @@ -32,7 +32,7 @@ public function definition(): array 'platforms' => $this->faker->randomElements($platforms, rand(1, 3)), 'difficulty' => $this->faker->randomElement($difficulties), 'pricing' => $this->faker->randomElement($pricings), - 'topic_tags' => ['data structures', 'algorithms'], + 'topic_tags' => ['data-structures', 'algorithms'], 'programming_language_tags' => ['python'], 'general_tags' => ['interactive', 'challenging'], ]; diff --git a/resources/js/Components/Form/TagSelector.vue b/resources/js/Components/Form/TagSelector.vue index a83fbd24..f05aabe5 100644 --- a/resources/js/Components/Form/TagSelector.vue +++ b/resources/js/Components/Form/TagSelector.vue @@ -25,10 +25,27 @@ watch( { immediate: true } ); +function sanitizeTag(tag) { + // Remove trailing spaces and lowercase + let transformedTag = tag.trim().toLowerCase(); + + // Apply rules from config('computerScienceResources.tags_rules') + // Transform the tag to lowercase and replace spaces with hyphens + transformedTag = transformedTag.replace(/\s+/g, "-"); + // Remove any characters that are not lowercase letters or hyphens + // Remove any characters that are not lowercase letters, numbers, or hyphens + transformedTag = transformedTag.replace(/[^a-z0-9-]/g, ""); + return transformedTag; +} + const addTag = (tag) => { - if (tag && !selectedTags.value.includes(tag)) { - selectedTags.value.push(tag); - model.value = [...selectedTags.value]; + if (tag) { + let transformedTag = sanitizeTag(tag); + + if (!selectedTags.value.includes(transformedTag)) { + selectedTags.value.push(transformedTag); + model.value = [...selectedTags.value]; + } } }; @@ -49,8 +66,8 @@ const handleSelect = (event) => { const handleKeydown = (event) => { if (event.key === "Enter") { - if (searchValue.value.trim()) { - addTag(searchValue.value.trim().toLowerCase()); + if (searchValue.value) { + addTag(searchValue.value); // Clear the input after adding via Enter nextTick(() => { searchValue.value = ""; diff --git a/tests/Feature/ResourceEditsTest.php b/tests/Feature/ResourceEditsTest.php index d0db1e47..391f8030 100644 --- a/tests/Feature/ResourceEditsTest.php +++ b/tests/Feature/ResourceEditsTest.php @@ -161,9 +161,9 @@ public function test_merged_edit_reflects_changes_on_original_resource(): void 'difficulty' => fake()->randomElement(config('computerScienceResource.difficulties')), 'platforms' => fake()->randomElements(config('computerScienceResource.platforms'), fake()->numberBetween(1, 3)), 'pricing' => fake()->randomElement(config('computerScienceResource.pricings')), - 'topic_tags' => ["{$i}_a", "{$i}_b", "{$i}_c"], - 'programming_language_tags' => ["{$i}_a", "{$i}_b", "{$i}_c"], - 'general_tags' => ["{$i}_a", "{$i}_b", "{$i}_c"], + 'topic_tags' => ["$i-a", "$i-b", "$i-c"], + 'programming_language_tags' => ["$i-a", "$i-b", "$i-c"], + 'general_tags' => ["$i-a", "$i-b", "$i-c"], ]; $this->makeAndApplyResourceEdits($resource->id, $changes); diff --git a/tests/RequestFactories/ResourceEdit/StoreResourceEditFactory.php b/tests/RequestFactories/ResourceEdit/StoreResourceEditFactory.php index 13f3c592..81776f05 100644 --- a/tests/RequestFactories/ResourceEdit/StoreResourceEditFactory.php +++ b/tests/RequestFactories/ResourceEdit/StoreResourceEditFactory.php @@ -12,11 +12,23 @@ public function definition(): array $difficulties = config('computerScienceResource.difficulties'); $pricings = config('computerScienceResource.pricings'); - // Ensure at least 3 unique topic tags + // Ensure at least 3 unique topic tags, all matching the regex do { $topicTags = array_unique($this->faker->words(mt_rand(4, 9))); + $topicTags = array_map([$this, 'sanitizeTag'], $topicTags); } while (count($topicTags) < 3); + // Ensure programming_language_tags and general_tags are sanitized and valid + do { + $programmingLanguageTags = array_unique($this->faker->words(mt_rand(1, 3))); + $programmingLanguageTags = array_map([$this, 'sanitizeTag'], $programmingLanguageTags); + } while (count($programmingLanguageTags) < 1); + + do { + $generalTags = array_unique($this->faker->words(mt_rand(1, 3))); + $generalTags = array_map([$this, 'sanitizeTag'], $generalTags); + } while (count($generalTags) < 1); + $possibleChanges = [ 'name' => $this->faker->words(mt_rand(2, 4), true), 'description' => $this->faker->paragraphs(2, true), @@ -26,8 +38,8 @@ public function definition(): array 'difficulty' => $this->faker->randomElement($difficulties), 'pricing' => $this->faker->randomElement($pricings), 'topic_tags' => array_values($topicTags), - 'programming_language_tags' => array_unique($this->faker->words(mt_rand(1, 3))), - 'general_tags' => array_unique($this->faker->words(mt_rand(1, 3))), + 'programming_language_tags' => array_values($programmingLanguageTags), + 'general_tags' => array_values($generalTags), ]; $proposedKeys = $this->faker->randomElements( @@ -46,4 +58,12 @@ public function definition(): array 'proposed_changes' => $proposedChanges, ]; } + + private function sanitizeTag(string $tag): string + { + // Replace spaces with hyphens, lowercase, and remove invalid characters + $tag = strtolower(str_replace(' ', '-', $tag)); + + return preg_replace('/[^a-z0-9-]/', '', $tag); + } }