diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 7b922ab4..6967796c 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -120,6 +120,7 @@ class ReportController extends Controller /** @var Texture */ $texture = $report->texture; if ($texture) { + $dispatcher->dispatch('texture.deleting', [$texture]); Storage::disk('textures')->delete($texture->hash); $texture->delete(); $dispatcher->dispatch('texture.deleted', [$texture]); diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index 7b19aac6..afbbdeed 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use App\Models\Player; use App\Models\Texture; use App\Models\User; use Auth; @@ -302,8 +301,15 @@ class SkinlibController extends Controller ]); } - public function delete(Texture $texture) + public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter) { + $can = $filter->apply('can_delete_texture', true, [$texture]); + if ($can instanceof Rejection) { + return json($can->getReason(), 1); + } + + $dispatcher->dispatch('texture.deleting', [$texture]); + // check if file occupied if (Texture::where('hash', $texture->hash)->count() === 1) { Storage::disk('textures')->delete($texture->hash); @@ -311,12 +317,19 @@ class SkinlibController extends Controller $texture->delete(); + $dispatcher->dispatch('texture.deleted', [$texture]); + return json(trans('skinlib.delete.success'), 0); } - public function privacy(Texture $texture) + public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter) { - $uploader = User::find($texture->uploader); + $can = $filter->apply('can_update_texture_privacy', true, [$texture]); + if ($can instanceof Rejection) { + return json($can->getReason(), 1); + } + + $uploader = $texture->owner; $score_diff = $texture->size * (option('private_score_per_storage') - option('score_per_storage')) * ($texture->public ? -1 : 1); @@ -327,19 +340,7 @@ class SkinlibController extends Controller return json(trans('skinlib.upload.lack-score'), 1); } - $type = $texture->type == 'cape' ? 'cape' : 'skin'; - Player::where("tid_$type", $texture->tid) - ->where('uid', '<>', session('uid')) - ->update(["tid_$type" => 0]); - - $texture->likers()->get()->each(function ($user) use ($texture) { - $user->closet()->detach($texture->tid); - if (option('return_score')) { - $user->score += option('score_per_closet_item'); - $user->save(); - } - $texture->likes--; - }); + $dispatcher->dispatch('texture.privacy.updating', [$texture]); $uploader->score += $score_diff; $uploader->save(); @@ -347,6 +348,8 @@ class SkinlibController extends Controller $texture->public = !$texture->public; $texture->save(); + $dispatcher->dispatch('texture.privacy.updated', [$texture]); + $message = trans('skinlib.privacy.success', [ 'privacy' => ( $texture->public @@ -357,8 +360,12 @@ class SkinlibController extends Controller return json($message, 0); } - public function rename(Request $request, Texture $texture) - { + public function rename( + Request $request, + Dispatcher $dispatcher, + Filter $filter, + Texture $texture + ) { $data = $request->validate(['name' => [ 'required', option('texture_name_regexp') @@ -367,22 +374,46 @@ class SkinlibController extends Controller ]]); $name = $data['name']; + $can = $filter->apply('can_update_texture_name', true, [$texture, $name]); + if ($can instanceof Rejection) { + return json($can->getReason(), 1); + } + + $dispatcher->dispatch('texture.name.updating', [$texture, $name]); + + $old = $texture->replicate(); $texture->name = $name; $texture->save(); + $dispatcher->dispatch('texture.name.updated', [$texture, $old]); + return json(trans('skinlib.rename.success', ['name' => $name]), 0); } - public function type(Request $request, Texture $texture) - { + public function type( + Request $request, + Dispatcher $dispatcher, + Filter $filter, + Texture $texture + ) { $data = $request->validate([ 'type' => ['required', Rule::in(['steve', 'alex', 'cape'])], ]); $type = $data['type']; + $can = $filter->apply('can_update_texture_type', true, [$texture, $type]); + if ($can instanceof Rejection) { + return json($can->getReason(), 1); + } + + $dispatcher->dispatch('texture.type.updating', [$texture, $type]); + + $old = $texture->replicate(); $texture->type = $type; $texture->save(); + $dispatcher->dispatch('texture.type.updated', [$texture, $old]); + return json(trans('skinlib.model.success', ['model' => $type]), 0); } } diff --git a/app/Listeners/CleanUpCloset.php b/app/Listeners/CleanUpCloset.php new file mode 100644 index 00000000..6dd70fa9 --- /dev/null +++ b/app/Listeners/CleanUpCloset.php @@ -0,0 +1,31 @@ +exists && $texture->public) { + return; + } + + $likers = $texture->likers()->get(); + $likers->each(function (User $user) use ($texture) { + $user->closet()->detach($texture->tid); + if (option('return_score')) { + $user->score += (int) option('score_per_closet_item'); + $user->save(); + } + }); + + if ($texture->exists) { + $texture->decrement('likes', $likers->count()); + } + } +} diff --git a/app/Listeners/ResetPlayers.php b/app/Listeners/ResetPlayers.php new file mode 100644 index 00000000..74fd5aae --- /dev/null +++ b/app/Listeners/ResetPlayers.php @@ -0,0 +1,28 @@ +exists && $texture->public) { + return; + } + + $type = $texture->type == 'cape' ? 'tid_cape' : 'tid_skin'; + $query = Player::where($type, $texture->tid); + + // texture was switched from "private" to "public" + if ($texture->exists) { + $query = $query->where('uid', '<>', $texture->uploader); + } + + $query->update([$type => 0]); + } +} diff --git a/app/Listeners/TextureRemoved.php b/app/Listeners/TextureRemoved.php deleted file mode 100644 index 55e0ee8d..00000000 --- a/app/Listeners/TextureRemoved.php +++ /dev/null @@ -1,39 +0,0 @@ -texture; - - $texture->likers()->get()->each(function ($user) use ($texture) { - $user->closet()->detach($texture->tid); - if (option('return_score')) { - $user->score += option('score_per_closet_item'); - $user->save(); - } - }); - - if ($uploader = User::find($texture->uploader)) { - $ret = 0; - if (option('return_score')) { - $ret += $texture->size * ( - $texture->public - ? option('score_per_storage') - : option('private_score_per_storage') - ); - } - - if ($texture->public && option('take_back_scores_after_deletion', true)) { - $ret -= option('score_award_per_texture', 0); - } - - $uploader->score += $ret; - $uploader->save(); - } - } -} diff --git a/app/Listeners/UpdateScoreForDeletedTexture.php b/app/Listeners/UpdateScoreForDeletedTexture.php new file mode 100644 index 00000000..84cc23a1 --- /dev/null +++ b/app/Listeners/UpdateScoreForDeletedTexture.php @@ -0,0 +1,28 @@ +owner; + if ($uploader) { + $ret = 0; + if (option('return_score')) { + $ret += $texture->size * ( + $texture->public + ? (int) option('score_per_storage') + : (int) option('private_score_per_storage') + ); + } + + if ($texture->public && option('take_back_scores_after_deletion', true)) { + $ret -= (int) option('score_award_per_texture', 0); + } + + $uploader->score += $ret; + $uploader->save(); + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 016f45c2..0f525508 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -12,9 +12,6 @@ class EventServiceProvider extends ServiceProvider 'App\Events\PlayerRetrieved' => [ Listeners\ResetInvalidTextureForPlayer::class, ], - 'App\Events\TextureDeleting' => [ - Listeners\TextureRemoved::class, - ], 'App\Events\PluginWasEnabled' => [ Listeners\CopyPluginAssets::class, Listeners\GeneratePluginTranslations::class, @@ -32,5 +29,14 @@ class EventServiceProvider extends ServiceProvider 'auth.registration.completed' => [ Listeners\SendEmailVerification::class, ], + 'texture.privacy.updated' => [ + Listeners\ResetPlayers::class, + Listeners\CleanUpCloset::class, + ], + 'texture.deleted' => [ + Listeners\UpdateScoreForDeletedTexture::class, + Listeners\ResetPlayers::class, + Listeners\CleanUpCloset::class, + ], ]; } diff --git a/tests/HttpTest/ControllersTest/ReportControllerTest.php b/tests/HttpTest/ControllersTest/ReportControllerTest.php index 9294622a..d2c9ae57 100644 --- a/tests/HttpTest/ControllersTest/ReportControllerTest.php +++ b/tests/HttpTest/ControllersTest/ReportControllerTest.php @@ -281,6 +281,12 @@ class ReportControllerTest extends TestCase return true; }); + Event::assertDispatched('texture.deleting', function ($event, $payload) use ($tid) { + [$texture] = $payload; + $this->assertEquals($tid, $texture->tid); + + return true; + }); Event::assertDispatched('texture.deleted', function ($event, $payload) use ($tid) { [$texture] = $payload; $this->assertEquals($tid, $texture->tid); diff --git a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php index bf411075..8b1899a2 100644 --- a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php +++ b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php @@ -2,7 +2,6 @@ namespace Tests; -use App\Models\Player; use App\Models\Texture; use App\Models\User; use Blessing\Rejection; @@ -469,13 +468,12 @@ class SkinlibControllerTest extends TestCase public function testDelete() { + Event::fake(); /** @var FilesystemAdapter */ $disk = Storage::fake('textures'); $uploader = factory(User::class)->create(); - $other = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - option(['return_score' => false]); $duplicate = factory(Texture::class)->create([ 'hash' => $texture->hash, @@ -492,6 +490,33 @@ class SkinlibControllerTest extends TestCase ]); $this->assertNull(Texture::find($duplicate->tid)); $disk->assertExists($texture->hash); + Event::assertDispatched( + 'texture.deleting', + function ($eventName, $payload) use ($duplicate) { + $this->assertTrue($duplicate->is($payload[0])); + + return true; + } + ); + Event::assertDispatched( + 'texture.deleted', + function ($eventName, $payload) use ($duplicate) { + $this->assertTrue($duplicate->is($payload[0])); + + return true; + } + ); + + // rejected + $filter = Fakes\Filter::fake(); + $filter->add('can_delete_texture', function ($can, $t) use ($texture) { + $this->assertTrue($texture->is($t)); + + return new Rejection('rejected'); + }); + $this->deleteJson(route('texture.delete', ['texture' => $texture])) + ->assertJson(['code' => 1, 'message' => 'rejected']); + $filter->remove('can_delete_texture'); $this->deleteJson(route('texture.delete', ['texture' => $texture])) ->assertJson([ @@ -500,81 +525,11 @@ class SkinlibControllerTest extends TestCase ]); $this->assertNull(Texture::find($texture->tid)); $disk->assertMissing($texture->hash); - - // return score - option(['return_score' => true]); - $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.delete.success'), - ]); - $this->assertEquals( - $uploader->score + $texture->size * option('score_per_storage'), - $uploader->fresh()->score - ); - - $uploader->refresh(); - $texture = factory(Texture::class)->create([ - 'uploader' => $uploader->uid, - 'public' => false, - ]); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson([ - 'code' => 0, - 'message' => trans('skinlib.delete.success'), - ]); - $this->assertEquals( - $uploader->score + $texture->size * option('private_score_per_storage'), - $uploader->fresh()->score - ); - - option(['return_score' => false]); - - // return the award - option(['score_award_per_texture' => 5]); - $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - $uploader->refresh(); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score - 5, $uploader->fresh()->score); - // option disabled - option(['take_back_scores_after_deletion' => false]); - $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - $uploader->refresh(); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score, $uploader->fresh()->score); - // private texture - $texture = factory(Texture::class)->create([ - 'uploader' => $uploader->uid, - 'public' => false, - ]); - $uploader->refresh(); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson(['code' => 0]); - $this->assertEquals($uploader->score, $uploader->fresh()->score); - - // remove from closet - option(['return_score' => true]); - $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); - $other->closet()->attach($texture->tid, ['item_name' => 'a']); - $other->score = 0; - $other->save(); - $this->actingAs($uploader) - ->deleteJson(route('texture.delete', ['texture' => $texture])) - ->assertJson(['code' => 0]); - $other->refresh(); - $this->assertEquals(option('score_per_closet_item'), $other->score); } public function testPrivacy() { + Event::fake(); $uploader = factory(User::class)->create(); $other = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); @@ -591,11 +546,24 @@ class SkinlibControllerTest extends TestCase ]); $this->assertTrue($texture->fresh()->public); + // rejected + $filter = Fakes\Filter::fake(); + $filter->add('can_update_texture_privacy', function ($can, $t) use ($texture) { + $this->assertTrue($texture->fresh()->is($t)); + + return new Rejection('rejected'); + }); + $this->actingAs($uploader) + ->putJson(route('texture.privacy', ['texture' => $texture])) + ->assertJson(['code' => 1, 'message' => 'rejected']); + $filter->remove('can_update_texture_privacy'); + $texture->public = true; $texture->save(); $uploader->score = $texture->size * (option('private_score_per_storage') - option('score_per_storage')); $uploader->save(); + $replicated = $texture->replicate(); $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson([ 'code' => 0, @@ -603,26 +571,34 @@ class SkinlibControllerTest extends TestCase ]); $this->assertEquals(0, $uploader->fresh()->score); $this->assertFalse($texture->fresh()->public); + Event::assertDispatched( + 'texture.privacy.updating', + function ($eventName, $payload) use ($replicated) { + $this->assertInstanceOf(Texture::class, $payload[0]); + $this->assertTrue($replicated->public); + + return true; + } + ); + Event::assertDispatched( + 'texture.privacy.updated', + function ($eventName, $payload) { + $this->assertFalse($payload[0]->public); + + return true; + } + ); // When setting a texture to be private, // other players should not be able to use it. $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); $uploader->score += $texture->size * option('private_score_per_storage'); $uploader->save(); - $player = factory(Player::class)->create(['tid_skin' => $texture->tid]); - $other = factory(User::class)->create(); - $other->closet()->attach($texture->tid, ['item_name' => 'a']); $this->putJson(route('texture.privacy', ['texture' => $texture])) ->assertJson([ 'code' => 0, 'message' => trans('skinlib.privacy.success', ['privacy' => trans('general.private')]), ]); - $this->assertEquals(0, $player->fresh()->tid_skin); - $this->assertEquals(0, $other->closet()->count()); - $this->assertEquals( - $other->score + option('score_per_closet_item'), - $other->fresh()->score - ); // take back the score option(['score_award_per_texture' => 5]); @@ -650,6 +626,7 @@ class SkinlibControllerTest extends TestCase public function testRename() { + Event::fake(); $uploader = factory(User::class)->create(); $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); @@ -675,13 +652,47 @@ class SkinlibControllerTest extends TestCase 'message' => trans('skinlib.rename.success', ['name' => 'abc']), ]); $this->assertEquals('abc', $texture->fresh()->name); + Event::assertDispatched( + 'texture.name.updating', + function ($eventName, $payload) use ($texture) { + $this->assertTrue($texture->is($payload[0])); + $this->assertEquals('abc', $payload[1]); + + return true; + } + ); + Event::assertDispatched( + 'texture.name.updated', + function ($eventName, $payload) use ($texture) { + $this->assertTrue($texture->fresh()->is($payload[0])); + $this->assertEquals($texture->name, $payload[1]->name); + + return true; + } + ); + + // rejected + $filter = Fakes\Filter::fake(); + $filter->add('can_update_texture_name', function ($can, $t, $name) use ($texture) { + $this->assertTrue($texture->is($t)); + $this->assertEquals('abc', $name); + + return new Rejection('rejected'); + }); + $this->putJson( + route('texture.name', ['texture' => $texture]), + ['name' => 'abc'] + )->assertJson(['code' => 1, 'message' => 'rejected']); } public function testType() { + Event::fake(); $uploader = factory(User::class)->create(); $other = factory(User::class)->create(); - $texture = factory(Texture::class)->create(['uploader' => $uploader->uid]); + $texture = factory(Texture::class) + ->states('alex') + ->create(['uploader' => $uploader->uid]); // missing `type` field $this->actingAs($uploader) @@ -703,6 +714,24 @@ class SkinlibControllerTest extends TestCase 'message' => trans('skinlib.model.success', ['model' => 'steve']), ]); $this->assertEquals('steve', $texture->fresh()->type); + Event::assertDispatched( + 'texture.type.updating', + function ($eventName, $payload) use ($texture) { + $this->assertTrue($texture->is($payload[0])); + $this->assertEquals('steve', $payload[1]); + + return true; + } + ); + Event::assertDispatched( + 'texture.type.updated', + function ($eventName, $payload) use ($texture) { + $this->assertTrue($texture->fresh()->is($payload[0])); + $this->assertEquals('alex', $payload[1]->type); + + return true; + } + ); $duplicate = factory(Texture::class)->states('alex')->create([ 'uploader' => $other->uid, @@ -719,5 +748,18 @@ class SkinlibControllerTest extends TestCase 'code' => 0, 'message' => trans('skinlib.model.success', ['model' => 'alex']), ]); + + // rejected + $filter = Fakes\Filter::fake(); + $filter->add('can_update_texture_type', function ($can, $t, $type) use ($texture) { + $this->assertTrue($texture->is($t)); + $this->assertEquals('steve', $type); + + return new Rejection('rejected'); + }); + $this->putJson( + route('texture.type', ['texture' => $texture]), + ['type' => 'steve'] + )->assertJson(['code' => 1, 'message' => 'rejected']); } } diff --git a/tests/ListenersTest/CleanUpClosetTest.php b/tests/ListenersTest/CleanUpClosetTest.php new file mode 100644 index 00000000..0669e0f1 --- /dev/null +++ b/tests/ListenersTest/CleanUpClosetTest.php @@ -0,0 +1,54 @@ + true]); + $texture = factory(Texture::class)->create(); + $user = factory(User::class)->create(['score' => 0]); + $user->closet()->attach($texture->tid, ['item_name' => '']); + + event('texture.privacy.updated', [$texture]); + $user->refresh(); + $this->assertEquals($texture->likes, $texture->fresh()->likes); + $this->assertEquals(0, $user->score); + } + + public function testPrivateTexture() + { + option(['return_score' => true]); + $texture = factory(Texture::class)->create(['public' => false]); + $user = factory(User::class)->create(['score' => 0]); + $user->closet()->attach($texture->tid, ['item_name' => '']); + + $replicated = $texture->replicate(); + event('texture.privacy.updated', [$texture]); + $user->refresh(); + $this->assertEquals($replicated->likes - 1, $texture->fresh()->likes); + $this->assertEquals((int) option('score_per_closet_item'), $user->score); + $this->assertNull($user->closet()->find($texture->tid)); + } + + public function testDeletedTexture() + { + option(['return_score' => true]); + $texture = factory(Texture::class)->create(); + $user = factory(User::class)->create(['score' => 0]); + $user->closet()->attach($texture->tid, ['item_name' => '']); + + $texture->delete(); + event('texture.deleted', [$texture]); + $user->refresh(); + $this->assertEquals((int) option('score_per_closet_item'), $user->score); + $this->assertNull($user->closet()->find($texture->tid)); + } +} diff --git a/tests/ListenersTest/ResetPlayersTest.php b/tests/ListenersTest/ResetPlayersTest.php new file mode 100644 index 00000000..2041220d --- /dev/null +++ b/tests/ListenersTest/ResetPlayersTest.php @@ -0,0 +1,57 @@ +create(); + $player = factory(Player::class)->create(['tid_skin' => $texture->tid]); + + event('texture.privacy.updated', [$texture]); + $player->refresh(); + $this->assertEquals($texture->tid, $player->tid_skin); + } + + public function testPrivateTexture() + { + $texture = factory(Texture::class)->create(['public' => false]); + $player = factory(Player::class)->create(['tid_skin' => $texture->tid]); + $playerOfUploader = factory(Player::class)->create([ + 'uid' => $texture->uploader, + 'tid_skin' => $texture->tid, + ]); + + event('texture.privacy.updated', [$texture]); + $player->refresh(); + $playerOfUploader->refresh(); + $texture->refresh(); + $this->assertEquals(0, $player->tid_skin); + $this->assertEquals($texture->tid, $playerOfUploader->tid_skin); + } + + public function testDeletedTexture() + { + $texture = factory(Texture::class)->create(); + $player = factory(Player::class)->create(['tid_skin' => $texture->tid]); + $playerOfUploader = factory(Player::class)->create([ + 'uid' => $texture->uploader, + 'tid_skin' => $texture->tid, + ]); + + $texture->delete(); + event('texture.deleted', [$texture]); + $player->refresh(); + $playerOfUploader->refresh(); + $texture->refresh(); + $this->assertEquals(0, $player->tid_skin); + $this->assertEquals(0, $playerOfUploader->tid_skin); + } +} diff --git a/tests/ListenersTest/UpdateScoreForDeletedTextureTest.php b/tests/ListenersTest/UpdateScoreForDeletedTextureTest.php new file mode 100644 index 00000000..c4c37345 --- /dev/null +++ b/tests/ListenersTest/UpdateScoreForDeletedTextureTest.php @@ -0,0 +1,73 @@ + true]); + $texture = factory(Texture::class)->create(); + $uploader = $texture->owner->replicate(); + + event('texture.deleted', [$texture]); + $this->assertEquals( + $uploader->score + $texture->size * (int) option('score_per_storage'), + $texture->owner->fresh()->score + ); + } + + public function testPrivateTexture() + { + option(['return_score' => true]); + $texture = factory(Texture::class)->create(['public' => false]); + $uploader = $texture->owner->replicate(); + + event('texture.deleted', [$texture]); + $this->assertEquals( + $uploader->score + $texture->size * (int) option('private_score_per_storage'), + $texture->owner->fresh()->score + ); + } + + public function testTakeBackAwardOfPublicTexture() + { + option([ + 'score_award_per_texture' => 5, + 'take_back_scores_after_deletion' => true, + 'score_per_storage' => 0, + ]); + + $texture = factory(Texture::class)->create(); + $uploader = $texture->owner->replicate(); + + event('texture.deleted', [$texture]); + $this->assertEquals( + $uploader->score - 5, + $texture->owner->fresh()->score + ); + } + + public function testTakeBackAwardOfPrivateTexture() + { + option([ + 'score_award_per_texture' => 5, + 'take_back_scores_after_deletion' => true, + 'private_score_per_storage' => 0, + ]); + + $texture = factory(Texture::class)->create(['public' => false]); + $uploader = $texture->owner->replicate(); + + event('texture.deleted', [$texture]); + $this->assertEquals( + $uploader->score, + $texture->owner->fresh()->score + ); + } +}