Home | History | Annotate | Download | only in drive
      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