| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h" |
| #include "chrome/browser/enterprise/connectors/file_system/box_api_call_response.h" |
| |
| #include <string> |
| |
| #include "base/base64.h" |
| #include "base/files/file_util.h" |
| #include "base/hash/sha1.h" |
| #include "base/json/json_writer.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "base/values.h" |
| #include "chrome/browser/enterprise/connectors/file_system/box_api_call_endpoints.h" |
| #include "net/base/escape.h" |
| #include "net/base/mime_util.h" |
| #include "net/http/http_status_code.h" |
| #include "net/http/http_util.h" |
| #include "rename_handler.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| |
| #define PRINT_ERROR(net_error, head) \ |
| (net_error ? base::StringPrintf("net error = %d", net_error) \ |
| : base::StringPrintf("response_code = %d\nheader: %s", \ |
| head->headers->response_code(), \ |
| head->headers->raw_headers().c_str())) |
| |
| #define LOG_API_FAIL(severity, flow, net_error, head, body) \ |
| DCHECK(net_error || head); \ |
| const auto error_str = PRINT_ERROR(net_error, head); \ |
| DLOG(severity) << "[BoxApiCallFlow] " << flow << " failed; " << error_str \ |
| << ";\nbody: " << (body ? *body : "<null>"); |
| |
| #define LOG_PARSE_FAIL(severity, flow, result) \ |
| DLOG(severity) << "[BoxApiCallFlow] " << flow << "\nJson Parse Error: " \ |
| << (result.error ? result.error->data() : "<no error info>"); |
| |
| #define LOG_PARSE_FAIL_IF(condition, severity, flow, result) \ |
| if (condition) { \ |
| LOG_PARSE_FAIL(severity, flow, result); \ |
| } |
| |
| namespace { |
| |
| // Create folder at root. |
| static const char kParentFolderId[] = "0"; |
| |
| std::string ExtractId(const base::Value& entry) { |
| DCHECK(entry.is_dict()) << entry; |
| const base::Value* id_val = entry.FindPath("id"); |
| if (!id_val) { |
| DLOG(ERROR) << "[BoxApiCallFlow] Can't find id! " << entry; |
| return std::string(); |
| } |
| |
| std::string id; |
| switch (id_val->type()) { |
| case base::Value::Type::STRING: |
| id = id_val->GetString(); |
| break; |
| case base::Value::Type::INTEGER: |
| id = base::NumberToString(id_val->GetInt()); |
| break; |
| default: |
| DLOG(ERROR) << "[BoxApiCallFlow] Invalid id_val type: " |
| << id_val->GetTypeName(id_val->type()); |
| } |
| return id; |
| } |
| |
| std::string ExtractParentId(const base::Value& value) { |
| std::string id; |
| const base::Value* parent = nullptr; |
| const base::Value* parent_id = nullptr; |
| |
| parent = value.FindPath("parent"); |
| |
| if (parent) |
| parent_id = parent->FindPath("id"); |
| if (parent_id && parent_id->is_int()) |
| id = base::NumberToString(parent_id->GetInt()); |
| else |
| DLOG(ERROR) << "[BoxApiCallFlow] Parent ID not found"; |
| |
| return id; |
| } |
| |
| // For possible extensions: |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types |
| std::string GetMimeType(base::FilePath file_path) { |
| auto ext = file_path.FinalExtension(); |
| DCHECK(ext.size()) << file_path; |
| if (ext.front() == '.') { |
| ext.erase(ext.begin()); |
| } |
| DCHECK_NE(ext, FILE_PATH_LITERAL("crdownload")); |
| |
| std::string file_type; |
| bool result = net::GetMimeTypeFromExtension(ext, &file_type); |
| DCHECK(result || file_type.empty()); |
| return file_type; |
| } |
| |
| base::Value CreateEmptyDict() { |
| return base::Value(base::Value::Type::DICTIONARY); |
| } |
| |
| base::Value CreateSingleFieldDict(const std::string& key, |
| const std::string& value) { |
| base::Value dict(base::Value::Type::DICTIONARY); |
| dict.SetStringKey(key, value); |
| return dict; |
| } |
| |
| bool VerifyChunkedUploadParts(const base::Value& parts) { |
| DCHECK(parts.is_dict()) << parts; |
| DCHECK(parts.FindPath("parts")->is_list()) << parts; |
| auto parts_list = parts.FindPath("parts")->GetList(); |
| DCHECK(!parts_list.empty()); |
| for (auto p = parts_list.begin(); p != parts_list.end(); ++p) { |
| DCHECK(p->is_dict()) << parts; |
| DCHECK(p->FindPath("part_id")) << parts; |
| DCHECK(p->FindPath("offset")) << parts; |
| DCHECK(p->FindPath("size")) << parts; |
| DCHECK(p->FindPath("sha1")) << parts; |
| } |
| return true; |
| } |
| |
| using Box = enterprise_connectors::BoxApiCallFlow; |
| |
| bool ExtractEntriesList(const Box::ParseResult& result, |
| base::Value::ConstListView* list) { |
| if (!result.value) { |
| return false; |
| } |
| |
| const base::Value* entries = result.value->FindPath("entries"); |
| if (!entries || !entries->is_list()) { |
| return false; |
| } |
| |
| CHECK(list); |
| *list = entries->GetList(); |
| return true; |
| } |
| |
| std::string ExtractUploadedFileId(const Box::ParseResult& result) { |
| base::Value::ConstListView list; |
| std::string file_id; |
| if (ExtractEntriesList(result, &list) && !list.empty()) { |
| file_id = ExtractId(list.front()); |
| } |
| LOG_PARSE_FAIL_IF(file_id.empty(), ERROR, "ExtractUploadedFileId", result); |
| return file_id; |
| } |
| |
| void ProcessUploadSuccessResponse( |
| std::unique_ptr<std::string> body, |
| base::OnceCallback<void(const std::string&)> callback) { |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, base::BindOnce( |
| [](decltype(callback) cb, Box::ParseResult result) { |
| std::move(cb).Run(ExtractUploadedFileId(result)); |
| }, |
| std::move(callback))); |
| } |
| |
| } // namespace |
| |
| namespace enterprise_connectors { |
| |
| // File size limit according to https://developer.box.com/guides/uploads/: |
| // - Chucked upload APIs is only supported for file size >= 20 MB; |
| // - Whole file upload API is only supported for file size <= 50 MB. |
| const size_t BoxApiCallFlow::kChunkFileUploadMinSize = |
| 20 * 1024 * 1024; // 20 MB |
| const size_t BoxApiCallFlow::kWholeFileUploadMaxSize = |
| 50 * 1024 * 1024; // 50 MB |
| |
| BoxApiCallResponse MakeSuccess(int http_code) { |
| DCHECK_GT(http_code, 0); |
| return BoxApiCallResponse{true, http_code}; |
| } |
| |
| BoxApiCallResponse MakeNetworkFailure(int net_code) { |
| DCHECK_LT(net_code, 0); |
| return BoxApiCallResponse{false, net_code}; |
| } |
| |
| BoxApiCallResponse MakeApiFailure(int http_code, |
| std::string box_error_code, |
| std::string box_request_id) { |
| return BoxApiCallResponse{false, http_code, std::move(box_error_code), |
| std::move(box_request_id)}; |
| } |
| |
| BoxApiCallFlow::BoxApiCallFlow() = default; |
| BoxApiCallFlow::~BoxApiCallFlow() = default; |
| |
| GURL BoxApiCallFlow::CreateApiCallUrl() { |
| return GURL(kFileSystemBoxEndpointApi); |
| } |
| |
| std::string BoxApiCallFlow::CreateApiCallBody() { |
| return std::string(); |
| } |
| std::string BoxApiCallFlow::CreateApiCallBodyContentType() { |
| return "application/json"; |
| } |
| |
| void BoxApiCallFlow::ProcessApiCallFailure( |
| int net_error, |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| if (net_error) { |
| DCHECK(net_error < 0); |
| LOG_API_FAIL(ERROR, "network", net_error, head, body); |
| ProcessFailure(MakeNetworkFailure(net_error)); |
| } else if (head && head->headers && |
| head->headers->response_code() == net::HTTP_UNAUTHORIZED) { |
| ProcessFailure(Response{false, net::HTTP_UNAUTHORIZED}); |
| } else { |
| DCHECK(head); |
| DCHECK(head->headers); |
| DCHECK(body); |
| DCHECK(body->size()); |
| LOG_API_FAIL(ERROR, |
| "API request " << GetRequestTypeForBody("dummy body") << " to " |
| << CreateApiCallUrl(), |
| net_error, head, body); |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, base::BindOnce(&BoxApiCallFlow::OnFailureJsonParsed, |
| weak_factory_.GetWeakPtr(), |
| head->headers->response_code())); |
| } |
| } |
| |
| // API reference: https://developer.box.com/reference/resources/client-error/ |
| void BoxApiCallFlow::OnFailureJsonParsed(int http_code, ParseResult result) { |
| DCHECK(result.value); |
| base::Value *code = nullptr, *request_id = nullptr; |
| auto response = Response{false, http_code}; |
| if (result.value && (code = result.value->FindPath("code")) && |
| (request_id = result.value->FindPath("request_id"))) { |
| response = |
| MakeApiFailure(http_code, code->GetString(), request_id->GetString()); |
| } |
| LOG_PARSE_FAIL_IF(!code || !request_id, ERROR, "OnFailureJsonParsed", result); |
| ProcessFailure(response); |
| } |
| |
| // Box API reference: |
| net::PartialNetworkTrafficAnnotationTag |
| BoxApiCallFlow::GetNetworkTrafficAnnotationTag() { |
| return net::DefinePartialNetworkTrafficAnnotation( |
| "file_system_connector_to_box", "oauth2_api_call_flow", R"( |
| semantics { |
| sender: "Chrome Enterprise File System Connector" |
| description: |
| "Communication to Box API (https://developer.box.com/reference/) to " |
| "upload or download files." |
| trigger: |
| "A request from the user to download a file when the enterprise admin" |
| " has enabled file download redirection." |
| data: "Any file that is being downloaded/uploaded by the user." |
| destination: OTHER |
| destination_other: "Box storage in the cloud." |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "No settings control." |
| policy_exception_justification: "Not implemented yet." |
| })"); |
| // TODO(https://crbug.com/1157959): Add the policy to turn on/off connector. |
| } |
| |
| // static |
| std::string BoxApiCallFlow::FormatSHA1Digest(const std::string& sha_digest) { |
| return base::StringPrintf("sha=%s=", sha_digest.c_str()); |
| } |
| |
| // static |
| GURL BoxApiCallFlow::MakeUrlToShowFile(const std::string& file_id) { |
| return file_id.empty() ? GURL() |
| : GURL("https://app.box.com/file/").Resolve(file_id); |
| } |
| |
| // static |
| GURL BoxApiCallFlow::MakeUrlToShowFolder(const std::string& folder_id) { |
| return folder_id.empty() |
| ? GURL() |
| : GURL("https://app.box.com/folder/").Resolve(folder_id); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // GetFileFolder |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/resources/file/ |
| BoxGetFileFolderApiCallFlow::BoxGetFileFolderApiCallFlow( |
| TaskCallback callback, |
| const std::string& file_id) |
| : callback_(std::move(callback)), file_id_(file_id) {} |
| BoxGetFileFolderApiCallFlow::~BoxGetFileFolderApiCallFlow() = default; |
| |
| GURL BoxGetFileFolderApiCallFlow::CreateApiCallUrl() { |
| std::string path("2.0/files/" + file_id_); |
| return BoxApiCallFlow::CreateApiCallUrl().Resolve(path); |
| } |
| |
| void BoxGetFileFolderApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| CHECK_EQ(response_code, net::HTTP_OK); |
| |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, base::BindOnce(&BoxGetFileFolderApiCallFlow::OnSuccessJsonParsed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void BoxGetFileFolderApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, std::string()); |
| } |
| |
| void BoxGetFileFolderApiCallFlow::OnSuccessJsonParsed(ParseResult result) { |
| std::string folder_id; |
| |
| if (result.value.has_value()) |
| folder_id = ExtractParentId(result.value.value()); |
| |
| std::move(callback_).Run(Response{!folder_id.empty(), net::HTTP_OK}, |
| folder_id); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // FindUpstreamFolder |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/get-search/#param-200-application/json |
| BoxFindUpstreamFolderApiCallFlow::BoxFindUpstreamFolderApiCallFlow( |
| TaskCallback callback) |
| : callback_(std::move(callback)) {} |
| BoxFindUpstreamFolderApiCallFlow::~BoxFindUpstreamFolderApiCallFlow() = default; |
| |
| GURL BoxFindUpstreamFolderApiCallFlow::CreateApiCallUrl() { |
| std::string path("2.0/search?type=folder&query=ChromeDownloads"); |
| GURL call_url = BoxApiCallFlow::CreateApiCallUrl().Resolve(path); |
| return call_url; |
| } |
| |
| void BoxFindUpstreamFolderApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| CHECK_EQ(response_code, net::HTTP_OK); |
| |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, |
| base::BindOnce(&BoxFindUpstreamFolderApiCallFlow::OnSuccessJsonParsed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void BoxFindUpstreamFolderApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, std::string()); |
| } |
| |
| void BoxFindUpstreamFolderApiCallFlow::OnSuccessJsonParsed(ParseResult result) { |
| base::Value::ConstListView list; |
| std::string folder_id; |
| bool extracted = ExtractEntriesList(result, &list); |
| LOG_PARSE_FAIL_IF(!extracted, ERROR, "FindUpstreamFolder", result); |
| |
| if (extracted && !list.empty()) { |
| folder_id = ExtractId(list.front()); |
| extracted = !folder_id.empty(); |
| } |
| std::move(callback_).Run(Response{extracted, net::HTTP_OK}, folder_id); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // CreateUpstreamFolder |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: https://developer.box.com/reference/post-folders/ |
| BoxCreateUpstreamFolderApiCallFlow::BoxCreateUpstreamFolderApiCallFlow( |
| TaskCallback callback) |
| : callback_(std::move(callback)) {} |
| BoxCreateUpstreamFolderApiCallFlow::~BoxCreateUpstreamFolderApiCallFlow() = |
| default; |
| |
| GURL BoxCreateUpstreamFolderApiCallFlow::CreateApiCallUrl() { |
| return BoxApiCallFlow::CreateApiCallUrl().Resolve("2.0/folders"); |
| } |
| |
| std::string BoxCreateUpstreamFolderApiCallFlow::CreateApiCallBody() { |
| base::Value val(base::Value::Type::DICTIONARY); |
| val.SetStringKey("name", "ChromeDownloads"); |
| val.SetKey("parent", CreateSingleFieldDict("id", kParentFolderId)); |
| |
| std::string body; |
| base::JSONWriter::Write(val, &body); |
| return body; |
| } |
| |
| bool BoxCreateUpstreamFolderApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_CREATED; |
| } |
| |
| void BoxCreateUpstreamFolderApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| CHECK_EQ(response_code, net::HTTP_CREATED); |
| |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, |
| base::BindOnce(&BoxCreateUpstreamFolderApiCallFlow::OnSuccessJsonParsed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void BoxCreateUpstreamFolderApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, std::string()); |
| } |
| |
| void BoxCreateUpstreamFolderApiCallFlow::OnSuccessJsonParsed( |
| ParseResult result) { |
| std::string folder_id; |
| if (result.value) { |
| folder_id = ExtractId(*result.value); |
| } |
| LOG_PARSE_FAIL_IF(folder_id.empty(), ERROR, "CreateUpstreamFolder", result); |
| std::move(callback_).Run(Response{!folder_id.empty(), net::HTTP_CREATED}, |
| folder_id); |
| return; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // PreflightCheck |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/options-files-content/ |
| BoxPreflightCheckApiCallFlow::BoxPreflightCheckApiCallFlow( |
| TaskCallback callback, |
| const base::FilePath& target_file_name, |
| const std::string& folder_id) |
| : callback_(std::move(callback)), |
| target_file_name_(target_file_name), |
| folder_id_(folder_id) {} |
| BoxPreflightCheckApiCallFlow::~BoxPreflightCheckApiCallFlow() = default; |
| |
| GURL BoxPreflightCheckApiCallFlow::CreateApiCallUrl() { |
| return BoxApiCallFlow::CreateApiCallUrl().Resolve("2.0/files/content"); |
| } |
| |
| std::string BoxPreflightCheckApiCallFlow::GetRequestTypeForBody( |
| const std::string& body) { |
| CHECK(!body.empty()); |
| return "OPTIONS"; |
| } |
| |
| std::string BoxPreflightCheckApiCallFlow::CreateApiCallBody() { |
| base::Value val(base::Value::Type::DICTIONARY); |
| val.SetStringKey("name", target_file_name_.MaybeAsASCII()); |
| val.SetKey("parent", CreateSingleFieldDict("id", folder_id_)); |
| |
| std::string body; |
| base::JSONWriter::Write(val, &body); |
| return body; |
| } |
| |
| bool BoxPreflightCheckApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_OK; |
| } |
| |
| void BoxPreflightCheckApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| CHECK_EQ(response_code, net::HTTP_OK); |
| std::move(callback_).Run(MakeSuccess(response_code)); |
| } |
| |
| void BoxPreflightCheckApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // WholeFileUpload |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/post-files-content/ |
| |
| BoxWholeFileUploadApiCallFlow::BoxWholeFileUploadApiCallFlow( |
| TaskCallback callback, |
| const std::string& folder_id, |
| const base::FilePath& target_file_name, |
| const base::FilePath& local_file_path) |
| : folder_id_(folder_id), |
| target_file_name_(target_file_name), |
| local_file_path_(local_file_path), |
| multipart_boundary_(net::GenerateMimeMultipartBoundary()), |
| callback_(std::move(callback)) {} |
| |
| BoxWholeFileUploadApiCallFlow::~BoxWholeFileUploadApiCallFlow() = default; |
| |
| void BoxWholeFileUploadApiCallFlow::Start( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const std::string& access_token) { |
| // Forward the arguments via PostReadFileTask() then OnFileRead() into |
| // OAuth2CallFlow::Start(). |
| PostReadFileTask(url_loader_factory, access_token); |
| } |
| |
| void BoxWholeFileUploadApiCallFlow::PostReadFileTask( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const std::string& access_token) { |
| auto read_file_task = base::BindOnce(&BoxWholeFileUploadApiCallFlow::ReadFile, |
| local_file_path_, target_file_name_); |
| auto read_file_reply = base::BindOnce( |
| &BoxWholeFileUploadApiCallFlow::OnFileRead, weak_factory_.GetWeakPtr(), |
| url_loader_factory, access_token); |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, |
| std::move(read_file_task), std::move(read_file_reply)); |
| } |
| |
| // static |
| absl::optional<BoxWholeFileUploadApiCallFlow::FileRead> |
| BoxWholeFileUploadApiCallFlow::ReadFile( |
| const base::FilePath& path, |
| const base::FilePath& target_file_name) { |
| FileRead file_read; |
| file_read.mime = GetMimeType(target_file_name); |
| DCHECK(file_read.mime.size()); |
| if (!base::ReadFileToStringWithMaxSize(path, &file_read.content, |
| kWholeFileUploadMaxSize)) { |
| DLOG(ERROR) << "File " << path << " with target name " << target_file_name; |
| return absl::nullopt; |
| } |
| DCHECK_LE(file_read.content.size(), kWholeFileUploadMaxSize); |
| return absl::optional<FileRead>(std::move(file_read)); |
| } |
| |
| void BoxWholeFileUploadApiCallFlow::OnFileRead( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const std::string& access_token, |
| absl::optional<FileRead> file_read) { |
| if (!file_read) { |
| DLOG(ERROR) << "[BoxApiCallFlow] WholeFileUpload read file failed"; |
| // TODO(https://crbug.com/1165972): error handling |
| ProcessFailure(Response{false, 0}); |
| return; |
| } |
| DCHECK_LE(file_read->content.size(), kWholeFileUploadMaxSize); |
| file_read_ = std::move(*file_read); |
| |
| // Continue to the original call flow after file has been read. |
| OAuth2ApiCallFlow::Start(url_loader_factory, access_token); |
| } |
| |
| GURL BoxWholeFileUploadApiCallFlow::CreateApiCallUrl() { |
| return GURL(kFileSystemBoxEndpointWholeFileUpload); |
| } |
| |
| std::string BoxWholeFileUploadApiCallFlow::CreateApiCallBody() { |
| CHECK(!folder_id_.empty()); |
| CHECK(!target_file_name_.empty()); |
| CHECK(!file_read_.mime.empty()) << target_file_name_; |
| CHECK(!multipart_boundary_.empty()); |
| |
| base::Value attr(base::Value::Type::DICTIONARY); |
| attr.SetStringKey("name", target_file_name_.MaybeAsASCII()); |
| attr.SetKey("parent", CreateSingleFieldDict("id", folder_id_)); |
| |
| std::string attr_json; |
| base::JSONWriter::Write(attr, &attr_json); |
| |
| std::string body; |
| net::AddMultipartValueForUpload("attributes", attr_json, multipart_boundary_, |
| "application/json", &body); |
| |
| net::AddMultipartValueForUploadWithFileName( |
| "file", target_file_name_.MaybeAsASCII(), file_read_.content, |
| multipart_boundary_, file_read_.mime, &body); |
| net::AddMultipartFinalDelimiterForUpload(multipart_boundary_, &body); |
| |
| return body; |
| } |
| |
| // Header format for multipart/form-data reference: |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type |
| std::string BoxWholeFileUploadApiCallFlow::CreateApiCallBodyContentType() { |
| std::string content_type = "multipart/form-data; boundary="; |
| content_type.append(multipart_boundary_); |
| return content_type; |
| } |
| |
| bool BoxWholeFileUploadApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_CREATED; |
| } |
| |
| void BoxWholeFileUploadApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| DCHECK_EQ(head->headers->response_code(), net::HTTP_CREATED); |
| ProcessUploadSuccessResponse( |
| std::move(body), |
| base::BindOnce(std::move(callback_), MakeSuccess(net::HTTP_CREATED))); |
| } |
| |
| void BoxWholeFileUploadApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, std::string()); |
| } |
| |
| void BoxWholeFileUploadApiCallFlow::SetFileReadForTesting( |
| std::string content, |
| std::string mime_type) { |
| file_read_.content = std::move(content); |
| file_read_.mime = std::move(mime_type); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ChunkedUpload: CreateUploadSession |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/post-files-upload-sessions/ |
| |
| BoxCreateUploadSessionApiCallFlow::BoxCreateUploadSessionApiCallFlow( |
| TaskCallback callback, |
| const std::string& folder_id, |
| const size_t file_size, |
| const base::FilePath& file_name) |
| : callback_(std::move(callback)), |
| folder_id_(folder_id), |
| file_size_(file_size), |
| file_name_(file_name) {} |
| |
| BoxCreateUploadSessionApiCallFlow::~BoxCreateUploadSessionApiCallFlow() = |
| default; |
| |
| GURL BoxCreateUploadSessionApiCallFlow::CreateApiCallUrl() { |
| return GURL("https://upload.box.com/api/2.0/files/upload_sessions"); |
| } |
| |
| std::string BoxCreateUploadSessionApiCallFlow::CreateApiCallBody() { |
| base::Value val(base::Value::Type::DICTIONARY); |
| val.SetStringKey("folder_id", folder_id_); |
| val.SetIntKey("file_size", file_size_); // TODO(https://crbug.com/1187152) |
| val.SetStringKey("file_name", file_name_.MaybeAsASCII()); |
| |
| bool file_big_enough = file_size_ > kChunkFileUploadMinSize; |
| CHECK(file_big_enough) << file_size_; |
| |
| std::string body; |
| base::JSONWriter::Write(val, &body); |
| return body; |
| } |
| |
| bool BoxCreateUploadSessionApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_CREATED; |
| } |
| |
| void BoxCreateUploadSessionApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| CHECK_EQ(response_code, net::HTTP_CREATED); |
| |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, |
| base::BindOnce(&BoxCreateUploadSessionApiCallFlow::OnSuccessJsonParsed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void BoxCreateUploadSessionApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, CreateEmptyDict(), 0); |
| } |
| |
| void BoxCreateUploadSessionApiCallFlow::OnSuccessJsonParsed( |
| ParseResult result) { |
| LOG_PARSE_FAIL_IF(!result.value.has_value(), ERROR, "CreateUploadSession", |
| result); |
| |
| const auto http_code = net::HTTP_CREATED; |
| base::Value *endpoints = nullptr, *part_size = nullptr; |
| if (result.value.has_value() && |
| (part_size = result.value->FindPath("part_size")) && |
| (endpoints = result.value->FindPath("session_endpoints")) && |
| endpoints->FindPath("upload_part") && endpoints->FindPath("commit") && |
| endpoints->FindPath("abort")) { |
| std::move(callback_).Run(MakeSuccess(http_code), std::move(*endpoints), |
| part_size->GetInt()); |
| return; |
| } |
| LOG_PARSE_FAIL_IF(!result.value, ERROR, "CreateUploadSession", result); |
| ProcessFailure(MakeApiFailure(http_code, "bad_response", "parse_fail")); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ChunkedUpload: Base |
| //////////////////////////////////////////////////////////////////////////////// |
| BoxChunkedUploadBaseApiCallFlow::BoxChunkedUploadBaseApiCallFlow( |
| const GURL endpoint) |
| : endpoint_(endpoint) { |
| DCHECK(endpoint_.is_valid()); |
| } |
| |
| GURL BoxChunkedUploadBaseApiCallFlow::CreateApiCallUrl() { |
| return endpoint_; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ChunkedUpload: PartFileUpload |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/put-files-upload-sessions-id/ |
| |
| BoxPartFileUploadApiCallFlow::BoxPartFileUploadApiCallFlow( |
| TaskCallback callback, |
| const std::string& session_endpoint, |
| const std::string& file_part_content, |
| const size_t byte_from, |
| const size_t byte_to, |
| const size_t byte_total) |
| : BoxChunkedUploadBaseApiCallFlow(GURL(session_endpoint)), |
| callback_(std::move(callback)), |
| part_content_(file_part_content), |
| content_range_(base::StringPrintf("bytes %zu-%zu/%zu", |
| byte_from, |
| byte_to, |
| byte_total)) {} |
| |
| BoxPartFileUploadApiCallFlow::~BoxPartFileUploadApiCallFlow() = default; |
| |
| // static |
| std::string BoxPartFileUploadApiCallFlow::CreateFileDigest( |
| const std::string& content) { |
| // Box API requires the digest to be SHA1 and Base64 encoded. |
| std::string sha_encoded; |
| base::Base64Encode(base::SHA1HashString(content), &sha_encoded); |
| return FormatSHA1Digest(sha_encoded); |
| } |
| |
| net::HttpRequestHeaders BoxPartFileUploadApiCallFlow::CreateApiCallHeaders() { |
| net::HttpRequestHeaders headers; |
| headers.SetHeader("content-range", content_range_); |
| headers.SetHeader("digest", CreateFileDigest(part_content_)); |
| return headers; |
| } |
| |
| std::string BoxPartFileUploadApiCallFlow::CreateApiCallBody() { |
| return part_content_; |
| } |
| |
| std::string BoxPartFileUploadApiCallFlow::CreateApiCallBodyContentType() { |
| return "application/octet-stream"; |
| } |
| |
| std::string BoxPartFileUploadApiCallFlow::GetRequestTypeForBody( |
| const std::string& body) { |
| CHECK(!body.empty()) << content_range_; |
| return "PUT"; |
| } |
| |
| bool BoxPartFileUploadApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_OK; |
| } |
| |
| void BoxPartFileUploadApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| DCHECK(body); |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *body, base::BindOnce(&BoxPartFileUploadApiCallFlow::OnSuccessJsonParsed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void BoxPartFileUploadApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, base::Value()); |
| } |
| |
| void BoxPartFileUploadApiCallFlow::OnSuccessJsonParsed(ParseResult result) { |
| const auto http_code = net::HTTP_OK; |
| if (!result.value) { |
| LOG_PARSE_FAIL(ERROR, "PartFileUpload", result); |
| ProcessFailure(MakeApiFailure(http_code, "bad_response", "parse_fail")); |
| return; |
| } |
| |
| base::Value* part = result.value->FindPath("part"); |
| if (!part) { |
| DLOG(ERROR) << "[BoxApiCallFlow] No info for uploaded part"; |
| ProcessFailure(MakeApiFailure(http_code, "bad_response", "parse_fail")); |
| } else { |
| std::move(callback_).Run(MakeSuccess(http_code), std::move(*part)); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ChunkedUpload: AbortUploadSession |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/delete-files-upload-sessions-id/ |
| BoxAbortUploadSessionApiCallFlow::BoxAbortUploadSessionApiCallFlow( |
| TaskCallback callback, |
| const std::string& session_endpoint) |
| : BoxChunkedUploadBaseApiCallFlow(GURL(session_endpoint)), |
| callback_(std::move(callback)) {} |
| |
| BoxAbortUploadSessionApiCallFlow::~BoxAbortUploadSessionApiCallFlow() = default; |
| |
| bool BoxAbortUploadSessionApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_NO_CONTENT; |
| } |
| |
| std::string BoxAbortUploadSessionApiCallFlow::GetRequestTypeForBody( |
| const std::string& body) { |
| return "DELETE"; |
| } |
| |
| void BoxAbortUploadSessionApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| DCHECK(!body || body->empty()) << "Expecting an empty body."; |
| std::move(callback_).Run(MakeSuccess(net::HTTP_NO_CONTENT)); |
| } |
| |
| void BoxAbortUploadSessionApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ChunkedUpload: CommitUploadSession |
| //////////////////////////////////////////////////////////////////////////////// |
| // BoxApiCallFlow interface. |
| // API reference: |
| // https://developer.box.com/reference/post-files-upload-sessions-id-commit/ |
| BoxCommitUploadSessionApiCallFlow::BoxCommitUploadSessionApiCallFlow( |
| TaskCallback callback, |
| const std::string& session_endpoint, |
| const base::Value& parts, |
| const std::string digest) |
| : BoxChunkedUploadBaseApiCallFlow(GURL(session_endpoint)), |
| callback_(std::move(callback)), |
| sha_digest_(digest), |
| upload_session_parts_(parts.Clone()) {} |
| |
| BoxCommitUploadSessionApiCallFlow::~BoxCommitUploadSessionApiCallFlow() = |
| default; |
| |
| net::HttpRequestHeaders |
| BoxCommitUploadSessionApiCallFlow::CreateApiCallHeaders() { |
| net::HttpRequestHeaders headers; |
| headers.SetHeader("digest", FormatSHA1Digest(sha_digest_)); |
| return headers; |
| } |
| |
| std::string BoxCommitUploadSessionApiCallFlow::CreateApiCallBody() { |
| base::Value parts(base::Value::Type::DICTIONARY); |
| parts.SetKey("parts", std::move(upload_session_parts_)); |
| DCHECK(VerifyChunkedUploadParts(parts)); |
| std::string body; |
| base::JSONWriter::Write(parts, &body); |
| return body; |
| } |
| |
| bool BoxCommitUploadSessionApiCallFlow::IsExpectedSuccessCode(int code) const { |
| return code == net::HTTP_CREATED || code == net::HTTP_ACCEPTED; |
| } |
| |
| void BoxCommitUploadSessionApiCallFlow::ProcessApiCallSuccess( |
| const network::mojom::URLResponseHead* head, |
| std::unique_ptr<std::string> body) { |
| auto response_code = head->headers->response_code(); |
| |
| if (response_code == net::HTTP_CREATED) { |
| DCHECK(body); |
| DCHECK(body->size()); |
| auto created_cb = base::BindOnce( |
| std::move(callback_), MakeSuccess(response_code), base::TimeDelta()); |
| return ProcessUploadSuccessResponse(std::move(body), std::move(created_cb)); |
| } |
| |
| bool success = false; |
| base::TimeDelta retry_after; |
| if (response_code == net::HTTP_ACCEPTED) { |
| std::string retry_string; |
| success = |
| head->headers->EnumerateHeader(nullptr, "Retry-After", &retry_string) && |
| net::HttpUtil::ParseRetryAfterHeader(retry_string, base::Time::Now(), |
| &retry_after); |
| } |
| |
| DCHECK(success) << head->headers->raw_headers(); |
| std::move(callback_).Run(Response{success, response_code}, retry_after, |
| std::string()); |
| } |
| |
| void BoxCommitUploadSessionApiCallFlow::ProcessFailure(Response response) { |
| std::move(callback_).Run(response, base::TimeDelta(), std::string()); |
| } |
| |
| } // namespace enterprise_connectors |