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.
4
5#include "ui/base/clipboard/clipboard_util_win.h"
6
7#include <shellapi.h>
8#include <shlwapi.h>
9#include <wininet.h>  // For INTERNET_MAX_URL_LENGTH.
10
11#include "base/basictypes.h"
12#include "base/logging.h"
13#include "base/strings/string_util.h"
14#include "base/strings/stringprintf.h"
15#include "base/strings/sys_string_conversions.h"
16#include "base/strings/utf_string_conversions.h"
17#include "base/win/scoped_hglobal.h"
18#include "net/base/filename_util.h"
19#include "ui/base/clipboard/clipboard.h"
20#include "ui/base/clipboard/custom_data_helper.h"
21#include "url/gurl.h"
22
23namespace ui {
24
25namespace {
26
27bool HasData(IDataObject* data_object, const Clipboard::FormatType& format) {
28  FORMATETC format_etc = format.ToFormatEtc();
29  return SUCCEEDED(data_object->QueryGetData(&format_etc));
30}
31
32bool GetData(IDataObject* data_object,
33             const Clipboard::FormatType& format,
34             STGMEDIUM* medium) {
35  FORMATETC format_etc = format.ToFormatEtc();
36  return SUCCEEDED(data_object->GetData(&format_etc, medium));
37}
38
39bool GetUrlFromHDrop(IDataObject* data_object,
40                     GURL* url,
41                     base::string16* title) {
42  DCHECK(data_object && url && title);
43
44  bool success = false;
45  STGMEDIUM medium;
46  if (!GetData(data_object, Clipboard::GetCFHDropFormatType(), &medium))
47    return false;
48
49  {
50    base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);
51
52    if (!hdrop.get())
53      return false;
54
55    wchar_t filename[MAX_PATH];
56    if (DragQueryFileW(hdrop.get(), 0, filename, arraysize(filename))) {
57      wchar_t url_buffer[INTERNET_MAX_URL_LENGTH];
58      if (0 == _wcsicmp(PathFindExtensionW(filename), L".url") &&
59          GetPrivateProfileStringW(L"InternetShortcut",
60                                   L"url",
61                                   0,
62                                   url_buffer,
63                                   arraysize(url_buffer),
64                                   filename)) {
65        *url = GURL(url_buffer);
66        PathRemoveExtension(filename);
67        title->assign(PathFindFileName(filename));
68        success = url->is_valid();
69      }
70    }
71  }
72
73  ReleaseStgMedium(&medium);
74  return success;
75}
76
77void SplitUrlAndTitle(const base::string16& str,
78                      GURL* url,
79                      base::string16* title) {
80  DCHECK(url && title);
81  size_t newline_pos = str.find('\n');
82  if (newline_pos != base::string16::npos) {
83    *url = GURL(base::string16(str, 0, newline_pos));
84    title->assign(str, newline_pos + 1, base::string16::npos);
85  } else {
86    *url = GURL(str);
87    title->assign(str);
88  }
89}
90
91}  // namespace
92
93bool ClipboardUtil::HasUrl(IDataObject* data_object, bool convert_filenames) {
94  DCHECK(data_object);
95  return HasData(data_object, Clipboard::GetMozUrlFormatType()) ||
96         HasData(data_object, Clipboard::GetUrlWFormatType()) ||
97         HasData(data_object, Clipboard::GetUrlFormatType()) ||
98         (convert_filenames && HasFilenames(data_object));
99}
100
101bool ClipboardUtil::HasFilenames(IDataObject* data_object) {
102  DCHECK(data_object);
103  return HasData(data_object, Clipboard::GetCFHDropFormatType()) ||
104         HasData(data_object, Clipboard::GetFilenameWFormatType()) ||
105         HasData(data_object, Clipboard::GetFilenameFormatType());
106}
107
108bool ClipboardUtil::HasFileContents(IDataObject* data_object) {
109  DCHECK(data_object);
110  return HasData(data_object, Clipboard::GetFileContentZeroFormatType());
111}
112
113bool ClipboardUtil::HasHtml(IDataObject* data_object) {
114  DCHECK(data_object);
115  return HasData(data_object, Clipboard::GetHtmlFormatType()) ||
116         HasData(data_object, Clipboard::GetTextHtmlFormatType());
117}
118
119bool ClipboardUtil::HasPlainText(IDataObject* data_object) {
120  DCHECK(data_object);
121  return HasData(data_object, Clipboard::GetPlainTextWFormatType()) ||
122         HasData(data_object, Clipboard::GetPlainTextFormatType());
123}
124
125bool ClipboardUtil::GetUrl(IDataObject* data_object,
126                           GURL* url,
127                           base::string16* title,
128                           bool convert_filenames) {
129  DCHECK(data_object && url && title);
130  if (!HasUrl(data_object, convert_filenames))
131    return false;
132
133  // Try to extract a URL from |data_object| in a variety of formats.
134  STGMEDIUM store;
135  if (GetUrlFromHDrop(data_object, url, title))
136    return true;
137
138  if (GetData(data_object, Clipboard::GetMozUrlFormatType(), &store) ||
139      GetData(data_object, Clipboard::GetUrlWFormatType(), &store)) {
140    {
141      // Mozilla URL format or unicode URL
142      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
143      SplitUrlAndTitle(data.get(), url, title);
144    }
145    ReleaseStgMedium(&store);
146    return url->is_valid();
147  }
148
149  if (GetData(data_object, Clipboard::GetUrlFormatType(), &store)) {
150    {
151      // URL using ascii
152      base::win::ScopedHGlobal<char*> data(store.hGlobal);
153      SplitUrlAndTitle(base::UTF8ToWide(data.get()), url, title);
154    }
155    ReleaseStgMedium(&store);
156    return url->is_valid();
157  }
158
159  if (convert_filenames) {
160    std::vector<base::string16> filenames;
161    if (!GetFilenames(data_object, &filenames))
162      return false;
163    DCHECK_GT(filenames.size(), 0U);
164    *url = net::FilePathToFileURL(base::FilePath(filenames[0]));
165    return url->is_valid();
166  }
167
168  return false;
169}
170
171bool ClipboardUtil::GetFilenames(IDataObject* data_object,
172                                 std::vector<base::string16>* filenames) {
173  DCHECK(data_object && filenames);
174  if (!HasFilenames(data_object))
175    return false;
176
177  STGMEDIUM medium;
178  if (GetData(data_object, Clipboard::GetCFHDropFormatType(), &medium)) {
179    {
180      base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);
181      if (!hdrop.get())
182        return false;
183
184      const int kMaxFilenameLen = 4096;
185      const unsigned num_files = DragQueryFileW(hdrop.get(), 0xffffffff, 0, 0);
186      for (unsigned int i = 0; i < num_files; ++i) {
187        wchar_t filename[kMaxFilenameLen];
188        if (!DragQueryFileW(hdrop.get(), i, filename, kMaxFilenameLen))
189          continue;
190        filenames->push_back(filename);
191      }
192    }
193    ReleaseStgMedium(&medium);
194    return true;
195  }
196
197  if (GetData(data_object, Clipboard::GetFilenameWFormatType(), &medium)) {
198    {
199      // filename using unicode
200      base::win::ScopedHGlobal<wchar_t*> data(medium.hGlobal);
201      if (data.get() && data.get()[0])
202        filenames->push_back(data.get());
203    }
204    ReleaseStgMedium(&medium);
205    return true;
206  }
207
208  if (GetData(data_object, Clipboard::GetFilenameFormatType(), &medium)) {
209    {
210      // filename using ascii
211      base::win::ScopedHGlobal<char*> data(medium.hGlobal);
212      if (data.get() && data.get()[0])
213        filenames->push_back(base::SysNativeMBToWide(data.get()));
214    }
215    ReleaseStgMedium(&medium);
216    return true;
217  }
218
219  return false;
220}
221
222bool ClipboardUtil::GetPlainText(IDataObject* data_object,
223                                 base::string16* plain_text) {
224  DCHECK(data_object && plain_text);
225  if (!HasPlainText(data_object))
226    return false;
227
228  STGMEDIUM store;
229  if (GetData(data_object, Clipboard::GetPlainTextWFormatType(), &store)) {
230    {
231      // Unicode text
232      base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
233      plain_text->assign(data.get());
234    }
235    ReleaseStgMedium(&store);
236    return true;
237  }
238
239  if (GetData(data_object, Clipboard::GetPlainTextFormatType(), &store)) {
240    {
241      // ascii text
242      base::win::ScopedHGlobal<char*> data(store.hGlobal);
243      plain_text->assign(base::UTF8ToWide(data.get()));
244    }
245    ReleaseStgMedium(&store);
246    return true;
247  }
248
249  // If a file is dropped on the window, it does not provide either of the
250  // plain text formats, so here we try to forcibly get a url.
251  GURL url;
252  base::string16 title;
253  if (GetUrl(data_object, &url, &title, false)) {
254    *plain_text = base::UTF8ToUTF16(url.spec());
255    return true;
256  }
257  return false;
258}
259
260bool ClipboardUtil::GetHtml(IDataObject* data_object,
261                            base::string16* html, std::string* base_url) {
262  DCHECK(data_object && html && base_url);
263
264  STGMEDIUM store;
265  if (HasData(data_object, Clipboard::GetHtmlFormatType()) &&
266      GetData(data_object, Clipboard::GetHtmlFormatType(), &store)) {
267    {
268      // MS CF html
269      base::win::ScopedHGlobal<char*> data(store.hGlobal);
270
271      std::string html_utf8;
272      CFHtmlToHtml(std::string(data.get(), data.Size()), &html_utf8, base_url);
273      html->assign(base::UTF8ToWide(html_utf8));
274    }
275    ReleaseStgMedium(&store);
276    return true;
277  }
278
279  if (!HasData(data_object, Clipboard::GetTextHtmlFormatType()))
280    return false;
281
282  if (!GetData(data_object, Clipboard::GetTextHtmlFormatType(), &store))
283    return false;
284
285  {
286    // text/html
287    base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
288    html->assign(data.get());
289  }
290  ReleaseStgMedium(&store);
291  return true;
292}
293
294bool ClipboardUtil::GetFileContents(IDataObject* data_object,
295    base::string16* filename, std::string* file_contents) {
296  DCHECK(data_object && filename && file_contents);
297  if (!HasData(data_object, Clipboard::GetFileContentZeroFormatType()) &&
298      !HasData(data_object, Clipboard::GetFileDescriptorFormatType()))
299    return false;
300
301  STGMEDIUM content;
302  // The call to GetData can be very slow depending on what is in
303  // |data_object|.
304  if (GetData(
305          data_object, Clipboard::GetFileContentZeroFormatType(), &content)) {
306    if (TYMED_HGLOBAL == content.tymed) {
307      base::win::ScopedHGlobal<char*> data(content.hGlobal);
308      file_contents->assign(data.get(), data.Size());
309    }
310    ReleaseStgMedium(&content);
311  }
312
313  STGMEDIUM description;
314  if (GetData(data_object,
315              Clipboard::GetFileDescriptorFormatType(),
316              &description)) {
317    {
318      base::win::ScopedHGlobal<FILEGROUPDESCRIPTOR*> fgd(description.hGlobal);
319      // We expect there to be at least one file in here.
320      DCHECK_GE(fgd->cItems, 1u);
321      filename->assign(fgd->fgd[0].cFileName);
322    }
323    ReleaseStgMedium(&description);
324  }
325  return true;
326}
327
328bool ClipboardUtil::GetWebCustomData(
329    IDataObject* data_object,
330    std::map<base::string16, base::string16>* custom_data) {
331  DCHECK(data_object && custom_data);
332
333  if (!HasData(data_object, Clipboard::GetWebCustomDataFormatType()))
334    return false;
335
336  STGMEDIUM store;
337  if (GetData(data_object, Clipboard::GetWebCustomDataFormatType(), &store)) {
338    {
339      base::win::ScopedHGlobal<char*> data(store.hGlobal);
340      ReadCustomDataIntoMap(data.get(), data.Size(), custom_data);
341    }
342    ReleaseStgMedium(&store);
343    return true;
344  }
345  return false;
346}
347
348
349// HtmlToCFHtml and CFHtmlToHtml are based on similar methods in
350// WebCore/platform/win/ClipboardUtilitiesWin.cpp.
351/*
352 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
353 *
354 * Redistribution and use in source and binary forms, with or without
355 * modification, are permitted provided that the following conditions
356 * are met:
357 * 1. Redistributions of source code must retain the above copyright
358 *    notice, this list of conditions and the following disclaimer.
359 * 2. Redistributions in binary form must reproduce the above copyright
360 *    notice, this list of conditions and the following disclaimer in the
361 *    documentation and/or other materials provided with the distribution.
362 *
363 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
364 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
365 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
366 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
367 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
368 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
369 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
370 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
371 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
372 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
373 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
374 */
375
376// Helper method for converting from text/html to MS CF_HTML.
377// Documentation for the CF_HTML format is available at
378// http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx
379std::string ClipboardUtil::HtmlToCFHtml(const std::string& html,
380                                        const std::string& base_url) {
381  if (html.empty())
382    return std::string();
383
384  #define MAX_DIGITS 10
385  #define MAKE_NUMBER_FORMAT_1(digits) MAKE_NUMBER_FORMAT_2(digits)
386  #define MAKE_NUMBER_FORMAT_2(digits) "%0" #digits "u"
387  #define NUMBER_FORMAT MAKE_NUMBER_FORMAT_1(MAX_DIGITS)
388
389  static const char* header = "Version:0.9\r\n"
390      "StartHTML:" NUMBER_FORMAT "\r\n"
391      "EndHTML:" NUMBER_FORMAT "\r\n"
392      "StartFragment:" NUMBER_FORMAT "\r\n"
393      "EndFragment:" NUMBER_FORMAT "\r\n";
394  static const char* source_url_prefix = "SourceURL:";
395
396  static const char* start_markup =
397      "<html>\r\n<body>\r\n<!--StartFragment-->";
398  static const char* end_markup =
399      "<!--EndFragment-->\r\n</body>\r\n</html>";
400
401  // Calculate offsets
402  size_t start_html_offset = strlen(header) - strlen(NUMBER_FORMAT) * 4 +
403      MAX_DIGITS * 4;
404  if (!base_url.empty()) {
405    start_html_offset += strlen(source_url_prefix) +
406        base_url.length() + 2;  // Add 2 for \r\n.
407  }
408  size_t start_fragment_offset = start_html_offset + strlen(start_markup);
409  size_t end_fragment_offset = start_fragment_offset + html.length();
410  size_t end_html_offset = end_fragment_offset + strlen(end_markup);
411
412  std::string result = base::StringPrintf(header,
413                                          start_html_offset,
414                                          end_html_offset,
415                                          start_fragment_offset,
416                                          end_fragment_offset);
417  if (!base_url.empty()) {
418    result.append(source_url_prefix);
419    result.append(base_url);
420    result.append("\r\n");
421  }
422  result.append(start_markup);
423  result.append(html);
424  result.append(end_markup);
425
426  #undef MAX_DIGITS
427  #undef MAKE_NUMBER_FORMAT_1
428  #undef MAKE_NUMBER_FORMAT_2
429  #undef NUMBER_FORMAT
430
431  return result;
432}
433
434// Helper method for converting from MS CF_HTML to text/html.
435void ClipboardUtil::CFHtmlToHtml(const std::string& cf_html,
436                                 std::string* html,
437                                 std::string* base_url) {
438  size_t fragment_start = std::string::npos;
439  size_t fragment_end = std::string::npos;
440
441  ClipboardUtil::CFHtmlExtractMetadata(
442      cf_html, base_url, NULL, &fragment_start, &fragment_end);
443
444  if (html &&
445      fragment_start != std::string::npos &&
446      fragment_end != std::string::npos) {
447    *html = cf_html.substr(fragment_start, fragment_end - fragment_start);
448    base::TrimWhitespace(*html, base::TRIM_ALL, html);
449  }
450}
451
452void ClipboardUtil::CFHtmlExtractMetadata(const std::string& cf_html,
453                                          std::string* base_url,
454                                          size_t* html_start,
455                                          size_t* fragment_start,
456                                          size_t* fragment_end) {
457  // Obtain base_url if present.
458  if (base_url) {
459    static std::string src_url_str("SourceURL:");
460    size_t line_start = cf_html.find(src_url_str);
461    if (line_start != std::string::npos) {
462      size_t src_end = cf_html.find("\n", line_start);
463      size_t src_start = line_start + src_url_str.length();
464      if (src_end != std::string::npos && src_start != std::string::npos) {
465        *base_url = cf_html.substr(src_start, src_end - src_start);
466        base::TrimWhitespace(*base_url, base::TRIM_ALL, base_url);
467      }
468    }
469  }
470
471  // Find the markup between "<!--StartFragment-->" and "<!--EndFragment-->".
472  // If the comments cannot be found, like copying from OpenOffice Writer,
473  // we simply fall back to using StartFragment/EndFragment bytecount values
474  // to determine the fragment indexes.
475  std::string cf_html_lower = base::StringToLowerASCII(cf_html);
476  size_t markup_start = cf_html_lower.find("<html", 0);
477  if (html_start) {
478    *html_start = markup_start;
479  }
480  size_t tag_start = cf_html.find("<!--StartFragment", markup_start);
481  if (tag_start == std::string::npos) {
482    static std::string start_fragment_str("StartFragment:");
483    size_t start_fragment_start = cf_html.find(start_fragment_str);
484    if (start_fragment_start != std::string::npos) {
485      *fragment_start = static_cast<size_t>(atoi(cf_html.c_str() +
486          start_fragment_start + start_fragment_str.length()));
487    }
488
489    static std::string end_fragment_str("EndFragment:");
490    size_t end_fragment_start = cf_html.find(end_fragment_str);
491    if (end_fragment_start != std::string::npos) {
492      *fragment_end = static_cast<size_t>(atoi(cf_html.c_str() +
493          end_fragment_start + end_fragment_str.length()));
494    }
495  } else {
496    *fragment_start = cf_html.find('>', tag_start) + 1;
497    size_t tag_end = cf_html.rfind("<!--EndFragment", std::string::npos);
498    *fragment_end = cf_html.rfind('<', tag_end);
499  }
500}
501
502}  // namespace ui
503