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