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