1/*
2 * Copyright (C) 2012 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21#include "WebKitFileChooserRequest.h"
22
23#include "APIArray.h"
24#include "APIString.h"
25#include "WebKitFileChooserRequestPrivate.h"
26#include "WebOpenPanelParameters.h"
27#include "WebOpenPanelResultListenerProxy.h"
28#include <WebCore/FileSystem.h>
29#include <glib/gi18n-lib.h>
30#include <wtf/gobject/GRefPtr.h>
31#include <wtf/gobject/GUniquePtr.h>
32#include <wtf/text/CString.h>
33
34using namespace WebKit;
35using namespace WebCore;
36
37/**
38 * SECTION: WebKitFileChooserRequest
39 * @Short_description: A request to open a file chooser
40 * @Title: WebKitFileChooserRequest
41 * @See_also: #WebKitWebView
42 *
43 * Whenever the user interacts with an &lt;input type='file' /&gt;
44 * HTML element, WebKit will need to show a dialog to choose one or
45 * more files to be uploaded to the server along with the rest of the
46 * form data. For that to happen in a general way, instead of just
47 * opening a #GtkFileChooserDialog (which might be not desirable in
48 * some cases, which could prefer to use their own file chooser
49 * dialog), WebKit will fire the #WebKitWebView::run-file-chooser
50 * signal with a #WebKitFileChooserRequest object, which will allow
51 * the client application to specify the files to be selected, to
52 * inspect the details of the request (e.g. if multiple selection
53 * should be allowed) and to cancel the request, in case nothing was
54 * selected.
55 *
56 * In case the client application does not wish to handle this signal,
57 * WebKit will provide a default handler which will asynchronously run
58 * a regular #GtkFileChooserDialog for the user to interact with.
59 */
60
61struct _WebKitFileChooserRequestPrivate {
62    RefPtr<WebOpenPanelParameters> parameters;
63    RefPtr<WebOpenPanelResultListenerProxy> listener;
64    GRefPtr<GtkFileFilter> filter;
65    GRefPtr<GPtrArray> mimeTypes;
66    GRefPtr<GPtrArray> selectedFiles;
67    bool handledRequest;
68};
69
70WEBKIT_DEFINE_TYPE(WebKitFileChooserRequest, webkit_file_chooser_request, G_TYPE_OBJECT)
71
72enum {
73    PROP_0,
74    PROP_FILTER,
75    PROP_MIME_TYPES,
76    PROP_SELECT_MULTIPLE,
77    PROP_SELECTED_FILES,
78};
79
80static void webkitFileChooserRequestDispose(GObject* object)
81{
82    WebKitFileChooserRequest* request = WEBKIT_FILE_CHOOSER_REQUEST(object);
83
84    // Make sure the request is always handled before finalizing.
85    if (!request->priv->handledRequest)
86        webkit_file_chooser_request_cancel(request);
87
88    G_OBJECT_CLASS(webkit_file_chooser_request_parent_class)->dispose(object);
89}
90
91static void webkitFileChooserRequestGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
92{
93    WebKitFileChooserRequest* request = WEBKIT_FILE_CHOOSER_REQUEST(object);
94    switch (propId) {
95    case PROP_FILTER:
96        g_value_set_object(value, webkit_file_chooser_request_get_mime_types_filter(request));
97        break;
98    case PROP_MIME_TYPES:
99        g_value_set_boxed(value, webkit_file_chooser_request_get_mime_types(request));
100        break;
101    case PROP_SELECT_MULTIPLE:
102        g_value_set_boolean(value, webkit_file_chooser_request_get_select_multiple(request));
103        break;
104    case PROP_SELECTED_FILES:
105        g_value_set_boxed(value, webkit_file_chooser_request_get_selected_files(request));
106        break;
107    default:
108        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
109        break;
110    }
111}
112
113static void webkit_file_chooser_request_class_init(WebKitFileChooserRequestClass* requestClass)
114{
115    GObjectClass* objectClass = G_OBJECT_CLASS(requestClass);
116    objectClass->dispose = webkitFileChooserRequestDispose;
117    objectClass->get_property = webkitFileChooserRequestGetProperty;
118
119    /**
120     * WebKitFileChooserRequest:filter:
121     *
122     * The filter currently associated with the request. See
123     * webkit_file_chooser_request_get_mime_types_filter() for more
124     * details.
125     */
126    g_object_class_install_property(objectClass,
127                                    PROP_FILTER,
128                                    g_param_spec_object("filter",
129                                                      _("MIME types filter"),
130                                                      _("The filter currently associated with the request"),
131                                                      GTK_TYPE_FILE_FILTER,
132                                                      WEBKIT_PARAM_READABLE));
133    /**
134     * WebKitFileChooserRequest:mime-types:
135     *
136     * A %NULL-terminated array of strings containing the list of MIME
137     * types the file chooser dialog should handle. See
138     * webkit_file_chooser_request_get_mime_types() for more details.
139     */
140    g_object_class_install_property(objectClass,
141                                    PROP_MIME_TYPES,
142                                    g_param_spec_boxed("mime-types",
143                                                      _("MIME types"),
144                                                      _("The list of MIME types associated with the request"),
145                                                      G_TYPE_STRV,
146                                                      WEBKIT_PARAM_READABLE));
147    /**
148     * WebKitFileChooserRequest:select-multiple:
149     *
150     * Whether the file chooser should allow selecting multiple
151     * files. See
152     * webkit_file_chooser_request_get_select_multiple() for
153     * more details.
154     */
155    g_object_class_install_property(objectClass,
156                                    PROP_SELECT_MULTIPLE,
157                                    g_param_spec_boolean("select-multiple",
158                                                       _("Select multiple files"),
159                                                       _("Whether the file chooser should allow selecting multiple files"),
160                                                       FALSE,
161                                                       WEBKIT_PARAM_READABLE));
162    /**
163     * WebKitFileChooserRequest:selected-files:
164     *
165     * A %NULL-terminated array of strings containing the list of
166     * selected files associated to the current request. See
167     * webkit_file_chooser_request_get_selected_files() for more details.
168     */
169    g_object_class_install_property(objectClass,
170                                    PROP_SELECTED_FILES,
171                                    g_param_spec_boxed("selected-files",
172                                                      _("Selected files"),
173                                                      _("The list of selected files associated with the request"),
174                                                      G_TYPE_STRV,
175                                                      WEBKIT_PARAM_READABLE));
176}
177
178WebKitFileChooserRequest* webkitFileChooserRequestCreate(WebOpenPanelParameters* parameters, WebOpenPanelResultListenerProxy* listener)
179{
180    WebKitFileChooserRequest* request = WEBKIT_FILE_CHOOSER_REQUEST(g_object_new(WEBKIT_TYPE_FILE_CHOOSER_REQUEST, NULL));
181    request->priv->parameters = parameters;
182    request->priv->listener = listener;
183    return request;
184}
185
186/**
187 * webkit_file_chooser_request_get_mime_types:
188 * @request: a #WebKitFileChooserRequest
189 *
190 * Get the list of MIME types the file chooser dialog should handle,
191 * in the format specified in RFC 2046 for "media types". Its contents
192 * depend on the value of the 'accept' attribute for HTML input
193 * elements. This function should normally be called before presenting
194 * the file chooser dialog to the user, to decide whether to allow the
195 * user to select multiple files at once or only one.
196 *
197 * Returns: (array zero-terminated=1) (transfer none): a
198 * %NULL-terminated array of strings if a list of accepted MIME types
199 * is defined or %NULL otherwise, meaning that any MIME type should be
200 * accepted. This array and its contents are owned by WebKitGTK+ and
201 * should not be modified or freed.
202 */
203const gchar* const* webkit_file_chooser_request_get_mime_types(WebKitFileChooserRequest* request)
204{
205    g_return_val_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request), 0);
206    if (request->priv->mimeTypes)
207        return reinterpret_cast<gchar**>(request->priv->mimeTypes->pdata);
208
209    RefPtr<API::Array> mimeTypes = request->priv->parameters->acceptMIMETypes();
210    size_t numOfMimeTypes = mimeTypes->size();
211    if (!numOfMimeTypes)
212        return 0;
213
214    request->priv->mimeTypes = adoptGRef(g_ptr_array_new_with_free_func(g_free));
215    for (size_t i = 0; i < numOfMimeTypes; ++i) {
216        API::String* webMimeType = static_cast<API::String*>(mimeTypes->at(i));
217        String mimeTypeString = webMimeType->string();
218        if (mimeTypeString.isEmpty())
219            continue;
220        g_ptr_array_add(request->priv->mimeTypes.get(), g_strdup(mimeTypeString.utf8().data()));
221    }
222    g_ptr_array_add(request->priv->mimeTypes.get(), 0);
223
224    return reinterpret_cast<gchar**>(request->priv->mimeTypes->pdata);
225}
226
227/**
228 * webkit_file_chooser_request_get_mime_types_filter:
229 * @request: a #WebKitFileChooserRequest
230 *
231 * Get the filter currently associated with the request, ready to be
232 * used by #GtkFileChooser. This function should normally be called
233 * before presenting the file chooser dialog to the user, to decide
234 * whether to apply a filter so the user would not be allowed to
235 * select files with other MIME types.
236 *
237 * See webkit_file_chooser_request_get_mime_types() if you are
238 * interested in getting the list of accepted MIME types.
239 *
240 * Returns: (transfer none): a #GtkFileFilter if a list of accepted
241 * MIME types is defined or %NULL otherwise. The returned object is
242 * owned by WebKitGTK+ should not be modified or freed.
243 */
244GtkFileFilter* webkit_file_chooser_request_get_mime_types_filter(WebKitFileChooserRequest* request)
245{
246    g_return_val_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request), 0);
247    if (request->priv->filter)
248        return request->priv->filter.get();
249
250    RefPtr<API::Array> mimeTypes = request->priv->parameters->acceptMIMETypes();
251    size_t numOfMimeTypes = mimeTypes->size();
252    if (!numOfMimeTypes)
253        return 0;
254
255    // Do not use adoptGRef here, since we want to sink the floating
256    // reference for the new instance of GtkFileFilter, so we make
257    // sure we keep the ownership during the lifetime of the request.
258    request->priv->filter = gtk_file_filter_new();
259    for (size_t i = 0; i < numOfMimeTypes; ++i) {
260        API::String* webMimeType = static_cast<API::String*>(mimeTypes->at(i));
261        String mimeTypeString = webMimeType->string();
262        if (mimeTypeString.isEmpty())
263            continue;
264        gtk_file_filter_add_mime_type(request->priv->filter.get(), mimeTypeString.utf8().data());
265    }
266
267    return request->priv->filter.get();
268}
269
270/**
271 * webkit_file_chooser_request_get_select_multiple:
272 * @request: a #WebKitFileChooserRequest
273 *
274 * Determine whether the file chooser associated to this
275 * #WebKitFileChooserRequest should allow selecting multiple files,
276 * which depends on the HTML input element having a 'multiple'
277 * attribute defined.
278 *
279 * Returns: %TRUE if the file chooser should allow selecting multiple files or %FALSE otherwise.
280 */
281gboolean webkit_file_chooser_request_get_select_multiple(WebKitFileChooserRequest* request)
282{
283    g_return_val_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request), FALSE);
284    return request->priv->parameters->allowMultipleFiles();
285}
286
287/**
288 * webkit_file_chooser_request_select_files:
289 * @request: a #WebKitFileChooserRequest
290 * @files: (array zero-terminated=1) (transfer none): a
291 * %NULL-terminated array of strings, containing paths to local files.
292 *
293 * Ask WebKit to select local files for upload and complete the
294 * request.
295 */
296void webkit_file_chooser_request_select_files(WebKitFileChooserRequest* request, const gchar* const* files)
297{
298    g_return_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request));
299    g_return_if_fail(files);
300
301    GRefPtr<GPtrArray> selectedFiles = adoptGRef(g_ptr_array_new_with_free_func(g_free));
302    Vector<RefPtr<API::Object> > choosenFiles;
303    for (int i = 0; files[i]; i++) {
304        GRefPtr<GFile> filename = adoptGRef(g_file_new_for_path(files[i]));
305
306        // Make sure the file path is presented as an URI (escaped
307        // string, with the 'file://' prefix) to WebCore otherwise the
308        // FileChooser won't actually choose it.
309        GUniquePtr<char> uri(g_file_get_uri(filename.get()));
310        choosenFiles.append(API::URL::create(String::fromUTF8(uri.get())));
311
312        // Do not use the URI here because this won't reach WebCore.
313        g_ptr_array_add(selectedFiles.get(), g_strdup(files[i]));
314    }
315    g_ptr_array_add(selectedFiles.get(), 0);
316
317    // Select the files in WebCore and update local private attributes.
318    request->priv->listener->chooseFiles(API::Array::create(WTF::move(choosenFiles)).get());
319    request->priv->selectedFiles = selectedFiles;
320    request->priv->handledRequest = true;
321}
322
323/**
324 * webkit_file_chooser_request_get_selected_files:
325 * @request: a #WebKitFileChooserRequest
326 *
327 * Get the list of selected files currently associated to the
328 * request. Initially, the return value of this method contains any
329 * files selected in previous file chooser requests for this HTML
330 * input element. Once webkit_file_chooser_request_select_files, the
331 * value will reflect whatever files are given.
332 *
333 * This function should normally be called only before presenting the
334 * file chooser dialog to the user, to decide whether to perform some
335 * extra action, like pre-selecting the files from a previous request.
336 *
337 * Returns: (array zero-terminated=1) (transfer none): a
338 * %NULL-terminated array of strings if there are selected files
339 * associated with the request or %NULL otherwise. This array and its
340 * contents are owned by WebKitGTK+ and should not be modified or
341 * freed.
342 */
343const gchar* const* webkit_file_chooser_request_get_selected_files(WebKitFileChooserRequest* request)
344{
345    g_return_val_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request), 0);
346    if (request->priv->selectedFiles)
347        return reinterpret_cast<gchar**>(request->priv->selectedFiles->pdata);
348
349    RefPtr<API::Array> selectedFileNames = request->priv->parameters->selectedFileNames();
350    size_t numOfFiles = selectedFileNames->size();
351    if (!numOfFiles)
352        return 0;
353
354    request->priv->selectedFiles = adoptGRef(g_ptr_array_new_with_free_func(g_free));
355    for (size_t i = 0; i < numOfFiles; ++i) {
356        API::String* webFileName = static_cast<API::String*>(selectedFileNames->at(i));
357        if (webFileName->isEmpty())
358            continue;
359        CString filename = fileSystemRepresentation(webFileName->string());
360        g_ptr_array_add(request->priv->selectedFiles.get(), g_strdup(filename.data()));
361    }
362    g_ptr_array_add(request->priv->selectedFiles.get(), 0);
363
364    return reinterpret_cast<gchar**>(request->priv->selectedFiles->pdata);
365}
366
367/**
368 * webkit_file_chooser_request_cancel:
369 * @request: a #WebKitFileChooserRequest
370 *
371 * Ask WebKit to cancel the request. It's important to do this in case
372 * no selection has been made in the client, otherwise the request
373 * won't be properly completed and the browser will keep the request
374 * pending forever, which might cause the browser to hang.
375 */
376void webkit_file_chooser_request_cancel(WebKitFileChooserRequest* request)
377{
378    g_return_if_fail(WEBKIT_IS_FILE_CHOOSER_REQUEST(request));
379    request->priv->listener->cancel();
380    request->priv->handledRequest = true;
381}
382