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