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
24namespace drive {
25namespace util {
26namespace {
27
28struct HostedDocumentKind {
29  const char* mime_type;
30  const char* extension;
31};
32
33const HostedDocumentKind kHostedDocumentKinds[] = {
34    {kGoogleDocumentMimeType,     ".gdoc"},
35    {kGoogleSpreadsheetMimeType,  ".gsheet"},
36    {kGooglePresentationMimeType, ".gslides"},
37    {kGoogleDrawingMimeType,      ".gdraw"},
38    {kGoogleTableMimeType,        ".gtable"},
39    {kGoogleFormMimeType,         ".gform"}
40};
41
42const char kUnknownHostedDocumentExtension[] = ".glink";
43
44}  // namespace
45
46std::string EscapeQueryStringValue(const std::string& str) {
47  std::string result;
48  result.reserve(str.size());
49  for (size_t i = 0; i < str.size(); ++i) {
50    if (str[i] == '\\' || str[i] == '\'') {
51      result.push_back('\\');
52    }
53    result.push_back(str[i]);
54  }
55  return result;
56}
57
58std::string TranslateQuery(const std::string& original_query) {
59  // In order to handle non-ascii white spaces correctly, convert to UTF16.
60  base::string16 query = base::UTF8ToUTF16(original_query);
61  const base::string16 kDelimiter(
62      base::kWhitespaceUTF16 + base::ASCIIToUTF16("\""));
63
64  std::string result;
65  for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
66       index != base::string16::npos;
67       index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
68    bool is_exclusion = (query[index] == '-');
69    if (is_exclusion)
70      ++index;
71    if (index == query.length()) {
72      // Here, the token is '-' and it should be ignored.
73      continue;
74    }
75
76    size_t begin_token = index;
77    base::string16 token;
78    if (query[begin_token] == '"') {
79      // Quoted query.
80      ++begin_token;
81      size_t end_token = query.find('"', begin_token);
82      if (end_token == base::string16::npos) {
83        // This is kind of syntax error, since quoted string isn't finished.
84        // However, the query is built by user manually, so here we treat
85        // whole remaining string as a token as a fallback, by appending
86        // a missing double-quote character.
87        end_token = query.length();
88        query.push_back('"');
89      }
90
91      token = query.substr(begin_token, end_token - begin_token);
92      index = end_token + 1;  // Consume last '"', too.
93    } else {
94      size_t end_token = query.find_first_of(kDelimiter, begin_token);
95      if (end_token == base::string16::npos) {
96        end_token = query.length();
97      }
98
99      token = query.substr(begin_token, end_token - begin_token);
100      index = end_token;
101    }
102
103    if (token.empty()) {
104      // Just ignore an empty token.
105      continue;
106    }
107
108    if (!result.empty()) {
109      // If there are two or more tokens, need to connect with "and".
110      result.append(" and ");
111    }
112
113    // The meaning of "fullText" should include title, description and content.
114    base::StringAppendF(
115        &result,
116        "%sfullText contains \'%s\'",
117        is_exclusion ? "not " : "",
118        EscapeQueryStringValue(base::UTF16ToUTF8(token)).c_str());
119  }
120
121  return result;
122}
123
124std::string ExtractResourceIdFromUrl(const GURL& url) {
125  return net::UnescapeURLComponent(url.ExtractFileName(),
126                                   net::UnescapeRule::URL_SPECIAL_CHARS);
127}
128
129std::string CanonicalizeResourceId(const std::string& resource_id) {
130  // If resource ID is in the old WAPI format starting with a prefix like
131  // "document:", strip it and return the remaining part.
132  std::string stripped_resource_id;
133  if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
134                     &stripped_resource_id))
135    return stripped_resource_id;
136  return resource_id;
137}
138
139scoped_ptr<google_apis::ResourceEntry>
140ConvertFileResourceToResourceEntry(
141    const google_apis::FileResource& file_resource) {
142  scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);
143
144  // ResourceEntry
145  entry->set_resource_id(file_resource.file_id());
146  entry->set_id(file_resource.file_id());
147  if (file_resource.IsDirectory())
148    entry->set_kind(google_apis::ResourceEntry::ENTRY_KIND_FOLDER);
149  else if (file_resource.IsHostedDocument())
150    entry->set_kind(google_apis::ResourceEntry::ENTRY_KIND_UNKNOWN);
151  else
152    entry->set_kind(google_apis::ResourceEntry::ENTRY_KIND_FILE);
153  entry->set_title(file_resource.title());
154  entry->set_published_time(file_resource.created_date());
155
156  std::vector<std::string> labels;
157  if (!file_resource.shared_with_me_date().is_null())
158    labels.push_back("shared-with-me");
159  if (file_resource.shared())
160    labels.push_back("shared");
161  entry->set_labels(labels);
162
163  // This should be the url to download the file_resource.
164  {
165    google_apis::Content content;
166    content.set_mime_type(file_resource.mime_type());
167    entry->set_content(content);
168  }
169  // TODO(kochi): entry->resource_links_
170
171  // For file entries
172  entry->set_filename(file_resource.title());
173  entry->set_suggested_filename(file_resource.title());
174  entry->set_file_md5(file_resource.md5_checksum());
175  entry->set_file_size(file_resource.file_size());
176
177  // If file is removed completely, that information is only available in
178  // ChangeResource, and is reflected in |removed_|. If file is trashed, the
179  // file entry still exists but with its "trashed" label true.
180  entry->set_deleted(file_resource.labels().is_trashed());
181
182  // ImageMediaMetadata
183  entry->set_image_width(file_resource.image_media_metadata().width());
184  entry->set_image_height(file_resource.image_media_metadata().height());
185  entry->set_image_rotation(file_resource.image_media_metadata().rotation());
186
187  // CommonMetadata
188  entry->set_etag(file_resource.etag());
189  // entry->authors_
190  // entry->links_.
191  ScopedVector<google_apis::Link> links;
192  for (size_t i = 0; i < file_resource.parents().size(); ++i) {
193    google_apis::Link* link = new google_apis::Link;
194    link->set_type(google_apis::Link::LINK_PARENT);
195    link->set_href(file_resource.parents()[i].parent_link());
196    links.push_back(link);
197  }
198  if (!file_resource.alternate_link().is_empty()) {
199    google_apis::Link* link = new google_apis::Link;
200    link->set_type(google_apis::Link::LINK_ALTERNATE);
201    link->set_href(file_resource.alternate_link());
202    links.push_back(link);
203  }
204  entry->set_links(links.Pass());
205
206  // entry->categories_
207  entry->set_updated_time(file_resource.modified_date());
208  entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());
209
210  entry->FillRemainingFields();
211  return entry.Pass();
212}
213
214scoped_ptr<google_apis::ResourceEntry>
215ConvertChangeResourceToResourceEntry(
216    const google_apis::ChangeResource& change_resource) {
217  scoped_ptr<google_apis::ResourceEntry> entry;
218  if (change_resource.file())
219    entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
220  else
221    entry.reset(new google_apis::ResourceEntry);
222
223  entry->set_resource_id(change_resource.file_id());
224  // If |is_deleted()| returns true, the file is removed from Drive.
225  entry->set_removed(change_resource.is_deleted());
226  entry->set_changestamp(change_resource.change_id());
227  entry->set_modification_date(change_resource.modification_date());
228
229  return entry.Pass();
230}
231
232scoped_ptr<google_apis::ResourceList>
233ConvertFileListToResourceList(const google_apis::FileList& file_list) {
234  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
235
236  const ScopedVector<google_apis::FileResource>& items = file_list.items();
237  ScopedVector<google_apis::ResourceEntry> entries;
238  for (size_t i = 0; i < items.size(); ++i)
239    entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
240  feed->set_entries(entries.Pass());
241
242  ScopedVector<google_apis::Link> links;
243  if (!file_list.next_link().is_empty()) {
244    google_apis::Link* link = new google_apis::Link;
245    link->set_type(google_apis::Link::LINK_NEXT);
246    link->set_href(file_list.next_link());
247    links.push_back(link);
248  }
249  feed->set_links(links.Pass());
250
251  return feed.Pass();
252}
253
254scoped_ptr<google_apis::ResourceList>
255ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
256  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
257
258  const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
259  ScopedVector<google_apis::ResourceEntry> entries;
260  for (size_t i = 0; i < items.size(); ++i) {
261    entries.push_back(
262        ConvertChangeResourceToResourceEntry(*items[i]).release());
263  }
264  feed->set_entries(entries.Pass());
265
266  feed->set_largest_changestamp(change_list.largest_change_id());
267
268  ScopedVector<google_apis::Link> links;
269  if (!change_list.next_link().is_empty()) {
270    google_apis::Link* link = new google_apis::Link;
271    link->set_type(google_apis::Link::LINK_NEXT);
272    link->set_href(change_list.next_link());
273    links.push_back(link);
274  }
275  feed->set_links(links.Pass());
276
277  return feed.Pass();
278}
279
280std::string GetMd5Digest(const base::FilePath& file_path) {
281  const int kBufferSize = 512 * 1024;  // 512kB.
282
283  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
284  if (!file.IsValid())
285    return std::string();
286
287  base::MD5Context context;
288  base::MD5Init(&context);
289
290  int64 offset = 0;
291  scoped_ptr<char[]> buffer(new char[kBufferSize]);
292  while (true) {
293    int result = file.Read(offset, buffer.get(), kBufferSize);
294    if (result < 0) {
295      // Found an error.
296      return std::string();
297    }
298
299    if (result == 0) {
300      // End of file.
301      break;
302    }
303
304    offset += result;
305    base::MD5Update(&context, base::StringPiece(buffer.get(), result));
306  }
307
308  base::MD5Digest digest;
309  base::MD5Final(&digest, &context);
310  return MD5DigestToBase16(digest);
311}
312
313std::string GetHostedDocumentExtension(const std::string& mime_type) {
314  for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) {
315    if (mime_type == kHostedDocumentKinds[i].mime_type)
316      return kHostedDocumentKinds[i].extension;
317  }
318  return kUnknownHostedDocumentExtension;
319}
320
321bool IsKnownHostedDocumentMimeType(const std::string& mime_type) {
322  for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) {
323    if (mime_type == kHostedDocumentKinds[i].mime_type)
324      return true;
325  }
326  return false;
327}
328
329bool HasHostedDocumentExtension(const base::FilePath& path) {
330  const std::string extension = base::FilePath(path.Extension()).AsUTF8Unsafe();
331  for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) {
332    if (extension == kHostedDocumentKinds[i].extension)
333      return true;
334  }
335  return extension == kUnknownHostedDocumentExtension;
336}
337
338}  // namespace util
339}  // namespace drive
340