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/command_line.h"
10#include "base/files/scoped_platform_file_closer.h"
11#include "base/logging.h"
12#include "base/md5.h"
13#include "base/platform_file.h"
14#include "base/strings/string16.h"
15#include "base/strings/string_util.h"
16#include "base/strings/stringprintf.h"
17#include "base/strings/utf_string_conversions.h"
18#include "base/values.h"
19#include "chrome/browser/drive/drive_switches.h"
20#include "content/public/browser/browser_thread.h"
21#include "google_apis/drive/drive_api_parser.h"
22#include "google_apis/drive/gdata_wapi_parser.h"
23#include "net/base/escape.h"
24#include "third_party/re2/re2/re2.h"
25#include "url/gurl.h"
26
27namespace drive {
28namespace util {
29namespace {
30
31// Google Apps MIME types:
32const char kGoogleDocumentMimeType[] = "application/vnd.google-apps.document";
33const char kGoogleDrawingMimeType[] = "application/vnd.google-apps.drawing";
34const char kGooglePresentationMimeType[] =
35    "application/vnd.google-apps.presentation";
36const char kGoogleSpreadsheetMimeType[] =
37    "application/vnd.google-apps.spreadsheet";
38const char kGoogleTableMimeType[] = "application/vnd.google-apps.table";
39const char kGoogleFormMimeType[] = "application/vnd.google-apps.form";
40const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder";
41
42ScopedVector<std::string> CopyScopedVectorString(
43    const ScopedVector<std::string>& source) {
44  ScopedVector<std::string> result;
45  result.reserve(source.size());
46  for (size_t i = 0; i < source.size(); ++i)
47    result.push_back(new std::string(*source[i]));
48
49  return result.Pass();
50}
51
52// Converts AppIcon (of GData WAPI) to DriveAppIcon.
53scoped_ptr<google_apis::DriveAppIcon>
54ConvertAppIconToDriveAppIcon(const google_apis::AppIcon& app_icon) {
55  scoped_ptr<google_apis::DriveAppIcon> resource(
56      new google_apis::DriveAppIcon);
57  switch (app_icon.category()) {
58    case google_apis::AppIcon::ICON_UNKNOWN:
59      resource->set_category(google_apis::DriveAppIcon::UNKNOWN);
60      break;
61    case google_apis::AppIcon::ICON_DOCUMENT:
62      resource->set_category(google_apis::DriveAppIcon::DOCUMENT);
63      break;
64    case google_apis::AppIcon::ICON_APPLICATION:
65      resource->set_category(google_apis::DriveAppIcon::APPLICATION);
66      break;
67    case google_apis::AppIcon::ICON_SHARED_DOCUMENT:
68      resource->set_category(google_apis::DriveAppIcon::SHARED_DOCUMENT);
69      break;
70    default:
71      NOTREACHED();
72  }
73
74  resource->set_icon_side_length(app_icon.icon_side_length());
75  resource->set_icon_url(app_icon.GetIconURL());
76  return resource.Pass();
77}
78
79// Converts InstalledApp to AppResource.
80scoped_ptr<google_apis::AppResource>
81ConvertInstalledAppToAppResource(
82    const google_apis::InstalledApp& installed_app) {
83  scoped_ptr<google_apis::AppResource> resource(new google_apis::AppResource);
84  resource->set_application_id(installed_app.app_id());
85  resource->set_name(installed_app.app_name());
86  resource->set_object_type(installed_app.object_type());
87  resource->set_supports_create(installed_app.supports_create());
88  resource->set_product_url(installed_app.GetProductUrl());
89
90  {
91    ScopedVector<std::string> primary_mimetypes(
92        CopyScopedVectorString(installed_app.primary_mimetypes()));
93    resource->set_primary_mimetypes(primary_mimetypes.Pass());
94  }
95  {
96    ScopedVector<std::string> secondary_mimetypes(
97        CopyScopedVectorString(installed_app.secondary_mimetypes()));
98    resource->set_secondary_mimetypes(secondary_mimetypes.Pass());
99  }
100  {
101    ScopedVector<std::string> primary_file_extensions(
102        CopyScopedVectorString(installed_app.primary_extensions()));
103    resource->set_primary_file_extensions(primary_file_extensions.Pass());
104  }
105  {
106    ScopedVector<std::string> secondary_file_extensions(
107        CopyScopedVectorString(installed_app.secondary_extensions()));
108    resource->set_secondary_file_extensions(secondary_file_extensions.Pass());
109  }
110
111  {
112    const ScopedVector<google_apis::AppIcon>& app_icons =
113        installed_app.app_icons();
114    ScopedVector<google_apis::DriveAppIcon> icons;
115    icons.reserve(app_icons.size());
116    for (size_t i = 0; i < app_icons.size(); ++i) {
117      icons.push_back(ConvertAppIconToDriveAppIcon(*app_icons[i]).release());
118    }
119    resource->set_icons(icons.Pass());
120  }
121
122  // supports_import, installed and authorized are not supported in
123  // InstalledApp.
124
125  return resource.Pass();
126}
127
128// Returns the argument string.
129std::string Identity(const std::string& resource_id) { return resource_id; }
130
131}  // namespace
132
133
134bool IsDriveV2ApiEnabled() {
135  const CommandLine* command_line = CommandLine::ForCurrentProcess();
136
137  // Enable Drive API v2 by default.
138  if (!command_line->HasSwitch(switches::kEnableDriveV2Api))
139    return true;
140
141  std::string value =
142      command_line->GetSwitchValueASCII(switches::kEnableDriveV2Api);
143  StringToLowerASCII(&value);
144  // The value must be "" or "true" for true, or "false" for false.
145  DCHECK(value.empty() || value == "true" || value == "false");
146  return value != "false";
147}
148
149std::string EscapeQueryStringValue(const std::string& str) {
150  std::string result;
151  result.reserve(str.size());
152  for (size_t i = 0; i < str.size(); ++i) {
153    if (str[i] == '\\' || str[i] == '\'') {
154      result.push_back('\\');
155    }
156    result.push_back(str[i]);
157  }
158  return result;
159}
160
161std::string TranslateQuery(const std::string& original_query) {
162  // In order to handle non-ascii white spaces correctly, convert to UTF16.
163  base::string16 query = UTF8ToUTF16(original_query);
164  const base::string16 kDelimiter(
165      base::kWhitespaceUTF16 + base::string16(1, static_cast<char16>('"')));
166
167  std::string result;
168  for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
169       index != base::string16::npos;
170       index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
171    bool is_exclusion = (query[index] == '-');
172    if (is_exclusion)
173      ++index;
174    if (index == query.length()) {
175      // Here, the token is '-' and it should be ignored.
176      continue;
177    }
178
179    size_t begin_token = index;
180    base::string16 token;
181    if (query[begin_token] == '"') {
182      // Quoted query.
183      ++begin_token;
184      size_t end_token = query.find('"', begin_token);
185      if (end_token == base::string16::npos) {
186        // This is kind of syntax error, since quoted string isn't finished.
187        // However, the query is built by user manually, so here we treat
188        // whole remaining string as a token as a fallback, by appending
189        // a missing double-quote character.
190        end_token = query.length();
191        query.push_back('"');
192      }
193
194      token = query.substr(begin_token, end_token - begin_token);
195      index = end_token + 1;  // Consume last '"', too.
196    } else {
197      size_t end_token = query.find_first_of(kDelimiter, begin_token);
198      if (end_token == base::string16::npos) {
199        end_token = query.length();
200      }
201
202      token = query.substr(begin_token, end_token - begin_token);
203      index = end_token;
204    }
205
206    if (token.empty()) {
207      // Just ignore an empty token.
208      continue;
209    }
210
211    if (!result.empty()) {
212      // If there are two or more tokens, need to connect with "and".
213      result.append(" and ");
214    }
215
216    // The meaning of "fullText" should include title, description and content.
217    base::StringAppendF(
218        &result,
219        "%sfullText contains \'%s\'",
220        is_exclusion ? "not " : "",
221        EscapeQueryStringValue(UTF16ToUTF8(token)).c_str());
222  }
223
224  return result;
225}
226
227std::string ExtractResourceIdFromUrl(const GURL& url) {
228  return net::UnescapeURLComponent(url.ExtractFileName(),
229                                   net::UnescapeRule::URL_SPECIAL_CHARS);
230}
231
232std::string CanonicalizeResourceId(const std::string& resource_id) {
233  // If resource ID is in the old WAPI format starting with a prefix like
234  // "document:", strip it and return the remaining part.
235  std::string stripped_resource_id;
236  if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
237                     &stripped_resource_id))
238    return stripped_resource_id;
239  return resource_id;
240}
241
242ResourceIdCanonicalizer GetIdentityResourceIdCanonicalizer() {
243  return base::Bind(&Identity);
244}
245
246const char kDocsListScope[] = "https://docs.google.com/feeds/";
247const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";
248
249void ParseShareUrlAndRun(const google_apis::GetShareUrlCallback& callback,
250                         google_apis::GDataErrorCode error,
251                         scoped_ptr<base::Value> value) {
252  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
253
254  if (!value) {
255    callback.Run(error, GURL());
256    return;
257  }
258
259  // Parsing ResourceEntry is cheap enough to do on UI thread.
260  scoped_ptr<google_apis::ResourceEntry> entry =
261      google_apis::ResourceEntry::ExtractAndParse(*value);
262  if (!entry) {
263    callback.Run(google_apis::GDATA_PARSE_ERROR, GURL());
264    return;
265  }
266
267  const google_apis::Link* share_link =
268      entry->GetLinkByType(google_apis::Link::LINK_SHARE);
269  callback.Run(error, share_link ? share_link->href() : GURL());
270}
271
272scoped_ptr<google_apis::AboutResource>
273ConvertAccountMetadataToAboutResource(
274    const google_apis::AccountMetadata& account_metadata,
275    const std::string& root_resource_id) {
276  scoped_ptr<google_apis::AboutResource> resource(
277      new google_apis::AboutResource);
278  resource->set_largest_change_id(account_metadata.largest_changestamp());
279  resource->set_quota_bytes_total(account_metadata.quota_bytes_total());
280  resource->set_quota_bytes_used(account_metadata.quota_bytes_used());
281  resource->set_root_folder_id(root_resource_id);
282  return resource.Pass();
283}
284
285scoped_ptr<google_apis::AppList>
286ConvertAccountMetadataToAppList(
287    const google_apis::AccountMetadata& account_metadata) {
288  scoped_ptr<google_apis::AppList> resource(new google_apis::AppList);
289
290  const ScopedVector<google_apis::InstalledApp>& installed_apps =
291      account_metadata.installed_apps();
292  ScopedVector<google_apis::AppResource> app_resources;
293  app_resources.reserve(installed_apps.size());
294  for (size_t i = 0; i < installed_apps.size(); ++i) {
295    app_resources.push_back(
296        ConvertInstalledAppToAppResource(*installed_apps[i]).release());
297  }
298  resource->set_items(app_resources.Pass());
299
300  // etag is not supported in AccountMetadata.
301
302  return resource.Pass();
303}
304
305
306scoped_ptr<google_apis::FileResource> ConvertResourceEntryToFileResource(
307    const google_apis::ResourceEntry& entry) {
308  scoped_ptr<google_apis::FileResource> file(new google_apis::FileResource);
309
310  file->set_file_id(entry.resource_id());
311  file->set_title(entry.title());
312  file->set_created_date(entry.published_time());
313
314  if (std::find(entry.labels().begin(), entry.labels().end(),
315                "shared-with-me") != entry.labels().end()) {
316    // Set current time to mark the file is shared_with_me, since ResourceEntry
317    // doesn't have |shared_with_me_date| equivalent.
318    file->set_shared_with_me_date(base::Time::Now());
319  }
320
321  file->set_shared(std::find(entry.labels().begin(), entry.labels().end(),
322                             "shared") != entry.labels().end());
323
324  file->set_download_url(entry.download_url());
325  if (entry.is_folder())
326    file->set_mime_type(kDriveFolderMimeType);
327  else
328    file->set_mime_type(entry.content_mime_type());
329
330  file->set_md5_checksum(entry.file_md5());
331  file->set_file_size(entry.file_size());
332
333  file->mutable_labels()->set_trashed(entry.deleted());
334  file->set_etag(entry.etag());
335
336  google_apis::ImageMediaMetadata* image_media_metadata =
337    file->mutable_image_media_metadata();
338  image_media_metadata->set_width(entry.image_width());
339  image_media_metadata->set_height(entry.image_height());
340  image_media_metadata->set_rotation(entry.image_rotation());
341
342  ScopedVector<google_apis::ParentReference> parents;
343  for (size_t i = 0; i < entry.links().size(); ++i) {
344    using google_apis::Link;
345    const Link& link = *entry.links()[i];
346    switch (link.type()) {
347      case Link::LINK_PARENT: {
348        scoped_ptr<google_apis::ParentReference> parent(
349            new google_apis::ParentReference);
350        parent->set_parent_link(link.href());
351
352        std::string file_id =
353            drive::util::ExtractResourceIdFromUrl(link.href());
354        parent->set_file_id(file_id);
355        parent->set_is_root(file_id == kWapiRootDirectoryResourceId);
356        parents.push_back(parent.release());
357        break;
358      }
359      case Link::LINK_EDIT:
360        file->set_self_link(link.href());
361        break;
362      case Link::LINK_THUMBNAIL:
363        file->set_thumbnail_link(link.href());
364        break;
365      case Link::LINK_ALTERNATE:
366        file->set_alternate_link(link.href());
367        break;
368      case Link::LINK_EMBED:
369        file->set_embed_link(link.href());
370        break;
371      default:
372        break;
373    }
374  }
375  file->set_parents(parents.Pass());
376
377  file->set_modified_date(entry.updated_time());
378  file->set_last_viewed_by_me_date(entry.last_viewed_time());
379
380  return file.Pass();
381}
382
383google_apis::DriveEntryKind GetKind(
384    const google_apis::FileResource& file_resource) {
385  if (file_resource.IsDirectory())
386    return google_apis::ENTRY_KIND_FOLDER;
387
388  const std::string& mime_type = file_resource.mime_type();
389  if (mime_type == kGoogleDocumentMimeType)
390    return google_apis::ENTRY_KIND_DOCUMENT;
391  if (mime_type == kGoogleSpreadsheetMimeType)
392    return google_apis::ENTRY_KIND_SPREADSHEET;
393  if (mime_type == kGooglePresentationMimeType)
394    return google_apis::ENTRY_KIND_PRESENTATION;
395  if (mime_type == kGoogleDrawingMimeType)
396    return google_apis::ENTRY_KIND_DRAWING;
397  if (mime_type == kGoogleTableMimeType)
398    return google_apis::ENTRY_KIND_TABLE;
399  if (mime_type == kGoogleFormMimeType)
400    return google_apis::ENTRY_KIND_FORM;
401  if (mime_type == "application/pdf")
402    return google_apis::ENTRY_KIND_PDF;
403  return google_apis::ENTRY_KIND_FILE;
404}
405
406scoped_ptr<google_apis::ResourceEntry>
407ConvertFileResourceToResourceEntry(
408    const google_apis::FileResource& file_resource) {
409  scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);
410
411  // ResourceEntry
412  entry->set_resource_id(file_resource.file_id());
413  entry->set_id(file_resource.file_id());
414  entry->set_kind(GetKind(file_resource));
415  entry->set_title(file_resource.title());
416  entry->set_published_time(file_resource.created_date());
417
418  std::vector<std::string> labels;
419  if (!file_resource.shared_with_me_date().is_null())
420    labels.push_back("shared-with-me");
421  if (file_resource.shared())
422    labels.push_back("shared");
423  entry->set_labels(labels);
424
425  // This should be the url to download the file_resource.
426  {
427    google_apis::Content content;
428    content.set_url(file_resource.download_url());
429    content.set_mime_type(file_resource.mime_type());
430    entry->set_content(content);
431  }
432  // TODO(kochi): entry->resource_links_
433
434  // For file entries
435  entry->set_filename(file_resource.title());
436  entry->set_suggested_filename(file_resource.title());
437  entry->set_file_md5(file_resource.md5_checksum());
438  entry->set_file_size(file_resource.file_size());
439
440  // If file is removed completely, that information is only available in
441  // ChangeResource, and is reflected in |removed_|. If file is trashed, the
442  // file entry still exists but with its "trashed" label true.
443  entry->set_deleted(file_resource.labels().is_trashed());
444
445  // ImageMediaMetadata
446  entry->set_image_width(file_resource.image_media_metadata().width());
447  entry->set_image_height(file_resource.image_media_metadata().height());
448  entry->set_image_rotation(file_resource.image_media_metadata().rotation());
449
450  // CommonMetadata
451  entry->set_etag(file_resource.etag());
452  // entry->authors_
453  // entry->links_.
454  ScopedVector<google_apis::Link> links;
455  for (size_t i = 0; i < file_resource.parents().size(); ++i) {
456    google_apis::Link* link = new google_apis::Link;
457    link->set_type(google_apis::Link::LINK_PARENT);
458    link->set_href(file_resource.parents()[i]->parent_link());
459    links.push_back(link);
460  }
461  if (!file_resource.self_link().is_empty()) {
462    google_apis::Link* link = new google_apis::Link;
463    link->set_type(google_apis::Link::LINK_EDIT);
464    link->set_href(file_resource.self_link());
465    links.push_back(link);
466  }
467  if (!file_resource.thumbnail_link().is_empty()) {
468    google_apis::Link* link = new google_apis::Link;
469    link->set_type(google_apis::Link::LINK_THUMBNAIL);
470    link->set_href(file_resource.thumbnail_link());
471    links.push_back(link);
472  }
473  if (!file_resource.alternate_link().is_empty()) {
474    google_apis::Link* link = new google_apis::Link;
475    link->set_type(google_apis::Link::LINK_ALTERNATE);
476    link->set_href(file_resource.alternate_link());
477    links.push_back(link);
478  }
479  if (!file_resource.embed_link().is_empty()) {
480    google_apis::Link* link = new google_apis::Link;
481    link->set_type(google_apis::Link::LINK_EMBED);
482    link->set_href(file_resource.embed_link());
483    links.push_back(link);
484  }
485  entry->set_links(links.Pass());
486
487  // entry->categories_
488  entry->set_updated_time(file_resource.modified_date());
489  entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());
490
491  entry->FillRemainingFields();
492  return entry.Pass();
493}
494
495scoped_ptr<google_apis::ResourceEntry>
496ConvertChangeResourceToResourceEntry(
497    const google_apis::ChangeResource& change_resource) {
498  scoped_ptr<google_apis::ResourceEntry> entry;
499  if (change_resource.file())
500    entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
501  else
502    entry.reset(new google_apis::ResourceEntry);
503
504  entry->set_resource_id(change_resource.file_id());
505  // If |is_deleted()| returns true, the file is removed from Drive.
506  entry->set_removed(change_resource.is_deleted());
507  entry->set_changestamp(change_resource.change_id());
508
509  return entry.Pass();
510}
511
512scoped_ptr<google_apis::ResourceList>
513ConvertFileListToResourceList(const google_apis::FileList& file_list) {
514  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
515
516  const ScopedVector<google_apis::FileResource>& items = file_list.items();
517  ScopedVector<google_apis::ResourceEntry> entries;
518  for (size_t i = 0; i < items.size(); ++i)
519    entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
520  feed->set_entries(entries.Pass());
521
522  ScopedVector<google_apis::Link> links;
523  if (!file_list.next_link().is_empty()) {
524    google_apis::Link* link = new google_apis::Link;
525    link->set_type(google_apis::Link::LINK_NEXT);
526    link->set_href(file_list.next_link());
527    links.push_back(link);
528  }
529  feed->set_links(links.Pass());
530
531  return feed.Pass();
532}
533
534scoped_ptr<google_apis::ResourceList>
535ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
536  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
537
538  const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
539  ScopedVector<google_apis::ResourceEntry> entries;
540  for (size_t i = 0; i < items.size(); ++i) {
541    entries.push_back(
542        ConvertChangeResourceToResourceEntry(*items[i]).release());
543  }
544  feed->set_entries(entries.Pass());
545
546  feed->set_largest_changestamp(change_list.largest_change_id());
547
548  ScopedVector<google_apis::Link> links;
549  if (!change_list.next_link().is_empty()) {
550    google_apis::Link* link = new google_apis::Link;
551    link->set_type(google_apis::Link::LINK_NEXT);
552    link->set_href(change_list.next_link());
553    links.push_back(link);
554  }
555  feed->set_links(links.Pass());
556
557  return feed.Pass();
558}
559
560std::string GetMd5Digest(const base::FilePath& file_path) {
561  const int kBufferSize = 512 * 1024;  // 512kB.
562
563  base::PlatformFile file = base::CreatePlatformFile(
564      file_path, base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ,
565      NULL, NULL);
566  if (file == base::kInvalidPlatformFileValue)
567    return std::string();
568  base::ScopedPlatformFileCloser file_closer(&file);
569
570  base::MD5Context context;
571  base::MD5Init(&context);
572
573  int64 offset = 0;
574  scoped_ptr<char[]> buffer(new char[kBufferSize]);
575  while (true) {
576    // Avoid using ReadPlatformFileCurPosNoBestEffort for now.
577    // http://crbug.com/145873
578    int result = base::ReadPlatformFileNoBestEffort(
579        file, offset, buffer.get(), kBufferSize);
580
581    if (result < 0) {
582      // Found an error.
583      return std::string();
584    }
585
586    if (result == 0) {
587      // End of file.
588      break;
589    }
590
591    offset += result;
592    base::MD5Update(&context, base::StringPiece(buffer.get(), result));
593  }
594
595  base::MD5Digest digest;
596  base::MD5Final(&digest, &context);
597  return MD5DigestToBase16(digest);
598}
599
600const char kWapiRootDirectoryResourceId[] = "folder:root";
601
602}  // namespace util
603}  // namespace drive
604