...
This commit is contained in:
@@ -11,6 +11,37 @@ class FileTreeActions {
|
||||
this.clipboard = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize filename/folder name
|
||||
* Returns { valid: boolean, sanitized: string, message: string }
|
||||
*/
|
||||
validateFileName(name, isFolder = false) {
|
||||
const type = isFolder ? 'folder' : 'file';
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return { valid: false, message: `${type} name cannot be empty` };
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
const validPattern = /^[a-z0-9_]+(\.[a-z0-9_]+)*$/;
|
||||
|
||||
if (!validPattern.test(name)) {
|
||||
const sanitized = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_.]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
sanitized,
|
||||
message: `Invalid characters in ${type} name. Only lowercase letters, numbers, and underscores allowed.\n\nSuggestion: "${sanitized}"`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, sanitized: name, message: '' };
|
||||
}
|
||||
|
||||
async execute(action, targetPath, isDirectory) {
|
||||
const handler = this.actions[action];
|
||||
if (!handler) {
|
||||
@@ -35,25 +66,62 @@ class FileTreeActions {
|
||||
|
||||
'new-file': async function(path, isDir) {
|
||||
if (!isDir) return;
|
||||
const filename = await this.showInputDialog('Enter filename:', 'new-file.md');
|
||||
if (filename) {
|
||||
|
||||
await this.showInputDialog('Enter filename (lowercase, underscore only):', 'new_file.md', async (filename) => {
|
||||
if (!filename) return;
|
||||
|
||||
const validation = this.validateFileName(filename, false);
|
||||
|
||||
if (!validation.valid) {
|
||||
showNotification(validation.message, 'warning');
|
||||
|
||||
// Ask if user wants to use sanitized version
|
||||
if (validation.sanitized) {
|
||||
if (await this.showConfirmDialog('Use sanitized name?', `${filename} → ${validation.sanitized}`)) {
|
||||
filename = validation.sanitized;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fullPath = `${path}/${filename}`.replace(/\/+/g, '/');
|
||||
await this.webdavClient.put(fullPath, '# New File\n\n');
|
||||
await this.fileTree.load();
|
||||
showNotification(`Created ${filename}`, 'success');
|
||||
await this.editor.loadFile(fullPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'new-folder': async function(path, isDir) {
|
||||
if (!isDir) return;
|
||||
const foldername = await this.showInputDialog('Enter folder name:', 'new-folder');
|
||||
if (foldername) {
|
||||
|
||||
await this.showInputDialog('Enter folder name (lowercase, underscore only):', 'new_folder', async (foldername) => {
|
||||
if (!foldername) return;
|
||||
|
||||
const validation = this.validateFileName(foldername, true);
|
||||
|
||||
if (!validation.valid) {
|
||||
showNotification(validation.message, 'warning');
|
||||
|
||||
if (validation.sanitized) {
|
||||
if (await this.showConfirmDialog('Use sanitized name?', `${foldername} → ${validation.sanitized}`)) {
|
||||
foldername = validation.sanitized;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fullPath = `${path}/${foldername}`.replace(/\/+/g, '/');
|
||||
await this.webdavClient.mkcol(fullPath);
|
||||
await this.fileTree.load();
|
||||
showNotification(`Created folder ${foldername}`, 'success');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
rename: async function(path, isDir) {
|
||||
@@ -140,27 +208,36 @@ class FileTreeActions {
|
||||
};
|
||||
|
||||
// Modern dialog implementations
|
||||
async showInputDialog(title, placeholder = '') {
|
||||
async showInputDialog(title, placeholder = '', callback) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = this.createInputDialog(title, placeholder);
|
||||
const input = dialog.querySelector('input');
|
||||
const confirmBtn = dialog.querySelector('.btn-primary');
|
||||
const cancelBtn = dialog.querySelector('.btn-secondary');
|
||||
|
||||
const cleanup = () => {
|
||||
const cleanup = (value) => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(dialog);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
dialog.remove();
|
||||
const backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) backdrop.remove();
|
||||
document.body.classList.remove('modal-open');
|
||||
resolve(value);
|
||||
if (callback) callback(value);
|
||||
};
|
||||
|
||||
confirmBtn.onclick = () => {
|
||||
resolve(input.value.trim());
|
||||
cleanup();
|
||||
cleanup(input.value.trim());
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
cleanup(null);
|
||||
};
|
||||
|
||||
dialog.addEventListener('hidden.bs.modal', () => {
|
||||
resolve(null);
|
||||
cleanup();
|
||||
cleanup(null);
|
||||
});
|
||||
|
||||
input.onkeypress = (e) => {
|
||||
@@ -175,26 +252,35 @@ class FileTreeActions {
|
||||
});
|
||||
}
|
||||
|
||||
async showConfirmDialog(title, message = '') {
|
||||
async showConfirmDialog(title, message = '', callback) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = this.createConfirmDialog(title, message);
|
||||
const confirmBtn = dialog.querySelector('.btn-danger');
|
||||
const cancelBtn = dialog.querySelector('.btn-secondary');
|
||||
|
||||
const cleanup = () => {
|
||||
const cleanup = (value) => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(dialog);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
dialog.remove();
|
||||
const backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) backdrop.remove();
|
||||
document.body.classList.remove('modal-open');
|
||||
resolve(value);
|
||||
if (callback) callback(value);
|
||||
};
|
||||
|
||||
confirmBtn.onclick = () => {
|
||||
resolve(true);
|
||||
cleanup();
|
||||
cleanup(true);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
cleanup(false);
|
||||
};
|
||||
|
||||
dialog.addEventListener('hidden.bs.modal', () => {
|
||||
resolve(false);
|
||||
cleanup();
|
||||
cleanup(false);
|
||||
});
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
Reference in New Issue
Block a user