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 struct HostedDocumentKind { 29 const char* mime_type; 30 const char* extension; 31 }; 32 33 const HostedDocumentKind kHostedDocumentKinds[] = { 34 {kGoogleDocumentMimeType, ".gdoc"}, 35 {kGoogleSpreadsheetMimeType, ".gsheet"}, 36 {kGooglePresentationMimeType, ".gslides"}, 37 {kGoogleDrawingMimeType, ".gdraw"}, 38 {kGoogleTableMimeType, ".gtable"}, 39 {kGoogleFormMimeType, ".gform"} 40 }; 41 42 const char kUnknownHostedDocumentExtension[] = ".glink"; 43 44 } // namespace 45 46 std::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 58 std::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 124 std::string ExtractResourceIdFromUrl(const GURL& url) { 125 return net::UnescapeURLComponent(url.ExtractFileName(), 126 net::UnescapeRule::URL_SPECIAL_CHARS); 127 } 128 129 std::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 139 scoped_ptr<google_apis::ResourceEntry> 140 ConvertFileResourceToResourceEntry( 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 214 scoped_ptr<google_apis::ResourceEntry> 215 ConvertChangeResourceToResourceEntry( 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 232 scoped_ptr<google_apis::ResourceList> 233 ConvertFileListToResourceList(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 254 scoped_ptr<google_apis::ResourceList> 255 ConvertChangeListToResourceList(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 280 std::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 313 std::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 321 bool 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 329 bool 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