1// Copyright (c) 2013 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 <ctype.h>
6#include <stdarg.h>
7#include <stdio.h>
8#include <string.h>
9
10#include <string>
11#include <vector>
12
13#include "json/reader.h"
14#include "json/writer.h"
15#include "ppapi/c/pp_errors.h"
16#include "ppapi/cpp/completion_callback.h"
17#include "ppapi/cpp/instance.h"
18#include "ppapi/cpp/module.h"
19#include "ppapi/cpp/url_loader.h"
20#include "ppapi/cpp/url_request_info.h"
21#include "ppapi/cpp/url_response_info.h"
22#include "ppapi/cpp/var.h"
23#include "ppapi/utility/completion_callback_factory.h"
24#include "ppapi/utility/threading/simple_thread.h"
25
26namespace {
27
28// When we upload files, we also upload the metadata at the same time. To do so,
29// we use the mimetype multipart/related. This mimetype requires specifying a
30// boundary between the JSON metadata and the file content.
31const char kBoundary[] = "NACL_BOUNDARY_600673";
32
33// This is a simple implementation of JavaScript's encodeUriComponent. We
34// assume the data is already UTF-8. See
35// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent.
36std::string EncodeUriComponent(const std::string& s) {
37  char hex[] = "0123456789ABCDEF";
38  std::string result;
39  for (size_t i = 0; i < s.length(); ++i) {
40    char c = s[i];
41    if (isalpha(c) || isdigit(c) || strchr("-_.!~*'()", c)) {
42      result += c;
43    } else {
44      result += '%';
45      result += hex[(c >> 4) & 0xf];
46      result += hex[c & 0xf];
47    }
48  }
49  return result;
50}
51
52std::string IntToString(int x) {
53  char buffer[32];
54  snprintf(&buffer[0], 32, "%d", x);
55  return &buffer[0];
56}
57
58void AddQueryParameter(std::string* s,
59                       const std::string& key,
60                       const std::string& value,
61                       bool first) {
62  *s += first ? '?' : '&';
63  *s += EncodeUriComponent(key);
64  *s += '=';
65  *s += EncodeUriComponent(value);
66}
67
68void AddQueryParameter(std::string* s,
69                       const std::string& key,
70                       int value,
71                       bool first) {
72  AddQueryParameter(s, key, IntToString(value), first);
73}
74
75void AddAuthTokenHeader(std::string* s, const std::string& auth_token) {
76  *s += "Authorization: Bearer ";
77  *s += auth_token;
78  *s += "\n";
79}
80
81void AddHeader(std::string* s, const char* key, const std::string& value) {
82  *s += key;
83  *s += ": ";
84  *s += value;
85  *s += "\n";
86}
87
88}  // namespace
89
90//
91// ReadUrl
92//
93struct ReadUrlParams {
94  std::string url;
95  std::string method;
96  std::string request_headers;
97  std::string request_body;
98};
99
100// This function blocks so it needs to be called off the main thread.
101int32_t ReadUrl(pp::Instance* instance,
102                const ReadUrlParams& params,
103                std::string* output) {
104  pp::URLRequestInfo url_request(instance);
105  pp::URLLoader url_loader(instance);
106
107  url_request.SetURL(params.url);
108  url_request.SetMethod(params.method);
109  url_request.SetHeaders(params.request_headers);
110  url_request.SetRecordDownloadProgress(true);
111  if (params.request_body.size()) {
112    url_request.AppendDataToBody(params.request_body.data(),
113                                 params.request_body.size());
114  }
115
116  int32_t result = url_loader.Open(url_request, pp::BlockUntilComplete());
117  if (result != PP_OK) {
118    return result;
119  }
120
121  pp::URLResponseInfo url_response = url_loader.GetResponseInfo();
122  if (url_response.GetStatusCode() != 200)
123    return PP_ERROR_FAILED;
124
125  output->clear();
126
127  int64_t bytes_received = 0;
128  int64_t total_bytes_to_be_received = 0;
129  if (url_loader.GetDownloadProgress(&bytes_received,
130                                     &total_bytes_to_be_received)) {
131    if (total_bytes_to_be_received > 0) {
132      output->reserve(total_bytes_to_be_received);
133    }
134  }
135
136  url_request.SetRecordDownloadProgress(false);
137
138  const int32_t kReadBufferSize = 16 * 1024;
139  uint8_t* buffer_ = new uint8_t[kReadBufferSize];
140
141  do {
142    result = url_loader.ReadResponseBody(
143        buffer_, kReadBufferSize, pp::BlockUntilComplete());
144    if (result > 0) {
145      assert(result <= kReadBufferSize);
146      size_t num_bytes = result;
147      output->insert(output->end(), buffer_, buffer_ + num_bytes);
148    }
149  } while (result > 0);
150
151  delete[] buffer_;
152
153  return result;
154}
155
156//
157// ListFiles
158//
159// This is a simplistic implementation of the files.list method defined here:
160// https://developers.google.com/drive/v2/reference/files/list
161//
162struct ListFilesParams {
163  int max_results;
164  std::string page_token;
165  std::string query;
166};
167
168int32_t ListFiles(pp::Instance* instance,
169                  const std::string& auth_token,
170                  const ListFilesParams& params,
171                  Json::Value* root) {
172  static const char base_url[] = "https://www.googleapis.com/drive/v2/files";
173
174  ReadUrlParams p;
175  p.method = "GET";
176  p.url = base_url;
177  AddQueryParameter(&p.url, "maxResults", params.max_results, true);
178  if (params.page_token.length())
179    AddQueryParameter(&p.url, "pageToken", params.page_token, false);
180  AddQueryParameter(&p.url, "q", params.query, false);
181  // Request a "partial response". See
182  // https://developers.google.com/drive/performance#partial for more
183  // information.
184  AddQueryParameter(&p.url, "fields", "items(id,downloadUrl)", false);
185  AddAuthTokenHeader(&p.request_headers, auth_token);
186
187  std::string output;
188  int32_t result = ReadUrl(instance, p, &output);
189  if (result != PP_OK) {
190    return result;
191  }
192
193  Json::Reader reader(Json::Features::strictMode());
194  if (!reader.parse(output, *root, false)) {
195    return PP_ERROR_FAILED;
196  }
197
198  return PP_OK;
199}
200
201//
202// InsertFile
203//
204// This is a simplistic implementation of the files.update and files.insert
205// methods defined here:
206// https://developers.google.com/drive/v2/reference/files/insert
207// https://developers.google.com/drive/v2/reference/files/update
208//
209struct InsertFileParams {
210  // If file_id is empty, create a new file (files.insert). If file_id is not
211  // empty, update that file (files.update)
212  std::string file_id;
213  std::string content;
214  std::string description;
215  std::string mime_type;
216  std::string title;
217};
218
219std::string BuildRequestBody(const InsertFileParams& params) {
220  // This generates the multipart-upload request body for InsertFile. See
221  // https://developers.google.com/drive/manage-uploads#multipart for more
222  // information.
223  std::string result;
224  result += "--";
225  result += kBoundary;
226  result += "\nContent-Type: application/json; charset=UTF-8\n\n";
227
228  Json::Value value(Json::objectValue);
229  if (!params.description.empty())
230    value["description"] = Json::Value(params.description);
231
232  if (!params.mime_type.empty())
233    value["mimeType"] = Json::Value(params.mime_type);
234
235  if (!params.title.empty())
236    value["title"] = Json::Value(params.title);
237
238  Json::FastWriter writer;
239  std::string metadata = writer.write(value);
240
241  result += metadata;
242  result += "--";
243  result += kBoundary;
244  result += "\nContent-Type: ";
245  result += params.mime_type;
246  result += "\n\n";
247  result += params.content;
248  result += "\n--";
249  result += kBoundary;
250  result += "--";
251  return result;
252}
253
254int32_t InsertFile(pp::Instance* instance,
255                   const std::string& auth_token,
256                   const InsertFileParams& params,
257                   Json::Value* root) {
258  static const char base_url[] =
259      "https://www.googleapis.com/upload/drive/v2/files";
260
261  ReadUrlParams p;
262  p.url = base_url;
263
264  // If file_id is defined, we are actually updating an existing file.
265  if (!params.file_id.empty()) {
266    p.url += "/";
267    p.url += params.file_id;
268    p.method = "PUT";
269  } else {
270    p.method = "POST";
271  }
272
273  // We always use the multipart upload interface, but see
274  // https://developers.google.com/drive/manage-uploads for other
275  // options.
276  AddQueryParameter(&p.url, "uploadType", "multipart", true);
277  // Request a "partial response". See
278  // https://developers.google.com/drive/performance#partial for more
279  // information.
280  AddQueryParameter(&p.url, "fields", "id,downloadUrl", false);
281  AddAuthTokenHeader(&p.request_headers, auth_token);
282  AddHeader(&p.request_headers,
283            "Content-Type",
284            std::string("multipart/related; boundary=") + kBoundary + "\n");
285  p.request_body = BuildRequestBody(params);
286
287  std::string output;
288  int32_t result = ReadUrl(instance, p, &output);
289  if (result != PP_OK) {
290    return result;
291  }
292
293  Json::Reader reader(Json::Features::strictMode());
294  if (!reader.parse(output, *root, false)) {
295    return PP_ERROR_FAILED;
296  }
297
298  return PP_OK;
299}
300
301//
302// Instance
303//
304class Instance : public pp::Instance {
305 public:
306  Instance(PP_Instance instance);
307  virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]);
308  virtual void HandleMessage(const pp::Var& var_message);
309
310  void PostMessagef(const char* format, ...);
311
312 private:
313  void ThreadSetAuthToken(int32_t, const std::string& auth_token);
314  void ThreadRequestThunk(int32_t);
315  bool ThreadRequest();
316  bool ThreadGetFileMetadata(const char* title, Json::Value* metadata);
317  bool ThreadCreateFile(const char* title,
318                        const char* description,
319                        const char* content,
320                        Json::Value* metadata);
321  bool ThreadUpdateFile(const std::string& file_id,
322                        const std::string& content,
323                        Json::Value* metadata);
324  bool ThreadDownloadFile(const Json::Value& metadata, std::string* output);
325  bool GetMetadataKey(const Json::Value& metadata,
326                      const char* key,
327                      std::string* output);
328
329  pp::SimpleThread worker_thread_;
330  pp::CompletionCallbackFactory<Instance> callback_factory_;
331  std::string auth_token_;
332  bool is_processing_request_;
333};
334
335Instance::Instance(PP_Instance instance)
336    : pp::Instance(instance),
337      worker_thread_(this),
338      callback_factory_(this),
339      is_processing_request_(false) {}
340
341bool Instance::Init(uint32_t /*argc*/,
342                    const char * [] /*argn*/,
343                    const char * [] /*argv*/) {
344  worker_thread_.Start();
345  return true;
346}
347
348void Instance::HandleMessage(const pp::Var& var_message) {
349  const char kTokenMessage[] = "token:";
350  const size_t kTokenMessageLen = strlen(kTokenMessage);
351  const char kGetFileMessage[] = "getFile";
352
353  if (!var_message.is_string()) {
354    return;
355  }
356
357  std::string message = var_message.AsString();
358  printf("Got message: \"%s\"\n", message.c_str());
359  if (message.compare(0, kTokenMessageLen, kTokenMessage) == 0) {
360    // Auth token
361    std::string auth_token = message.substr(kTokenMessageLen);
362    worker_thread_.message_loop().PostWork(callback_factory_.NewCallback(
363        &Instance::ThreadSetAuthToken, auth_token));
364  } else if (message == kGetFileMessage) {
365    // Request
366    if (!is_processing_request_) {
367      is_processing_request_ = true;
368      worker_thread_.message_loop().PostWork(
369          callback_factory_.NewCallback(&Instance::ThreadRequestThunk));
370    }
371  }
372}
373
374void Instance::PostMessagef(const char* format, ...) {
375  const size_t kBufferSize = 1024;
376  char buffer[kBufferSize];
377  va_list args;
378  va_start(args, format);
379  vsnprintf(&buffer[0], kBufferSize, format, args);
380
381  PostMessage(buffer);
382}
383
384void Instance::ThreadSetAuthToken(int32_t /*result*/,
385                                  const std::string& auth_token) {
386  printf("Got auth token: %s\n", auth_token.c_str());
387  auth_token_ = auth_token;
388}
389
390void Instance::ThreadRequestThunk(int32_t /*result*/) {
391  ThreadRequest();
392  is_processing_request_ = false;
393}
394
395bool Instance::ThreadRequest() {
396  static int request_count = 0;
397  static const char kTitle[] = "hello nacl.txt";
398  Json::Value metadata;
399  std::string output;
400
401  PostMessagef("log:\n Got request (#%d).\n", ++request_count);
402  PostMessagef("log: Looking for file: \"%s\".\n", kTitle);
403
404  if (!ThreadGetFileMetadata(kTitle, &metadata)) {
405    PostMessage("log: Not found! Creating a new file...\n");
406    // No data found, write a new file.
407    static const char kDescription[] = "A file generated by NaCl!";
408    static const char kInitialContent[] = "Hello, Google Drive!";
409
410    if (!ThreadCreateFile(kTitle, kDescription, kInitialContent, &metadata)) {
411      PostMessage("log: Creating the new file failed...\n");
412      return false;
413    }
414  } else {
415    PostMessage("log: Found it! Downloading the file...\n");
416    // Found the file, download it's data.
417    if (!ThreadDownloadFile(metadata, &output)) {
418      PostMessage("log: Downloading the file failed...\n");
419      return false;
420    }
421
422    // Modify it.
423    output += "\nHello, again Google Drive!";
424
425    std::string file_id;
426    if (!GetMetadataKey(metadata, "id", &file_id)) {
427      PostMessage("log: Couldn't find the file id...\n");
428      return false;
429    }
430
431    PostMessage("log: Updating the file...\n");
432    if (!ThreadUpdateFile(file_id, output, &metadata)) {
433      PostMessage("log: Failed to update the file...\n");
434      return false;
435    }
436  }
437
438  PostMessage("log: Done!\n");
439  PostMessage("log: Downloading the newly written file...\n");
440  if (!ThreadDownloadFile(metadata, &output)) {
441    PostMessage("log: Downloading the file failed...\n");
442    return false;
443  }
444
445  PostMessage("log: Done!\n");
446  PostMessage(output);
447  return true;
448}
449
450bool Instance::ThreadGetFileMetadata(const char* title, Json::Value* metadata) {
451  ListFilesParams p;
452  p.max_results = 1;
453  p.query = "title = \'";
454  p.query += title;
455  p.query += "\'";
456
457  Json::Value root;
458  int32_t result = ListFiles(this, auth_token_, p, &root);
459  if (result != PP_OK) {
460    PostMessagef("log: ListFiles failed with result %d\n", result);
461    return false;
462  }
463
464  // Extract the first item's metadata.
465  if (!root.isMember("items")) {
466    PostMessage("log: ListFiles returned no items...\n");
467    return false;
468  }
469
470  Json::Value items = root["items"];
471  if (!items.isValidIndex(0)) {
472    PostMessage("log: Expected items[0] to be valid.\n");
473    return false;
474  }
475
476  *metadata = items[0U];
477  return true;
478}
479
480bool Instance::ThreadCreateFile(const char* title,
481                                const char* description,
482                                const char* content,
483                                Json::Value* metadata) {
484  InsertFileParams p;
485  p.content = content;
486  p.description = description;
487  p.mime_type = "text/plain";
488  p.title = title;
489
490  int32_t result = InsertFile(this, auth_token_, p, metadata);
491  if (result != PP_OK) {
492    PostMessagef("log: Creating file failed with result %d\n", result);
493    return false;
494  }
495
496  return true;
497}
498
499bool Instance::ThreadUpdateFile(const std::string& file_id,
500                                const std::string& content,
501                                Json::Value* metadata) {
502  InsertFileParams p;
503  p.file_id = file_id;
504  p.content = content;
505  p.mime_type = "text/plain";
506
507  int32_t result = InsertFile(this, auth_token_, p, metadata);
508  if (result != PP_OK) {
509    PostMessagef("log: Updating file failed with result %d\n", result);
510    return false;
511  }
512
513  return true;
514}
515
516bool Instance::ThreadDownloadFile(const Json::Value& metadata,
517                                  std::string* output) {
518  ReadUrlParams p;
519  p.method = "GET";
520
521  if (!GetMetadataKey(metadata, "downloadUrl", &p.url)) {
522    return false;
523  }
524
525  AddAuthTokenHeader(&p.request_headers, auth_token_);
526
527  int32_t result = ReadUrl(this, p, output);
528  if (result != PP_OK) {
529    PostMessagef("log: Downloading failed with result %d\n", result);
530    return false;
531  }
532
533  return true;
534}
535
536bool Instance::GetMetadataKey(const Json::Value& metadata,
537                              const char* key,
538                              std::string* output) {
539  Json::Value value = metadata[key];
540  if (!value.isString()) {
541    PostMessagef("log: Expected metadata.%s to be a string.\n", key);
542    return false;
543  }
544
545  *output = value.asString();
546  return true;
547}
548
549class Module : public pp::Module {
550 public:
551  Module() : pp::Module() {}
552  virtual ~Module() {}
553
554  virtual pp::Instance* CreateInstance(PP_Instance instance) {
555    return new Instance(instance);
556  }
557};
558
559namespace pp {
560
561Module* CreateModule() { return new ::Module(); }
562
563}  // namespace pp
564