Mark drags starting in web content as tainted to avoid file path forgery
[chromium-blink-merge.git] / content / browser / web_contents / web_drag_dest_gtk.cc
blob0594a2e2b0506416b8f87444639f8aeb769af980
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "content/browser/web_contents/web_drag_dest_gtk.h"
7 #include <string>
9 #include "base/bind.h"
10 #include "base/files/file_path.h"
11 #include "base/message_loop/message_loop.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "content/browser/renderer_host/render_view_host_impl.h"
14 #include "content/browser/web_contents/drag_utils_gtk.h"
15 #include "content/browser/web_contents/web_contents_impl.h"
16 #include "content/public/browser/web_contents_delegate.h"
17 #include "content/public/browser/web_drag_dest_delegate.h"
18 #include "content/public/common/url_constants.h"
19 #include "net/base/net_util.h"
20 #include "third_party/WebKit/public/web/WebInputEvent.h"
21 #include "ui/base/clipboard/custom_data_helper.h"
22 #include "ui/base/dragdrop/gtk_dnd_util.h"
23 #include "ui/base/gtk/gtk_screen_util.h"
25 using blink::WebDragOperation;
26 using blink::WebDragOperationNone;
28 namespace content {
30 namespace {
31 const int kNumGtkHandlers = 5;
33 int GetModifierFlags(GtkWidget* widget) {
34 int modifier_state = 0;
35 GdkModifierType state;
36 gdk_window_get_pointer(gtk_widget_get_window(widget), NULL, NULL, &state);
38 if (state & GDK_SHIFT_MASK)
39 modifier_state |= blink::WebInputEvent::ShiftKey;
40 if (state & GDK_CONTROL_MASK)
41 modifier_state |= blink::WebInputEvent::ControlKey;
42 if (state & GDK_MOD1_MASK)
43 modifier_state |= blink::WebInputEvent::AltKey;
44 if (state & GDK_META_MASK)
45 modifier_state |= blink::WebInputEvent::MetaKey;
46 return modifier_state;
49 } // namespace
51 WebDragDestGtk::WebDragDestGtk(WebContents* web_contents, GtkWidget* widget)
52 : web_contents_(web_contents),
53 widget_(widget),
54 context_(NULL),
55 data_requests_(0),
56 delegate_(NULL),
57 canceled_(false),
58 method_factory_(this) {
59 gtk_drag_dest_set(widget, static_cast<GtkDestDefaults>(0),
60 NULL, 0,
61 static_cast<GdkDragAction>(GDK_ACTION_COPY |
62 GDK_ACTION_LINK |
63 GDK_ACTION_MOVE));
65 // If adding a handler, make sure to update kNumGtkHandlers and add it to the
66 // |handlers_| array so that it can be disconnected later on.
67 handlers_.reset(new int[kNumGtkHandlers]);
68 handlers_.get()[0] = g_signal_connect(
69 widget, "drag-motion", G_CALLBACK(OnDragMotionThunk), this);
70 handlers_.get()[1] = g_signal_connect(
71 widget, "drag-leave", G_CALLBACK(OnDragLeaveThunk), this);
72 handlers_.get()[2] = g_signal_connect(
73 widget, "drag-drop", G_CALLBACK(OnDragDropThunk), this);
74 handlers_.get()[3] = g_signal_connect(
75 widget, "drag-data-received", G_CALLBACK(OnDragDataReceivedThunk), this);
76 // TODO(tony): Need a drag-data-delete handler for moving content out of
77 // the WebContents. http://crbug.com/38989
79 handlers_.get()[4] = g_signal_connect(
80 widget, "destroy", G_CALLBACK(gtk_widget_destroyed), &widget_);
83 WebDragDestGtk::~WebDragDestGtk() {
84 if (widget_) {
85 gtk_drag_dest_unset(widget_);
86 for (int i = 0; i < kNumGtkHandlers; ++i)
87 g_signal_handler_disconnect(widget_, handlers_.get()[i]);
91 void WebDragDestGtk::UpdateDragStatus(WebDragOperation operation) {
92 if (context_) {
93 is_drop_target_ = operation != WebDragOperationNone;
94 gdk_drag_status(context_, WebDragOpToGdkDragAction(operation),
95 drag_over_time_);
99 void WebDragDestGtk::DragLeave() {
100 GetRenderViewHost()->DragTargetDragLeave();
101 if (delegate())
102 delegate()->OnDragLeave();
104 drop_data_.reset();
107 gboolean WebDragDestGtk::OnDragMotion(GtkWidget* sender,
108 GdkDragContext* context,
109 gint x, gint y,
110 guint time) {
111 if (context_ != context) {
112 context_ = context;
113 drop_data_.reset(new DropData);
114 is_drop_target_ = false;
116 if (delegate())
117 delegate()->DragInitialize(web_contents_);
119 // text/plain must come before text/uri-list. This is a hack that works in
120 // conjunction with OnDragDataReceived. Since some file managers populate
121 // text/plain with file URLs when dragging files, we want to handle
122 // text/uri-list after text/plain so that the plain text can be cleared if
123 // it's a file drag.
124 // Similarly, renderer taint must occur before anything else so we can
125 // ignore potentially forged filenames when handling text/uri-list.
126 static int supported_targets[] = {
127 ui::RENDERER_TAINT,
128 ui::TEXT_PLAIN,
129 ui::TEXT_URI_LIST,
130 ui::TEXT_HTML,
131 ui::NETSCAPE_URL,
132 ui::CHROME_NAMED_URL,
133 // TODO(estade): support image drags?
134 ui::CUSTOM_DATA,
137 // Add the delegate's requested target if applicable. Need to do this here
138 // since gtk_drag_get_data will dispatch to our drag-data-received.
139 data_requests_ = arraysize(supported_targets) + (delegate() ? 1 : 0);
140 for (size_t i = 0; i < arraysize(supported_targets); ++i) {
141 gtk_drag_get_data(widget_, context,
142 ui::GetAtomForTarget(supported_targets[i]),
143 time);
146 if (delegate()) {
147 gtk_drag_get_data(widget_, context, delegate()->GetBookmarkTargetAtom(),
148 time);
150 } else if (data_requests_ == 0) {
151 if (canceled_)
152 return FALSE;
154 GetRenderViewHost()->DragTargetDragOver(
155 ui::ClientPoint(widget_),
156 ui::ScreenPoint(widget_),
157 GdkDragActionToWebDragOp(context->actions),
158 GetModifierFlags(widget_));
160 if (delegate())
161 delegate()->OnDragOver();
163 drag_over_time_ = time;
166 // Pretend we are a drag destination because we don't want to wait for
167 // the renderer to tell us if we really are or not.
168 return TRUE;
171 void WebDragDestGtk::OnDragDataReceived(
172 GtkWidget* sender, GdkDragContext* context, gint x, gint y,
173 GtkSelectionData* data, guint info, guint time) {
174 // We might get the data from an old get_data() request that we no longer
175 // care about.
176 if (context != context_)
177 return;
179 data_requests_--;
181 // Decode the data.
182 gint data_length = gtk_selection_data_get_length(data);
183 const guchar* raw_data = gtk_selection_data_get_data(data);
184 GdkAtom target = gtk_selection_data_get_target(data);
185 if (raw_data && data_length > 0) {
186 // If the source can't provide us with valid data for a requested target,
187 // raw_data will be NULL.
188 if (target == ui::GetAtomForTarget(ui::RENDERER_TAINT)) {
189 drop_data_->did_originate_from_renderer = true;
190 } else if (target == ui::GetAtomForTarget(ui::TEXT_PLAIN)) {
191 guchar* text = gtk_selection_data_get_text(data);
192 if (text) {
193 drop_data_->text = base::NullableString16(
194 base::UTF8ToUTF16(std::string(reinterpret_cast<const char*>(text))),
195 false);
196 g_free(text);
198 } else if (target == ui::GetAtomForTarget(ui::TEXT_URI_LIST)) {
199 gchar** uris = gtk_selection_data_get_uris(data);
200 if (uris) {
201 drop_data_->url = GURL();
202 for (gchar** uri_iter = uris; *uri_iter; uri_iter++) {
203 // Most file managers populate text/uri-list with file URLs when
204 // dragging files. To avoid exposing file system paths to web content,
205 // file URLs are never set as the URL content for the drop.
206 // TODO(estade): Can the filenames have a non-UTF8 encoding?
207 GURL url(*uri_iter);
208 base::FilePath file_path;
209 if (url.SchemeIs(kFileScheme) &&
210 net::FileURLToFilePath(url, &file_path)) {
211 drop_data_->filenames.push_back(
212 DropData::FileInfo(base::UTF8ToUTF16(file_path.value()),
213 base::string16()));
214 // This is a hack. Some file managers also populate text/plain with
215 // a file URL when dragging files, so we clear it to avoid exposing
216 // it to the web content.
217 drop_data_->text = base::NullableString16();
218 } else if (!drop_data_->url.is_valid()) {
219 // Also set the first non-file URL as the URL content for the drop.
220 drop_data_->url = url;
223 g_strfreev(uris);
225 } else if (target == ui::GetAtomForTarget(ui::TEXT_HTML)) {
226 // TODO(estade): Can the html have a non-UTF8 encoding?
227 drop_data_->html = base::NullableString16(
228 base::UTF8ToUTF16(std::string(reinterpret_cast<const char*>(raw_data),
229 data_length)),
230 false);
231 // We leave the base URL empty.
232 } else if (target == ui::GetAtomForTarget(ui::NETSCAPE_URL)) {
233 std::string netscape_url(reinterpret_cast<const char*>(raw_data),
234 data_length);
235 size_t split = netscape_url.find_first_of('\n');
236 if (split != std::string::npos) {
237 drop_data_->url = GURL(netscape_url.substr(0, split));
238 if (split < netscape_url.size() - 1) {
239 drop_data_->url_title =
240 base::UTF8ToUTF16(netscape_url.substr(split + 1));
243 } else if (target == ui::GetAtomForTarget(ui::CHROME_NAMED_URL)) {
244 ui::ExtractNamedURL(data, &drop_data_->url, &drop_data_->url_title);
245 } else if (target == ui::GetAtomForTarget(ui::CUSTOM_DATA)) {
246 ui::ReadCustomDataIntoMap(
247 raw_data, data_length, &drop_data_->custom_data);
251 if (data_requests_ == 0) {
252 // Give the delegate an opportunity to cancel the drag.
253 canceled_ = !web_contents_->GetDelegate()->CanDragEnter(
254 web_contents_,
255 *drop_data_,
256 GdkDragActionToWebDragOp(context->actions));
257 if (canceled_) {
258 drag_over_time_ = time;
259 UpdateDragStatus(WebDragOperationNone);
260 drop_data_.reset();
261 return;
265 // For CHROME_BOOKMARK_ITEM, we have to handle the case where the drag source
266 // doesn't have any data available for us. In this case we try to synthesize a
267 // URL bookmark.
268 // Note that bookmark drag data is encoded in the same format for both
269 // GTK and Views, hence we can share the same logic here.
270 if (delegate() && target == delegate()->GetBookmarkTargetAtom()) {
271 if (raw_data && data_length > 0) {
272 delegate()->OnReceiveDataFromGtk(data);
273 } else {
274 delegate()->OnReceiveProcessedData(drop_data_->url,
275 drop_data_->url_title);
279 if (data_requests_ == 0) {
280 // Tell the renderer about the drag.
281 // |x| and |y| are seemingly arbitrary at this point.
282 GetRenderViewHost()->DragTargetDragEnter(
283 *drop_data_.get(),
284 ui::ClientPoint(widget_),
285 ui::ScreenPoint(widget_),
286 GdkDragActionToWebDragOp(context->actions),
287 GetModifierFlags(widget_));
289 if (delegate())
290 delegate()->OnDragEnter();
292 drag_over_time_ = time;
296 // The drag has left our widget; forward this information to the renderer.
297 void WebDragDestGtk::OnDragLeave(GtkWidget* sender, GdkDragContext* context,
298 guint time) {
299 // Set |context_| to NULL to make sure we will recognize the next DragMotion
300 // as an enter.
301 context_ = NULL;
303 if (canceled_)
304 return;
306 // Sometimes we get a drag-leave event before getting a drag-data-received
307 // event. In that case, we don't want to bother the renderer with a
308 // DragLeave event.
309 if (data_requests_ != 0)
310 return;
312 // When GTK sends us a drag-drop signal, it is shortly (and synchronously)
313 // preceded by a drag-leave. The renderer doesn't like getting the signals
314 // in this order so delay telling it about the drag-leave till we are sure
315 // we are not getting a drop as well.
316 base::MessageLoop::current()->PostTask(
317 FROM_HERE,
318 base::Bind(&WebDragDestGtk::DragLeave, method_factory_.GetWeakPtr()));
321 // Called by GTK when the user releases the mouse, executing a drop.
322 gboolean WebDragDestGtk::OnDragDrop(GtkWidget* sender, GdkDragContext* context,
323 gint x, gint y, guint time) {
324 // Cancel that drag leave!
325 method_factory_.InvalidateWeakPtrs();
327 GetRenderViewHost()->
328 DragTargetDrop(ui::ClientPoint(widget_), ui::ScreenPoint(widget_),
329 GetModifierFlags(widget_));
331 if (delegate())
332 delegate()->OnDrop();
334 // The second parameter is just an educated guess as to whether or not the
335 // drag succeeded, but at least we will get the drag-end animation right
336 // sometimes.
337 gtk_drag_finish(context, is_drop_target_, FALSE, time);
339 return TRUE;
342 RenderViewHostImpl* WebDragDestGtk::GetRenderViewHost() const {
343 return static_cast<RenderViewHostImpl*>(web_contents_->GetRenderViewHost());
346 } // namespace content