diff --git a/app/Http/Requests/ShowUpdateRequest.php b/app/Http/Requests/ShowUpdateRequest.php index 62ceea1b..4da40dc8 100644 --- a/app/Http/Requests/ShowUpdateRequest.php +++ b/app/Http/Requests/ShowUpdateRequest.php @@ -3,6 +3,7 @@ namespace KRLX\Http\Requests; use KRLX\Track; +use KRLX\Rules\Profanity; use Illuminate\Foundation\Http\FormRequest; class ShowUpdateRequest extends FormRequest @@ -49,7 +50,7 @@ protected function baseRules() { $baseRules = [ 'submitted' => ['boolean'], - 'title' => ['string', 'min:3', 'max:200'], + 'title' => ['string', 'min:3', 'max:200', new Profanity], 'content' => ['array'], 'scheduling' => ['array'], 'conflicts' => ['array', 'min:0'], diff --git a/app/Rules/Profanity.php b/app/Rules/Profanity.php new file mode 100644 index 00000000..27809ceb --- /dev/null +++ b/app/Rules/Profanity.php @@ -0,0 +1,117 @@ +word = $bad_word; + + return false; + } + } + + return $this->partialWordsPass($target); + } + + /** + * Check the partial words - these are words which could appear as they + * normally are, or in any number of creative derivatives. + * + * @param mixed $value + * @return bool + */ + protected function partialWordsPass($value) + { + $bad_words = $this->assembleDerivatives(); + foreach ($bad_words as $word => $derivatives) { + if (! $this->singleWordDerivativesPass($word, $derivatives, $value)) { + return false; + } + } + + return true; + } + + /** + * Check if a single word's derivatives show up. + * + * @param string $word + * @param array $derivatives + * @param string $value + * @return bool + */ + private function singleWordDerivativesPass($word, $derivatives, $value) + { + foreach ($derivatives as $derivative) { + if (strpos($value, $derivative) !== false) { + $this->word = $word; + + return false; + } + } + + return true; + } + + /** + * Assemble the derivatives list used in partial word validation. + * + * @return array + */ + protected function assembleDerivatives() + { + $bad_words = []; + foreach (config('defaults.banned_words.partial') as $bad_word) { + $bad_words[$bad_word] = [ + $bad_word, + str_plural($bad_word), + $bad_word[0].str_repeat('*', strlen($bad_word) - 1), + $bad_word[0].str_repeat('*', strlen($bad_word) - 2).$bad_word[-1], + ]; + } + + return $bad_words; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return "The word {$this->word} can't appear in the :attribute."; + } +} diff --git a/config/defaults.php b/config/defaults.php index 4f94daab..e4218cd2 100644 --- a/config/defaults.php +++ b/config/defaults.php @@ -1,6 +1,23 @@ [ + /* + * The following words are not allowed in show titles, or any other + * custom validation field that has a "profanity" flag on it. This list + * is adapted from the UK Office of Communications' 2010 study on + * acceptable language to use in broadcasting. Presence of any of these + * strings, even in longer words, will throw a validation fault. + */ + 'partial' => ['shit', 'piss', 'fuck', 'cunt', 'cock', 'tit', 'bitch', 'bastard'], + + /* + * These words must be present in isolation to trigger a fault (usually + * because there are "safe" words that include these strings, such as + * "pass" being a safe word) + */ + 'full' => ['ass', 'asshole', 'pussy'], + ], 'directory' => 'https://apps.carleton.edu/stock/ldapimage.php?id=', 'title' => 'KRLX Community', 'salt' => env('OAUTH_SALT', 'krlx'), diff --git a/tests/Feature/CustomRuleTest.php b/tests/Feature/CustomRuleTest.php index 4964e6dd..b5d0eaf0 100644 --- a/tests/Feature/CustomRuleTest.php +++ b/tests/Feature/CustomRuleTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature; +use KRLX\Show; +use KRLX\User; use KRLX\Track; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -39,4 +41,50 @@ public function testValidationRuleValidationRule() $this->assertEquals(422, $request->status(), "Did not receive HTTP 422 on rule $rule."); } } + + /** + * Test the "Profanity" validation rule: Does a string contain bad words? + * + * @return void + */ + public function testProfanityRule() + { + $show = factory(Show::class)->create(); + $user = factory(User::class)->create(); + $show->hosts()->attach($user, ['accepted' => true]); + + $partial = config('defaults.banned_words.partial')[0]; + $full = config('defaults.banned_words.full')[0]; + $bad_words = [ + $partial, + $partial.'hole', + str_plural($partial), + $full, + str_plural($full), + 'F***', + 'F**k', + 'F@$#', + ]; + $good_words = [ + 'prefix'.$full, + 'hole', + 'pals!!!', + ]; + foreach ($good_words as $word) { + $request = $this->actingAs($user, 'api')->json('PATCH', "/api/v1/shows/{$show->id}", [ + 'title' => $word, + ]); + if ($request->status() == 500) { + dump($request->json()); + } + $this->assertEquals(200, $request->status(), "Did not receive HTTP 200 with word $word."); + } + foreach ($bad_words as $word) { + $request = $this->actingAs($user, 'api')->json('PATCH', "/api/v1/shows/{$show->id}", [ + 'title' => $word, + ]); + $this->assertEquals(422, $request->status(), "Did not receive HTTP 422 with word $word."); + $this->assertNotEquals('The word can\'t appear in the title', array_get($request->json(), 'errors.title.0')); + } + } }