-
Notifications
You must be signed in to change notification settings - Fork 15
[WIP, NOT ready for review] 直接替换maidata.txt谱面的功能 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2cbdcda
104429d
6f99149
3c01ae0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,15 +95,29 @@ public void EditChartEnable(int id, int level, [FromBody] bool value, string ass | |
| } | ||
|
|
||
| [HttpPost] | ||
| public void ReplaceChart(int id, int level, IFormFile file, string assetDir) | ||
| public void ReplaceChart(int id, int level, IFormFile file, string assetDir, | ||
| [FromForm] ImportChartController.ShiftMethod? shift) | ||
| { | ||
| var music = settings.GetMusic(id, assetDir); | ||
| if (music == null || file == null) return; | ||
| // TODO 判断是MA2还是maidata.txt,走不同的逻辑 | ||
| var targetChart = music.Charts[level]; | ||
| targetChart.Path = $"{id:000000}_0{level}.ma2"; | ||
| using var stream = System.IO.File.Open(Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{id:000000}", targetChart.Path), FileMode.Create); | ||
| file.CopyTo(stream); | ||
| targetChart.Problems.Clear(); | ||
| stream.Close(); | ||
|
|
||
| // 检查新谱面ma2的音符数量是否有变化,如果有修正之 | ||
| string fileContent; | ||
| using (var reader = new StreamReader(file.OpenReadStream())) | ||
| { | ||
| fileContent = reader.ReadToEnd(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个不用太管吧,后端是在用户自己的电脑上运行的,我DoS我自己吗? |
||
| } | ||
| var newMaxNotes = ImportChartController.ParseTNumAllFromMa2(fileContent); | ||
| if (newMaxNotes != 0 && targetChart.MaxNotes != newMaxNotes) | ||
| { | ||
| targetChart.MaxNotes = newMaxNotes; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,68 @@ | ||
| import { t } from '@/locales'; | ||
| import { globalCapture, selectedADir, selectedLevel, selectedMusic, selectMusicId, updateMusicList } from '@/store/refs'; | ||
| import { NButton, NFlex, NModal, useMessage } from 'naive-ui'; | ||
| import { defineComponent, PropType, ref, computed, watch, shallowRef } from 'vue'; | ||
| import { NButton, NFlex, NModal, useDialog, useMessage } from 'naive-ui'; | ||
| import { defineComponent, ref, shallowRef } from 'vue'; | ||
| import JacketBox from '../JacketBox'; | ||
| import { DIFFICULTY, LEVEL_COLOR } from '@/consts'; | ||
| import api from '@/client/api'; | ||
| import CheckingModal from "@/components/ImportCreateChartButton/ImportChartButton/CheckingModal"; | ||
|
|
||
| export const replaceChartFileHandle = shallowRef<FileSystemFileHandle | null>(null); | ||
| export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { | ||
| } | ||
|
|
||
| export default defineComponent({ | ||
| // props: { | ||
| // }, | ||
| setup(props, { emit }) { | ||
| const message = useMessage(); | ||
| const dialog = useDialog(); | ||
|
|
||
| const replaceChart = async () => { | ||
| if (!replaceChartFileHandle.value) return; | ||
| const checking = ref(false); | ||
| const ma2Handle = shallowRef<FileSystemFileHandle | null>(null); | ||
|
|
||
| prepareReplaceChart = async (fileHandle?: FileSystemFileHandle) => { | ||
| if (!fileHandle) { | ||
| [fileHandle] = await window.showOpenFilePicker({ | ||
| id: 'chart', | ||
| startIn: 'downloads', | ||
| types: [ | ||
| { | ||
| description: t('music.edit.supportedFileTypes'), | ||
| accept: { | ||
| "application/x-supported": [".ma2", ".txt"], // 没办法限定只匹配maidata.txt,就只好先把一切txt都作为匹配 | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| if (!fileHandle) return; // 用户未选择文件 | ||
|
|
||
| const name = fileHandle.name; | ||
| // 对maidata.txt和ma2分类讨论,前者执行ImportCheck | ||
| if (name == "maidata.txt") { | ||
| try { | ||
| checking.value = true; | ||
| const file = await fileHandle.getFile(); | ||
| const checkRet = (await api.ImportChartCheck({file, isReplacement: true})).data; | ||
| if (!checking.value) return; // 说明检查期间用户点击了关闭按钮、取消了操作。则不再执行后续流程。 | ||
| // TODO 显示导入界面(类似ErrorDisplayIdInput)、完成导入流程 | ||
| console.log(checkRet) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| dialog.error({title: "NotImplemented"}) | ||
| } finally { | ||
| checking.value = false; | ||
| } | ||
| } else if (name.endsWith(".ma2")) { | ||
| ma2Handle.value = fileHandle | ||
| } else { | ||
| dialog.error({title: t('error.unsupportedFileType'), content: t('music.edit.notValidChartFile')}) | ||
| } | ||
| } | ||
|
|
||
| const replaceMa2 = async () => { | ||
| if (!ma2Handle.value) return; | ||
| try { | ||
| const file = await replaceChartFileHandle.value.getFile(); | ||
| replaceChartFileHandle.value = null; | ||
| const file = await ma2Handle.value.getFile(); | ||
| ma2Handle.value = null; | ||
| await api.ReplaceChart(selectMusicId.value, selectedLevel.value, selectedADir.value, { file }); | ||
| message.success(t('music.edit.replaceChartSuccess')); | ||
| await updateMusicList(); | ||
|
|
@@ -28,34 +72,37 @@ export default defineComponent({ | |
| } | ||
| } | ||
|
|
||
| return () => <NModal | ||
| preset="card" | ||
| class="w-[min(90vw,50em)]" | ||
| title={t('music.edit.replaceChart')} | ||
| show={replaceChartFileHandle.value !== null} | ||
| onUpdateShow={() => replaceChartFileHandle.value = null} | ||
| >{{ | ||
| default: () => <div class="flex flex-col gap-2"> | ||
| {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} | ||
| <div class="text-4.5 text-center">{replaceChartFileHandle.value?.name}</div> | ||
| <div class="text-6 text-center">↓</div> | ||
| <div class="flex justify-center gap-2"> | ||
| <JacketBox info={selectedMusic.value!} class="h-8em w-8em" upload={false} /> | ||
| <div class="flex flex-col gap-1 max-w-24em justify-end"> | ||
| <div class="text-3.5 op-70">#{selectMusicId.value}</div> | ||
| <div class="text-2xl overflow-hidden text-ellipsis whitespace-nowrap">{selectedMusic.value!.name}</div> | ||
| <div class="flex"> | ||
| <div class="c-white rounded-full px-2" style={{ backgroundColor: LEVEL_COLOR[selectedLevel.value!] }}> | ||
| {selectedMusic.value!.charts![selectedLevel.value!]?.level}.{selectedMusic.value!.charts![selectedLevel.value!]?.levelDecimal} | ||
| return () => <div> | ||
| <NModal | ||
| preset="card" | ||
| class="w-[min(90vw,50em)]" | ||
| title={t('music.edit.replaceChart')} | ||
| show={ma2Handle.value !== null} | ||
| onUpdateShow={() => ma2Handle.value = null} | ||
| >{{ | ||
| default: () => <div class="flex flex-col gap-2"> | ||
| {t('music.edit.replaceChartConfirm', { level: DIFFICULTY[selectedLevel.value!] })} | ||
| <div class="text-4.5 text-center">{ma2Handle.value?.name}</div> | ||
| <div class="text-6 text-center">↓</div> | ||
| <div class="flex justify-center gap-2"> | ||
| <JacketBox info={selectedMusic.value!} class="h-8em w-8em" upload={false} /> | ||
| <div class="flex flex-col gap-1 max-w-24em justify-end"> | ||
| <div class="text-3.5 op-70">#{selectMusicId.value}</div> | ||
| <div class="text-2xl overflow-hidden text-ellipsis whitespace-nowrap">{selectedMusic.value!.name}</div> | ||
| <div class="flex"> | ||
| <div class="c-white rounded-full px-2" style={{ backgroundColor: LEVEL_COLOR[selectedLevel.value!] }}> | ||
| {selectedMusic.value!.charts![selectedLevel.value!]?.level}.{selectedMusic.value!.charts![selectedLevel.value!]?.levelDecimal} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div>, | ||
| footer: () => <NFlex justify="end"> | ||
| <NButton onClick={() => replaceChartFileHandle.value = null}>{t('common.cancel')}</NButton> | ||
| <NButton onClick={replaceChart} type="primary">{t('common.confirm')}</NButton> | ||
| </NFlex> | ||
| }}</NModal>; | ||
| </div>, | ||
| footer: () => <NFlex justify="end"> | ||
| <NButton onClick={() => ma2Handle.value = null}>{t('common.cancel')}</NButton> | ||
| <NButton onClick={replaceMa2} type="primary">{t('common.confirm')}</NButton> | ||
| </NFlex> | ||
| }}</NModal> | ||
| <CheckingModal title={t('chart.import.checkingTitle')} show={checking.value} closeModal={()=>checking.value=false} /> | ||
| </div>; | ||
| }, | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
assetDirparameter, which is a route parameter and thus user-controlled, is used to construct file paths usingPath.Combine(e.g., on line 106) without any validation or sanitization. An attacker can provide a path containing directory traversal sequences (e.g.,..) to escape the intended directory and overwrite arbitrary files on the server where the application has write permissions.Remediation: Validate the
assetDirparameter against an allowlist of expected directory names or sanitize it to remove any path traversal sequences before using it in file system operations.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@clansty 这个用管吗😰 虽然他说有问题的这段并不是我写的,我只是加了个shift参数而已(