diff --git a/src/Surface.zig b/src/Surface.zig index 476359dbe..9865efcfb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2177,7 +2177,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool // If you split it across two then the shell can interpret it // as two literals. var buf: [128]u8 = undefined; - const full_data = try std.fmt.bufPrint(&buf, "\x1b{s}{s}", .{if(action==.csi)"["else"", data}); + const full_data = try std.fmt.bufPrint(&buf, "\x1b{s}{s}", .{ if (action == .csi) "[" else "", data }); _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, full_data, @@ -2457,9 +2457,10 @@ pub fn completeClipboardRequest( self: *Surface, req: apprt.ClipboardRequest, data: []const u8, + force: bool, // Dialog has been shown, and ignoring unsafe pastes. ) !void { switch (req) { - .paste => try self.completeClipboardPaste(data), + .paste => try self.completeClipboardPaste(data, force), .osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind), } } @@ -2485,9 +2486,24 @@ fn startClipboardRequest( try self.rt_surface.clipboardRequest(loc, req); } -fn completeClipboardPaste(self: *Surface, data: []const u8) !void { +fn sanatizeClipboardPaste(data: []const u8) !void { + // Split into lines. + var lines = std.mem.splitSequence(u8, data, "\n"); + + // If there's only one line no need to proceed. + if (std.mem.eql(u8, lines.next().?, data)) return; + + // Warning popup. + return error.UnsafePaste; +} + +fn completeClipboardPaste(self: *Surface, data: []const u8, force: bool) !void { if (data.len == 0) return; + if (!force) { + try sanatizeClipboardPaste(data); + } + const bracketed = bracketed: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 5e5c143b4..68a2d1d0d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -24,6 +24,7 @@ const build_options = @import("build_options"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); +const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const c = @import("c.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -47,6 +48,9 @@ menu: ?*c.GMenu = null, /// The configuration errors window, if it is currently open. config_errors_window: ?*ConfigErrorsWindow = null, +/// The unsafe paste window, if it is currently open. +unsafe_paste_window: ?*UnsafePasteWindow = null, + /// This is set to false when the main loop should exit. running: bool = true, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1ec79184c..1b7fff41a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -17,6 +17,8 @@ const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); +const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); + const log = std.log.scoped(.gtk); /// This is detected by the OpenGL renderer to move to a single-threaded @@ -546,9 +548,22 @@ fn gtkClipboardRead( return; } defer c.g_free(cstr); - const str = std.mem.sliceTo(cstr, 0); - self.core_surface.completeClipboardRequest(req.state, str) catch |err| { + + self.core_surface.completeClipboardRequest(req.state, str, false) catch |err| { + if (err == error.UnsafePaste) { + // Create a dialog and ask the user if they want to paste anyway. + UnsafePasteWindow.create( + self.app, + str, + self.core_surface, + req.state, + ) catch |window_err| { + log.err("failed to create unsafe paste window err={}", .{window_err}); + }; + return; + } + log.err("failed to complete clipboard request err={}", .{err}); }; } @@ -559,7 +574,6 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa .selection => c.gtk_widget_get_primary_clipboard(widget), }; } - pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { return self.cursor_pos; } diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/UnsafePasteWindow.zig new file mode 100644 index 000000000..081329a09 --- /dev/null +++ b/src/apprt/gtk/UnsafePasteWindow.zig @@ -0,0 +1,216 @@ +/// Configuration errors window. +const UnsafePaste = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const CoreSurface = @import("../../Surface.zig"); +const ClipboardRequest = @import("../structs.zig").ClipboardRequest; +const App = @import("App.zig"); +const View = @import("View.zig"); +const c = @import("c.zig"); + +const log = std.log.scoped(.gtk); + +app: *App, +window: *c.GtkWindow, +view: PrimaryView, + +data: []u8, +core_surface: CoreSurface, +pending_req: ClipboardRequest, + +pub fn create( + app: *App, + data: []const u8, + core_surface: CoreSurface, + request: ClipboardRequest, +) !void { + if (app.unsafe_paste_window != null) return error.WindowAlreadyExists; + + const alloc = app.core_app.alloc; + const self = try alloc.create(UnsafePaste); + errdefer alloc.destroy(self); + try self.init( + app, + data, + core_surface, + request, + ); + + app.unsafe_paste_window = self; +} + +/// Not public because this should be called by the GTK lifecycle. +fn destroy(self: *UnsafePaste) void { + const alloc = self.app.core_app.alloc; + self.app.config_errors_window = null; + alloc.destroy(self); +} + +fn init( + self: *UnsafePaste, + app: *App, + data: []const u8, + core_surface: CoreSurface, + request: ClipboardRequest, +) !void { + // Create the window + const window = c.gtk_window_new(); + const gtk_window: *c.GtkWindow = @ptrCast(window); + errdefer c.gtk_window_destroy(gtk_window); + c.gtk_window_set_title(gtk_window, "Unsafe Paste!"); + c.gtk_window_set_default_size(gtk_window, 600, 400); + c.gtk_window_set_resizable(gtk_window, 0); + _ = c.g_signal_connect_data( + window, + "destroy", + c.G_CALLBACK(>kDestroy), + self, + null, + c.G_CONNECT_DEFAULT, + ); + + // Set some state + self.* = .{ + .app = app, + .window = gtk_window, + .view = undefined, + .data = try app.core_app.alloc.dupe(u8, data), + .core_surface = core_surface, + .pending_req = request, + }; + + // Show the window + const view = try PrimaryView.init(self, data); + self.view = view; + c.gtk_window_set_child(@ptrCast(window), view.root); + c.gtk_widget_show(window); +} + +fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); + self.destroy(); +} + +fn userdataSelf(ud: *anyopaque) *UnsafePaste { + return @ptrCast(@alignCast(ud)); +} + +const PrimaryView = struct { + root: *c.GtkWidget, + text: *c.GtkTextView, + + pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { + // All our widgets + const label = c.gtk_label_new( + \\ Pasting this text into the terminal may be dangerous as + \\ unintended commands may be be executed. + ); + const buf = unsafeBuffer(data); + defer c.g_object_unref(buf); + const buttons = try ButtonsView.init(root); + const text_scroll = c.gtk_scrolled_window_new(); + errdefer c.g_object_unref(text_scroll); + const text = c.gtk_text_view_new_with_buffer(buf); + errdefer c.g_object_unref(text); + c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text); + + // Create our view + const view = try View.init(&.{ + .{ .name = "label", .widget = label }, + .{ .name = "text", .widget = text_scroll }, + .{ .name = "buttons", .widget = buttons.root }, + }, &vfl); + errdefer view.deinit(); + + // We can do additional settings once the layout is setup + c.gtk_label_set_wrap(@ptrCast(label), 1); + c.gtk_text_view_set_editable(@ptrCast(text), 0); + c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0); + c.gtk_text_view_set_top_margin(@ptrCast(text), 8); + c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8); + c.gtk_text_view_set_left_margin(@ptrCast(text), 8); + c.gtk_text_view_set_right_margin(@ptrCast(text), 8); + + return .{ .root = view.root, .text = @ptrCast(text) }; + } + + /// Returns the GtkTextBuffer for the data that was unsafe. + fn unsafeBuffer(data: []const u8) *c.GtkTextBuffer { + const buf = c.gtk_text_buffer_new(null); + errdefer c.g_object_unref(buf); + + c.gtk_text_buffer_insert_at_cursor(buf, data.ptr, @intCast(data.len)); + + return buf; + } + + const vfl = [_][*:0]const u8{ + "H:|-8-[label]-8-|", + "H:|[text]|", + "H:|[buttons]|", + "V:|[label(<=80)][text(>=100)]-[buttons]-|", + }; +}; + +const ButtonsView = struct { + root: *c.GtkWidget, + + pub fn init(root: *UnsafePaste) !ButtonsView { + const cancel_button = c.gtk_button_new_with_label("Cancel"); + errdefer c.g_object_unref(cancel_button); + + const paste_button = c.gtk_button_new_with_label("Paste"); + errdefer c.g_object_unref(paste_button); + + // Create our view + const view = try View.init(&.{ + .{ .name = "cancel", .widget = cancel_button }, + .{ .name = "paste", .widget = paste_button }, + }, &vfl); + + // Signals + _ = c.g_signal_connect_data( + cancel_button, + "clicked", + c.G_CALLBACK(>kCancelClick), + root, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + paste_button, + "clicked", + c.G_CALLBACK(>kPasteClick), + root, + null, + c.G_CONNECT_DEFAULT, + ); + + return .{ .root = view.root }; + } + + fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self: *UnsafePaste = @ptrCast(@alignCast(ud)); + c.gtk_window_destroy(@ptrCast(self.window)); + + self.app.unsafe_paste_window = null; + } + + fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self: *UnsafePaste = @ptrCast(@alignCast(ud)); + + // Requeue the paste, this time forcing it. + self.core_surface.completeClipboardRequest(self.pending_req, self.data, true) catch |err| { + std.log.err("Failed to requeue clipboard request: {}", .{err}); + }; + + c.gtk_window_destroy(@ptrCast(self.window)); + self.app.unsafe_paste_window = null; + } + + const vfl = [_][*:0]const u8{ + "H:[cancel]-8-[paste]-8-|", + }; +};