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 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. 48 std::string Identity(const std::string& resource_id) { return resource_id; } 49 50 } // namespace 51 52 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 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 131 std::string ExtractResourceIdFromUrl(const GURL& url) { 132 return net::UnescapeURLComponent(url.ExtractFileName(), 133 net::UnescapeRule::URL_SPECIAL_CHARS); 134 } 135 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 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 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 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 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> 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> 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> 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> 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 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