1// Copyright (c) 2009 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 "chrome/browser/cocoa/file_metadata.h"
6
7#include <ApplicationServices/ApplicationServices.h>
8#include <Foundation/Foundation.h>
9
10#include "base/file_path.h"
11#include "base/logging.h"
12#include "base/mac/mac_util.h"
13#include "base/mac/scoped_cftyperef.h"
14#include "googleurl/src/gurl.h"
15
16namespace file_metadata {
17
18// As of Mac OS X 10.4 ("Tiger"), files can be tagged with metadata describing
19// various attributes.  Metadata is integrated with the system's Spotlight
20// feature and is searchable.  Ordinarily, metadata can only be set by
21// Spotlight importers, which requires that the importer own the target file.
22// However, there's an attribute intended to describe the origin of a
23// file, that can store the source URL and referrer of a downloaded file.
24// It's stored as a "com.apple.metadata:kMDItemWhereFroms" extended attribute,
25// structured as a binary1-format plist containing a list of sources.  This
26// attribute can only be populated by the downloader, not a Spotlight importer.
27// Safari on 10.4 and later populates this attribute.
28//
29// With this metadata set, you can locate downloads by performing a Spotlight
30// search for their source or referrer URLs, either from within the Spotlight
31// UI or from the command line:
32//     mdfind 'kMDItemWhereFroms == "http://releases.mozilla.org/*"'
33//
34// There is no documented API to set metadata on a file directly as of the
35// 10.5 SDK.  The MDSetItemAttribute function does exist to perform this task,
36// but it's undocumented.
37void AddOriginMetadataToFile(const FilePath& file, const GURL& source,
38                             const GURL& referrer) {
39  // There's no declaration for MDItemSetAttribute in any known public SDK.
40  // It exists in the 10.4 and 10.5 runtimes.  To play it safe, do the lookup
41  // at runtime instead of declaring it ourselves and linking against what's
42  // provided.  This has two benefits:
43  //  - If Apple relents and declares the function in a future SDK (it's
44  //    happened before), our build won't break.
45  //  - If Apple removes or renames the function in a future runtime, the
46  //    loader won't refuse to let the application launch.  Instead, we'll
47  //    silently fail to set any metadata.
48  typedef OSStatus (*MDItemSetAttribute_type)(MDItemRef, CFStringRef,
49                                              CFTypeRef);
50  static MDItemSetAttribute_type md_item_set_attribute_func = NULL;
51
52  static bool did_symbol_lookup = false;
53  if (!did_symbol_lookup) {
54    did_symbol_lookup = true;
55    CFBundleRef metadata_bundle =
56        CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Metadata"));
57    if (!metadata_bundle)
58      return;
59
60    md_item_set_attribute_func = (MDItemSetAttribute_type)
61        CFBundleGetFunctionPointerForName(metadata_bundle,
62                                          CFSTR("MDItemSetAttribute"));
63  }
64  if (!md_item_set_attribute_func)
65    return;
66
67  NSString* file_path =
68      [NSString stringWithUTF8String:file.value().c_str()];
69  if (!file_path)
70    return;
71
72  base::mac::ScopedCFTypeRef<MDItemRef> md_item(
73      MDItemCreate(NULL, base::mac::NSToCFCast(file_path)));
74  if (!md_item)
75    return;
76
77  // We won't put any more than 2 items into the attribute.
78  NSMutableArray* list = [NSMutableArray arrayWithCapacity:2];
79
80  // Follow Safari's lead: the first item in the list is the source URL of
81  // the downloaded file. If the referrer is known, store that, too.
82  NSString* origin_url = [NSString stringWithUTF8String:source.spec().c_str()];
83  if (origin_url)
84    [list addObject:origin_url];
85  NSString* referrer_url =
86      [NSString stringWithUTF8String:referrer.spec().c_str()];
87  if (referrer_url)
88    [list addObject:referrer_url];
89
90  md_item_set_attribute_func(md_item, kMDItemWhereFroms,
91                             base::mac::NSToCFCast(list));
92}
93
94// The OS will automatically quarantine files due to the
95// LSFileQuarantineEnabled entry in our Info.plist, but it knows relatively
96// little about the files. We add more information about the download to
97// improve the UI shown by the OS when the users tries to open the file.
98void AddQuarantineMetadataToFile(const FilePath& file, const GURL& source,
99                                 const GURL& referrer) {
100  FSRef file_ref;
101  if (!base::mac::FSRefFromPath(file.value(), &file_ref))
102    return;
103
104  NSMutableDictionary* quarantine_properties = nil;
105  CFTypeRef quarantine_properties_base = NULL;
106  if (LSCopyItemAttribute(&file_ref, kLSRolesAll, kLSItemQuarantineProperties,
107                            &quarantine_properties_base) == noErr) {
108    if (CFGetTypeID(quarantine_properties_base) ==
109        CFDictionaryGetTypeID()) {
110      // Quarantine properties will already exist if LSFileQuarantineEnabled
111      // is on and the file doesn't match an exclusion.
112      quarantine_properties =
113          [[(NSDictionary*)quarantine_properties_base mutableCopy] autorelease];
114    } else {
115      LOG(WARNING) << "kLSItemQuarantineProperties is not a dictionary on file "
116                   << file.value();
117    }
118    CFRelease(quarantine_properties_base);
119  }
120
121  if (!quarantine_properties) {
122    // If there are no quarantine properties, then the file isn't quarantined
123    // (e.g., because the user has set up exclusions for certain file types).
124    // We don't want to add any metadata, because that will cause the file to
125    // be quarantined against the user's wishes.
126    return;
127  }
128
129  // kLSQuarantineAgentNameKey, kLSQuarantineAgentBundleIdentifierKey, and
130  // kLSQuarantineTimeStampKey are set for us (see LSQuarantine.h), so we only
131  // need to set the values that the OS can't infer.
132
133  if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineTypeKey]) {
134    CFStringRef type = (source.SchemeIs("http") || source.SchemeIs("https"))
135                       ? kLSQuarantineTypeWebDownload
136                       : kLSQuarantineTypeOtherDownload;
137    [quarantine_properties setValue:(NSString*)type
138                             forKey:(NSString*)kLSQuarantineTypeKey];
139  }
140
141  if (![quarantine_properties
142          valueForKey:(NSString*)kLSQuarantineOriginURLKey] &&
143      referrer.is_valid()) {
144    NSString* referrer_url =
145        [NSString stringWithUTF8String:referrer.spec().c_str()];
146    [quarantine_properties setValue:referrer_url
147                             forKey:(NSString*)kLSQuarantineOriginURLKey];
148  }
149
150  if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineDataURLKey] &&
151      source.is_valid()) {
152    NSString* origin_url =
153        [NSString stringWithUTF8String:source.spec().c_str()];
154    [quarantine_properties setValue:origin_url
155                             forKey:(NSString*)kLSQuarantineDataURLKey];
156  }
157
158  OSStatus os_error = LSSetItemAttribute(&file_ref, kLSRolesAll,
159                                         kLSItemQuarantineProperties,
160                                         quarantine_properties);
161  if (os_error != noErr) {
162    LOG(WARNING) << "Unable to set quarantine attributes on file "
163                 << file.value();
164  }
165}
166
167}  // namespace file_metadata
168