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 "chrome/browser/drive/drive_api_util.h"
6
7 #include <string>
8
9 #include "base/files/file.h"
10 #include "base/logging.h"
11 #include "base/md5.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/values.h"
17 #include "content/public/browser/browser_thread.h"
18 #include "google_apis/drive/drive_api_parser.h"
19 #include "google_apis/drive/gdata_wapi_parser.h"
20 #include "net/base/escape.h"
21 #include "third_party/re2/re2/re2.h"
22 #include "url/gurl.h"
23
24 namespace drive {
25 namespace util {
26 namespace {
27
GetMimeTypeFromEntryKind(google_apis::DriveEntryKind kind)28 std::string GetMimeTypeFromEntryKind(google_apis::DriveEntryKind kind) {
29 switch (kind) {
30 case google_apis::ENTRY_KIND_DOCUMENT:
31 return kGoogleDocumentMimeType;
32 case google_apis::ENTRY_KIND_SPREADSHEET:
33 return kGoogleSpreadsheetMimeType;
34 case google_apis::ENTRY_KIND_PRESENTATION:
35 return kGooglePresentationMimeType;
36 case google_apis::ENTRY_KIND_DRAWING:
37 return kGoogleDrawingMimeType;
38 case google_apis::ENTRY_KIND_TABLE:
39 return kGoogleTableMimeType;
40 case google_apis::ENTRY_KIND_FORM:
41 return kGoogleFormMimeType;
42 default:
43 return std::string();
44 }
45 }
46
47 // Returns the argument string.
Identity(const std::string & resource_id)48 std::string Identity(const std::string& resource_id) { return resource_id; }
49
50 } // namespace
51
52
EscapeQueryStringValue(const std::string & str)53 std::string EscapeQueryStringValue(const std::string& str) {
54 std::string result;
55 result.reserve(str.size());
56 for (size_t i = 0; i < str.size(); ++i) {
57 if (str[i] == '\\' || str[i] == '\'') {
58 result.push_back('\\');
59 }
60 result.push_back(str[i]);
61 }
62 return result;
63 }
64
TranslateQuery(const std::string & original_query)65 std::string TranslateQuery(const std::string& original_query) {
66 // In order to handle non-ascii white spaces correctly, convert to UTF16.
67 base::string16 query = base::UTF8ToUTF16(original_query);
68 const base::string16 kDelimiter(
69 base::kWhitespaceUTF16 + base::ASCIIToUTF16("\""));
70
71 std::string result;
72 for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
73 index != base::string16::npos;
74 index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
75 bool is_exclusion = (query[index] == '-');
76 if (is_exclusion)
77 ++index;
78 if (index == query.length()) {
79 // Here, the token is '-' and it should be ignored.
80 continue;
81 }
82
83 size_t begin_token = index;
84 base::string16 token;
85 if (query[begin_token] == '"') {
86 // Quoted query.
87 ++begin_token;
88 size_t end_token = query.find('"', begin_token);
89 if (end_token == base::string16::npos) {
90 // This is kind of syntax error, since quoted string isn't finished.
91 // However, the query is built by user manually, so here we treat
92 // whole remaining string as a token as a fallback, by appending
93 // a missing double-quote character.
94 end_token = query.length();
95 query.push_back('"');
96 }
97
98 token = query.substr(begin_token, end_token - begin_token);
99 index = end_token + 1; // Consume last '"', too.
100 } else {
101 size_t end_token = query.find_first_of(kDelimiter, begin_token);
102 if (end_token == base::string16::npos) {
103 end_token = query.length();
104 }
105
106 token = query.substr(begin_token, end_token - begin_token);
107 index = end_token;
108 }
109
110 if (token.empty()) {
111 // Just ignore an empty token.
112 continue;
113 }
114
115 if (!result.empty()) {
116 // If there are two or more tokens, need to connect with "and".
117 result.append(" and ");
118 }
119
120 // The meaning of "fullText" should include title, description and content.
121 base::StringAppendF(
122 &result,
123 "%sfullText contains \'%s\'",
124 is_exclusion ? "not " : "",
125 EscapeQueryStringValue(base::UTF16ToUTF8(token)).c_str());
126 }
127
128 return result;
129 }
130
ExtractResourceIdFromUrl(const GURL & url)131 std::string ExtractResourceIdFromUrl(const GURL& url) {
132 return net::UnescapeURLComponent(url.ExtractFileName(),
133 net::UnescapeRule::URL_SPECIAL_CHARS);
134 }
135
CanonicalizeResourceId(const std::string & resource_id)136 std::string CanonicalizeResourceId(const std::string& resource_id) {
137 // If resource ID is in the old WAPI format starting with a prefix like
138 // "document:", strip it and return the remaining part.
139 std::string stripped_resource_id;
140 if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
141 &stripped_resource_id))
142 return stripped_resource_id;
143 return resource_id;
144 }
145
GetIdentityResourceIdCanonicalizer()146 ResourceIdCanonicalizer GetIdentityResourceIdCanonicalizer() {
147 return base::Bind(&Identity);
148 }
149
150 const char kDocsListScope[] = "https://docs.google.com/feeds/";
151 const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";
152
ParseShareUrlAndRun(const google_apis::GetShareUrlCallback & callback,google_apis::GDataErrorCode error,scoped_ptr<base::Value> value)153 void ParseShareUrlAndRun(const google_apis::GetShareUrlCallback& callback,
154 google_apis::GDataErrorCode error,
155 scoped_ptr<base::Value> value) {
156 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
157
158 if (!value) {
159 callback.Run(error, GURL());
160 return;
161 }
162
163 // Parsing ResourceEntry is cheap enough to do on UI thread.
164 scoped_ptr<google_apis::ResourceEntry> entry =
165 google_apis::ResourceEntry::ExtractAndParse(*value);
166 if (!entry) {
167 callback.Run(google_apis::GDATA_PARSE_ERROR, GURL());
168 return;
169 }
170
171 const google_apis::Link* share_link =
172 entry->GetLinkByType(google_apis::Link::LINK_SHARE);
173 callback.Run(error, share_link ? share_link->href() : GURL());
174 }
175
ConvertResourceEntryToFileResource(const google_apis::ResourceEntry & entry)176 scoped_ptr<google_apis::FileResource> ConvertResourceEntryToFileResource(
177 const google_apis::ResourceEntry& entry) {
178 scoped_ptr<google_apis::FileResource> file(new google_apis::FileResource);
179
180 file->set_file_id(entry.resource_id());
181 file->set_title(entry.title());
182 file->set_created_date(entry.published_time());
183
184 if (std::find(entry.labels().begin(), entry.labels().end(),
185 "shared-with-me") != entry.labels().end()) {
186 // Set current time to mark the file is shared_with_me, since ResourceEntry
187 // doesn't have |shared_with_me_date| equivalent.
188 file->set_shared_with_me_date(base::Time::Now());
189 }
190
191 file->set_shared(std::find(entry.labels().begin(), entry.labels().end(),
192 "shared") != entry.labels().end());
193
194 if (entry.is_folder()) {
195 file->set_mime_type(kDriveFolderMimeType);
196 } else {
197 std::string mime_type = GetMimeTypeFromEntryKind(entry.kind());
198 if (mime_type.empty())
199 mime_type = entry.content_mime_type();
200 file->set_mime_type(mime_type);
201 }
202
203 file->set_md5_checksum(entry.file_md5());
204 file->set_file_size(entry.file_size());
205
206 file->mutable_labels()->set_trashed(entry.deleted());
207 file->set_etag(entry.etag());
208
209 google_apis::ImageMediaMetadata* image_media_metadata =
210 file->mutable_image_media_metadata();
211 image_media_metadata->set_width(entry.image_width());
212 image_media_metadata->set_height(entry.image_height());
213 image_media_metadata->set_rotation(entry.image_rotation());
214
215 std::vector<google_apis::ParentReference>* parents = file->mutable_parents();
216 for (size_t i = 0; i < entry.links().size(); ++i) {
217 using google_apis::Link;
218 const Link& link = *entry.links()[i];
219 switch (link.type()) {
220 case Link::LINK_PARENT: {
221 google_apis::ParentReference parent;
222 parent.set_parent_link(link.href());
223
224 std::string file_id =
225 drive::util::ExtractResourceIdFromUrl(link.href());
226 parent.set_file_id(file_id);
227 parents->push_back(parent);
228 break;
229 }
230 case Link::LINK_ALTERNATE:
231 file->set_alternate_link(link.href());
232 break;
233 default:
234 break;
235 }
236 }
237
238 file->set_modified_date(entry.updated_time());
239 file->set_last_viewed_by_me_date(entry.last_viewed_time());
240
241 return file.Pass();
242 }
243
GetKind(const google_apis::FileResource & file_resource)244 google_apis::DriveEntryKind GetKind(
245 const google_apis::FileResource& file_resource) {
246 if (file_resource.IsDirectory())
247 return google_apis::ENTRY_KIND_FOLDER;
248
249 const std::string& mime_type = file_resource.mime_type();
250 if (mime_type == kGoogleDocumentMimeType)
251 return google_apis::ENTRY_KIND_DOCUMENT;
252 if (mime_type == kGoogleSpreadsheetMimeType)
253 return google_apis::ENTRY_KIND_SPREADSHEET;
254 if (mime_type == kGooglePresentationMimeType)
255 return google_apis::ENTRY_KIND_PRESENTATION;
256 if (mime_type == kGoogleDrawingMimeType)
257 return google_apis::ENTRY_KIND_DRAWING;
258 if (mime_type == kGoogleTableMimeType)
259 return google_apis::ENTRY_KIND_TABLE;
260 if (mime_type == kGoogleFormMimeType)
261 return google_apis::ENTRY_KIND_FORM;
262 if (mime_type == "application/pdf")
263 return google_apis::ENTRY_KIND_PDF;
264 return google_apis::ENTRY_KIND_FILE;
265 }
266
267 scoped_ptr<google_apis::ResourceEntry>
ConvertFileResourceToResourceEntry(const google_apis::FileResource & file_resource)268 ConvertFileResourceToResourceEntry(
269 const google_apis::FileResource& file_resource) {
270 scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);
271
272 // ResourceEntry
273 entry->set_resource_id(file_resource.file_id());
274 entry->set_id(file_resource.file_id());
275 entry->set_kind(GetKind(file_resource));
276 entry->set_title(file_resource.title());
277 entry->set_published_time(file_resource.created_date());
278
279 std::vector<std::string> labels;
280 if (!file_resource.shared_with_me_date().is_null())
281 labels.push_back("shared-with-me");
282 if (file_resource.shared())
283 labels.push_back("shared");
284 entry->set_labels(labels);
285
286 // This should be the url to download the file_resource.
287 {
288 google_apis::Content content;
289 content.set_mime_type(file_resource.mime_type());
290 entry->set_content(content);
291 }
292 // TODO(kochi): entry->resource_links_
293
294 // For file entries
295 entry->set_filename(file_resource.title());
296 entry->set_suggested_filename(file_resource.title());
297 entry->set_file_md5(file_resource.md5_checksum());
298 entry->set_file_size(file_resource.file_size());
299
300 // If file is removed completely, that information is only available in
301 // ChangeResource, and is reflected in |removed_|. If file is trashed, the
302 // file entry still exists but with its "trashed" label true.
303 entry->set_deleted(file_resource.labels().is_trashed());
304
305 // ImageMediaMetadata
306 entry->set_image_width(file_resource.image_media_metadata().width());
307 entry->set_image_height(file_resource.image_media_metadata().height());
308 entry->set_image_rotation(file_resource.image_media_metadata().rotation());
309
310 // CommonMetadata
311 entry->set_etag(file_resource.etag());
312 // entry->authors_
313 // entry->links_.
314 ScopedVector<google_apis::Link> links;
315 for (size_t i = 0; i < file_resource.parents().size(); ++i) {
316 google_apis::Link* link = new google_apis::Link;
317 link->set_type(google_apis::Link::LINK_PARENT);
318 link->set_href(file_resource.parents()[i].parent_link());
319 links.push_back(link);
320 }
321 if (!file_resource.alternate_link().is_empty()) {
322 google_apis::Link* link = new google_apis::Link;
323 link->set_type(google_apis::Link::LINK_ALTERNATE);
324 link->set_href(file_resource.alternate_link());
325 links.push_back(link);
326 }
327 entry->set_links(links.Pass());
328
329 // entry->categories_
330 entry->set_updated_time(file_resource.modified_date());
331 entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());
332
333 entry->FillRemainingFields();
334 return entry.Pass();
335 }
336
337 scoped_ptr<google_apis::ResourceEntry>
ConvertChangeResourceToResourceEntry(const google_apis::ChangeResource & change_resource)338 ConvertChangeResourceToResourceEntry(
339 const google_apis::ChangeResource& change_resource) {
340 scoped_ptr<google_apis::ResourceEntry> entry;
341 if (change_resource.file())
342 entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
343 else
344 entry.reset(new google_apis::ResourceEntry);
345
346 entry->set_resource_id(change_resource.file_id());
347 // If |is_deleted()| returns true, the file is removed from Drive.
348 entry->set_removed(change_resource.is_deleted());
349 entry->set_changestamp(change_resource.change_id());
350 entry->set_modification_date(change_resource.modification_date());
351
352 return entry.Pass();
353 }
354
355 scoped_ptr<google_apis::ResourceList>
ConvertFileListToResourceList(const google_apis::FileList & file_list)356 ConvertFileListToResourceList(const google_apis::FileList& file_list) {
357 scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
358
359 const ScopedVector<google_apis::FileResource>& items = file_list.items();
360 ScopedVector<google_apis::ResourceEntry> entries;
361 for (size_t i = 0; i < items.size(); ++i)
362 entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
363 feed->set_entries(entries.Pass());
364
365 ScopedVector<google_apis::Link> links;
366 if (!file_list.next_link().is_empty()) {
367 google_apis::Link* link = new google_apis::Link;
368 link->set_type(google_apis::Link::LINK_NEXT);
369 link->set_href(file_list.next_link());
370 links.push_back(link);
371 }
372 feed->set_links(links.Pass());
373
374 return feed.Pass();
375 }
376
377 scoped_ptr<google_apis::ResourceList>
ConvertChangeListToResourceList(const google_apis::ChangeList & change_list)378 ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
379 scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
380
381 const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
382 ScopedVector<google_apis::ResourceEntry> entries;
383 for (size_t i = 0; i < items.size(); ++i) {
384 entries.push_back(
385 ConvertChangeResourceToResourceEntry(*items[i]).release());
386 }
387 feed->set_entries(entries.Pass());
388
389 feed->set_largest_changestamp(change_list.largest_change_id());
390
391 ScopedVector<google_apis::Link> links;
392 if (!change_list.next_link().is_empty()) {
393 google_apis::Link* link = new google_apis::Link;
394 link->set_type(google_apis::Link::LINK_NEXT);
395 link->set_href(change_list.next_link());
396 links.push_back(link);
397 }
398 feed->set_links(links.Pass());
399
400 return feed.Pass();
401 }
402
GetMd5Digest(const base::FilePath & file_path)403 std::string GetMd5Digest(const base::FilePath& file_path) {
404 const int kBufferSize = 512 * 1024; // 512kB.
405
406 base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
407 if (!file.IsValid())
408 return std::string();
409
410 base::MD5Context context;
411 base::MD5Init(&context);
412
413 int64 offset = 0;
414 scoped_ptr<char[]> buffer(new char[kBufferSize]);
415 while (true) {
416 int result = file.Read(offset, buffer.get(), kBufferSize);
417 if (result < 0) {
418 // Found an error.
419 return std::string();
420 }
421
422 if (result == 0) {
423 // End of file.
424 break;
425 }
426
427 offset += result;
428 base::MD5Update(&context, base::StringPiece(buffer.get(), result));
429 }
430
431 base::MD5Digest digest;
432 base::MD5Final(&digest, &context);
433 return MD5DigestToBase16(digest);
434 }
435
436 const char kWapiRootDirectoryResourceId[] = "folder:root";
437
438 } // namespace util
439 } // namespace drive
440