File: clipboard_util_mac.mm

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (461 lines) | stat: -rw-r--r-- 15,284 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/clipboard/clipboard_util_mac.h"

#include <AppKit/AppKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#include <string>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/files/file_path.h"
#include "base/notreached.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/clipboard/url_file_parser.h"
#include "ui/base/ui_base_features.h"
#include "url/gurl.h"

@interface URLAndTitle ()

@property(copy) NSString* URL;
@property(copy) NSString* title;

+ (instancetype)URLAndTitleWithURL:(NSString*)url title:(NSString*)title;

@end

@implementation URLAndTitle

@synthesize URL = _url;
@synthesize title = _title;

+ (instancetype)URLAndTitleWithURL:(NSString*)url title:(NSString*)title {
  URLAndTitle* result = [[URLAndTitle alloc] init];
  result.URL = url;
  result.title = title;
  return result;
}

@end

namespace ui {

namespace {

// Reads the "WebKitWebURLsWithTitles" type put onto the pasteboard by Safari
// and returns the URLs/titles found within.
NSArray<URLAndTitle*>* ReadWebURLsWithTitlesPboardType(NSPasteboard* pboard) {
  NSArray* bookmark_pairs = base::apple::ObjCCast<NSArray>(
      [pboard propertyListForType:kUTTypeWebKitWebUrlsWithTitles]);
  if (!bookmark_pairs) {
    return [NSArray array];
  }
  if (bookmark_pairs.count != 2) {
    return [NSArray array];
  }

  NSArray<NSString*>* urls_array =
      base::apple::ObjCCast<NSArray>(bookmark_pairs[0]);
  NSArray<NSString*>* titles_array =
      base::apple::ObjCCast<NSArray>(bookmark_pairs[1]);

  if (!urls_array || !titles_array) {
    return [NSArray array];
  }
  if (urls_array.count < 1) {
    return [NSArray array];
  }
  if (urls_array.count != titles_array.count) {
    return [NSArray array];
  }
  for (id obj in urls_array) {
    if (![obj isKindOfClass:[NSString class]]) {
      return [NSArray array];
    }
  }

  for (id obj in titles_array) {
    if (![obj isKindOfClass:[NSString class]]) {
      return [NSArray array];
    }
  }

  NSMutableArray<URLAndTitle*>* result = [NSMutableArray array];
  for (NSUInteger i = 0; i < urls_array.count; ++i) {
    [result addObject:[URLAndTitle URLAndTitleWithURL:urls_array[i]
                                                title:titles_array[i]]];
  }

  return result;
}

// Returns the user-visible name of the file, optionally without any extension.
// If given a non-empty `file_url`, will always return a title.
NSString* DeriveTitleFromFilename(NSURL* file_url, bool strip_extension) {
  NSString* localized_name = nil;
  BOOL success = [file_url getResourceValue:&localized_name
                                     forKey:NSURLLocalizedNameKey
                                      error:nil];
  if (!success || !localized_name) {
    // For the case where the actual display name of a file cannot be obtained,
    // derive a quick-and-dirty version by swapping in "/" for ":", as that's
    // the most common difference between the last path component of a file and
    // how that file is presented to the user. See -[NSFileManager
    // displayNameAtPath:] for an example of macOS doing this. Also, given that
    // this is a failure case, don't bother trying to figure out the extension
    // situation.
    NSString* last_path_component = file_url.lastPathComponent;
    return [last_path_component stringByReplacingOccurrencesOfString:@":"
                                                          withString:@"/"];
  }

  if (!strip_extension) {
    return localized_name;
  }

  NSNumber* has_hidden_extension = nil;
  success = [file_url getResourceValue:&has_hidden_extension
                                forKey:NSURLHasHiddenExtensionKey
                                 error:nil];
  if (!success || !has_hidden_extension || has_hidden_extension.boolValue) {
    // If it's unknown if the extension is hidden, or if the extension is
    // already hidden, return the filename unaltered.
    return localized_name;
  }

  return [localized_name stringByDeletingPathExtension];
}

// Returns a URL and title if standard URL and URL title types are present on
// the pasteboard item. Because the Finder and/or the core macOS drag code
// automatically turn .webloc file drags into standard URL types, .webloc file
// drags are also handled by this function.
URLAndTitle* ExtractStandardURLAndTitle(NSPasteboardItem* item) {
  NSString* url = [item stringForType:NSPasteboardTypeURL];
  if (!url) {
    return nil;
  }

  NSString* title = [item stringForType:kUTTypeUrlName];

  if (!title) {
    // If there is no title on the drag, check to see if it's a URL drag
    // reconstituted from a Finder .webloc. If so, use the name of the file as
    // the title.
    NSString* file = [item stringForType:NSPasteboardTypeFileURL];
    if (file) {
      NSURL* file_url = [NSURL URLWithString:file].filePathURL;

      // The UTType for .webloc files is com.apple.web-internet-location, but
      // there is no official constant for that. However, that type does conform
      // to the generic "internet location" type (aka .inetloc), so check for
      // that.
      UTType* type;
      if (![file_url getResourceValue:&type
                               forKey:NSURLContentTypeKey
                                error:nil]) {
        return nil;
      }
      if (![type conformsToType:UTTypeInternetLocation]) {
        return nil;
      }

      title = DeriveTitleFromFilename(file_url, /*strip_extension=*/true);
    }
  }

  if (!title) {
    // If still no title, use the hostname as the last resort.
    title = [NSURL URLWithString:url].host;
  }

  return [URLAndTitle URLAndTitleWithURL:url title:title];
}

// Returns a URL and title if the pasteboard item is of a standard Microsoft
// Windows IShellLink-style .url file.
URLAndTitle* ExtractURLFromURLFile(NSPasteboardItem* item) {
  NSString* file = [item stringForType:NSPasteboardTypeFileURL];
  if (!file) {
    return nil;
  }
  NSURL* file_url = [NSURL URLWithString:file].filePathURL;

  NSDictionary* resource_values;
  resource_values =
      [file_url resourceValuesForKeys:@[ NSURLFileSizeKey, NSURLContentTypeKey ]
                                error:nil];
  if (!resource_values) {
    return nil;
  }

  NSNumber* file_size = resource_values[NSURLFileSizeKey];
  if (file_size.unsignedLongValue >
      clipboard_util::internal::kMaximumParsableFileSize) {
    return nil;
  }

  UTType* type = resource_values[NSURLContentTypeKey];
  if (![type conformsToType:UTTypeInternetShortcut]) {
    return nil;
  }

  // Windows codepage 1252 (aka WinLatin1) is the best guess.
  NSString* contents =
      [NSString stringWithContentsOfURL:file_url
                               encoding:NSWindowsCP1252StringEncoding
                                  error:nil];
  if (!contents) {
    return nil;
  }

  std::string found_url =
      clipboard_util::internal::ExtractURLFromURLFileContents(
          base::SysNSStringToUTF8(contents));
  if (found_url.empty()) {
    return nil;
  }

  NSString* title = DeriveTitleFromFilename(file_url, /*strip_extension=*/true);

  return [URLAndTitle URLAndTitleWithURL:base::SysUTF8ToNSString(found_url)
                                   title:title];
}

// Returns a URL and title if a string on the pasteboard item is formatted as a
// URL but doesn't actually have the URL type.
URLAndTitle* ExtractURLFromStringValue(NSPasteboardItem* item,
                                       bool is_renderer_tainted) {
  NSString* string = [item stringForType:NSPasteboardTypeString];
  if (!string) {
    return nil;
  }

  string = [string
      stringByTrimmingCharactersInSet:NSCharacterSet
                                          .whitespaceAndNewlineCharacterSet];

  // Check to see if this string is a valid URL; use GURL to do so. NSURL was
  // found in 2010 to not be strict enough; see https://crbug.com/43100. It's
  // unknown if things have changed since then, but there's no reason to revert.
  // FYI earlier code also allowed all "javascript:" and "data:" URLs as
  // "loosely validated". TODO(avi): If that "loosely validated" escape hatch
  // needed? If significant time goes by and no one complains, remove this TODO
  // and don't put that back in.
  GURL url(base::SysNSStringToUTF8(string));
  if (!url.is_valid()) {
    return nil;
  }

  if (base::FeatureList::IsEnabled(
          features::kDragDropOnlySynthesizeHttpOrHttpsUrlsFromText) &&
      is_renderer_tainted && !url.SchemeIsHTTPOrHTTPS()) {
    return nil;
  }

  // The hostname is the best that can be done for the title.
  return [URLAndTitle URLAndTitleWithURL:string
                                   title:base::SysUTF8ToNSString(url.host())];
}

// If there is a file URL on the pasteboard, returns that file as the URL. For
// compatibility with other platforms, return no title.
URLAndTitle* ExtractFileURL(NSPasteboardItem* item) {
  NSString* file = [item stringForType:NSPasteboardTypeFileURL];
  if (!file) {
    return nil;
  }
  NSURL* file_url = [NSURL URLWithString:file].filePathURL;

  return [URLAndTitle URLAndTitleWithURL:file_url.absoluteString title:@""];
}

// Reads the given pasteboard, and returns URLs/titles found on it. If
// `include_files` is set, then any file references on the pasteboard will be
// returned as file URLs. Returns true if at least one URL was found on the
// pasteboard, and false if none were.
NSArray<URLAndTitle*>* ReadURLItemsWithTitles(NSPasteboard* pboard,
                                              bool include_files) {
  NSMutableArray<URLAndTitle*>* result = [NSMutableArray array];

  const bool is_renderer_tainted =
      [pboard.types containsObject:kUTTypeChromiumRendererInitiatedDrag];
  for (NSPasteboardItem* item in pboard.pasteboardItems) {
    // Try each of several ways of getting URLs from the pasteboard item and
    // stop with the first one that works.

    URLAndTitle* url_and_title = ExtractStandardURLAndTitle(item);

    if (!url_and_title) {
      url_and_title = ExtractURLFromURLFile(item);
    }

    if (!url_and_title) {
      url_and_title = ExtractURLFromStringValue(item, is_renderer_tainted);
    }

    if (!url_and_title && include_files) {
      url_and_title = ExtractFileURL(item);
    }

    if (url_and_title) {
      [result addObject:url_and_title];
    }
  }

  return result;
}

}  // namespace

UniquePasteboard::UniquePasteboard()
    : pasteboard_([NSPasteboard pasteboardWithUniqueName]) {}

UniquePasteboard::~UniquePasteboard() {
  [pasteboard_ releaseGlobally];
}

namespace clipboard_util {

NSArray<NSPasteboardItem*>* PasteboardItemsFromUrls(
    NSArray<NSString*>* urls,
    NSArray<NSString*>* titles) {
  DCHECK_EQ(urls.count, titles.count);

  NSMutableArray<NSPasteboardItem*>* items = [NSMutableArray array];

  for (NSUInteger i = 0; i < urls.count; ++i) {
    NSPasteboardItem* item = [[NSPasteboardItem alloc] init];

    NSString* url_string = urls[i];
    NSString* title = titles[i];

    NSURL* url = [NSURL URLWithString:url_string];
    if (url.isFileURL && [url checkResourceIsReachableAndReturnError:nil]) {
      [item setString:url_string forType:NSPasteboardTypeFileURL];
    }

    [item setString:url_string forType:NSPasteboardTypeString];
    [item setString:url_string forType:NSPasteboardTypeURL];
    if (title.length) {
      [item setString:title forType:kUTTypeUrlName];
    }

    // Safari puts the "Web URLs and Titles" pasteboard type onto the first
    // pasteboard item.
    if (i == 0) {
      [item setPropertyList:@[ urls, titles ]
                    forType:kUTTypeWebKitWebUrlsWithTitles];
    }

    [items addObject:item];
  }

  return items;
}

void AddDataToPasteboard(NSPasteboard* pboard, NSPasteboardItem* item) {
  NSSet* old_types = [NSSet setWithArray:[pboard types]];
  NSMutableSet* new_types = [NSMutableSet setWithArray:[item types]];
  [new_types minusSet:old_types];

  [pboard addTypes:[new_types allObjects] owner:nil];
  for (NSString* type in new_types) {
    // Technically, the object associated with |type| might be an NSString or a
    // property list. It doesn't matter though, since the type gets pulled from
    // and shoved into an NSDictionary.
    [pboard setData:[item dataForType:type] forType:type];
  }
}

NSArray<URLAndTitle*>* URLsAndTitlesFromPasteboard(NSPasteboard* pboard,
                                                   bool include_files) {
  NSArray<URLAndTitle*>* result = ReadWebURLsWithTitlesPboardType(pboard);
  if (result.count) {
    return result;
  }

  return ReadURLItemsWithTitles(pboard, include_files);
}

std::vector<FileInfo> FilesFromPasteboard(NSPasteboard* pboard) {
  std::vector<FileInfo> results;
  for (NSPasteboardItem* item in pboard.pasteboardItems) {
    NSString* file_url_string = [item stringForType:NSPasteboardTypeFileURL];
    if (!file_url_string) {
      continue;
    }
    NSURL* file_url = [NSURL URLWithString:file_url_string].filePathURL;

    // Despite the second value being the "display name", it must be the full
    // filename because deep in Blink it's used to determine the file's type.
    // See https://crbug.com/1412205.
    results.emplace_back(
        base::apple::NSURLToFilePath(file_url),
        base::apple::NSStringToFilePath(file_url.lastPathComponent));
  }

  return results;
}

void WriteFilesToPasteboard(NSPasteboard* pboard,
                            const std::vector<FileInfo>& files) {
  if (files.empty()) {
    return;
  }

  NSMutableArray<NSPasteboardItem*>* items =
      [NSMutableArray arrayWithCapacity:files.size()];
  for (const auto& file : files) {
    NSURL* url = base::apple::FilePathToNSURL(file.path);
    NSPasteboardItem* item = [[NSPasteboardItem alloc] init];
    [item setString:url.absoluteString forType:NSPasteboardTypeFileURL];
    [items addObject:item];
  }

  [pboard writeObjects:items];
}

NSPasteboard* PasteboardFromBuffer(ClipboardBuffer buffer) {
  NSString* buffer_type = nil;
  switch (buffer) {
    case ClipboardBuffer::kCopyPaste:
      buffer_type = NSPasteboardNameGeneral;
      break;
    case ClipboardBuffer::kDrag:
      buffer_type = NSPasteboardNameDrag;
      break;
    case ClipboardBuffer::kSelection:
      NOTREACHED();
  }

  return [NSPasteboard pasteboardWithName:buffer_type];
}

NSString* GetHTMLFromRTFOnPasteboard(NSPasteboard* pboard) {
  NSData* rtf_data = [pboard dataForType:NSPasteboardTypeRTF];
  if (!rtf_data)
    return nil;

  NSAttributedString* attributed =
      [[NSAttributedString alloc] initWithRTF:rtf_data documentAttributes:nil];
  NSData* html_data =
      [attributed dataFromRange:NSMakeRange(0, attributed.length)
             documentAttributes:@{
               NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType
             }
                          error:nil];

  // According to the docs, NSHTMLTextDocumentType is UTF-8.
  return [[NSString alloc] initWithData:html_data
                               encoding:NSUTF8StringEncoding];
}

}  // namespace clipboard_util

}  // namespace ui