diff --git a/app/Http/Controllers/OptionsController.php b/app/Http/Controllers/OptionsController.php index a308e0c0..14eb5e84 100644 --- a/app/Http/Controllers/OptionsController.php +++ b/app/Http/Controllers/OptionsController.php @@ -163,6 +163,10 @@ class OptionsController extends Controller ->text('max_upload_file_size')->addon('KB') ->hint(trans('options.general.max_upload_file_size.hint', ['size' => ini_get('upload_max_filesize')])); + $form->group('max_texture_width') + ->text('max_texture_width')->addon('px') + ->hint(trans('options.general.max_texture_width.hint')); + $form->select('player_name_rule') ->option('official', trans('options.general.player_name_rule.official')) ->option('cjk', trans('options.general.player_name_rule.cjk')) diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php index 2310050f..99120973 100644 --- a/app/Http/Controllers/SkinlibController.php +++ b/app/Http/Controllers/SkinlibController.php @@ -220,6 +220,16 @@ class SkinlibController extends Controller $type = $data['type']; $size = getimagesize($file); + $maxWidth = option('max_texture_width', 8192); + if ($size[0] > $maxWidth) { + $message = trans('skinlib.upload.too-wide', [ + 'width' => $size[0], + 'maxWidth' => $maxWidth, + ]); + + return json($message, 1); + } + if ($size[0] % 64 != 0 || $size[1] % 32 != 0) { $message = trans('skinlib.upload.invalid-size', [ 'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'), @@ -253,8 +263,19 @@ class SkinlibController extends Controller } } - $hash = hash_file('sha256', $file); - $hash = $filter->apply('uploaded_texture_hash', $hash, [$file]); + $image = imagecreatefrompng($file); + $imageSanitized = imagecreatetruecolor($size[0], $size[1]); + imagealphablending($imageSanitized, false); + imagesavealpha($imageSanitized, true); + imagecopy($imageSanitized, $image, 0, 0, 0, 0, $size[0], $size[1]); + + ob_start(); + imagepng($imageSanitized); + $fileSanitized = ob_get_contents(); + ob_end_clean(); + + $hash = hash('sha256', $fileSanitized); + $hash = $filter->apply('uploaded_texture_hash', $hash, [$fileSanitized]); /** @var User */ $user = Auth::user(); @@ -270,11 +291,11 @@ class SkinlibController extends Controller return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]); } - $size = ceil($file->getSize() / 1024); + $fileSize = ceil(strlen($fileSanitized) / 1024); $isPublic = is_string($data['public']) ? $data['public'] === '1' : $data['public']; - $cost = $size * ( + $cost = $fileSize * ( $isPublic ? option('score_per_storage') : option('private_score_per_storage') @@ -285,13 +306,13 @@ class SkinlibController extends Controller return json(trans('skinlib.upload.lack-score'), 1); } - $dispatcher->dispatch('texture.uploading', [$file, $name, $hash]); + $dispatcher->dispatch('texture.uploading', [$fileSanitized, $name, $hash]); $texture = new Texture(); $texture->name = $name; $texture->type = $type; $texture->hash = $hash; - $texture->size = $size; + $texture->size = $fileSize; $texture->public = $isPublic; $texture->uploader = $user->uid; $texture->likes = 1; @@ -300,14 +321,14 @@ class SkinlibController extends Controller /** @var FilesystemAdapter */ $disk = Storage::disk('textures'); if ($disk->missing($hash)) { - $file->storePubliclyAs('', $hash, ['disk' => 'textures']); + $disk->put($hash, $fileSanitized); } $user->score -= $cost; $user->closet()->attach($texture->tid, ['item_name' => $name]); $user->save(); - $dispatcher->dispatch('texture.uploaded', [$texture, $file]); + $dispatcher->dispatch('texture.uploaded', [$texture, $fileSanitized]); return json(trans('skinlib.upload.success', ['name' => $name]), 0, [ 'tid' => $texture->tid, diff --git a/database/migrations/2025_06_29_064944_add_max_texture_width_option.php b/database/migrations/2025_06_29_064944_add_max_texture_width_option.php new file mode 100644 index 00000000..39e2b17b --- /dev/null +++ b/database/migrations/2025_06_29_064944_add_max_texture_width_option.php @@ -0,0 +1,21 @@ +对于任何恶意修改页面右下角的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权利。" + description: '每种支持的语言都可以对应不同的程序版权信息,如果想要编辑某种特定语言下的版权信息,请在右上角切换至该语言后再提交修改。对于任何恶意修改页面右下角的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权利。' copyright_text: title: 自定义版权文字 description: 自定义版权文字内可使用占位符,{site_name} 将会被自动替换为站点名称,{site_url} 会被替换为站点地址。每种支持的语言都可以对应不同的自定义版权文字,如果想要编辑某种特定语言下的版权文字,请在右上角切换至该语言后再提交修改。 @@ -97,7 +97,10 @@ general: regs_per_ip: 每个 IP 限制注册数 max_upload_file_size: title: 最大允许上传大小 - hint: "PHP 限制::size,定义在 php.ini 中。" + hint: 'PHP 限制::size,定义在 php.ini 中。' + max_texture_width: + title: 最大允许材质宽度 + hint: 允许上传的材质的最大的宽度,必须是 64 的整数倍 player_name_rule: title: 角色名规则 official: 大小写字母数字下划线(Mojang 官方的用户名规则) diff --git a/resources/lang/zh_CN/skinlib.yml b/resources/lang/zh_CN/skinlib.yml index d1b7051b..18b186b2 100644 --- a/resources/lang/zh_CN/skinlib.yml +++ b/resources/lang/zh_CN/skinlib.yml @@ -12,6 +12,7 @@ upload: private-score-notice: 私密材质将会消耗更多的积分:每 KB 存储空间 :score 积分 invalid-size: 不是有效的 :type 文件(宽 :width,高 :height) invalid-hd-skin: 不是有效的高清皮肤(宽和高不是 32 的整数倍) + too-wide: 材质过宽(:widthpx),本站允许的最大宽度为 :maxWidthpx lack-score: 积分不足 repeated: 已经有人上传过这个材质了,直接添加到衣柜使用吧~ success: 材质 :name 上传成功 diff --git a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php index 02260e9a..081e8883 100644 --- a/tests/HttpTest/ControllersTest/SkinlibControllerTest.php +++ b/tests/HttpTest/ControllersTest/SkinlibControllerTest.php @@ -303,6 +303,22 @@ class SkinlibControllerTest extends TestCase 'type' => 'steve', ])->assertJsonValidationErrors('public'); + // too wide texture + option(['max_texture_width' => 128]); + $this->postJson(route('texture.upload'), [ + 'name' => 'texture', + 'file' => UploadedFile::fake()->image('wide.png', 256, 256), + 'type' => 'steve', + 'public' => true, + ])->assertJson([ + 'code' => 1, + 'message' => trans('skinlib.upload.too-wide', [ + 'width' => 256, + 'maxWidth' => 128, + ]), + ]); + option(['max_texture_width' => 8192]); + // invalid skin size $this->postJson(route('texture.upload'), [ 'name' => 'texture', @@ -437,7 +453,7 @@ class SkinlibControllerTest extends TestCase 'uploaded_texture_hash', function ($hash, $file) use ($texture) { $this->assertEquals($texture->hash, $hash); - $this->assertInstanceOf(UploadedFile::class, $file); + $this->assertIsString($file); return true; } @@ -445,7 +461,7 @@ class SkinlibControllerTest extends TestCase Event::assertDispatched( 'texture.uploading', function ($eventName, $payload) use ($texture) { - $this->assertInstanceOf(UploadedFile::class, $payload[0]); + $this->assertIsString($payload[0]); $this->assertEquals($texture->name, $payload[1]); $this->assertEquals($texture->hash, $payload[2]); @@ -456,7 +472,7 @@ class SkinlibControllerTest extends TestCase 'texture.uploaded', function ($eventName, $payload) use ($texture) { $this->assertTrue($texture->is($payload[0])); - $this->assertInstanceOf(UploadedFile::class, $payload[1]); + $this->assertIsString($payload[1]); return true; }