Compare commits

..

40 Commits

Author SHA1 Message Date
Pig Fang
52f6fefed0
chore: remove unavailable sponsor info 2026-03-30 21:42:23 +08:00
Steven Qiu
840555df42
make tests happy 2026-03-09 23:13:47 +08:00
Steven Qiu
9aa867e8aa
refactor: do not catch exceptions when cannot read options 2026-03-09 20:21:43 +08:00
Steven Qiu
d70b39f445
feat: add Auto-Submitted header to emails
@see https://datatracker.ietf.org/doc/html/rfc3834
Hopefully this could prevent sender being spammed by auto replies...
2025-10-09 01:54:37 +08:00
SANYE-YA
33055ecbf9
fix avatar (refactor needed) (#666) 2025-08-07 05:08:13 +08:00
Steven Qiu
2e39fbce77
fix: avatar (refactor needed) 2025-07-31 19:59:29 +08:00
Steven Qiu
1b3b020d52
fix: make imagick sanitize result stable 2025-07-27 03:34:35 +08:00
Steven Qiu
33d805ee82
fix: skinlib 2d preview (refactor needed) 2025-07-26 21:38:33 +08:00
Steven Qiu
57c02dd51c
fix: check if imagick installed 2025-07-25 17:43:53 +08:00
Steven Qiu
5b6bb98860
chore: update README 2025-07-25 17:39:02 +08:00
Steven Qiu
c01112a6c1
chore: use imagick for Intervention\Image 2025-07-25 17:13:54 +08:00
Steven Qiu
064b0967fc
chore: complete Facade namespaces in use statements 2025-07-02 19:29:19 +08:00
Steven Qiu
9f4c59abec
chore: no .DS_Store [skip ci] 2025-07-02 19:29:19 +08:00
Steven Qiu
d8547a0a3d
refactor: use Intervention/Image to sanitize textures 2025-07-02 19:12:46 +08:00
Steven Qiu
9c51bd602b chore: remove redundant VSCode launch profile 2025-06-29 16:50:14 +08:00
Steven Qiu
bc3f504ca3 chore: ci 2025-06-29 16:50:14 +08:00
Steven Qiu
761cbb7828
feat: max texture width & texture sanitize (#662)
* feat: sanitize uploaded file when user upload texture

* feat: limit max texture width to avoid png bomb

* style: apply php-cs-fixer fixes

* chore: set default value for max_texture_width option

* Update skinlib.yml

Co-authored-by: Pig Fang <g-plane@hotmail.com>

---------

Co-authored-by: Pig Fang <g-plane@hotmail.com>
2025-06-29 16:09:55 +08:00
Steven Qiu
01fe3eb4cb
chore: set filename for ci snapshot build artifact 2025-06-28 17:48:16 +08:00
Steven Qiu
f03dd8122b
chore: remove redundant command in ci 2025-06-28 17:47:28 +08:00
Steven Qiu
cfda2a6bf8
style: apply php-cs-fixer fixes 2025-06-28 06:17:40 +08:00
Steven Qiu
74ce668221
fix: phpunit test 2025-06-28 06:16:49 +08:00
Steven Qiu
5a18d24464
fix: db exception in tests 2025-06-28 03:46:17 +08:00
Steven Qiu
5125862f80
fix: unexpected db query during composer install 2025-06-27 19:23:20 +08:00
Steven Qiu
fa791857ec
fix: ci snapshot build 2025-06-26 21:47:35 +08:00
Steven Qiu
24ad29ea99
style: apply php-cs-fixer fixes 2025-06-26 21:16:56 +08:00
Steven Qiu
cdfb972bd0 fix: scopes missing after cache clear 2025-06-26 21:15:53 +08:00
Zephyr Lykos
1985ce6ff8
config: switch default registry 2025-06-25 22:57:43 +08:00
Steven Qiu
d84eb65d55
Handle null route when request is handled by middleware 2025-06-22 21:50:57 +08:00
Steven Qiu
16474fb5d0
Remove locale cookie for API requests (#660)
* Do not set locale cookie for API requests

https://t.me/blessing_skin/184899

* Remove redundant code
2025-06-22 17:48:48 +08:00
Jerry
186138b884
Fix Netlify links (#656)
* Update README-zh.md

fix: wrong link

* Fix Netlify link

---------

Co-authored-by: Steven Qiu <tnqzh123@littlesk.in>
2025-06-22 17:40:50 +08:00
Zephyr Lykos
9ca6e37e39
chore: update deps 2025-01-18 16:44:33 +08:00
Steven Qiu
866e182c31
Fix #583 (#584) 2024-01-12 20:44:58 +08:00
Zephyr Lykos
739b9c97d7
tests: fix CheckRoleTest warning 2024-01-12 20:41:37 +08:00
Zephyr Lykos
0a43c5aa67
style: format code 2024-01-12 20:37:11 +08:00
Zephyr Lykos
35bd4524e6
ci: bump snapshot builds to PHP 8.2 2024-01-12 20:35:16 +08:00
Zephyr Lykos
eba91d5f1d
chore: update frontend deps 2024-01-12 20:31:53 +08:00
Zephyr Lykos
d764836244
ci: update deps 2024-01-12 20:31:37 +08:00
Zephyr Lykos
b1b4a71c3e
chore: update deps 2024-01-12 20:24:47 +08:00
Zephyr Lykos
fb74b43d64
upgrade dependencies 2023-08-26 14:29:53 +08:00
Zephyr Lykos
2a24506c15
config: switch default registry to CloudFront 2023-07-22 15:10:29 +08:00
187 changed files with 12721 additions and 13853 deletions

View File

@ -20,20 +20,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: 8.1 php-version: 8.3
coverage: none coverage: none
extensions: mbstring, dom, fileinfo, gd extensions: mbstring, dom, fileinfo, gd, imagick
- name: Install dependencies - name: Install dependencies
run: | run: |
composer install --prefer-dist --no-progress composer install --prefer-dist --no-progress
- name: Prepare - name: Prepare
run: | run: |
cp .env.example .env cp .env.example .env
php artisan key:generate
mkdir -p resources/views/overrides mkdir -p resources/views/overrides
- name: Validate Twig templates - name: Validate Twig templates
run: php artisan twig:lint -v run: php artisan twig:lint -v
@ -45,24 +44,24 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: ['8.1', '8.2'] php: ['8.2', '8.3']
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup PHP only - name: Setup PHP only
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
if: matrix.php != '8.0' if: matrix.php != '8.3'
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
coverage: none coverage: none
extensions: mbstring, dom, fileinfo, sqlite, gd, zip extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
- name: Setup PHP with Xdebug - name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
if: matrix.php == '8.0' if: matrix.php == '8.3'
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
coverage: xdebug coverage: xdebug
extensions: mbstring, dom, fileinfo, sqlite, gd, zip extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
- name: Cache Composer dependencies - name: Cache Composer dependencies
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@ -72,14 +71,14 @@ jobs:
- name: Install Composer dependencies - name: Install Composer dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run tests only - name: Run tests only
if: matrix.php != '8.0' if: matrix.php != '8.3'
run: ./vendor/bin/phpunit run: ./vendor/bin/phpunit
- name: Run tests with coverage report - name: Run tests with coverage report
if: matrix.php == '8.0' if: matrix.php == '8.3'
run: ./vendor/bin/phpunit --coverage-clover=coverage.xml run: ./vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage report - name: Upload coverage report
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
if: matrix.php == '8.0' && success() if: matrix.php == '8.3' && success()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
name: github-actions name: github-actions
@ -88,7 +87,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Run checks - name: Run checks
@ -101,7 +100,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Run tests - name: Run tests
@ -115,8 +114,14 @@ jobs:
name: Snapshot Build name: Snapshot Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
coverage: none
extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Cache Node dependencies - name: Cache Node dependencies
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@ -138,11 +143,13 @@ jobs:
yarn build yarn build
cp resources/assets/src/images/bg.webp public/app/ cp resources/assets/src/images/bg.webp public/app/
cp resources/assets/src/images/favicon.ico public/app/ cp resources/assets/src/images/favicon.ico public/app/
- uses: benjlevesque/short-sha@v1.2 - uses: benjlevesque/short-sha@v3.0
id: short-sha id: short-sha
- name: Archive release - name: Archive release
run: zip -9 -r blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip app bootstrap config database plugins public resources/lang resources/views resources/misc/textures routes storage vendor .env.example artisan LICENSE README.md README-zh.md index.html run: zip -9 -r blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip app bootstrap config database plugins public resources/lang resources/views resources/misc/textures routes storage vendor .env.example artisan LICENSE README.md README-zh.md index.html
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
if-no-files-found: error
name: blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip
path: blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip path: blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Build and create archive - name: Build and create archive
run: ./tools/release.ps1 run: ./tools/release.ps1
shell: pwsh shell: pwsh

2
.gitignore vendored
View File

@ -25,3 +25,5 @@ storage/options.php
.phpunit.result.cache .phpunit.result.cache
.php-cs-fixer.cache .php-cs-fixer.cache
resources/views/overrides resources/views/overrides
.DS_Store
*/.DS_Store

68
.vscode/launch.json vendored
View File

@ -1,38 +1,34 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Jest Tests", "name": "Launch Jest Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest", "program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["${file}"], "args": ["${file}"],
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**" },
] {
}, "type": "php",
{ "request": "launch",
"type": "php", "name": "Launch with XDebug",
"request": "launch", "ignore": ["**/vendor/**/*.php"]
"name": "Launch with XDebug", },
"ignore": [ {
"**/vendor/**/*.php" "type": "firefox",
] "request": "launch",
}, "reAttach": true,
{ "name": "Launch with Firefox Debugger",
"type": "firefox", "url": "http://localhost/",
"request": "launch", "webRoot": "${workspaceFolder}",
"reAttach": true, "pathMappings": [
"name": "Launch with Firefox Debugger", {
"url": "http://localhost/", "url": "webpack:///",
"webRoot": "${workspaceFolder}", "path": "${workspaceFolder}/"
"pathMappings": [ }
{ ]
"url": "webpack:///", }
"path": "${workspaceFolder}/" ]
}
]
}
]
} }

View File

@ -21,7 +21,7 @@ WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
COPY tsconfig.build.json tsconfig.json webpack.config.ts ./ COPY postcss.config.js tsconfig.build.json tsconfig.json webpack.config.ts ./
COPY tools/*Plugin.ts ./tools/ COPY tools/*Plugin.ts ./tools/
COPY resources ./resources COPY resources ./resources

View File

@ -52,6 +52,7 @@ Blessing Skin 对您的服务器有一定的要求。在大多数情况下,下
- JSON - JSON
- fileinfo - fileinfo
- zip - zip
- Imagick
## 快速使用 ## 快速使用
@ -61,105 +62,9 @@ Blessing Skin 对您的服务器有一定的要求。在大多数情况下,下
Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。 Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。
## 支持并赞助 Blessing Skin
如果您觉得这个软件对您很有帮助,欢迎通过赞助来支持开发!
目前可在 [爱发电](https://afdian.net/@blessing-skin) 上赞助。
### Sponsors
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@gao_cai_sheng">
<img src="https://pic1.afdiancdn.com/user/2aac23481b1b11ea9f6e52540025c377/avatar/96a8b23d98cbac5aa36601db15a27e5e_w512_h512_s234.jpg" width="120" height="120">
<br>
gao_cai_sheng
</a>
</td>
<td align=center>
<a href="https://afdian.net/@LD_fantasy">
<img src="https://pic1.afdiancdn.com/user/9bed7bb454f011eb821652540025c377/avatar/cb679e3eac693e0eea2eac527c7954e0_w700_h1307_s137.jpg" width="120" height="120">
<br>
K_LazyCat
</a>
</td>
<td align=center>
<a href="https://afdian.net/@nmzy2018">
<img src="https://pic1.afdiancdn.com/user/a66f79d2f5a311e9af4e52540025c377/avatar/98682fb3c5914a39c8986bb1e97b5501_w512_h512_s248.jpg" width="120" height="120">
<br>
伊南
</a>
</td>
<td align=center>
<a href="">
<img src="https://pic1.afdiancdn.com/default/avatar/avatar-blue.png" width="120" height="120">
<br>
家乐
</a>
</td>
<td align=center>
<a href="https://afdian.net/@oar-01">
<img src="https://pic1.afdiancdn.com/user/e391f6ccdfa911ebb0e352540025c377/avatar/74da4afa92fa2666c306d43ab7a8804b_w1920_h1080_s338.jpg" width="120" height="120">
<br>
黄金鞘翅的郡主
</a>
</td>
</tr>
<tr>
<td align=center>
<a href="https://www.bilibili.plus/caucmc1.orz">
<img src="https://pic1.afdiancdn.com/user/edde2efc879611e889f552540025c377/avatar/d6a712efd6560b28989ac33f99c8915d_w473_h454_s24.jpg" width="120" height="120">
<br>
睡觉塞牙
</a>
</td>
</tr>
</tbody>
</table>
### Backers
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@ValiantShishu976400">
<img src="https://pic1.afdiancdn.com/user/178a08963a5e11e9addd52540025c377/avatar/ece9f089aaf2c2f83204a8de11697caf_w350_h350_s16.jpg" width="75" height="75">
<br>
飒爽师叔
</a>
</td>
<td align=center>
<a href="https://afdian.net/@PAKingdom">
<img src="https://pic1.afdiancdn.com/user/18ad3338e58a11e9b29352540025c377/avatar/1e8b6476b589ddac545ac1ce13166e59_w584_h797_s59.jpg" width="75" height="75">
<br>
皮皮帕
</a>
</td>
<td align=center>
<a href="https://afdian.net/@oar-01">
<img src="https://pic1.afdiancdn.com/user/e391f6ccdfa911ebb0e352540025c377/avatar/74da4afa92fa2666c306d43ab7a8804b_w1920_h1080_s338.jpg" width="75" height="75">
<br>
黄金鞘翅的郡主
</a>
</td>
<td align=center>
<a href="">
<img src="https://pic1.afdiancdn.com/user/fc143860efa111ebb3e552540025c377/avatar/6e1d0f3f6ffb80b89b44269f59aa775f_w1080_h1080_s107.jpg" width="75" height="75">
<br>
♂sudo rm -rf /*[幼稚鬼]
</a>
</td>
</tr>
</tbody>
</table>
## 自行构建 ## 自行构建
详情可阅读 [这里](https://blessing.netlify.com/build.html)。 详情可阅读 [这里](https://blessing.netlify.app/build.html)。
> 您可以订阅我们的 Telegram 频道 [Blessing Skin News](https://t.me/blessing_skin_news) 来获取最新开发动态。当有新的 Commit 被推送时,我们的机器人将会在频道内发送一条消息来提示您能否拉取最新代码,以及拉取后应该做什么。 > 您可以订阅我们的 Telegram 频道 [Blessing Skin News](https://t.me/blessing_skin_news) 来获取最新开发动态。当有新的 Commit 被推送时,我们的机器人将会在频道内发送一条消息来提示您能否拉取最新代码,以及拉取后应该做什么。
@ -171,7 +76,7 @@ Blessing Skin 可支持多种语言,当前支持英语、简体中文和西班
## 问题报告 ## 问题报告
请参阅 [报告问题的正确姿势](https://blessing.netlify.com/report.html)。 请参阅 [报告问题的正确姿势](https://blessing.netlify.app/report.html)。
## 相关链接 ## 相关链接

View File

@ -52,6 +52,7 @@ Blessing Skin has only a few system requirements. In most cases, these PHP exten
- JSON - JSON
- fileinfo - fileinfo
- zip - zip
- Imagick
## Quick Install ## Quick Install
@ -61,102 +62,6 @@ Please read [Installation Guide](https://blessing.netlify.app/en/setup.html).
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins. Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
## Supporting Blessing Skin
Welcome to sponsoring Blessing Skin if this software is useful for you!
Currently you can sponsor us via [爱发电](https://afdian.net/@blessing-skin).
### Sponsors
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@gao_cai_sheng">
<img src="https://pic1.afdiancdn.com/user/2aac23481b1b11ea9f6e52540025c377/avatar/96a8b23d98cbac5aa36601db15a27e5e_w512_h512_s234.jpg" width="120" height="120">
<br>
gao_cai_sheng
</a>
</td>
<td align=center>
<a href="https://afdian.net/@LD_fantasy">
<img src="https://pic1.afdiancdn.com/user/9bed7bb454f011eb821652540025c377/avatar/cb679e3eac693e0eea2eac527c7954e0_w700_h1307_s137.jpg" width="120" height="120">
<br>
K_LazyCat
</a>
</td>
<td align=center>
<a href="https://afdian.net/@nmzy2018">
<img src="https://pic1.afdiancdn.com/user/a66f79d2f5a311e9af4e52540025c377/avatar/98682fb3c5914a39c8986bb1e97b5501_w512_h512_s248.jpg" width="120" height="120">
<br>
伊南
</a>
</td>
<td align=center>
<a href="">
<img src="https://pic1.afdiancdn.com/default/avatar/avatar-blue.png" width="120" height="120">
<br>
家乐
</a>
</td>
<td align=center>
<a href="https://afdian.net/@oar-01">
<img src="https://pic1.afdiancdn.com/user/e391f6ccdfa911ebb0e352540025c377/avatar/74da4afa92fa2666c306d43ab7a8804b_w1920_h1080_s338.jpg" width="120" height="120">
<br>
黄金鞘翅的郡主
</a>
</td>
</tr>
<tr>
<td align=center>
<a href="https://www.bilibili.plus/caucmc1.orz">
<img src="https://pic1.afdiancdn.com/user/edde2efc879611e889f552540025c377/avatar/d6a712efd6560b28989ac33f99c8915d_w473_h454_s24.jpg" width="120" height="120">
<br>
睡觉塞牙
</a>
</td>
</tr>
</tbody>
</table>
### Backers
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@ValiantShishu976400">
<img src="https://pic1.afdiancdn.com/user/178a08963a5e11e9addd52540025c377/avatar/ece9f089aaf2c2f83204a8de11697caf_w350_h350_s16.jpg" width="75" height="75">
<br>
飒爽师叔
</a>
</td>
<td align=center>
<a href="https://afdian.net/@PAKingdom">
<img src="https://pic1.afdiancdn.com/user/18ad3338e58a11e9b29352540025c377/avatar/1e8b6476b589ddac545ac1ce13166e59_w584_h797_s59.jpg" width="75" height="75">
<br>
皮皮帕
</a>
</td>
<td align=center>
<a href="https://afdian.net/@oar-01">
<img src="https://pic1.afdiancdn.com/user/e391f6ccdfa911ebb0e352540025c377/avatar/74da4afa92fa2666c306d43ab7a8804b_w1920_h1080_s338.jpg" width="75" height="75">
<br>
黄金鞘翅的郡主
</a>
</td>
<td align=center>
<a href="">
<img src="https://pic1.afdiancdn.com/user/fc143860efa111ebb3e552540025c377/avatar/6e1d0f3f6ffb80b89b44269f59aa775f_w1080_h1080_s107.jpg" width="75" height="75">
<br>
♂sudo rm -rf /*[幼稚鬼]
</a>
</td>
</tr>
</tbody>
</table>
## Build From Source ## Build From Source
Please refer to [Manual Build](https://blessing.netlify.app/build.html). Please refer to [Manual Build](https://blessing.netlify.app/build.html).

View File

@ -55,6 +55,7 @@ class Handler extends ExceptionHandler
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
$isFromPlugins = !app()->runningUnitTests() $isFromPlugins = !app()->runningUnitTests()
&& Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all()); && Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all());
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
return Str::startsWith($trace['file'], 'app') || $isFromPlugins; return Str::startsWith($trace['file'], 'app') || $isFromPlugins;
}) })

View File

@ -86,7 +86,7 @@ class AdminController extends Controller
Request $request, Request $request,
PluginManager $plugins, PluginManager $plugins,
Filesystem $filesystem, Filesystem $filesystem,
Filter $filter Filter $filter,
) { ) {
$db = config('database.connections.'.config('database.default')); $db = config('database.connections.'.config('database.default'));
$dbType = Arr::get([ $dbType = Arr::get([

View File

@ -8,16 +8,16 @@ use App\Mail\ForgotPassword;
use App\Models\Player; use App\Models\Player;
use App\Models\User; use App\Models\User;
use App\Rules; use App\Rules;
use Auth;
use Blessing\Filter; use Blessing\Filter;
use Blessing\Rejection; use Blessing\Rejection;
use Cache;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Mail; use Illuminate\Support\Facades\Auth;
use Session; use Illuminate\Support\Facades\Cache;
use URL; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL;
use Vectorface\Whip\Whip; use Vectorface\Whip\Whip;
class AuthController extends Controller class AuthController extends Controller
@ -50,7 +50,7 @@ class AuthController extends Controller
Request $request, Request $request,
Rules\Captcha $captcha, Rules\Captcha $captcha,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter Filter $filter,
) { ) {
$data = $request->validate([ $data = $request->validate([
'identification' => 'required', 'identification' => 'required',
@ -151,7 +151,7 @@ class AuthController extends Controller
Request $request, Request $request,
Rules\Captcha $captcha, Rules\Captcha $captcha,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter Filter $filter,
) { ) {
$can = $filter->apply('can_register', null); $can = $filter->apply('can_register', null);
if ($can instanceof Rejection) { if ($can instanceof Rejection) {
@ -248,7 +248,7 @@ class AuthController extends Controller
Request $request, Request $request,
Rules\Captcha $captcha, Rules\Captcha $captcha,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter Filter $filter,
) { ) {
$data = $request->validate([ $data = $request->validate([
'email' => 'required|email', 'email' => 'required|email',

View File

@ -4,12 +4,12 @@ namespace App\Http\Controllers;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use Auth;
use Blessing\Filter; use Blessing\Filter;
use Blessing\Rejection; use Blessing\Rejection;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ClosetController extends Controller class ClosetController extends Controller
{ {
@ -75,7 +75,7 @@ class ClosetController extends Controller
public function add( public function add(
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter Filter $filter,
) { ) {
['tid' => $tid, 'name' => $name] = $request->validate([ ['tid' => $tid, 'name' => $name] = $request->validate([
'tid' => 'required|integer', 'tid' => 'required|integer',
@ -132,7 +132,7 @@ class ClosetController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
$tid $tid,
) { ) {
['name' => $name] = $request->validate(['name' => 'required']); ['name' => $name] = $request->validate(['name' => 'required']);
/** @var User */ /** @var User */

View File

@ -163,6 +163,10 @@ class OptionsController extends Controller
->text('max_upload_file_size')->addon('KB') ->text('max_upload_file_size')->addon('KB')
->hint(trans('options.general.max_upload_file_size.hint', ['size' => ini_get('upload_max_filesize')])); ->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') $form->select('player_name_rule')
->option('official', trans('options.general.player_name_rule.official')) ->option('official', trans('options.general.player_name_rule.official'))
->option('cjk', trans('options.general.player_name_rule.cjk')) ->option('cjk', trans('options.general.player_name_rule.cjk'))

View File

@ -10,11 +10,11 @@ use App\Models\Player;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use App\Rules; use App\Rules;
use Auth;
use Blessing\Filter; use Blessing\Filter;
use Blessing\Rejection; use Blessing\Rejection;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class PlayerController extends Controller class PlayerController extends Controller
@ -124,7 +124,7 @@ class PlayerController extends Controller
public function delete( public function delete(
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Player $player Player $player,
) { ) {
/** @var User */ /** @var User */
$user = auth()->user(); $user = auth()->user();
@ -157,7 +157,7 @@ class PlayerController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Player $player Player $player,
) { ) {
$name = $request->validate([ $name = $request->validate([
'name' => [ 'name' => [
@ -194,7 +194,7 @@ class PlayerController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Player $player Player $player,
) { ) {
/** @var User */ /** @var User */
$user = auth()->user(); $user = auth()->user();
@ -234,7 +234,7 @@ class PlayerController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Player $player Player $player,
) { ) {
$types = $request->input('type', []); $types = $request->input('type', []);

View File

@ -44,7 +44,7 @@ class PlayersManagementController extends Controller
public function name( public function name(
Player $player, Player $player,
Request $request, Request $request,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$name = $request->validate([ $name = $request->validate([
'player_name' => [ 'player_name' => [
@ -70,7 +70,7 @@ class PlayersManagementController extends Controller
public function owner( public function owner(
Player $player, Player $player,
Request $request, Request $request,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$uid = $request->validate(['uid' => 'required|integer'])['uid']; $uid = $request->validate(['uid' => 'required|integer'])['uid'];
@ -96,7 +96,7 @@ class PlayersManagementController extends Controller
public function texture( public function texture(
Player $player, Player $player,
Request $request, Request $request,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$data = $request->validate([ $data = $request->validate([
'tid' => 'required|integer', 'tid' => 'required|integer',
@ -123,7 +123,7 @@ class PlayersManagementController extends Controller
public function delete( public function delete(
Player $player, Player $player,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$dispatcher->dispatch('player.deleting', [$player]); $dispatcher->dispatch('player.deleting', [$player]);

View File

@ -77,7 +77,7 @@ class ReportController extends Controller
public function review( public function review(
Report $report, Report $report,
Request $request, Request $request,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$data = $request->validate([ $data = $request->validate([
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])], 'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],

View File

@ -20,7 +20,7 @@ class SetupController extends Controller
Request $request, Request $request,
Filesystem $filesystem, Filesystem $filesystem,
Connection $connection, Connection $connection,
DatabaseManager $manager DatabaseManager $manager,
) { ) {
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
try { try {
@ -121,7 +121,7 @@ class SetupController extends Controller
'database/migrations', 'database/migrations',
'vendor/laravel/passport/database/migrations', 'vendor/laravel/passport/database/migrations',
], ],
]); ]);
$siteUrl = url('/'); $siteUrl = url('/');
if (Str::endsWith($siteUrl, '/index.php')) { if (Str::endsWith($siteUrl, '/index.php')) {

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use Auth;
use Blessing\Filter; use Blessing\Filter;
use Blessing\Rejection; use Blessing\Rejection;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
@ -12,10 +11,12 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Intervention\Image\Facades\Image;
use League\CommonMark\GithubFlavoredMarkdownConverter; use League\CommonMark\GithubFlavoredMarkdownConverter;
use Storage;
class SkinlibController extends Controller class SkinlibController extends Controller
{ {
@ -189,7 +190,7 @@ class SkinlibController extends Controller
public function handleUpload( public function handleUpload(
Request $request, Request $request,
Filter $filter, Filter $filter,
Dispatcher $dispatcher Dispatcher $dispatcher,
) { ) {
$file = $request->file('file'); $file = $request->file('file');
if ($file && !$file->isValid()) { if ($file && !$file->isValid()) {
@ -220,6 +221,16 @@ class SkinlibController extends Controller
$type = $data['type']; $type = $data['type'];
$size = getimagesize($file); $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) { if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
$message = trans('skinlib.upload.invalid-size', [ $message = trans('skinlib.upload.invalid-size', [
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'), 'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
@ -253,8 +264,17 @@ class SkinlibController extends Controller
} }
} }
$hash = hash_file('sha256', $file); $image = Image::make($file);
$hash = $filter->apply('uploaded_texture_hash', $hash, [$file]); $imagick = $image->getCore();
$imagick->setOption('png:compression-filter', '0');
$imagick->setOption('png:compression-level', '9');
$imagick->setOption('png:compression-strategy', '0');
$imagick->setOption('png:exclude-chunk', 'all');
$imagick->stripImage();
$sanitized = $image->encode('png')->getEncoded();
$hash = hash('sha256', $image->encoded);
$hash = $filter->apply('uploaded_texture_hash', $hash, [$image]);
/** @var User */ /** @var User */
$user = Auth::user(); $user = Auth::user();
@ -270,11 +290,11 @@ class SkinlibController extends Controller
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]); return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
} }
$size = ceil($file->getSize() / 1024); $fileSize = ceil(strlen($sanitized) / 1024);
$isPublic = is_string($data['public']) $isPublic = is_string($data['public'])
? $data['public'] === '1' ? $data['public'] === '1'
: $data['public']; : $data['public'];
$cost = $size * ( $cost = $fileSize * (
$isPublic $isPublic
? option('score_per_storage') ? option('score_per_storage')
: option('private_score_per_storage') : option('private_score_per_storage')
@ -285,13 +305,13 @@ class SkinlibController extends Controller
return json(trans('skinlib.upload.lack-score'), 1); return json(trans('skinlib.upload.lack-score'), 1);
} }
$dispatcher->dispatch('texture.uploading', [$file, $name, $hash]); $dispatcher->dispatch('texture.uploading', [$image, $name, $hash]);
$texture = new Texture(); $texture = new Texture();
$texture->name = $name; $texture->name = $name;
$texture->type = $type; $texture->type = $type;
$texture->hash = $hash; $texture->hash = $hash;
$texture->size = $size; $texture->size = $fileSize;
$texture->public = $isPublic; $texture->public = $isPublic;
$texture->uploader = $user->uid; $texture->uploader = $user->uid;
$texture->likes = 1; $texture->likes = 1;
@ -300,14 +320,14 @@ class SkinlibController extends Controller
/** @var FilesystemAdapter */ /** @var FilesystemAdapter */
$disk = Storage::disk('textures'); $disk = Storage::disk('textures');
if ($disk->missing($hash)) { if ($disk->missing($hash)) {
$file->storePubliclyAs('', $hash, ['disk' => 'textures']); $disk->put($hash, $sanitized);
} }
$user->score -= $cost; $user->score -= $cost;
$user->closet()->attach($texture->tid, ['item_name' => $name]); $user->closet()->attach($texture->tid, ['item_name' => $name]);
$user->save(); $user->save();
$dispatcher->dispatch('texture.uploaded', [$texture, $file]); $dispatcher->dispatch('texture.uploaded', [$texture, $image]);
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [ return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
'tid' => $texture->tid, 'tid' => $texture->tid,
@ -386,7 +406,7 @@ class SkinlibController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Texture $texture Texture $texture,
) { ) {
$data = $request->validate(['name' => [ $data = $request->validate(['name' => [
'required', 'required',
@ -416,7 +436,7 @@ class SkinlibController extends Controller
Request $request, Request $request,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter, Filter $filter,
Texture $texture Texture $texture,
) { ) {
$data = $request->validate([ $data = $request->validate([
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])], 'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],

View File

@ -6,11 +6,11 @@ use App\Models\Player;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use Blessing\Minecraft; use Blessing\Minecraft;
use Cache;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Image; use Illuminate\Support\Facades\Cache;
use Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class TextureController extends Controller class TextureController extends Controller
{ {
@ -71,7 +71,8 @@ class TextureController extends Controller
$lastModified = $disk->lastModified($hash); $lastModified = $disk->lastModified($hash);
return Image::make($image) // TODO: refactor
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
->response($usePNG ? 'png' : 'webp', 100) ->response($usePNG ? 'png' : 'webp', 100)
->setLastModified(Carbon::createFromTimestamp($lastModified)); ->setLastModified(Carbon::createFromTimestamp($lastModified));
} }
@ -145,7 +146,8 @@ class TextureController extends Controller
$disk = Storage::disk('textures'); $disk = Storage::disk('textures');
if (is_null($texture) || $disk->missing($texture->hash)) { if (is_null($texture) || $disk->missing($texture->hash)) {
return Image::make(resource_path("misc/textures/avatar$mode.png")) // TODO: refactor
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make(resource_path("misc/textures/avatar$mode.png"))
->resize($size, $size) ->resize($size, $size)
->response($usePNG ? 'png' : 'webp', 100); ->response($usePNG ? 'png' : 'webp', 100);
} }
@ -165,7 +167,8 @@ class TextureController extends Controller
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash)); $lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
return Image::make($image) // TODO: refactor
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
->resize($size, $size) ->resize($size, $size)
->response($usePNG ? 'png' : 'webp', 100) ->response($usePNG ? 'png' : 'webp', 100)
->setLastModified($lastModified); ->setLastModified($lastModified);

View File

@ -40,7 +40,7 @@ class TranslationsController extends Controller
Request $request, Request $request,
Application $app, Application $app,
JavaScript $js, JavaScript $js,
LanguageLine $line LanguageLine $line,
) { ) {
$data = $request->validate(['text' => 'required|string']); $data = $request->validate(['text' => 'required|string']);
@ -57,7 +57,7 @@ class TranslationsController extends Controller
public function delete( public function delete(
Application $app, Application $app,
JavaScript $js, JavaScript $js,
LanguageLine $line LanguageLine $line,
) { ) {
$line->delete(); $line->delete();

View File

@ -6,16 +6,16 @@ use App\Events\UserProfileUpdated;
use App\Mail\EmailVerification; use App\Mail\EmailVerification;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use Auth;
use Blessing\Filter; use Blessing\Filter;
use Blessing\Rejection; use Blessing\Rejection;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL;
use League\CommonMark\GithubFlavoredMarkdownConverter; use League\CommonMark\GithubFlavoredMarkdownConverter;
use Mail;
use Session;
use URL;
class UserController extends Controller class UserController extends Controller
{ {

View File

@ -17,8 +17,8 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\Illuminate\Foundation\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\ConvertEmptyStringsToNull::class, Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\DetectLanguagePrefer::class, Middleware\DetectLanguagePrefer::class,
]; ];
/** /**
@ -33,8 +33,8 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
\App\Http\Middleware\EnforceEverGreen::class, Middleware\EnforceEverGreen::class,
\App\Http\Middleware\RedirectToSetup::class, Middleware\RedirectToSetup::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
@ -44,9 +44,9 @@ class Kernel extends HttpKernel
'authorize' => [ 'authorize' => [
'auth:web', 'auth:web',
\App\Http\Middleware\RejectBannedUser::class, Middleware\RejectBannedUser::class,
\App\Http\Middleware\EnsureEmailFilled::class, Middleware\EnsureEmailFilled::class,
\App\Http\Middleware\FireUserAuthenticated::class, Middleware\FireUserAuthenticated::class,
], ],
]; ];
@ -58,13 +58,13 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string> * @var array<string, class-string|string>
*/ */
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => Middleware\Authenticate::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => Middleware\RedirectIfAuthenticated::class,
'role' => \App\Http\Middleware\CheckRole::class, 'role' => Middleware\CheckRole::class,
'setup' => \App\Http\Middleware\CheckInstallation::class, 'setup' => Middleware\CheckInstallation::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \App\Http\Middleware\CheckUserVerified::class, 'verified' => Middleware\CheckUserVerified::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class, 'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, 'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
]; ];

View File

@ -9,10 +9,10 @@ use Illuminate\Http\Request;
class CheckRole class CheckRole
{ {
protected $roles = [ protected $roles = [
'banned' => USER::BANNED, 'banned' => User::BANNED,
'normal' => USER::NORMAL, 'normal' => User::NORMAL,
'admin' => USER::ADMIN, 'admin' => User::ADMIN,
'super-admin' => USER::SUPER_ADMIN, 'super-admin' => User::SUPER_ADMIN,
]; ];
public function handle(Request $request, Closure $next, $role) public function handle(Request $request, Closure $next, $role)

View File

@ -28,7 +28,9 @@ class DetectLanguagePrefer
/** @var Response */ /** @var Response */
$response = $next($request); $response = $next($request);
$response->cookie('locale', $locale, 120); if (!in_array('api', optional($request->route())->middleware() ?? [])) {
$response->cookie('locale', $locale, 120);
}
return $response; return $response;
} }

View File

@ -22,7 +22,7 @@ class FootComposer
Request $request, Request $request,
JavaScript $javascript, JavaScript $javascript,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filter $filter Filter $filter,
) { ) {
$this->request = $request; $this->request = $request;
$this->javascript = $javascript; $this->javascript = $javascript;

View File

@ -20,7 +20,7 @@ class HeadComposer
public function __construct( public function __construct(
Dispatcher $dispatcher, Dispatcher $dispatcher,
Request $request, Request $request,
Filter $filter Filter $filter,
) { ) {
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->request = $request; $this->request = $request;

View File

@ -3,7 +3,7 @@
namespace App\Listeners; namespace App\Listeners;
use App\Models\User; use App\Models\User;
use Event; use Illuminate\Support\Facades\Event;
class NotifyFailedPlugin class NotifyFailedPlugin
{ {

View File

@ -4,6 +4,7 @@ namespace App\Mail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class EmailVerification extends Mailable class EmailVerification extends Mailable
@ -26,4 +27,13 @@ class EmailVerification extends Mailable
->subject(trans('user.verification.mail.title', ['sitename' => $site_name])) ->subject(trans('user.verification.mail.title', ['sitename' => $site_name]))
->view('mails.email-verification'); ->view('mails.email-verification');
} }
public function headers(): Headers
{
return new Headers(
text: [
'Auto-Submitted' => 'auto-generated',
]
);
}
} }

View File

@ -4,6 +4,7 @@ namespace App\Mail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ForgotPassword extends Mailable class ForgotPassword extends Mailable
@ -26,4 +27,13 @@ class ForgotPassword extends Mailable
->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name])) ->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name]))
->view('mails.password-reset'); ->view('mails.password-reset');
} }
public function headers(): Headers
{
return new Headers(
text: [
'Auto-Submitted' => 'auto-generated',
]
);
}
} }

View File

@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Events\PlayerProfileUpdated; use App\Events\PlayerProfileUpdated;
use App\Models;
use DateTimeInterface; use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -57,17 +56,17 @@ class Player extends Model
public function user() public function user()
{ {
return $this->belongsTo(Models\User::class, 'uid'); return $this->belongsTo(User::class, 'uid');
} }
public function skin() public function skin()
{ {
return $this->belongsTo(Models\Texture::class, 'tid_skin'); return $this->belongsTo(Texture::class, 'tid_skin');
} }
public function cape() public function cape()
{ {
return $this->belongsTo(Models\Texture::class, 'tid_cape'); return $this->belongsTo(Texture::class, 'tid_cape');
} }
public function getModelAttribute() public function getModelAttribute()

View File

@ -2,8 +2,10 @@
namespace App\Providers; namespace App\Providers;
use App\Models\Scope;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Laravel\Passport\Passport; use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
@ -39,7 +41,19 @@ class AuthServiceProvider extends ServiceProvider
'ReportsManagement.ReadWrite' => 'auth.oauth.scope.reports-management.readwrite', 'ReportsManagement.ReadWrite' => 'auth.oauth.scope.reports-management.readwrite',
]; ];
$scopes = Cache::get('scopes', []); /*
* Return empty scopes if running unit tests or before installation.
* In these cases, migrations arent run yet, so DB queries will fail.
* OAuth isnt tested in unit tests, so returning empty scopes should be fine...?
* Maybe the best approach is to run migrations before bootstrap in tests,
* but this seems impossible for DB_DATABASE=:memory:;
* Or change how scopes are registered so they don't depend on the database,
* but that may introduce BREAKING CHANGES and plugin incompatibility.
* PRs welcome for better solutions!
*/
$scopes = (app()->runningUnitTests() || !Storage::disk('root')->exists('storage/install.lock')) ? [] : Cache::rememberForever('scopes', function () {
return Scope::pluck('description', 'name')->toArray();
});
Passport::tokensCan(array_merge($defaultScopes, $scopes)); Passport::tokensCan(array_merge($defaultScopes, $scopes));

View File

@ -29,7 +29,7 @@ class RouteServiceProvider extends ServiceProvider
foreach ($router->getRoutes()->getRoutesByName() as $name => $route) { foreach ($router->getRoutes()->getRoutesByName() as $name => $route) {
if (Str::startsWith($name, ['passport.authorizations', 'passport.tokens', 'passport.clients'])) { if (Str::startsWith($name, ['passport.authorizations', 'passport.tokens', 'passport.clients'])) {
$route->middleware('verified'); $route->middleware(['auth', 'verified']);
} }
} }

View File

@ -8,10 +8,10 @@ use App\Events;
use App\Notifications; use App\Notifications;
use Blessing\Filter; use Blessing\Filter;
use Closure; use Closure;
use Event;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Notification;
class Hook class Hook
{ {

View File

@ -2,10 +2,10 @@
namespace App\Services; namespace App\Services;
use DB;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
class Option class Option
{ {
@ -20,13 +20,14 @@ class Option
return; return;
} }
try { if (!file_exists(storage_path('install.lock')) || app()->runningUnitTests()) {
$this->items = DB::table('options')
->get()
->mapWithKeys(fn ($item) => [$item->option_name => $item->option_value]);
} catch (QueryException $e) {
$this->items = collect(); $this->items = collect();
return;
} }
$this->items = DB::table('options')
->get()
->mapWithKeys(fn ($item) => [$item->option_name => $item->option_value]);
} }
public function get($key, $default = null, $raw = false) public function get($key, $default = null, $raw = false)

View File

@ -2,10 +2,10 @@
namespace App\Services; namespace App\Services;
use App\Services\Facades\Option;
use BadMethodCallException; use BadMethodCallException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Option;
use ReflectionClass; use ReflectionClass;
/** /**
@ -59,7 +59,7 @@ class OptionForm
} }
/** /**
* @throws \BadMethodCallException * @throws BadMethodCallException
*/ */
public function __call(string $method, array $params): OptionFormItem public function __call(string $method, array $params): OptionFormItem
{ {
@ -203,7 +203,7 @@ class OptionForm
/** /**
* Handle the HTTP post request and update modified options. * Handle the HTTP post request and update modified options.
*/ */
public function handle(callable $callback = null): self public function handle(?callable $callback = null): self
{ {
$request = request(); $request = request();
$allPostData = $request->all(); $allPostData = $request->all();

View File

@ -36,7 +36,7 @@ class PluginManager
Application $app, Application $app,
Option $option, Option $option,
Dispatcher $dispatcher, Dispatcher $dispatcher,
Filesystem $filesystem Filesystem $filesystem,
) { ) {
$this->app = $app; $this->app = $app;
$this->option = $option; $this->option = $option;
@ -366,7 +366,7 @@ class PluginManager
*/ */
public function formatUnresolved( public function formatUnresolved(
Collection $unsatisfied, Collection $unsatisfied,
Collection $conflicts Collection $conflicts,
): array { ): array {
$unsatisfied = $unsatisfied->map(function ($detail, $name) { $unsatisfied = $unsatisfied->map(function ($detail, $name) {
if ($name === 'blessing-skin-server') { if ($name === 'blessing-skin-server') {

View File

@ -20,7 +20,7 @@ class JavaScript
public function __construct( public function __construct(
Filesystem $filesystem, Filesystem $filesystem,
Repository $cache, Repository $cache,
PluginManager $plugins PluginManager $plugins,
) { ) {
$this->filesystem = $filesystem; $this->filesystem = $filesystem;
$this->cache = $cache; $this->cache = $cache;

View File

@ -51,6 +51,7 @@ ini_set('display_errors', true);
'json', 'json',
'fileinfo', 'fileinfo',
'zip', 'zip',
'imagick',
], ],
'write_permission' => [ 'write_permission' => [
'bootstrap/cache', 'bootstrap/cache',

View File

@ -6,6 +6,7 @@
"php": "^8.1", "php": "^8.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-imagick": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-openssl": "*", "ext-openssl": "*",

3861
composer.lock generated

File diff suppressed because it is too large Load Diff

20
config/image.php Normal file
View File

@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => 'imagick'
];

View File

@ -33,7 +33,7 @@ return [
*/ */
'registry' => env( 'registry' => env(
'PLUGINS_REGISTRY', 'PLUGINS_REGISTRY',
'https://git.qvq.network/bs-community/plugins-dist/-/raw/master/registry_{lang}.json' 'https://bs-plugins.littleservice.cn/registry_{lang}.json'
), ),
/* /*

View File

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAllTables extends Migration class CreateAllTables extends Migration
{ {

View File

@ -1,6 +1,8 @@
<?php <?php
use App\Services\Facades\Option;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class ImportOptions extends Migration class ImportOptions extends Migration
{ {

View File

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddVerificationToUsersTable extends Migration class AddVerificationToUsersTable extends Migration
{ {

View File

@ -0,0 +1,21 @@
<?php
use App\Services\Facades\Option;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Option::set('max_texture_width', 8192);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -6,7 +6,6 @@
"type": "git", "type": "git",
"url": "https://github.com/bs-community/blessing-skin-server" "url": "https://github.com/bs-community/blessing-skin-server"
}, },
"type": "module",
"author": "printempw", "author": "printempw",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
@ -22,86 +21,92 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.0.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.0.0",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.3.0",
"@hot-loader/react-dom": "^17.0.2", "@hot-loader/react-dom": "^17.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^18.5.0",
"admin-lte": "^3.2.0", "admin-lte": "^3.2.0",
"blessing-skin-shell": "^0.3.4", "blessing-skin-shell": "^0.3.4",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.1",
"cac": "6.7.14", "cac": "6.6.1",
"cli-spinners": "^2.9.0", "cli-spinners": "^2.5.0",
"clsx": "^1.2.1", "clsx": "^1.1.1",
"echarts": "^5.4.2", "echarts": "^5.1.2",
"immer": "^10.0.2", "events": "^3.2.0",
"jquery": "^3.7.0", "immer": "^7.0.4",
"jquery": "^3.6.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"nanoid": "^4.0.2", "nanoid": "^3.1.9",
"prompts": "^2.4.2", "prompts": "^2.4.0",
"react": "^17.0.2", "react": "^17.0.1",
"react-autosuggest": "^10.1.0", "react-autosuggest": "^10.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.1",
"react-draggable": "^4.4.5", "react-draggable": "^4.4.2",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.12.21",
"react-loading-skeleton": "^3.3.1", "react-loading-skeleton": "^2.1.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"reaptcha": "^1.12.1", "reaptcha": "^1.7.2",
"rxjs": "^7.8.1", "rxjs": "^6.5.5",
"skinview-utils": "^0.7.0", "skinview-utils": "^0.5.5",
"skinview3d": "3.0.0-alpha.1", "skinview3d": "^3.0.0-alpha.1",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.8",
"use-immer": "^0.9.0", "use-immer": "^0.4.2",
"xterm": "^5.2.1", "xterm": "^4.6.0",
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@gplane/tsconfig": "^5.0.0", "@gplane/tsconfig": "^4.2.0",
"@swc/register": "^0.1.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^11.2.6",
"@testing-library/react": "^12.1.5", "@types/bootstrap": "^4.3.3",
"@types/bootstrap": "^5.2.6", "@types/css-minimizer-webpack-plugin": "^1.1.0",
"@types/jest": "^29.5.2", "@types/jest": "^26.0.23",
"@types/jquery": "^3.5.16", "@types/jquery": "^3.5.13",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^3.12.4",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.6",
"@types/prompts": "^2.4.4", "@types/mini-css-extract-plugin": "^1.2.1",
"@types/react": "17", "@types/prompts": "^2.0.9",
"@types/react-autosuggest": "^10.1.6", "@types/react": "^16.9.35",
"@types/react-dom": "^17.0.20", "@types/react-autosuggest": "^9.3.14",
"@types/testing-library__jest-dom": "^5.14.7", "@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@types/tween.js": "^18.5.0",
"@typescript-eslint/parser": "^5.61.0", "@types/webpack-dev-server": "^3.11.0",
"autoprefixer": "^10.4.14", "@typescript-eslint/eslint-plugin": "^3.6.0",
"css-loader": "^6.8.1", "@typescript-eslint/parser": "^3.6.0",
"css-minimizer-webpack-plugin": "^5.0.1", "autoprefixer": "^10.2.6",
"eslint": "^8.44.0", "css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.1",
"eslint": "^7.4.0",
"eslint-formatter-beauty": "^3.0.0", "eslint-formatter-beauty": "^3.0.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.3.0",
"html-webpack-plugin": "^5.5.3", "html-webpack-plugin": "^5.3.1",
"husky": "^8.0.3", "husky": "^7.0.4",
"jest": "^29.6.1", "jest": "^27.0.4",
"jest-environment-jsdom": "^29.6.1", "jest-extended": "^0.11.5",
"jest-extended": "^4.0.0", "js-yaml": "^3.13.1",
"js-yaml": "^4.1.0", "mini-css-extract-plugin": "^1.6.0",
"mini-css-extract-plugin": "^2.7.6", "postcss": "^8.3.0",
"postcss": "^8.4.25", "postcss-loader": "^5.3.0",
"postcss-loader": "^7.3.3", "prettier": "^2.3.0",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"style-loader": "^3.3.3", "style-loader": "^2.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^27.0.2",
"ts-loader": "^9.4.4", "ts-loader": "^9.2.2",
"typescript": "^4.9.5", "ts-node": "^10.0.0",
"webpack": "^5.88.1", "typescript": "^4.3.2",
"webpack-cli": "^5.1.4", "webpack": "^5.38.1",
"webpack-dev-server": "^4.15.1" "webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"resolutions": {
"kleur": "^4.1.3"
}, },
"browserslist": [ "browserslist": [
"Chrome >= 87", "> 1%",
"Firefox >= 84", "not dead",
"iOS >= 12.5", "not ie 11",
"not dead" "Chrome > 52"
], ],
"prettier": { "prettier": {
"printWidth": 80, "printWidth": 80,
@ -110,13 +115,17 @@
"trailingComma": "all", "trailingComma": "all",
"tabWidth": 2 "tabWidth": 2
}, },
"postcss": {
"autoprefixer": {}
},
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",
"resetMocks": true, "resetMocks": true,
"testEnvironment": "jsdom", "testEnvironment": "jsdom",
"moduleFileExtensions": [
"js",
"ts",
"tsx",
"json",
"node"
],
"moduleNameMapper": { "moduleNameMapper": {
"\\.css$": "<rootDir>/resources/assets/tests/__mocks__/style.ts", "\\.css$": "<rootDir>/resources/assets/tests/__mocks__/style.ts",
"\\.(png|webp)$": "<rootDir>/resources/assets/tests/__mocks__/file.ts", "\\.(png|webp)$": "<rootDir>/resources/assets/tests/__mocks__/file.ts",
@ -149,5 +158,6 @@
"isolatedModules": true "isolatedModules": true
} }
} }
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

File diff suppressed because it is too large Load Diff

5
postcss.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer'),
],
}

View File

@ -1,3 +1,5 @@
import React from 'react'
type AlertType = 'success' | 'info' | 'warning' | 'danger' type AlertType = 'success' | 'info' | 'warning' | 'danger'
const icons = new Map<AlertType, string>([ const icons = new Map<AlertType, string>([

View File

@ -1,3 +1,5 @@
import React from 'react'
interface Props { interface Props {
title?: string title?: string
onClick: React.MouseEventHandler<HTMLAnchorElement> onClick: React.MouseEventHandler<HTMLAnchorElement>

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import React, { useState } from 'react'
import * as fetch from '@/scripts/net' import * as fetch from '@/scripts/net'
interface Props { interface Props {

View File

@ -1,5 +1,5 @@
/** @jsxImportSource @emotion/react */ /** @jsxImportSource @emotion/react */
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import Autosuggest from 'react-autosuggest' import Autosuggest from 'react-autosuggest'
import { css } from '@emotion/react' import { css } from '@emotion/react'
import { emit } from '@/scripts/event' import { emit } from '@/scripts/event'

View File

@ -1,3 +1,5 @@
import React from 'react'
const Loading = () => ( const Loading = () => (
<div className="container text-center" title="Loading..."> <div className="container text-center" title="Loading...">
<i className="fas fa-sync fa-spin"></i> <i className="fas fa-sync fa-spin"></i>

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import $ from 'jquery' import $ from 'jquery'
import 'bootstrap' import 'bootstrap'
import { t } from '../scripts/i18n' import { t } from '../scripts/i18n'
@ -66,14 +66,9 @@ const Modal: React.FC<ModalOptions & Props> = (props) => {
return return
} }
const onClose = const onHidden = () => props.onClose?.()
props.onClose ||
(() => {
/* noop */
})
const onHidden = () => onClose()
const el = $(ref.current as HTMLElement) const el = $(ref.current!)
el.on('hidden.bs.modal', onHidden) el.on('hidden.bs.modal', onHidden)
return () => { return () => {
@ -97,27 +92,28 @@ const Modal: React.FC<ModalOptions & Props> = (props) => {
} }
props.onConfirm?.({ value }) props.onConfirm?.({ value })
$(ref.current as HTMLElement).modal('hide') $(ref.current!).modal('hide')
// The "hidden.bs.modal" event can't be trigged automatically when testing. // The "hidden.bs.modal" event can't be trigged automatically when testing.
/* istanbul ignore next */
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
$(ref.current as HTMLElement).trigger('hidden.bs.modal') $(ref.current!).trigger('hidden.bs.modal')
} }
} }
const dismiss = () => { const dismiss = () => {
props.onDismiss?.() props.onDismiss?.()
$(ref.current as HTMLElement).modal('hide') $(ref.current!).modal('hide')
/* istanbul ignore next */
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
$(ref.current as HTMLElement).trigger('hidden.bs.modal') $(ref.current!).trigger('hidden.bs.modal')
} }
} }
useEffect(() => { useEffect(() => {
if (show) { if (show) {
setTimeout(() => $(ref.current as HTMLElement).modal('show'), 50) setTimeout(() => $(ref.current!).modal('show'), 50)
} }
}, [show]) }, [show])

View File

@ -1,3 +1,4 @@
import React from 'react'
import ModalContent from './ModalContent' import ModalContent from './ModalContent'
import ModalInput from './ModalInput' import ModalInput from './ModalInput'
import type { Props as ContentProps } from './ModalContent' import type { Props as ContentProps } from './ModalContent'

View File

@ -1,3 +1,5 @@
import React from 'react'
export interface Props { export interface Props {
text?: string text?: string
dangerousHTML?: string dangerousHTML?: string

View File

@ -1,3 +1,5 @@
import React from 'react'
export interface Props { export interface Props {
flexFooter?: boolean flexFooter?: boolean
okButtonText?: string okButtonText?: string

View File

@ -1,3 +1,5 @@
import React from 'react'
export interface Props { export interface Props {
title?: string title?: string
} }

View File

@ -1,4 +1,4 @@
import type { HTMLAttributes } from 'react' import React, { HTMLAttributes } from 'react'
export interface Props { export interface Props {
inputType?: string inputType?: string

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import PaginationItem from './PaginationItem' import PaginationItem from './PaginationItem'

View File

@ -1,3 +1,5 @@
import React from 'react'
interface Props { interface Props {
disabled?: boolean disabled?: boolean
active?: boolean active?: boolean

View File

@ -1,5 +1,5 @@
/** @jsxImportSource @emotion/react */ /** @jsxImportSource @emotion/react */
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { css } from '@emotion/react' import { css } from '@emotion/react'
export type ToastType = 'success' | 'info' | 'warning' | 'error' export type ToastType = 'success' | 'info' | 'warning' | 'error'

View File

@ -1,5 +1,5 @@
/** @jsxImportSource @emotion/react */ /** @jsxImportSource @emotion/react */
import { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import { useMeasure } from 'react-use' import { useMeasure } from 'react-use'
import { css } from '@emotion/react' import { css } from '@emotion/react'
import styled from '@emotion/styled' import styled from '@emotion/styled'

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
const ViewerSkeleton: React.FC = () => ( const ViewerSkeleton: React.FC = () => (

View File

@ -30,7 +30,8 @@ if (route) {
</React.Suspense> </React.Suspense>
</React.StrictMode> </React.StrictMode>
) )
const c = document.querySelector(route.el) const c =
typeof route.el === 'string' ? document.querySelector(route.el) : route.el
ReactDOM.render(<Root />, c) ReactDOM.render(<Root />, c)
} }
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { Terminal } from 'xterm' import { Terminal } from 'xterm'
@ -57,8 +57,10 @@ const TerminalWindow: React.FC<{ onClose(): void }> = (props) => {
const terminal = new Terminal() const terminal = new Terminal()
const fitAddon = new FitAddon() const fitAddon = new FitAddon()
terminal.loadAddon(fitAddon) terminal.loadAddon(fitAddon)
terminal.options.fontFamily = terminal.setOption(
'Monaco, Consolas, "Roboto Mono", "Noto Sans", "Droid Sans Mono"' 'fontFamily',
'Monaco, Consolas, "Roboto Mono", "Noto Sans", "Droid Sans Mono"',
)
terminal.open(el) terminal.open(el)
fitAddon.fit() fitAddon.fit()
@ -81,7 +83,6 @@ const TerminalWindow: React.FC<{ onClose(): void }> = (props) => {
if (stack?.includes('outputHelp')) { if (stack?.includes('outputHelp')) {
terminal.writeln(data.replace(/\n/g, '\r\n')) terminal.writeln(data.replace(/\n/g, '\r\n'))
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
originalLogger(data, ...args) originalLogger(data, ...args)
} }
} }

View File

@ -22,7 +22,7 @@ export default async function pacman(stdio: Stdio, args: string[]) {
const { options } = program.parse(['', ''].concat(args), { run: false }) const { options } = program.parse(['', ''].concat(args), { run: false })
const opts: Options = options const opts: Options = options
/* istanbul ignore else */
if (opts.sync) { if (opts.sync) {
await install(opts.sync, stdio) await install(opts.sync, stdio)
} else if (opts.remove) { } else if (opts.remove) {

View File

@ -1,6 +1,7 @@
import type { Stdio } from 'blessing-skin-shell' import type { Stdio } from 'blessing-skin-shell'
import * as event from '../event' import * as event from '../event'
/* istanbul ignore next */
export function hackStdin() { export function hackStdin() {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
return process.stdin return process.stdin
@ -23,6 +24,7 @@ export function hackStdin() {
} as NodeJS.ReadStream & { _off(): void } } as NodeJS.ReadStream & { _off(): void }
} }
/* istanbul ignore next */
export function hackStdout(stdio: Stdio) { export function hackStdout(stdio: Stdio) {
return { return {
write(msg: string) { write(msg: string) {

View File

@ -1,3 +1,4 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import DarkModeButton from '@/components/DarkModeButton' import DarkModeButton from '@/components/DarkModeButton'

View File

@ -1,3 +1,4 @@
import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import EmailVerification from '@/views/widgets/EmailVerification' import EmailVerification from '@/views/widgets/EmailVerification'

View File

@ -1,6 +1,6 @@
export function getExtraData(): Record<string, any> { export function getExtraData(): Record<string, any> {
const jsonElement = document.querySelector('#blessing-extra') const jsonElement = document.querySelector('#blessing-extra')
/* istanbul ignore next */
if (jsonElement) { if (jsonElement) {
return JSON.parse(jsonElement.textContent ?? '{}') return JSON.parse(jsonElement.textContent ?? '{}')
} else { } else {

View File

@ -2,7 +2,7 @@ import { getExtraData } from './extra'
export function scrollHander() { export function scrollHander() {
const header = document.querySelector('.navbar') const header = document.querySelector('.navbar')
/* istanbul ignore else */
if (header) { if (header) {
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (window.scrollY >= (window.innerHeight * 2) / 3) { if (window.scrollY >= (window.innerHeight * 2) / 3) {
@ -14,6 +14,7 @@ export function scrollHander() {
} }
} }
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
const { transparent_navbar } = getExtraData() as { const { transparent_navbar } = getExtraData() as {
transparent_navbar: boolean transparent_navbar: boolean

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import * as fetch from '../net' import * as fetch from '../net'
import { type Texture, TextureType } from '../types' import { Texture, TextureType } from '../types'
export default function useTexture() { export default function useTexture() {
const [tid, setTid] = useState(0) const [tid, setTid] = useState(0)

View File

@ -8,6 +8,7 @@ export function t(key: string, parameters = Object.create(null)): string {
let result = '' let result = ''
for (const segment of segments) { for (const segment of segments) {
/* istanbul ignore next */
const middle = temp?.[segment] const middle = temp?.[segment]
if (!middle) { if (!middle) {
return key return key
@ -19,11 +20,9 @@ export function t(key: string, parameters = Object.create(null)): string {
} }
} }
/* eslint-disable @typescript-eslint/no-unsafe-argument */
Object.keys(parameters).forEach( Object.keys(parameters).forEach(
(slot) => (result = result.replace(`:${slot}`, parameters[slot])), (slot) => (result = result.replace(`:${slot}`, parameters[slot])),
) )
/* eslint-enable @typescript-eslint/no-unsafe-argument */
return result return result
} }

View File

@ -18,5 +18,5 @@ export async function logout() {
} }
const button = document.querySelector('#logout-button') const button = document.querySelector('#logout-button')
/* istanbul ignore next */
button?.addEventListener('click', logout) button?.addEventListener('click', logout)

View File

@ -1,5 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import Modal, { type ModalOptions, type ModalResult } from '../components/Modal' import Modal, { ModalOptions, ModalResult } from '../components/Modal'
export function showModal(options: ModalOptions = {}): Promise<ModalResult> { export function showModal(options: ModalOptions = {}): Promise<ModalResult> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -80,7 +80,7 @@ export async function walkFetch(request: Request): Promise<any> {
message = `${message}<br><details>${trace}</details>` message = `${message}<br><details>${trace}</details>`
} }
throw new HTTPError(message || (body as string), cloned) throw new HTTPError(message || body, cloned)
} catch (error: any) { } catch (error: any) {
emit('fetchError', error) emit('fetchError', error)
await showModal({ await showModal({
@ -95,10 +95,7 @@ export async function walkFetch(request: Request): Promise<any> {
} }
} }
export function get<T = any>( export function get<T = any>(url: string, params = empty): Promise<T> {
url: string,
params: Record<string, any> = empty,
): Promise<T> {
emit('beforeFetch', { emit('beforeFetch', {
method: 'GET', method: 'GET',
url, url,

View File

@ -1,3 +1,4 @@
import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import NotificationsList from '@/views/widgets/NotificationsList' import NotificationsList from '@/views/widgets/NotificationsList'

View File

@ -3,6 +3,7 @@ import { Toast } from './toast'
export const toast = new Toast() export const toast = new Toast()
/* istanbul ignore next */
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
afterEach(() => { afterEach(() => {
toast.clear() toast.clear()

View File

@ -1,3 +1,5 @@
import React from 'react'
export default [ export default [
{ {
path: 'user', path: 'user',

View File

@ -1,5 +1,6 @@
import { loadSkinToCanvas } from 'skinview-utils' import { loadSkinToCanvas } from 'skinview-utils'
/* istanbul ignore next */
function checkPixel( function checkPixel(
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
x: number, x: number,
@ -14,6 +15,7 @@ function checkPixel(
) )
} }
/* istanbul ignore next */
export function isAlex(texture: string): Promise<boolean> { export function isAlex(texture: string): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
const image = new Image() const image = new Image()

View File

@ -1,9 +1,8 @@
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import * as emitter from './event' import * as emitter from './event'
import ToastBox from '../components/Toast' import ToastBox, { ToastType } from '../components/Toast'
import type { ToastType } from '../components/Toast'
type QueueElement = { id: string; type: ToastType; message: string } type QueueElement = { id: string; type: ToastType; message: string }
type ToastQueue = QueueElement[] type ToastQueue = QueueElement[]

View File

@ -38,13 +38,9 @@ export function registerNavbarPicker(
const navbar = document.querySelector<HTMLElement>('.wrapper > nav') const navbar = document.querySelector<HTMLElement>('.wrapper > nav')
const picker = document.querySelector<HTMLDivElement>('#navbar-color-picker') const picker = document.querySelector<HTMLDivElement>('#navbar-color-picker')
/* istanbul ignore next */
if (navbar && picker) { if (navbar && picker) {
registerNavbarPicker( registerNavbarPicker(navbar, picker, blessing.extra.navbar || 'white')
navbar,
picker,
(blessing.extra.navbar as string) || 'white',
)
} }
export function registerSidebarPicker( export function registerSidebarPicker(
@ -77,10 +73,11 @@ const darkPicker = document.querySelector<HTMLDivElement>(
const lightPicker = document.querySelector<HTMLDivElement>( const lightPicker = document.querySelector<HTMLDivElement>(
'#sidebar-light-picker', '#sidebar-light-picker',
) )
/* istanbul ignore next */
if (sidebar && darkPicker && lightPicker) { if (sidebar && darkPicker && lightPicker) {
registerSidebarPicker( registerSidebarPicker(
sidebar, sidebar,
{ dark: darkPicker, light: lightPicker }, { dark: darkPicker, light: lightPicker },
(blessing.extra.sidebar as string) || 'dark-primary', blessing.extra.sidebar || 'dark-primary',
) )
} }

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import { showModal } from '@/scripts/notify' import { showModal } from '@/scripts/notify'
import type { Player } from '@/scripts/types' import type { Player } from '@/scripts/types'

View File

@ -1,3 +1,4 @@
import React from 'react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import Skeleton from 'react-loading-skeleton' import Skeleton from 'react-loading-skeleton'
import { Box } from './styles' import { Box } from './styles'

View File

@ -1,3 +1,4 @@
import React from 'react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import Skeleton from 'react-loading-skeleton' import Skeleton from 'react-loading-skeleton'

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import React, { useState } from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import { TextureType } from '@/scripts/types' import { TextureType } from '@/scripts/types'
import Modal from '@/components/Modal' import Modal from '@/components/Modal'

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { Player } from '@/scripts/types' import type { Player } from '@/scripts/types'
import ButtonEdit from '@/components/ButtonEdit' import ButtonEdit from '@/components/ButtonEdit'

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useLayoutEffect } from 'react' import React, { useState, useEffect, useLayoutEffect } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen' import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen'
@ -47,7 +47,6 @@ const PlayersManagement: React.FC = () => {
useEffect(() => { useEffect(() => {
getPlayers() getPlayers()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page]) }, [page])
const handleModeChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleModeChange = (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -1,3 +1,4 @@
import React from 'react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { Plugin } from './types' import type { Plugin } from './types'

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { Plugin } from './types' import type { Plugin } from './types'

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react' import React, { useState, useEffect, useMemo } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { enableMapSet } from 'immer' import { enableMapSet } from 'immer'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'

View File

@ -1,7 +1,8 @@
import React from 'react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { Texture } from '@/scripts/types' import type { Texture } from '@/scripts/types'
import { type Report, Status } from './types' import { Report, Status } from './types'
const Card = styled.div` const Card = styled.div`
width: 240px; width: 240px;

View File

@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader/root'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net' import * as fetch from '@/scripts/net'
import { type Paginator, type Texture, TextureType } from '@/scripts/types' import { Paginator, Texture, TextureType } from '@/scripts/types'
import { toast, showModal } from '@/scripts/notify' import { toast, showModal } from '@/scripts/notify'
import Loading from '@/components/Loading' import Loading from '@/components/Loading'
import Pagination from '@/components/Pagination' import Pagination from '@/components/Pagination'
@ -37,7 +37,6 @@ const ReportsManagement: React.FC = () => {
useEffect(() => { useEffect(() => {
getReports() getReports()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page]) }, [page])
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -1,5 +1,5 @@
import styled from '@emotion/styled' import styled from '@emotion/styled'
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { Line } from './types' import type { Line } from './types'

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'

View File

@ -1,4 +1,4 @@
import { post, type ResponseBody } from '../../scripts/net' import { post, ResponseBody } from '../../scripts/net'
import { showModal } from '../../scripts/notify' import { showModal } from '../../scripts/notify'
import { t } from '../../scripts/i18n' import { t } from '../../scripts/i18n'

View File

@ -1,3 +1,4 @@
import React from 'react'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import type { User } from '@/scripts/types' import type { User } from '@/scripts/types'
import { Box, Icon, InfoTable } from './styles' import { Box, Icon, InfoTable } from './styles'

View File

@ -1,3 +1,4 @@
import React from 'react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import Skeleton from 'react-loading-skeleton' import Skeleton from 'react-loading-skeleton'
import { t } from '@/scripts/i18n' import { t } from '@/scripts/i18n'

Some files were not shown because too many files have changed in this diff Show More