seitime-frappe/frappe/public/js/frappe/views/file/file_view.js
Akhil Narang 3dfa9f35dc
fix: escape HTML in filename before display (#34289)
* Revert "fix: sanitize HTML in file names before saving (#34192)"

This reverts commit 0120410593.

* feat: escape file name before display

Signed-off-by: Akhil Narang <me@akhilnarang.dev>

---------

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2025-10-07 11:50:09 +05:30

540 lines
13 KiB
JavaScript

frappe.provide("frappe.views");
frappe.views.FileView = class FileView extends frappe.views.ListView {
static load_last_view() {
const route = frappe.get_route();
if (route.length === 2) {
const view_user_settings = frappe.get_user_settings("File", "File");
frappe.set_route(
"List",
"File",
view_user_settings.last_folder || frappe.boot.home_folder
);
return true;
}
return redirect_to_home_if_invalid_route();
}
get view_name() {
return "File";
}
show() {
if (!redirect_to_home_if_invalid_route()) {
super.show();
}
}
setup_view() {
this.render_header();
this.setup_events();
this.$page.find(".layout-main-section-wrapper").addClass("file-view");
this.add_file_action_buttons();
this.page.add_button(__("Toggle Grid View"), () => {
frappe.views.FileView.grid_view = !frappe.views.FileView.grid_view;
this.refresh();
});
}
setup_no_result_area() {
this.$no_result = $(`<div class="no-result">
<div class="breadcrumbs">${this.get_breadcrumbs_html()}</div>
<div class="text-muted flex justify-center align-center">
${this.get_no_result_message()}
</div>
</div>`).hide();
this.$frappe_list.append(this.$no_result);
}
get_args() {
let args = super.get_args();
if (frappe.views.FileView.grid_view) {
Object.assign(args, {
order_by: `is_folder desc, ${this.sort_by} ${this.sort_order}`,
});
}
return args;
}
set_breadcrumbs() {
const route = frappe.get_route();
route.splice(-1);
const last_folder = route[route.length - 1];
if (last_folder === "File") return;
frappe.breadcrumbs.add({
type: "Custom",
label: __("Home"),
route: "/app/List/File/Home",
});
}
setup_defaults() {
return super.setup_defaults().then(() => {
this.page_title = __("File Manager");
const route = frappe.get_route();
this.current_folder = route.slice(2).join("/") || "Home";
this.filters = [["File", "folder", "=", this.current_folder, true]];
this.order_by = this.view_user_settings.order_by || "file_name asc";
this.menu_items = this.menu_items.concat(this.file_menu_items());
});
}
file_menu_items() {
return [
{
label: __("Home"),
action: () => {
frappe.set_route("List", "File", "Home");
},
},
{
label: __("New Folder"),
action: () => {
frappe.prompt(
__("Name"),
(values) => {
if (values.value.indexOf("/") > -1) {
frappe.throw(__("Folder name should not include '/' (slash)"));
}
const data = {
file_name: values.value,
folder: this.current_folder,
};
frappe.call({
method: "frappe.core.api.file.create_new_folder",
args: data,
});
},
__("Enter folder name"),
__("Create")
);
},
},
{
label: __("Import Zip"),
action: () => {
new frappe.ui.FileUploader({
folder: this.current_folder,
restrictions: {
allowed_file_types: [".zip"],
},
on_success: (file) => {
frappe.show_alert(__("Unzipping files..."));
frappe
.call("frappe.core.api.file.unzip_file", {
name: file.name,
})
.then((r) => {
if (r.message) {
frappe.show_alert(__("Unzipped {0} files", [r.message]));
}
});
},
});
},
},
];
}
add_file_action_buttons() {
this.$cut_button = this.page
.add_button(__("Cut"), () => {
frappe.file_manager.cut(this.get_checked_items(), this.current_folder);
this.$checks.parents(".file-wrapper").addClass("cut");
})
.hide();
this.$paste_btn = this.page
.add_button(__("Paste"), () => frappe.file_manager.paste(this.current_folder))
.hide();
this.page.add_actions_menu_item(__("Export as zip"), () => {
let docnames = this.get_checked_items(true);
if (docnames.length) {
open_url_post("/api/method/frappe.core.api.file.zip_files", {
files: JSON.stringify(docnames),
});
}
});
}
set_fields() {
this.fields = this.meta.fields
.filter((df) => frappe.model.is_value_type(df.fieldtype) && !df.hidden)
.map((df) => df.fieldname)
.concat(["name", "modified", "creation"]);
}
prepare_data(data) {
super.prepare_data(data);
this.prepare_file_data();
}
prepare_file_data() {
this.data = this.data.map((d) => this.prepare_datum(d));
// Bring folders to the top
const { sort_by } = this.sort_selector;
if (sort_by === "file_name") {
this.data.sort((a, b) => {
if (a.is_folder && !b.is_folder) {
return -1;
}
if (!a.is_folder && b.is_folder) {
return 1;
}
return 0;
});
}
}
prepare_datum(d) {
let icon_class = "";
let type = "";
let title;
if (d.is_folder) {
icon_class = "folder-normal";
type = "folder";
} else if (frappe.utils.is_image_file(d.file_name)) {
icon_class = "image";
type = "image";
} else {
icon_class = "file";
type = "file";
}
if (type === "folder") {
title = this.get_folder_title(d.file_name);
} else {
title = d.file_name || d.file_url;
}
title = frappe.utils.escape_html(title);
title = title.slice(0, 60);
d._title = title;
d.icon_class = icon_class;
d._type = type;
d.subject_html = `
${frappe.utils.icon(icon_class)}
<span>${title}</span>
${d.is_private ? '<i class="fa fa-lock fa-fw text-warning"></i>' : ""}
`;
return d;
}
get_folder_title(folder_name) {
// "Home" and "Attachments" are default folders that are always created in english.
// So we can and should translate them to the user's language.
if (["Home", "Attachments"].includes(folder_name)) {
return __(folder_name);
} else {
return folder_name;
}
}
before_render() {
super.before_render();
frappe.model.user_settings.save("File", "grid_view", frappe.views.FileView.grid_view);
this.save_view_user_settings({
last_folder: this.current_folder,
});
}
render() {
this.$result.empty().removeClass("file-grid-view");
if (frappe.views.FileView.grid_view) {
this.prepare_file_data();
this.render_grid_view();
} else {
super.render();
this.render_header();
this.render_count();
}
}
after_render() {}
render_list() {
if (frappe.views.FileView.grid_view) {
this.prepare_file_data();
this.render_grid_view();
} else {
super.render_list();
}
}
remove_list_items(names) {
if (frappe.views.FileView.grid_view) {
for (let name of names) {
this.$result
.find(`.file-wrapper[data-name='${name.replace(/'/g, "\\'")}']`)
.remove();
}
} else {
super.remove_list_items(names);
}
}
render_grid_view() {
const base_url = frappe.urllib.get_base_url();
let html = this.data
.map((d) => {
const icon_class = d.icon_class + "-large";
const align_file_body_class =
d._type == "image" ? "align-flex-start" : "align-center";
const file_url = frappe.utils.escape_html(d.file_url);
const absolute_file_url = base_url + file_url;
let file_body_html =
d._type == "image"
? `<div class="file-image"><img class="w-100" src="${file_url}" alt="${d.file_name}"></div>`
: frappe.utils.icon(icon_class, {
width: "40px",
height: "45px",
});
const name = escape(d.name);
const copy_url_btn = `
<div class="copy-file-url hidden-xs" title="${__(
"Copy File URL"
)}" data-file-url="${absolute_file_url}">
<svg class="es-icon es-line icon-sm" aria-hidden="true">
<use class="" href="#es-line-copy-light"></use>
</svg>
</div>
`;
const draggable = d.type == "Folder" ? false : true;
return `
<a href="${this.get_route_url(d)}"
draggable="${draggable}" class="file-wrapper ellipsis" data-name="${name}">
<div class="file-header level w-100">
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" data-name="${name}">
${!d.is_folder ? copy_url_btn : ""}
</div>
<div class="file-body ${align_file_body_class}">
${file_body_html}
</div>
<div class="file-footer">
<div class="file-title ellipsis">${d._title}</div>
<div class="file-creation">${this.get_creation_date(d)}</div>
</div>
</a>
`;
})
.join("");
this.$result.addClass("file-grid-view");
this.$result.empty().html(
`<div class="file-grid">
${html}
</div>`
);
}
get_breadcrumbs_html() {
const route = frappe.get_route();
const folders = route.slice(2);
return folders
.map((folder, i) => {
const title = this.get_folder_title(folder);
if (i === folders.length - 1) {
return `<span>${title}</span>`;
}
const route = folders.reduce((acc, curr, j) => {
if (j <= i) {
acc += "/" + curr;
}
return acc;
}, "/app/file/view");
return `<a href="${route}">${title}</a>`;
})
.join("&nbsp;/&nbsp;");
}
get_header_html() {
const breadcrumbs_html = this.get_breadcrumbs_html();
let header_selector_html = !frappe.views.FileView.grid_view
? `<input class="level-item list-check-all hidden-xs" type="checkbox" title="${__(
"Select All"
)}">`
: "";
let header_columns_html = !frappe.views.FileView.grid_view
? `<div class="list-row-col ellipsis hidden-xs">
<span>${__("Size")}</span>
</div>
<div class="list-row-col ellipsis hidden-xs">
<span>${__("Type")}</span>
</div>
<div class="list-row-col ellipsis hidden-xs">
<span>${__("Created")}</span>
</div>`
: "";
let subject_html = `
<div class="list-row-col list-subject level">
${header_selector_html}
<span class="level-item">${breadcrumbs_html}</span>
</div>
${header_columns_html}
`;
return this.get_header_html_skeleton(subject_html, '<span class="list-count"></span>');
}
get_route_url(file) {
return file.is_folder ? "/app/List/File/" + file.name : this.get_form_link(file);
}
get_creation_date(file) {
const [date] = file.creation.split(" ");
let created_on;
if (date === frappe.datetime.now_date()) {
created_on = comment_when(file.creation);
} else {
created_on = frappe.datetime.str_to_user(date);
}
return created_on;
}
get_left_html(file) {
file = this.prepare_datum(file);
const file_size = file.file_size ? frappe.form.formatters.FileSize(file.file_size) : "";
const route_url = this.get_route_url(file);
return `
<div class="list-row-col ellipsis list-subject level">
<span class="level-item file-select">
<input class="list-row-checkbox"
type="checkbox" data-name="${file.name}">
</span>
<span class="level-item ellipsis" title="${frappe.utils.escape_html(file.file_name)}">
<a class="ellipsis" href="${route_url}" title="${frappe.utils.escape_html(file.file_name)}">
${file.subject_html}
</a>
</span>
</div>
<div class="list-row-col ellipsis hidden-xs text-muted">
<span>${file_size}</span>
</div>
<div class="list-row-col ellipsis hidden-xs text-muted">
<span>${file.file_type || ""}</span>
</div>
<div class="list-row-col ellipsis hidden-xs text-muted">
<span>${this.get_creation_date(file)}</span>
</div>
`;
}
get_right_html(file) {
return `
<div class="level-item list-row-activity">
${comment_when(file.modified)}
</div>
`;
}
setup_events() {
super.setup_events();
this.setup_drag_events();
this.setup_copy_event();
}
setup_drag_events() {
this.$result.on("dragstart", ".files .file-wrapper", (e) => {
e.stopPropagation();
e.originalEvent.dataTransfer.setData("Text", $(e.currentTarget).attr("data-name"));
e.target.style.opacity = "0.4";
frappe.file_manager.cut(
[{ name: $(e.currentTarget).attr("data-name") }],
this.current_folder
);
});
this.$result.on(
"dragover",
(e) => {
e.preventDefault();
},
false
);
this.$result.on("dragend", ".files .file-wrapper", (e) => {
e.preventDefault();
e.stopPropagation();
e.target.style.opacity = "1";
});
this.$result.on("drop", (e) => {
e.stopPropagation();
e.preventDefault();
const $el = $(e.target).parents(".file-wrapper");
let dataTransfer = e.originalEvent.dataTransfer;
if (!dataTransfer) return;
if (dataTransfer.files && dataTransfer.files.length > 0) {
new frappe.ui.FileUploader({
files: dataTransfer.files,
folder: this.current_folder,
});
} else if (dataTransfer.getData("Text")) {
if ($el.parents(".folders").length !== 0) {
const file_name = dataTransfer.getData("Text");
const folder_name = decodeURIComponent($el.attr("data-name"));
frappe.file_manager.paste(folder_name);
frappe.show_alert(`File ${file_name} moved to ${folder_name}`);
}
}
});
}
setup_copy_event() {
this.$result.on("click", ".copy-file-url", (e) => {
frappe.utils.copy_to_clipboard(e.currentTarget.getAttribute("data-file-url"));
e.preventDefault();
e.stopPropagation();
});
}
toggle_result_area() {
super.toggle_result_area();
this.toggle_cut_paste_buttons();
}
on_row_checked() {
super.on_row_checked();
this.toggle_cut_paste_buttons();
}
toggle_cut_paste_buttons() {
const hide_paste_btn =
!frappe.file_manager.can_paste ||
frappe.file_manager.old_folder === this.current_folder;
const hide_cut_btn = !(this.$checks && this.$checks.length > 0);
this.$paste_btn.toggle(!hide_paste_btn);
this.$cut_button.toggle(!hide_cut_btn);
}
};
frappe.views.FileView.grid_view = frappe.get_user_settings("File").grid_view || false;
function redirect_to_home_if_invalid_route() {
const route = frappe.get_route();
if (route[2] === "List") {
// if the user somehow redirects to List/File/List
// redirect back to Home
frappe.set_route("List", "File", "Home");
return true;
}
return false;
}