diff options
author | Andras Becsi <andras.becsi@digia.com> | 2014-03-18 13:16:26 +0100 |
---|---|---|
committer | Frederik Gladhorn <frederik.gladhorn@digia.com> | 2014-03-20 15:55:39 +0100 |
commit | 3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch) | |
tree | 92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/google_apis | |
parent | e90d7c4b152c56919d963987e2503f9909a666d2 (diff) | |
download | qtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz |
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies
needed on Windows.
Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42
Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu>
Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/google_apis')
115 files changed, 23037 insertions, 135 deletions
diff --git a/chromium/google_apis/OWNERS b/chromium/google_apis/OWNERS index 4975363007f..e3221f4c57c 100644 --- a/chromium/google_apis/OWNERS +++ b/chromium/google_apis/OWNERS @@ -1 +1,2 @@ joi@chromium.org +rogerta@chromium.org diff --git a/chromium/google_apis/cup/client_update_protocol.cc b/chromium/google_apis/cup/client_update_protocol.cc index e730557164f..afde3ab46b5 100644 --- a/chromium/google_apis/cup/client_update_protocol.cc +++ b/chromium/google_apis/cup/client_update_protocol.cc @@ -122,8 +122,7 @@ std::vector<uint8> RsaPad(size_t rsa_key_size, // needed. Call the standard Base64 encoder/decoder and then apply fixups. std::string UrlSafeB64Encode(const std::vector<uint8>& data) { std::string result; - if (!base::Base64Encode(ByteVectorToSP(data), &result)) - return std::string(); + base::Base64Encode(ByteVectorToSP(data), &result); // Do an tr|+/|-_| on the output, and strip any '=' padding. for (std::string::iterator it = result.begin(); it != result.end(); ++it) { @@ -138,7 +137,7 @@ std::string UrlSafeB64Encode(const std::vector<uint8>& data) { break; } } - TrimString(result, "=", &result); + base::TrimString(result, "=", &result); return result; } @@ -302,4 +301,3 @@ bool ClientUpdateProtocol::DeriveSharedKey(const std::vector<uint8>& source) { return true; } - diff --git a/chromium/google_apis/drive/DEPS b/chromium/google_apis/drive/DEPS new file mode 100644 index 00000000000..5827c268b07 --- /dev/null +++ b/chromium/google_apis/drive/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+third_party/libxml", +] diff --git a/chromium/google_apis/drive/OWNERS b/chromium/google_apis/drive/OWNERS new file mode 100644 index 00000000000..3db88c0b171 --- /dev/null +++ b/chromium/google_apis/drive/OWNERS @@ -0,0 +1,5 @@ +hashimoto@chromium.org +hidehiko@chromium.org +kinaba@chromium.org +satorux@chromium.org +yoshiki@chromium.org diff --git a/chromium/google_apis/drive/auth_service.cc b/chromium/google_apis/drive/auth_service.cc new file mode 100644 index 00000000000..18623686f94 --- /dev/null +++ b/chromium/google_apis/drive/auth_service.cc @@ -0,0 +1,239 @@ +// Copyright (c) 2012 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 "google_apis/drive/auth_service.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/location.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "google_apis/drive/auth_service_observer.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/url_request/url_request_context_getter.h" + +namespace google_apis { + +namespace { + +// Used for success ratio histograms. 0 for failure, 1 for success, +// 2 for no connection (likely offline). +const int kSuccessRatioHistogramFailure = 0; +const int kSuccessRatioHistogramSuccess = 1; +const int kSuccessRatioHistogramNoConnection = 2; +const int kSuccessRatioHistogramTemporaryFailure = 3; +const int kSuccessRatioHistogramMaxValue = 4; // The max value is exclusive. + +// OAuth2 authorization token retrieval request. +class AuthRequest : public OAuth2TokenService::Consumer { + public: + AuthRequest(OAuth2TokenService* oauth2_token_service, + const std::string& account_id, + net::URLRequestContextGetter* url_request_context_getter, + const AuthStatusCallback& callback, + const std::vector<std::string>& scopes); + virtual ~AuthRequest(); + + private: + // Overridden from OAuth2TokenService::Consumer: + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + AuthStatusCallback callback_; + scoped_ptr<OAuth2TokenService::Request> request_; + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(AuthRequest); +}; + +AuthRequest::AuthRequest( + OAuth2TokenService* oauth2_token_service, + const std::string& account_id, + net::URLRequestContextGetter* url_request_context_getter, + const AuthStatusCallback& callback, + const std::vector<std::string>& scopes) + : callback_(callback) { + DCHECK(!callback_.is_null()); + request_ = oauth2_token_service-> + StartRequestWithContext( + account_id, + url_request_context_getter, + OAuth2TokenService::ScopeSet(scopes.begin(), scopes.end()), + this); +} + +AuthRequest::~AuthRequest() {} + +// Callback for OAuth2AccessTokenFetcher on success. |access_token| is the token +// used to start fetching user data. +void AuthRequest::OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + DCHECK(thread_checker_.CalledOnValidThread()); + + UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", + kSuccessRatioHistogramSuccess, + kSuccessRatioHistogramMaxValue); + + callback_.Run(HTTP_SUCCESS, access_token); + delete this; +} + +// Callback for OAuth2AccessTokenFetcher on failure. +void AuthRequest::OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + LOG(WARNING) << "AuthRequest: token request using refresh token failed: " + << error.ToString(); + + // There are many ways to fail, but if the failure is due to connection, + // it's likely that the device is off-line. We treat the error differently + // so that the file manager works while off-line. + if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED) { + UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", + kSuccessRatioHistogramNoConnection, + kSuccessRatioHistogramMaxValue); + callback_.Run(GDATA_NO_CONNECTION, std::string()); + } else if (error.state() == GoogleServiceAuthError::SERVICE_UNAVAILABLE) { + // Temporary auth error. + UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", + kSuccessRatioHistogramTemporaryFailure, + kSuccessRatioHistogramMaxValue); + callback_.Run(HTTP_FORBIDDEN, std::string()); + } else { + // Permanent auth error. + UMA_HISTOGRAM_ENUMERATION("GData.AuthSuccess", + kSuccessRatioHistogramFailure, + kSuccessRatioHistogramMaxValue); + callback_.Run(HTTP_UNAUTHORIZED, std::string()); + } + delete this; +} + +} // namespace + +AuthService::AuthService( + OAuth2TokenService* oauth2_token_service, + const std::string& account_id, + net::URLRequestContextGetter* url_request_context_getter, + const std::vector<std::string>& scopes) + : oauth2_token_service_(oauth2_token_service), + account_id_(account_id), + url_request_context_getter_(url_request_context_getter), + scopes_(scopes), + weak_ptr_factory_(this) { + DCHECK(oauth2_token_service); + + // Get OAuth2 refresh token (if we have any) and register for its updates. + oauth2_token_service_->AddObserver(this); + has_refresh_token_ = oauth2_token_service_->RefreshTokenIsAvailable( + account_id_); +} + +AuthService::~AuthService() { + oauth2_token_service_->RemoveObserver(this); +} + +void AuthService::StartAuthentication(const AuthStatusCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + scoped_refptr<base::MessageLoopProxy> relay_proxy( + base::MessageLoopProxy::current()); + + if (HasAccessToken()) { + // We already have access token. Give it back to the caller asynchronously. + relay_proxy->PostTask(FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, access_token_)); + } else if (HasRefreshToken()) { + // We have refresh token, let's get an access token. + new AuthRequest(oauth2_token_service_, + account_id_, + url_request_context_getter_, + base::Bind(&AuthService::OnAuthCompleted, + weak_ptr_factory_.GetWeakPtr(), + callback), + scopes_); + } else { + relay_proxy->PostTask(FROM_HERE, + base::Bind(callback, GDATA_NOT_READY, std::string())); + } +} + +bool AuthService::HasAccessToken() const { + return !access_token_.empty(); +} + +bool AuthService::HasRefreshToken() const { + return has_refresh_token_; +} + +const std::string& AuthService::access_token() const { + return access_token_; +} + +void AuthService::ClearAccessToken() { + access_token_.clear(); +} + +void AuthService::ClearRefreshToken() { + has_refresh_token_ = false; + + FOR_EACH_OBSERVER(AuthServiceObserver, + observers_, + OnOAuth2RefreshTokenChanged()); +} + +void AuthService::OnAuthCompleted(const AuthStatusCallback& callback, + GDataErrorCode error, + const std::string& access_token) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == HTTP_SUCCESS) { + access_token_ = access_token; + } else if (error == HTTP_UNAUTHORIZED) { + // Refreshing access token using the refresh token is failed with 401 error + // (HTTP_UNAUTHORIZED). This means the current refresh token is invalid for + // Drive, hence we clear the refresh token here to make HasRefreshToken() + // false, thus the invalidness is clearly observable. + // This is not for triggering refetch of the refresh token. UI should + // show some message to encourage user to log-off and log-in again in order + // to fetch new valid refresh token. + ClearRefreshToken(); + } + + callback.Run(error, access_token); +} + +void AuthService::AddObserver(AuthServiceObserver* observer) { + observers_.AddObserver(observer); +} + +void AuthService::RemoveObserver(AuthServiceObserver* observer) { + observers_.RemoveObserver(observer); +} + +void AuthService::OnRefreshTokenAvailable(const std::string& account_id) { + OnHandleRefreshToken(true); +} + +void AuthService::OnRefreshTokenRevoked(const std::string& account_id) { + OnHandleRefreshToken(false); +} + +void AuthService::OnHandleRefreshToken(bool has_refresh_token) { + access_token_.clear(); + has_refresh_token_ = has_refresh_token; + + FOR_EACH_OBSERVER(AuthServiceObserver, + observers_, + OnOAuth2RefreshTokenChanged()); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/auth_service.h b/chromium/google_apis/drive/auth_service.h new file mode 100644 index 00000000000..f055a1ccb31 --- /dev/null +++ b/chromium/google_apis/drive/auth_service.h @@ -0,0 +1,84 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_ +#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_ + +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "google_apis/drive/auth_service_interface.h" +#include "google_apis/gaia/oauth2_token_service.h" + +namespace net { +class URLRequestContextGetter; +} + +namespace google_apis { + +class AuthServiceObserver; + +// This class provides authentication for Google services. +// It integrates specific service integration with OAuth2 stack +// (OAuth2TokenService) and provides OAuth2 token refresh infrastructure. +// All public functions must be called on UI thread. +class AuthService : public AuthServiceInterface, + public OAuth2TokenService::Observer { + public: + // |url_request_context_getter| is used to perform authentication with + // URLFetcher. + // + // |scopes| specifies OAuth2 scopes. + AuthService(OAuth2TokenService* oauth2_token_service, + const std::string& account_id, + net::URLRequestContextGetter* url_request_context_getter, + const std::vector<std::string>& scopes); + virtual ~AuthService(); + + // Overriden from AuthServiceInterface: + virtual void AddObserver(AuthServiceObserver* observer) OVERRIDE; + virtual void RemoveObserver(AuthServiceObserver* observer) OVERRIDE; + virtual void StartAuthentication(const AuthStatusCallback& callback) OVERRIDE; + virtual bool HasAccessToken() const OVERRIDE; + virtual bool HasRefreshToken() const OVERRIDE; + virtual const std::string& access_token() const OVERRIDE; + virtual void ClearAccessToken() OVERRIDE; + virtual void ClearRefreshToken() OVERRIDE; + + // Overridden from OAuth2TokenService::Observer: + virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE; + virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE; + + private: + // Called when the state of the refresh token changes. + void OnHandleRefreshToken(bool has_refresh_token); + + // Called when authentication request from StartAuthentication() is + // completed. + void OnAuthCompleted(const AuthStatusCallback& callback, + GDataErrorCode error, + const std::string& access_token); + + OAuth2TokenService* oauth2_token_service_; + std::string account_id_; + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + bool has_refresh_token_; + std::string access_token_; + std::vector<std::string> scopes_; + ObserverList<AuthServiceObserver> observers_; + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<AuthService> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(AuthService); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_H_ diff --git a/chromium/google_apis/drive/auth_service_interface.h b/chromium/google_apis/drive/auth_service_interface.h new file mode 100644 index 00000000000..40a1905d54d --- /dev/null +++ b/chromium/google_apis/drive/auth_service_interface.h @@ -0,0 +1,58 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_ +#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_ + +#include <string> + +#include "base/callback_forward.h" +#include "google_apis/drive/gdata_errorcode.h" + +namespace google_apis { + +class AuthServiceObserver; + +// Called when fetching of access token is complete. +typedef base::Callback<void(GDataErrorCode error, + const std::string& access_token)> + AuthStatusCallback; + +// This defines an interface for the authentication service which is required +// by authenticated requests (AuthenticatedRequestInterface). +// All functions must be called on UI thread. +class AuthServiceInterface { + public: + virtual ~AuthServiceInterface() {} + + // Adds and removes the observer. + virtual void AddObserver(AuthServiceObserver* observer) = 0; + virtual void RemoveObserver(AuthServiceObserver* observer) = 0; + + // Starts fetching OAuth2 access token from the refresh token. + // |callback| must not be null. + virtual void StartAuthentication(const AuthStatusCallback& callback) = 0; + + // True if an OAuth2 access token is retrieved and believed to be fresh. + // The access token is used to access the Drive server. + virtual bool HasAccessToken() const = 0; + + // True if an OAuth2 refresh token is present. Its absence means that user + // is not properly authenticated. + // The refresh token is used to get the access token. + virtual bool HasRefreshToken() const = 0; + + // Returns OAuth2 access token. + virtual const std::string& access_token() const = 0; + + // Clears OAuth2 access token. + virtual void ClearAccessToken() = 0; + + // Clears OAuth2 refresh token. + virtual void ClearRefreshToken() = 0; +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_INTERFACE_H_ diff --git a/chromium/google_apis/drive/auth_service_observer.h b/chromium/google_apis/drive/auth_service_observer.h new file mode 100644 index 00000000000..2fefbf20f3c --- /dev/null +++ b/chromium/google_apis/drive/auth_service_observer.h @@ -0,0 +1,23 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_ +#define GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_ + +namespace google_apis { + +// Interface for classes that need to observe events from AuthService. +// All events are notified on UI thread. +class AuthServiceObserver { + public: + // Triggered when a new OAuth2 refresh token is received from AuthService. + virtual void OnOAuth2RefreshTokenChanged() = 0; + + protected: + virtual ~AuthServiceObserver() {} +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_AUTH_SERVICE_OBSERVER_H_ diff --git a/chromium/google_apis/drive/base_requests.cc b/chromium/google_apis/drive/base_requests.cc new file mode 100644 index 00000000000..be64f783c24 --- /dev/null +++ b/chromium/google_apis/drive/base_requests.cc @@ -0,0 +1,799 @@ +// Copyright (c) 2012 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 "google_apis/drive/base_requests.h" + +#include "base/json/json_reader.h" +#include "base/location.h" +#include "base/sequenced_task_runner.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/task_runner_util.h" +#include "base/values.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/task_util.h" +#include "net/base/io_buffer.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/http/http_byte_range.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_util.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_status.h" + +using net::URLFetcher; + +namespace { + +// Template for optional OAuth2 authorization HTTP header. +const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s"; +// Template for GData API version HTTP header. +const char kGDataVersionHeader[] = "GData-Version: 3.0"; + +// Maximum number of attempts for re-authentication per request. +const int kMaxReAuthenticateAttemptsPerRequest = 1; + +// Template for initiate upload of both GData WAPI and Drive API v2. +const char kUploadContentType[] = "X-Upload-Content-Type: "; +const char kUploadContentLength[] = "X-Upload-Content-Length: "; +const char kUploadResponseLocation[] = "location"; + +// Template for upload data range of both GData WAPI and Drive API v2. +const char kUploadContentRange[] = "Content-Range: bytes "; +const char kUploadResponseRange[] = "range"; + +// Parse JSON string to base::Value object. +scoped_ptr<base::Value> ParseJsonInternal(const std::string& json) { + int error_code = -1; + std::string error_message; + scoped_ptr<base::Value> value(base::JSONReader::ReadAndReturnError( + json, base::JSON_PARSE_RFC, &error_code, &error_message)); + + if (!value.get()) { + std::string trimmed_json; + if (json.size() < 80) { + trimmed_json = json; + } else { + // Take the first 50 and the last 10 bytes. + trimmed_json = base::StringPrintf( + "%s [%s bytes] %s", + json.substr(0, 50).c_str(), + base::Uint64ToString(json.size() - 60).c_str(), + json.substr(json.size() - 10).c_str()); + } + LOG(WARNING) << "Error while parsing entry response: " << error_message + << ", code: " << error_code << ", json:\n" << trimmed_json; + } + return value.Pass(); +} + +// Returns response headers as a string. Returns a warning message if +// |url_fetcher| does not contain a valid response. Used only for debugging. +std::string GetResponseHeadersAsString( + const URLFetcher* url_fetcher) { + // net::HttpResponseHeaders::raw_headers(), as the name implies, stores + // all headers in their raw format, i.e each header is null-terminated. + // So logging raw_headers() only shows the first header, which is probably + // the status line. GetNormalizedHeaders, on the other hand, will show all + // the headers, one per line, which is probably what we want. + std::string headers; + // Check that response code indicates response headers are valid (i.e. not + // malformed) before we retrieve the headers. + if (url_fetcher->GetResponseCode() == URLFetcher::RESPONSE_CODE_INVALID) { + headers.assign("Response headers are malformed!!"); + } else { + url_fetcher->GetResponseHeaders()->GetNormalizedHeaders(&headers); + } + return headers; +} + +bool IsSuccessfulResponseCode(int response_code) { + return 200 <= response_code && response_code <= 299; +} + +} // namespace + +namespace google_apis { + +void ParseJson(base::TaskRunner* blocking_task_runner, + const std::string& json, + const ParseJsonCallback& callback) { + base::PostTaskAndReplyWithResult( + blocking_task_runner, + FROM_HERE, + base::Bind(&ParseJsonInternal, json), + callback); +} + +//=========================== ResponseWriter ================================== +ResponseWriter::ResponseWriter(base::SequencedTaskRunner* file_task_runner, + const base::FilePath& file_path, + const GetContentCallback& get_content_callback) + : get_content_callback_(get_content_callback), + weak_ptr_factory_(this) { + if (!file_path.empty()) { + file_writer_.reset( + new net::URLFetcherFileWriter(file_task_runner, file_path)); + } +} + +ResponseWriter::~ResponseWriter() { +} + +void ResponseWriter::DisownFile() { + DCHECK(file_writer_); + file_writer_->DisownFile(); +} + +int ResponseWriter::Initialize(const net::CompletionCallback& callback) { + if (file_writer_) + return file_writer_->Initialize(callback); + + data_.clear(); + return net::OK; +} + +int ResponseWriter::Write(net::IOBuffer* buffer, + int num_bytes, + const net::CompletionCallback& callback) { + if (!get_content_callback_.is_null()) { + get_content_callback_.Run( + HTTP_SUCCESS, + make_scoped_ptr(new std::string(buffer->data(), num_bytes))); + } + + if (file_writer_) { + const int result = file_writer_->Write( + buffer, num_bytes, + base::Bind(&ResponseWriter::DidWrite, + weak_ptr_factory_.GetWeakPtr(), + make_scoped_refptr(buffer), callback)); + if (result != net::ERR_IO_PENDING) + DidWrite(buffer, net::CompletionCallback(), result); + return result; + } + + data_.append(buffer->data(), num_bytes); + return num_bytes; +} + +int ResponseWriter::Finish(const net::CompletionCallback& callback) { + if (file_writer_) + return file_writer_->Finish(callback); + + return net::OK; +} + +void ResponseWriter::DidWrite(scoped_refptr<net::IOBuffer> buffer, + const net::CompletionCallback& callback, + int result) { + if (result > 0) { + // Even if file_writer_ is used, append the data to |data_|, so that it can + // be used to get error information in case of server side errors. + // The size limit is to avoid consuming too much redundant memory. + const size_t kMaxStringSize = 1024*1024; + if (data_.size() < kMaxStringSize) { + data_.append(buffer->data(), std::min(static_cast<size_t>(result), + kMaxStringSize - data_.size())); + } + } + + if (!callback.is_null()) + callback.Run(result); +} + +//============================ UrlFetchRequestBase =========================== + +UrlFetchRequestBase::UrlFetchRequestBase(RequestSender* sender) + : re_authenticate_count_(0), + sender_(sender), + error_code_(GDATA_OTHER_ERROR), + weak_ptr_factory_(this) { +} + +UrlFetchRequestBase::~UrlFetchRequestBase() {} + +void UrlFetchRequestBase::Start(const std::string& access_token, + const std::string& custom_user_agent, + const ReAuthenticateCallback& callback) { + DCHECK(CalledOnValidThread()); + DCHECK(!access_token.empty()); + DCHECK(!callback.is_null()); + DCHECK(re_authenticate_callback_.is_null()); + + re_authenticate_callback_ = callback; + + GURL url = GetURL(); + if (url.is_empty()) { + // Error is found on generating the url. Send the error message to the + // callback, and then return immediately without trying to connect + // to the server. + RunCallbackOnPrematureFailure(GDATA_OTHER_ERROR); + return; + } + DVLOG(1) << "URL: " << url.spec(); + + URLFetcher::RequestType request_type = GetRequestType(); + url_fetcher_.reset( + URLFetcher::Create(url, request_type, this)); + url_fetcher_->SetRequestContext(sender_->url_request_context_getter()); + // Always set flags to neither send nor save cookies. + url_fetcher_->SetLoadFlags( + net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES | + net::LOAD_DISABLE_CACHE); + + base::FilePath output_file_path; + GetContentCallback get_content_callback; + GetOutputFilePath(&output_file_path, &get_content_callback); + if (!get_content_callback.is_null()) + get_content_callback = CreateRelayCallback(get_content_callback); + response_writer_ = new ResponseWriter(blocking_task_runner(), + output_file_path, + get_content_callback); + url_fetcher_->SaveResponseWithWriter( + scoped_ptr<net::URLFetcherResponseWriter>(response_writer_)); + + // Add request headers. + // Note that SetExtraRequestHeaders clears the current headers and sets it + // to the passed-in headers, so calling it for each header will result in + // only the last header being set in request headers. + if (!custom_user_agent.empty()) + url_fetcher_->AddExtraRequestHeader("User-Agent: " + custom_user_agent); + url_fetcher_->AddExtraRequestHeader(kGDataVersionHeader); + url_fetcher_->AddExtraRequestHeader( + base::StringPrintf(kAuthorizationHeaderFormat, access_token.data())); + std::vector<std::string> headers = GetExtraRequestHeaders(); + for (size_t i = 0; i < headers.size(); ++i) { + url_fetcher_->AddExtraRequestHeader(headers[i]); + DVLOG(1) << "Extra header: " << headers[i]; + } + + // Set upload data if available. + std::string upload_content_type; + std::string upload_content; + if (GetContentData(&upload_content_type, &upload_content)) { + url_fetcher_->SetUploadData(upload_content_type, upload_content); + } else { + base::FilePath local_file_path; + int64 range_offset = 0; + int64 range_length = 0; + if (GetContentFile(&local_file_path, &range_offset, &range_length, + &upload_content_type)) { + url_fetcher_->SetUploadFilePath( + upload_content_type, + local_file_path, + range_offset, + range_length, + blocking_task_runner()); + } else { + // Even if there is no content data, UrlFetcher requires to set empty + // upload data string for POST, PUT and PATCH methods, explicitly. + // It is because that most requests of those methods have non-empty + // body, and UrlFetcher checks whether it is actually not forgotten. + if (request_type == URLFetcher::POST || + request_type == URLFetcher::PUT || + request_type == URLFetcher::PATCH) { + // Set empty upload content-type and upload content, so that + // the request will have no "Content-type: " header and no content. + url_fetcher_->SetUploadData(std::string(), std::string()); + } + } + } + + url_fetcher_->Start(); +} + +URLFetcher::RequestType UrlFetchRequestBase::GetRequestType() const { + return URLFetcher::GET; +} + +std::vector<std::string> UrlFetchRequestBase::GetExtraRequestHeaders() const { + return std::vector<std::string>(); +} + +bool UrlFetchRequestBase::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + return false; +} + +bool UrlFetchRequestBase::GetContentFile(base::FilePath* local_file_path, + int64* range_offset, + int64* range_length, + std::string* upload_content_type) { + return false; +} + +void UrlFetchRequestBase::GetOutputFilePath( + base::FilePath* local_file_path, + GetContentCallback* get_content_callback) { +} + +void UrlFetchRequestBase::Cancel() { + response_writer_ = NULL; + url_fetcher_.reset(NULL); + RunCallbackOnPrematureFailure(GDATA_CANCELLED); + sender_->RequestFinished(this); +} + +GDataErrorCode UrlFetchRequestBase::GetErrorCode() { + return error_code_; +} + +bool UrlFetchRequestBase::CalledOnValidThread() { + return thread_checker_.CalledOnValidThread(); +} + +base::SequencedTaskRunner* UrlFetchRequestBase::blocking_task_runner() const { + return sender_->blocking_task_runner(); +} + +void UrlFetchRequestBase::OnProcessURLFetchResultsComplete() { + sender_->RequestFinished(this); +} + +void UrlFetchRequestBase::OnURLFetchComplete(const URLFetcher* source) { + DVLOG(1) << "Response headers:\n" << GetResponseHeadersAsString(source); + + // Determine error code. + error_code_ = static_cast<GDataErrorCode>(source->GetResponseCode()); + if (!source->GetStatus().is_success()) { + switch (source->GetStatus().error()) { + case net::ERR_NETWORK_CHANGED: + error_code_ = GDATA_NO_CONNECTION; + break; + default: + error_code_ = GDATA_OTHER_ERROR; + } + } + + // The server may return detailed error status in JSON. + // See https://developers.google.com/drive/handle-errors + if (!IsSuccessfulResponseCode(error_code_)) { + DVLOG(1) << response_writer_->data(); + + const char kErrorKey[] = "error"; + const char kErrorErrorsKey[] = "errors"; + const char kErrorReasonKey[] = "reason"; + const char kErrorMessageKey[] = "message"; + const char kErrorReasonRateLimitExceeded[] = "rateLimitExceeded"; + const char kErrorReasonUserRateLimitExceeded[] = "userRateLimitExceeded"; + + scoped_ptr<base::Value> value(ParseJsonInternal(response_writer_->data())); + base::DictionaryValue* dictionary = NULL; + base::DictionaryValue* error = NULL; + if (value && + value->GetAsDictionary(&dictionary) && + dictionary->GetDictionaryWithoutPathExpansion(kErrorKey, &error)) { + // Get error message. + std::string message; + error->GetStringWithoutPathExpansion(kErrorMessageKey, &message); + DLOG(ERROR) << "code: " << error_code_ << ", message: " << message; + + // Override the error code based on the reason of the first error. + base::ListValue* errors = NULL; + base::DictionaryValue* first_error = NULL; + if (error->GetListWithoutPathExpansion(kErrorErrorsKey, &errors) && + errors->GetDictionary(0, &first_error)) { + std::string reason; + first_error->GetStringWithoutPathExpansion(kErrorReasonKey, &reason); + if (reason == kErrorReasonRateLimitExceeded || + reason == kErrorReasonUserRateLimitExceeded) + error_code_ = HTTP_SERVICE_UNAVAILABLE; + } + } + } + + // Handle authentication failure. + if (error_code_ == HTTP_UNAUTHORIZED) { + if (++re_authenticate_count_ <= kMaxReAuthenticateAttemptsPerRequest) { + // Reset re_authenticate_callback_ so Start() can be called again. + ReAuthenticateCallback callback = re_authenticate_callback_; + re_authenticate_callback_.Reset(); + callback.Run(this); + return; + } + + OnAuthFailed(error_code_); + return; + } + + // Overridden by each specialization + ProcessURLFetchResults(source); +} + +void UrlFetchRequestBase::OnAuthFailed(GDataErrorCode code) { + RunCallbackOnPrematureFailure(code); + sender_->RequestFinished(this); +} + +base::WeakPtr<AuthenticatedRequestInterface> +UrlFetchRequestBase::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +//============================ EntryActionRequest ============================ + +EntryActionRequest::EntryActionRequest(RequestSender* sender, + const EntryActionCallback& callback) + : UrlFetchRequestBase(sender), + callback_(callback) { + DCHECK(!callback_.is_null()); +} + +EntryActionRequest::~EntryActionRequest() {} + +void EntryActionRequest::ProcessURLFetchResults(const URLFetcher* source) { + callback_.Run(GetErrorCode()); + OnProcessURLFetchResultsComplete(); +} + +void EntryActionRequest::RunCallbackOnPrematureFailure(GDataErrorCode code) { + callback_.Run(code); +} + +//============================== GetDataRequest ============================== + +GetDataRequest::GetDataRequest(RequestSender* sender, + const GetDataCallback& callback) + : UrlFetchRequestBase(sender), + callback_(callback), + weak_ptr_factory_(this) { + DCHECK(!callback_.is_null()); +} + +GetDataRequest::~GetDataRequest() {} + +void GetDataRequest::ParseResponse(GDataErrorCode fetch_error_code, + const std::string& data) { + DCHECK(CalledOnValidThread()); + + VLOG(1) << "JSON received from " << GetURL().spec() << ": " + << data.size() << " bytes"; + ParseJson(blocking_task_runner(), + data, + base::Bind(&GetDataRequest::OnDataParsed, + weak_ptr_factory_.GetWeakPtr(), + fetch_error_code)); +} + +void GetDataRequest::ProcessURLFetchResults(const URLFetcher* source) { + GDataErrorCode fetch_error_code = GetErrorCode(); + + switch (fetch_error_code) { + case HTTP_SUCCESS: + case HTTP_CREATED: + ParseResponse(fetch_error_code, response_writer()->data()); + break; + default: + RunCallbackOnPrematureFailure(fetch_error_code); + OnProcessURLFetchResultsComplete(); + break; + } +} + +void GetDataRequest::RunCallbackOnPrematureFailure( + GDataErrorCode fetch_error_code) { + callback_.Run(fetch_error_code, scoped_ptr<base::Value>()); +} + +void GetDataRequest::OnDataParsed(GDataErrorCode fetch_error_code, + scoped_ptr<base::Value> value) { + DCHECK(CalledOnValidThread()); + + if (!value.get()) + fetch_error_code = GDATA_PARSE_ERROR; + + callback_.Run(fetch_error_code, value.Pass()); + OnProcessURLFetchResultsComplete(); +} + +//========================= InitiateUploadRequestBase ======================== + +InitiateUploadRequestBase::InitiateUploadRequestBase( + RequestSender* sender, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length) + : UrlFetchRequestBase(sender), + callback_(callback), + content_type_(content_type), + content_length_(content_length) { + DCHECK(!callback_.is_null()); + DCHECK(!content_type_.empty()); + DCHECK_GE(content_length_, 0); +} + +InitiateUploadRequestBase::~InitiateUploadRequestBase() {} + +void InitiateUploadRequestBase::ProcessURLFetchResults( + const URLFetcher* source) { + GDataErrorCode code = GetErrorCode(); + + std::string upload_location; + if (code == HTTP_SUCCESS) { + // Retrieve value of the first "Location" header. + source->GetResponseHeaders()->EnumerateHeader(NULL, + kUploadResponseLocation, + &upload_location); + } + + callback_.Run(code, GURL(upload_location)); + OnProcessURLFetchResultsComplete(); +} + +void InitiateUploadRequestBase::RunCallbackOnPrematureFailure( + GDataErrorCode code) { + callback_.Run(code, GURL()); +} + +std::vector<std::string> +InitiateUploadRequestBase::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(kUploadContentType + content_type_); + headers.push_back( + kUploadContentLength + base::Int64ToString(content_length_)); + return headers; +} + +//============================ UploadRangeResponse ============================= + +UploadRangeResponse::UploadRangeResponse() + : code(HTTP_SUCCESS), + start_position_received(0), + end_position_received(0) { +} + +UploadRangeResponse::UploadRangeResponse(GDataErrorCode code, + int64 start_position_received, + int64 end_position_received) + : code(code), + start_position_received(start_position_received), + end_position_received(end_position_received) { +} + +UploadRangeResponse::~UploadRangeResponse() { +} + +//========================== UploadRangeRequestBase ========================== + +UploadRangeRequestBase::UploadRangeRequestBase(RequestSender* sender, + const GURL& upload_url) + : UrlFetchRequestBase(sender), + upload_url_(upload_url), + weak_ptr_factory_(this) { +} + +UploadRangeRequestBase::~UploadRangeRequestBase() {} + +GURL UploadRangeRequestBase::GetURL() const { + // This is very tricky to get json from this request. To do that, &alt=json + // has to be appended not here but in InitiateUploadRequestBase::GetURL(). + return upload_url_; +} + +URLFetcher::RequestType UploadRangeRequestBase::GetRequestType() const { + return URLFetcher::PUT; +} + +void UploadRangeRequestBase::ProcessURLFetchResults( + const URLFetcher* source) { + GDataErrorCode code = GetErrorCode(); + net::HttpResponseHeaders* hdrs = source->GetResponseHeaders(); + + if (code == HTTP_RESUME_INCOMPLETE) { + // Retrieve value of the first "Range" header. + // The Range header is appeared only if there is at least one received + // byte. So, initialize the positions by 0 so that the [0,0) will be + // returned via the |callback_| for empty data case. + int64 start_position_received = 0; + int64 end_position_received = 0; + std::string range_received; + hdrs->EnumerateHeader(NULL, kUploadResponseRange, &range_received); + if (!range_received.empty()) { // Parse the range header. + std::vector<net::HttpByteRange> ranges; + if (net::HttpUtil::ParseRangeHeader(range_received, &ranges) && + !ranges.empty() ) { + // We only care about the first start-end pair in the range. + // + // Range header represents the range inclusively, while we are treating + // ranges exclusively (i.e., end_position_received should be one passed + // the last valid index). So "+ 1" is added. + start_position_received = ranges[0].first_byte_position(); + end_position_received = ranges[0].last_byte_position() + 1; + } + } + // The Range header has the received data range, so the start position + // should be always 0. + DCHECK_EQ(start_position_received, 0); + + OnRangeRequestComplete(UploadRangeResponse(code, + start_position_received, + end_position_received), + scoped_ptr<base::Value>()); + + OnProcessURLFetchResultsComplete(); + } else if (code == HTTP_CREATED || code == HTTP_SUCCESS) { + // The upload is successfully done. Parse the response which should be + // the entry's metadata. + ParseJson(blocking_task_runner(), + response_writer()->data(), + base::Bind(&UploadRangeRequestBase::OnDataParsed, + weak_ptr_factory_.GetWeakPtr(), + code)); + } else { + // Failed to upload. Run callbacks to notify the error. + OnRangeRequestComplete( + UploadRangeResponse(code, -1, -1), scoped_ptr<base::Value>()); + OnProcessURLFetchResultsComplete(); + } +} + +void UploadRangeRequestBase::OnDataParsed(GDataErrorCode code, + scoped_ptr<base::Value> value) { + DCHECK(CalledOnValidThread()); + DCHECK(code == HTTP_CREATED || code == HTTP_SUCCESS); + + OnRangeRequestComplete(UploadRangeResponse(code, -1, -1), value.Pass()); + OnProcessURLFetchResultsComplete(); +} + +void UploadRangeRequestBase::RunCallbackOnPrematureFailure( + GDataErrorCode code) { + OnRangeRequestComplete( + UploadRangeResponse(code, 0, 0), scoped_ptr<base::Value>()); +} + +//========================== ResumeUploadRequestBase ========================= + +ResumeUploadRequestBase::ResumeUploadRequestBase( + RequestSender* sender, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path) + : UploadRangeRequestBase(sender, upload_location), + start_position_(start_position), + end_position_(end_position), + content_length_(content_length), + content_type_(content_type), + local_file_path_(local_file_path) { + DCHECK_LE(start_position_, end_position_); +} + +ResumeUploadRequestBase::~ResumeUploadRequestBase() {} + +std::vector<std::string> +ResumeUploadRequestBase::GetExtraRequestHeaders() const { + if (content_length_ == 0) { + // For uploading an empty document, just PUT an empty content. + DCHECK_EQ(start_position_, 0); + DCHECK_EQ(end_position_, 0); + return std::vector<std::string>(); + } + + // The header looks like + // Content-Range: bytes <start_position>-<end_position>/<content_length> + // for example: + // Content-Range: bytes 7864320-8388607/13851821 + // The header takes inclusive range, so we adjust by "end_position - 1". + DCHECK_GE(start_position_, 0); + DCHECK_GT(end_position_, 0); + DCHECK_GE(content_length_, 0); + + std::vector<std::string> headers; + headers.push_back( + std::string(kUploadContentRange) + + base::Int64ToString(start_position_) + "-" + + base::Int64ToString(end_position_ - 1) + "/" + + base::Int64ToString(content_length_)); + return headers; +} + +bool ResumeUploadRequestBase::GetContentFile( + base::FilePath* local_file_path, + int64* range_offset, + int64* range_length, + std::string* upload_content_type) { + if (start_position_ == end_position_) { + // No content data. + return false; + } + + *local_file_path = local_file_path_; + *range_offset = start_position_; + *range_length = end_position_ - start_position_; + *upload_content_type = content_type_; + return true; +} + +//======================== GetUploadStatusRequestBase ======================== + +GetUploadStatusRequestBase::GetUploadStatusRequestBase(RequestSender* sender, + const GURL& upload_url, + int64 content_length) + : UploadRangeRequestBase(sender, upload_url), + content_length_(content_length) {} + +GetUploadStatusRequestBase::~GetUploadStatusRequestBase() {} + +std::vector<std::string> +GetUploadStatusRequestBase::GetExtraRequestHeaders() const { + // The header looks like + // Content-Range: bytes */<content_length> + // for example: + // Content-Range: bytes */13851821 + DCHECK_GE(content_length_, 0); + + std::vector<std::string> headers; + headers.push_back( + std::string(kUploadContentRange) + "*/" + + base::Int64ToString(content_length_)); + return headers; +} + +//============================ DownloadFileRequestBase ========================= + +DownloadFileRequestBase::DownloadFileRequestBase( + RequestSender* sender, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback, + const GURL& download_url, + const base::FilePath& output_file_path) + : UrlFetchRequestBase(sender), + download_action_callback_(download_action_callback), + get_content_callback_(get_content_callback), + progress_callback_(progress_callback), + download_url_(download_url), + output_file_path_(output_file_path) { + DCHECK(!download_action_callback_.is_null()); + DCHECK(!output_file_path_.empty()); + // get_content_callback may be null. +} + +DownloadFileRequestBase::~DownloadFileRequestBase() {} + +// Overridden from UrlFetchRequestBase. +GURL DownloadFileRequestBase::GetURL() const { + return download_url_; +} + +void DownloadFileRequestBase::GetOutputFilePath( + base::FilePath* local_file_path, + GetContentCallback* get_content_callback) { + // Configure so that the downloaded content is saved to |output_file_path_|. + *local_file_path = output_file_path_; + *get_content_callback = get_content_callback_; +} + +void DownloadFileRequestBase::OnURLFetchDownloadProgress( + const URLFetcher* source, + int64 current, + int64 total) { + if (!progress_callback_.is_null()) + progress_callback_.Run(current, total); +} + +void DownloadFileRequestBase::ProcessURLFetchResults(const URLFetcher* source) { + GDataErrorCode code = GetErrorCode(); + + // Take over the ownership of the the downloaded temp file. + base::FilePath temp_file; + if (code == HTTP_SUCCESS) { + response_writer()->DisownFile(); + temp_file = output_file_path_; + } + + download_action_callback_.Run(code, temp_file); + OnProcessURLFetchResultsComplete(); +} + +void DownloadFileRequestBase::RunCallbackOnPrematureFailure( + GDataErrorCode code) { + download_action_callback_.Run(code, base::FilePath()); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/base_requests.h b/chromium/google_apis/drive/base_requests.h new file mode 100644 index 00000000000..afe12a04fbe --- /dev/null +++ b/chromium/google_apis/drive/base_requests.h @@ -0,0 +1,539 @@ +// Copyright (c) 2012 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. +// +// This file provides base classes used to issue HTTP requests for Google +// APIs. + +#ifndef GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_ +#define GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_ + +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "google_apis/drive/gdata_errorcode.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_response_writer.h" +#include "url/gurl.h" + +namespace base { +class Value; +} // namespace base + +namespace google_apis { + +class RequestSender; + +// Callback used to pass parsed JSON from ParseJson(). If parsing error occurs, +// then the passed argument is null. +typedef base::Callback<void(scoped_ptr<base::Value> value)> ParseJsonCallback; + +// Callback used for DownloadFileRequest and ResumeUploadRequestBase. +typedef base::Callback<void(int64 progress, int64 total)> ProgressCallback; + +// Callback used to get the content from DownloadFileRequest. +typedef base::Callback<void( + GDataErrorCode error, + scoped_ptr<std::string> content)> GetContentCallback; + +// Parses JSON passed in |json| on |blocking_task_runner|. Runs |callback| on +// the calling thread when finished with either success or failure. +// The callback must not be null. +void ParseJson(base::TaskRunner* blocking_task_runner, + const std::string& json, + const ParseJsonCallback& callback); + +//======================= AuthenticatedRequestInterface ====================== + +// An interface class for implementing a request which requires OAuth2 +// authentication. +class AuthenticatedRequestInterface { + public: + // Called when re-authentication is required. See Start() for details. + typedef base::Callback<void(AuthenticatedRequestInterface* request)> + ReAuthenticateCallback; + + virtual ~AuthenticatedRequestInterface() {} + + // Starts the request with |access_token|. User-Agent header will be set + // to |custom_user_agent| if the value is not empty. + // + // |callback| is called when re-authentication is needed for a certain + // number of times (see kMaxReAuthenticateAttemptsPerRequest in .cc). + // The callback should retry by calling Start() again with a new access + // token, or just call OnAuthFailed() if a retry is not attempted. + // |callback| must not be null. + virtual void Start(const std::string& access_token, + const std::string& custom_user_agent, + const ReAuthenticateCallback& callback) = 0; + + // Invoked when the authentication failed with an error code |code|. + virtual void OnAuthFailed(GDataErrorCode code) = 0; + + // Gets a weak pointer to this request object. Since requests may be + // deleted when it is canceled by user action, for posting asynchronous tasks + // on the authentication request object, weak pointers have to be used. + // TODO(kinaba): crbug.com/134814 use more clean life time management than + // using weak pointers. + virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() = 0; + + // Cancels the request. It will invoke the callback object passed in + // each request's constructor with error code GDATA_CANCELLED. + virtual void Cancel() = 0; +}; + +//=========================== ResponseWriter ================================== + +// Saves the response for the request to a file or string. +class ResponseWriter : public net::URLFetcherResponseWriter { + public: + // If file_path is not empty, the response will be saved with file_writer_, + // otherwise it will be saved to data_. + ResponseWriter(base::SequencedTaskRunner* file_task_runner, + const base::FilePath& file_path, + const GetContentCallback& get_content_callback); + virtual ~ResponseWriter(); + + const std::string& data() const { return data_; } + + // Disowns the output file. + void DisownFile(); + + // URLFetcherResponseWriter overrides: + virtual int Initialize(const net::CompletionCallback& callback) OVERRIDE; + virtual int Write(net::IOBuffer* buffer, + int num_bytes, + const net::CompletionCallback& callback) OVERRIDE; + virtual int Finish(const net::CompletionCallback& callback) OVERRIDE; + + private: + void DidWrite(scoped_refptr<net::IOBuffer> buffer, + const net::CompletionCallback& callback, + int result); + + const GetContentCallback get_content_callback_; + std::string data_; + scoped_ptr<net::URLFetcherFileWriter> file_writer_; + base::WeakPtrFactory<ResponseWriter> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ResponseWriter); +}; + +//============================ UrlFetchRequestBase =========================== + +// Base class for requests that are fetching URLs. +class UrlFetchRequestBase : public AuthenticatedRequestInterface, + public net::URLFetcherDelegate { + public: + // AuthenticatedRequestInterface overrides. + virtual void Start(const std::string& access_token, + const std::string& custom_user_agent, + const ReAuthenticateCallback& callback) OVERRIDE; + virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() OVERRIDE; + virtual void Cancel() OVERRIDE; + + protected: + explicit UrlFetchRequestBase(RequestSender* sender); + virtual ~UrlFetchRequestBase(); + + // Gets URL for the request. + virtual GURL GetURL() const = 0; + + // Returns the request type. A derived class should override this method + // for a request type other than HTTP GET. + virtual net::URLFetcher::RequestType GetRequestType() const; + + // Returns the extra HTTP headers for the request. A derived class should + // override this method to specify any extra headers needed for the request. + virtual std::vector<std::string> GetExtraRequestHeaders() const; + + // Used by a derived class to add any content data to the request. + // Returns true if |upload_content_type| and |upload_content| are updated + // with the content type and data for the request. + // Note that this and GetContentFile() cannot be used together. + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content); + + // Used by a derived class to add content data which is the whole file or + // a part of the file at |local_file_path|. + // Returns true if all the arguments are updated for the content being + // uploaded. + // Note that this and GetContentData() cannot be used together. + virtual bool GetContentFile(base::FilePath* local_file_path, + int64* range_offset, + int64* range_length, + std::string* upload_content_type); + + // Used by a derived class to set an output file path if they want to save + // the downloaded content to a file at a specific path. + // Sets |get_content_callback|, which is called when some part of the response + // is read. + virtual void GetOutputFilePath(base::FilePath* local_file_path, + GetContentCallback* get_content_callback); + + // Invoked by OnURLFetchComplete when the request completes without an + // authentication error. Must be implemented by a derived class. + virtual void ProcessURLFetchResults(const net::URLFetcher* source) = 0; + + // Invoked by this base class upon an authentication error or cancel by + // a user request. Must be implemented by a derived class. + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) = 0; + + // Invoked from derived classes when ProcessURLFetchResults() is completed. + void OnProcessURLFetchResultsComplete(); + + // Returns an appropriate GDataErrorCode based on the HTTP response code and + // the status of the URLFetcher. + GDataErrorCode GetErrorCode(); + + // Returns true if called on the thread where the constructor was called. + bool CalledOnValidThread(); + + // Returns the writer which is used to save the response for the request. + ResponseWriter* response_writer() const { return response_writer_; } + + // Returns the task runner that should be used for blocking tasks. + base::SequencedTaskRunner* blocking_task_runner() const; + + private: + // URLFetcherDelegate overrides. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + // AuthenticatedRequestInterface overrides. + virtual void OnAuthFailed(GDataErrorCode code) OVERRIDE; + + ReAuthenticateCallback re_authenticate_callback_; + int re_authenticate_count_; + scoped_ptr<net::URLFetcher> url_fetcher_; + ResponseWriter* response_writer_; // Owned by |url_fetcher_|. + RequestSender* sender_; + GDataErrorCode error_code_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<UrlFetchRequestBase> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(UrlFetchRequestBase); +}; + +//============================ EntryActionRequest ============================ + +// Callback type for requests that return only error status, like: Delete/Move. +typedef base::Callback<void(GDataErrorCode error)> EntryActionCallback; + +// This class performs a simple action over a given entry (document/file). +// It is meant to be used for requests that return no JSON blobs. +class EntryActionRequest : public UrlFetchRequestBase { + public: + // |callback| is called when the request is finished either by success or by + // failure. It must not be null. + EntryActionRequest(RequestSender* sender, + const EntryActionCallback& callback); + virtual ~EntryActionRequest(); + + protected: + // Overridden from UrlFetchRequestBase. + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; + + private: + const EntryActionCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(EntryActionRequest); +}; + +//============================== GetDataRequest ============================== + +// Callback type for requests that returns JSON data. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<base::Value> json_data)> GetDataCallback; + +// This class performs the request for fetching and converting the fetched +// content into a base::Value. +class GetDataRequest : public UrlFetchRequestBase { + public: + // |callback| is called when the request finishes either by success or by + // failure. On success, a JSON Value object is passed. It must not be null. + GetDataRequest(RequestSender* sender, const GetDataCallback& callback); + virtual ~GetDataRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure( + GDataErrorCode fetch_error_code) OVERRIDE; + + private: + // Parses JSON response. + void ParseResponse(GDataErrorCode fetch_error_code, const std::string& data); + + // Called when ParseJsonOnBlockingPool() is completed. + void OnDataParsed(GDataErrorCode fetch_error_code, + scoped_ptr<base::Value> value); + + const GetDataCallback callback_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<GetDataRequest> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(GetDataRequest); +}; + + +//=========================== InitiateUploadRequestBase======================= + +// Callback type for DriveServiceInterface::InitiateUpload. +typedef base::Callback<void(GDataErrorCode error, + const GURL& upload_url)> InitiateUploadCallback; + +// This class provides base implementation for performing the request for +// initiating the upload of a file. +// |callback| will be called with the obtained upload URL. The URL will be +// used with requests for resuming the file uploading. +// +// Here's the flow of uploading: +// 1) Get the upload URL with a class inheriting InitiateUploadRequestBase. +// 2) Upload the first 1GB (see kUploadChunkSize in drive_uploader.cc) +// of the target file to the upload URL +// 3) If there is more data to upload, go to 2). +// +class InitiateUploadRequestBase : public UrlFetchRequestBase { + protected: + // |callback| will be called with the upload URL, where upload data is + // uploaded to with ResumeUploadRequestBase. It must not be null. + // |content_type| and |content_length| should be the attributes of the + // uploading file. + InitiateUploadRequestBase(RequestSender* sender, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length); + virtual ~InitiateUploadRequestBase(); + + // UrlFetchRequestBase overrides. + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const InitiateUploadCallback callback_; + const std::string content_type_; + const int64 content_length_; + + DISALLOW_COPY_AND_ASSIGN(InitiateUploadRequestBase); +}; + +//========================== UploadRangeRequestBase ========================== + +// Struct for response to ResumeUpload and GetUploadStatus. +struct UploadRangeResponse { + UploadRangeResponse(); + UploadRangeResponse(GDataErrorCode code, + int64 start_position_received, + int64 end_position_received); + ~UploadRangeResponse(); + + GDataErrorCode code; + // The values of "Range" header returned from the server. The values are + // used to continue uploading more data. These are set to -1 if an upload + // is complete. + // |start_position_received| is inclusive and |end_position_received| is + // exclusive to follow the common C++ manner, although the response from + // the server has "Range" header in inclusive format at both sides. + int64 start_position_received; + int64 end_position_received; +}; + +// Base class for a URL fetch request expecting the response containing the +// current uploading range. This class processes the response containing +// "Range" header and invoke OnRangeRequestComplete. +class UploadRangeRequestBase : public UrlFetchRequestBase { + protected: + // |upload_url| is the URL of where to upload the file to. + UploadRangeRequestBase(RequestSender* sender, const GURL& upload_url); + virtual ~UploadRangeRequestBase(); + + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; + + // This method will be called when the request is done, regardless of + // whether it is succeeded or failed. + // + // 1) If there is more data to upload, |code| of |response| is set to + // HTTP_RESUME_INCOMPLETE, and positions are set appropriately. Also, |value| + // will be set to NULL. + // 2) If the upload is complete, |code| is set to HTTP_CREATED for a new file + // or HTTP_SUCCESS for an existing file. Positions are set to -1, and |value| + // is set to a parsed JSON value representing the uploaded file. + // 3) If a premature failure is found, |code| is set to a value representing + // the situation. Positions are set to 0, and |value| is set to NULL. + // + // See also the comments for UploadRangeResponse. + // Note: Subclasses should have responsibility to run some callback + // in this method to notify the finish status to its clients (or ignore it + // under its responsibility). + virtual void OnRangeRequestComplete( + const UploadRangeResponse& response, scoped_ptr<base::Value> value) = 0; + + private: + // Called when ParseJson() is completed. + void OnDataParsed(GDataErrorCode code, scoped_ptr<base::Value> value); + + const GURL upload_url_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<UploadRangeRequestBase> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(UploadRangeRequestBase); +}; + +//========================== ResumeUploadRequestBase ========================= + +// This class performs the request for resuming the upload of a file. +// More specifically, this request uploads a chunk of data carried in |buf| +// of ResumeUploadResponseBase. This class is designed to share the +// implementation of upload resuming between GData WAPI and Drive API v2. +// The subclasses should implement OnRangeRequestComplete inherited by +// UploadRangeRequestBase, because the type of the response should be +// different (although the format in the server response is JSON). +class ResumeUploadRequestBase : public UploadRangeRequestBase { + protected: + // |start_position| is the start of range of contents currently stored in + // |buf|. |end_position| is the end of range of contents currently stared in + // |buf|. This is exclusive. For instance, if you are to upload the first + // 500 bytes of data, |start_position| is 0 and |end_position| is 500. + // |content_length| and |content_type| are the length and type of the + // file content to be uploaded respectively. + // |buf| holds current content to be uploaded. + // See also UploadRangeRequestBase's comment for remaining parameters + // meaning. + ResumeUploadRequestBase(RequestSender* sender, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path); + virtual ~ResumeUploadRequestBase(); + + // UrlFetchRequestBase overrides. + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual bool GetContentFile(base::FilePath* local_file_path, + int64* range_offset, + int64* range_length, + std::string* upload_content_type) OVERRIDE; + + private: + // The parameters for the request. See ResumeUploadParams for the details. + const int64 start_position_; + const int64 end_position_; + const int64 content_length_; + const std::string content_type_; + const base::FilePath local_file_path_; + + DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequestBase); +}; + +//======================== GetUploadStatusRequestBase ======================== + +// This class performs the request for getting the current upload status +// of a file. +// This request calls OnRangeRequestComplete() with: +// - HTTP_RESUME_INCOMPLETE and the range of previously uploaded data, +// if a file has been partially uploaded. |value| is not used. +// - HTTP_SUCCESS or HTTP_CREATED (up to the upload mode) and |value| +// for the uploaded data, if a file has been completely uploaded. +// See also UploadRangeRequestBase. +class GetUploadStatusRequestBase : public UploadRangeRequestBase { + public: + // |content_length| is the whole data size to be uploaded. + // See also UploadRangeRequestBase's constructor comment for other + // parameters. + GetUploadStatusRequestBase(RequestSender* sender, + const GURL& upload_url, + int64 content_length); + virtual ~GetUploadStatusRequestBase(); + + protected: + // UrlFetchRequestBase overrides. + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const int64 content_length_; + + DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequestBase); +}; + +//============================ DownloadFileRequest =========================== + +// Callback type for receiving the completion of DownloadFileRequest. +typedef base::Callback<void(GDataErrorCode error, + const base::FilePath& temp_file)> + DownloadActionCallback; + +// This is a base class for performing the request for downloading a file. +class DownloadFileRequestBase : public UrlFetchRequestBase { + public: + // download_action_callback: + // This callback is called when the download is complete. Must not be null. + // + // get_content_callback: + // This callback is called when some part of the content is + // read. Used to read the download content progressively. May be null. + // + // progress_callback: + // This callback is called for periodically reporting the number of bytes + // downloaded so far. May be null. + // + // download_url: + // Specifies the target file to download. + // + // output_file_path: + // Specifies the file path to save the downloaded file. + // + DownloadFileRequestBase( + RequestSender* sender, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback, + const GURL& download_url, + const base::FilePath& output_file_path); + virtual ~DownloadFileRequestBase(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual void GetOutputFilePath( + base::FilePath* local_file_path, + GetContentCallback* get_content_callback) OVERRIDE; + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; + + // net::URLFetcherDelegate overrides. + virtual void OnURLFetchDownloadProgress(const net::URLFetcher* source, + int64 current, int64 total) OVERRIDE; + + private: + const DownloadActionCallback download_action_callback_; + const GetContentCallback get_content_callback_; + const ProgressCallback progress_callback_; + const GURL download_url_; + const base::FilePath output_file_path_; + + DISALLOW_COPY_AND_ASSIGN(DownloadFileRequestBase); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_BASE_REQUESTS_H_ diff --git a/chromium/google_apis/drive/base_requests_server_unittest.cc b/chromium/google_apis/drive/base_requests_server_unittest.cc new file mode 100644 index 00000000000..bd54fcbdef8 --- /dev/null +++ b/chromium/google_apis/drive/base_requests_server_unittest.cc @@ -0,0 +1,131 @@ +// Copyright (c) 2013 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 "google_apis/drive/base_requests.h" + +#include "base/bind.h" +#include "base/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/task_util.h" +#include "google_apis/drive/test_util.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +namespace { + +const char kTestUserAgent[] = "test-user-agent"; + +} // namespace + +class BaseRequestsServerTest : public testing::Test { + protected: + BaseRequestsServerTest() { + } + + virtual void SetUp() OVERRIDE { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + + request_sender_.reset(new RequestSender( + new DummyAuthService, + request_context_getter_.get(), + message_loop_.message_loop_proxy(), + kTestUserAgent)); + + ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); + test_server_.RegisterRequestHandler( + base::Bind(&test_util::HandleDownloadFileRequest, + test_server_.base_url(), + base::Unretained(&http_request_))); + } + + // Returns a temporary file path suitable for storing the cache file. + base::FilePath GetTestCachedFilePath(const base::FilePath& file_name) { + return temp_dir_.path().Append(file_name); + } + + base::MessageLoopForIO message_loop_; // Test server needs IO thread. + net::test_server::EmbeddedTestServer test_server_; + scoped_ptr<RequestSender> request_sender_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + base::ScopedTempDir temp_dir_; + + // The incoming HTTP request is saved so tests can verify the request + // parameters like HTTP method (ex. some requests should use DELETE + // instead of GET). + net::test_server::HttpRequest http_request_; +}; + +TEST_F(BaseRequestsServerTest, DownloadFileRequest_ValidFile) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + base::FilePath temp_file; + { + base::RunLoop run_loop; + DownloadFileRequestBase* request = new DownloadFileRequestBase( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &temp_file)), + GetContentCallback(), + ProgressCallback(), + test_server_.GetURL("/files/gdata/testfile.txt"), + GetTestCachedFilePath( + base::FilePath::FromUTF8Unsafe("cached_testfile.txt"))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + std::string contents; + base::ReadFileToString(temp_file, &contents); + base::DeleteFile(temp_file, false); + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/files/gdata/testfile.txt", http_request_.relative_url); + + const base::FilePath expected_path = + test_util::GetTestFilePath("gdata/testfile.txt"); + std::string expected_contents; + base::ReadFileToString(expected_path, &expected_contents); + EXPECT_EQ(expected_contents, contents); +} + +TEST_F(BaseRequestsServerTest, DownloadFileRequest_NonExistentFile) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + base::FilePath temp_file; + { + base::RunLoop run_loop; + DownloadFileRequestBase* request = new DownloadFileRequestBase( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &temp_file)), + GetContentCallback(), + ProgressCallback(), + test_server_.GetURL("/files/gdata/no-such-file.txt"), + GetTestCachedFilePath( + base::FilePath::FromUTF8Unsafe("cache_no-such-file.txt"))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + EXPECT_EQ(HTTP_NOT_FOUND, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/files/gdata/no-such-file.txt", + http_request_.relative_url); + // Do not verify the not found message. +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/base_requests_unittest.cc b/chromium/google_apis/drive/base_requests_unittest.cc new file mode 100644 index 00000000000..3032d2abb37 --- /dev/null +++ b/chromium/google_apis/drive/base_requests_unittest.cc @@ -0,0 +1,204 @@ +// Copyright (c) 2012 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 "google_apis/drive/base_requests.h" + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/values.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/test_util.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +namespace { + +const char kValidJsonString[] = "{ \"test\": 123 }"; +const char kInvalidJsonString[] = "$$$"; + +class FakeUrlFetchRequest : public UrlFetchRequestBase { + public: + explicit FakeUrlFetchRequest(RequestSender* sender, + const EntryActionCallback& callback, + const GURL& url) + : UrlFetchRequestBase(sender), + callback_(callback), + url_(url) { + } + + virtual ~FakeUrlFetchRequest() { + } + + protected: + virtual GURL GetURL() const OVERRIDE { return url_; } + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE { + callback_.Run(GetErrorCode()); + } + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE { + callback_.Run(code); + } + + EntryActionCallback callback_; + GURL url_; +}; + +class FakeGetDataRequest : public GetDataRequest { + public: + explicit FakeGetDataRequest(RequestSender* sender, + const GetDataCallback& callback, + const GURL& url) + : GetDataRequest(sender, callback), + url_(url) { + } + + virtual ~FakeGetDataRequest() { + } + + protected: + virtual GURL GetURL() const OVERRIDE { return url_; } + + GURL url_; +}; + +} // namespace + +class BaseRequestsTest : public testing::Test { + public: + BaseRequestsTest() : response_code_(net::HTTP_OK) {} + + virtual void SetUp() OVERRIDE { + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + + sender_.reset(new RequestSender(new DummyAuthService, + request_context_getter_.get(), + message_loop_.message_loop_proxy(), + std::string() /* custom user agent */)); + + ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); + test_server_.RegisterRequestHandler( + base::Bind(&BaseRequestsTest::HandleRequest, base::Unretained(this))); + } + + scoped_ptr<net::test_server::HttpResponse> HandleRequest( + const net::test_server::HttpRequest& request) { + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(response_code_); + response->set_content(response_body_); + response->set_content_type("application/json"); + return response.PassAs<net::test_server::HttpResponse>(); + } + + base::MessageLoopForIO message_loop_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + scoped_ptr<RequestSender> sender_; + net::test_server::EmbeddedTestServer test_server_; + + net::HttpStatusCode response_code_; + std::string response_body_; +}; + +TEST_F(BaseRequestsTest, ParseValidJson) { + scoped_ptr<base::Value> json; + ParseJson(message_loop_.message_loop_proxy(), + kValidJsonString, + base::Bind(test_util::CreateCopyResultCallback(&json))); + base::RunLoop().RunUntilIdle(); + + DictionaryValue* root_dict = NULL; + ASSERT_TRUE(json); + ASSERT_TRUE(json->GetAsDictionary(&root_dict)); + + int int_value = 0; + ASSERT_TRUE(root_dict->GetInteger("test", &int_value)); + EXPECT_EQ(123, int_value); +} + +TEST_F(BaseRequestsTest, ParseInvalidJson) { + // Initialize with a valid pointer to verify that null is indeed assigned. + scoped_ptr<base::Value> json(base::Value::CreateNullValue()); + ParseJson(message_loop_.message_loop_proxy(), + kInvalidJsonString, + base::Bind(test_util::CreateCopyResultCallback(&json))); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(json); +} + +TEST_F(BaseRequestsTest, UrlFetchRequestBaseResponseCodeOverride) { + response_code_ = net::HTTP_FORBIDDEN; + response_body_ = + "{\"error\": {\n" + " \"errors\": [\n" + " {\n" + " \"domain\": \"usageLimits\",\n" + " \"reason\": \"rateLimitExceeded\",\n" + " \"message\": \"Rate Limit Exceeded\"\n" + " }\n" + " ],\n" + " \"code\": 403,\n" + " \"message\": \"Rate Limit Exceeded\"\n" + " }\n" + "}\n"; + + GDataErrorCode error = GDATA_OTHER_ERROR; + base::RunLoop run_loop; + sender_->StartRequestWithRetry( + new FakeUrlFetchRequest( + sender_.get(), + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error)), + test_server_.base_url())); + run_loop.Run(); + + // HTTP_FORBIDDEN (403) is overridden by the error reason. + EXPECT_EQ(HTTP_SERVICE_UNAVAILABLE, error); +} + +TEST_F(BaseRequestsTest, GetDataRequestParseValidResponse) { + response_body_ = kValidJsonString; + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<base::Value> value; + base::RunLoop run_loop; + sender_->StartRequestWithRetry( + new FakeGetDataRequest( + sender_.get(), + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error, &value)), + test_server_.base_url())); + run_loop.Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(value); +} + +TEST_F(BaseRequestsTest, GetDataRequestParseInvalidResponse) { + response_body_ = kInvalidJsonString; + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<base::Value> value; + base::RunLoop run_loop; + sender_->StartRequestWithRetry( + new FakeGetDataRequest( + sender_.get(), + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error, &value)), + test_server_.base_url())); + run_loop.Run(); + + EXPECT_EQ(GDATA_PARSE_ERROR, error); + EXPECT_FALSE(value); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_parser.cc b/chromium/google_apis/drive/drive_api_parser.cc new file mode 100644 index 00000000000..4b05e8b5844 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_parser.cc @@ -0,0 +1,729 @@ +// Copyright (c) 2012 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 "google_apis/drive/drive_api_parser.h" + +#include <algorithm> + +#include "base/basictypes.h" +#include "base/files/file_path.h" +#include "base/json/json_value_converter.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "google_apis/drive/time_util.h" + +using base::Value; +using base::DictionaryValue; +using base::ListValue; + +namespace google_apis { + +namespace { + +bool CreateFileResourceFromValue(const base::Value* value, + scoped_ptr<FileResource>* file) { + *file = FileResource::CreateFrom(*value); + return !!*file; +} + +// Converts |url_string| to |result|. Always returns true to be used +// for JSONValueConverter::RegisterCustomField method. +// TODO(mukai): make it return false in case of invalid |url_string|. +bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) { + *result = GURL(url_string.as_string()); + return true; +} + +// Converts |value| to |result|. The key of |value| is app_id, and its value +// is URL to open the resource on the web app. +bool GetOpenWithLinksFromDictionaryValue( + const base::Value* value, + std::vector<FileResource::OpenWithLink>* result) { + DCHECK(value); + DCHECK(result); + + const base::DictionaryValue* dictionary_value; + if (!value->GetAsDictionary(&dictionary_value)) + return false; + + result->reserve(dictionary_value->size()); + for (DictionaryValue::Iterator iter(*dictionary_value); + !iter.IsAtEnd(); iter.Advance()) { + std::string string_value; + if (!iter.value().GetAsString(&string_value)) + return false; + + FileResource::OpenWithLink open_with_link; + open_with_link.app_id = iter.key(); + open_with_link.open_url = GURL(string_value); + result->push_back(open_with_link); + } + + return true; +} + +// Drive v2 API JSON names. + +// Definition order follows the order of documentation in +// https://developers.google.com/drive/v2/reference/ + +// Common +const char kKind[] = "kind"; +const char kId[] = "id"; +const char kETag[] = "etag"; +const char kSelfLink[] = "selfLink"; +const char kItems[] = "items"; +const char kLargestChangeId[] = "largestChangeId"; + +// About Resource +// https://developers.google.com/drive/v2/reference/about +const char kAboutKind[] = "drive#about"; +const char kQuotaBytesTotal[] = "quotaBytesTotal"; +const char kQuotaBytesUsed[] = "quotaBytesUsed"; +const char kRootFolderId[] = "rootFolderId"; + +// App Icon +// https://developers.google.com/drive/v2/reference/apps +const char kCategory[] = "category"; +const char kSize[] = "size"; +const char kIconUrl[] = "iconUrl"; + +// Apps Resource +// https://developers.google.com/drive/v2/reference/apps +const char kAppKind[] = "drive#app"; +const char kName[] = "name"; +const char kObjectType[] = "objectType"; +const char kSupportsCreate[] = "supportsCreate"; +const char kSupportsImport[] = "supportsImport"; +const char kInstalled[] = "installed"; +const char kAuthorized[] = "authorized"; +const char kProductUrl[] = "productUrl"; +const char kPrimaryMimeTypes[] = "primaryMimeTypes"; +const char kSecondaryMimeTypes[] = "secondaryMimeTypes"; +const char kPrimaryFileExtensions[] = "primaryFileExtensions"; +const char kSecondaryFileExtensions[] = "secondaryFileExtensions"; +const char kIcons[] = "icons"; + +// Apps List +// https://developers.google.com/drive/v2/reference/apps/list +const char kAppListKind[] = "drive#appList"; + +// Parent Resource +// https://developers.google.com/drive/v2/reference/parents +const char kParentReferenceKind[] = "drive#parentReference"; +const char kParentLink[] = "parentLink"; +const char kIsRoot[] = "isRoot"; + +// File Resource +// https://developers.google.com/drive/v2/reference/files +const char kFileKind[] = "drive#file"; +const char kTitle[] = "title"; +const char kMimeType[] = "mimeType"; +const char kCreatedDate[] = "createdDate"; +const char kModifiedDate[] = "modifiedDate"; +const char kModifiedByMeDate[] = "modifiedByMeDate"; +const char kLastViewedByMeDate[] = "lastViewedByMeDate"; +const char kSharedWithMeDate[] = "sharedWithMeDate"; +const char kDownloadUrl[] = "downloadUrl"; +const char kFileExtension[] = "fileExtension"; +const char kMd5Checksum[] = "md5Checksum"; +const char kFileSize[] = "fileSize"; +const char kAlternateLink[] = "alternateLink"; +const char kEmbedLink[] = "embedLink"; +const char kParents[] = "parents"; +const char kThumbnailLink[] = "thumbnailLink"; +const char kWebContentLink[] = "webContentLink"; +const char kOpenWithLinks[] = "openWithLinks"; +const char kLabels[] = "labels"; +const char kImageMediaMetadata[] = "imageMediaMetadata"; +const char kShared[] = "shared"; +// These 5 flags are defined under |labels|. +const char kLabelStarred[] = "starred"; +const char kLabelHidden[] = "hidden"; +const char kLabelTrashed[] = "trashed"; +const char kLabelRestricted[] = "restricted"; +const char kLabelViewed[] = "viewed"; +// These 3 flags are defined under |imageMediaMetadata|. +const char kImageMediaMetadataWidth[] = "width"; +const char kImageMediaMetadataHeight[] = "height"; +const char kImageMediaMetadataRotation[] = "rotation"; + +const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder"; + +// Files List +// https://developers.google.com/drive/v2/reference/files/list +const char kFileListKind[] = "drive#fileList"; +const char kNextPageToken[] = "nextPageToken"; +const char kNextLink[] = "nextLink"; + +// Change Resource +// https://developers.google.com/drive/v2/reference/changes +const char kChangeKind[] = "drive#change"; +const char kFileId[] = "fileId"; +const char kDeleted[] = "deleted"; +const char kFile[] = "file"; + +// Changes List +// https://developers.google.com/drive/v2/reference/changes/list +const char kChangeListKind[] = "drive#changeList"; + +// Maps category name to enum IconCategory. +struct AppIconCategoryMap { + DriveAppIcon::IconCategory category; + const char* category_name; +}; + +const AppIconCategoryMap kAppIconCategoryMap[] = { + { DriveAppIcon::DOCUMENT, "document" }, + { DriveAppIcon::APPLICATION, "application" }, + { DriveAppIcon::SHARED_DOCUMENT, "documentShared" }, +}; + +// Checks if the JSON is expected kind. In Drive API, JSON data structure has +// |kind| property which denotes the type of the structure (e.g. "drive#file"). +bool IsResourceKindExpected(const base::Value& value, + const std::string& expected_kind) { + const base::DictionaryValue* as_dict = NULL; + std::string kind; + return value.GetAsDictionary(&as_dict) && + as_dict->HasKey(kKind) && + as_dict->GetString(kKind, &kind) && + kind == expected_kind; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// AboutResource implementation + +AboutResource::AboutResource() + : largest_change_id_(0), + quota_bytes_total_(0), + quota_bytes_used_(0) {} + +AboutResource::~AboutResource() {} + +// static +scoped_ptr<AboutResource> AboutResource::CreateFrom(const base::Value& value) { + scoped_ptr<AboutResource> resource(new AboutResource()); + if (!IsResourceKindExpected(value, kAboutKind) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid About resource JSON!"; + return scoped_ptr<AboutResource>(); + } + return resource.Pass(); +} + +// static +void AboutResource::RegisterJSONConverter( + base::JSONValueConverter<AboutResource>* converter) { + converter->RegisterCustomField<int64>(kLargestChangeId, + &AboutResource::largest_change_id_, + &base::StringToInt64); + converter->RegisterCustomField<int64>(kQuotaBytesTotal, + &AboutResource::quota_bytes_total_, + &base::StringToInt64); + converter->RegisterCustomField<int64>(kQuotaBytesUsed, + &AboutResource::quota_bytes_used_, + &base::StringToInt64); + converter->RegisterStringField(kRootFolderId, + &AboutResource::root_folder_id_); +} + +bool AboutResource::Parse(const base::Value& value) { + base::JSONValueConverter<AboutResource> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid About resource JSON!"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// DriveAppIcon implementation + +DriveAppIcon::DriveAppIcon() : category_(UNKNOWN), icon_side_length_(0) {} + +DriveAppIcon::~DriveAppIcon() {} + +// static +void DriveAppIcon::RegisterJSONConverter( + base::JSONValueConverter<DriveAppIcon>* converter) { + converter->RegisterCustomField<IconCategory>( + kCategory, + &DriveAppIcon::category_, + &DriveAppIcon::GetIconCategory); + converter->RegisterIntField(kSize, &DriveAppIcon::icon_side_length_); + converter->RegisterCustomField<GURL>(kIconUrl, + &DriveAppIcon::icon_url_, + GetGURLFromString); +} + +// static +scoped_ptr<DriveAppIcon> DriveAppIcon::CreateFrom(const base::Value& value) { + scoped_ptr<DriveAppIcon> resource(new DriveAppIcon()); + if (!resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid DriveAppIcon JSON!"; + return scoped_ptr<DriveAppIcon>(); + } + return resource.Pass(); +} + +bool DriveAppIcon::Parse(const base::Value& value) { + base::JSONValueConverter<DriveAppIcon> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid DriveAppIcon"; + return false; + } + return true; +} + +// static +bool DriveAppIcon::GetIconCategory(const base::StringPiece& category, + DriveAppIcon::IconCategory* result) { + for (size_t i = 0; i < arraysize(kAppIconCategoryMap); i++) { + if (category == kAppIconCategoryMap[i].category_name) { + *result = kAppIconCategoryMap[i].category; + return true; + } + } + DVLOG(1) << "Unknown icon category " << category; + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// AppResource implementation + +AppResource::AppResource() + : supports_create_(false), + supports_import_(false), + installed_(false), + authorized_(false) { +} + +AppResource::~AppResource() {} + +// static +void AppResource::RegisterJSONConverter( + base::JSONValueConverter<AppResource>* converter) { + converter->RegisterStringField(kId, &AppResource::application_id_); + converter->RegisterStringField(kName, &AppResource::name_); + converter->RegisterStringField(kObjectType, &AppResource::object_type_); + converter->RegisterBoolField(kSupportsCreate, &AppResource::supports_create_); + converter->RegisterBoolField(kSupportsImport, &AppResource::supports_import_); + converter->RegisterBoolField(kInstalled, &AppResource::installed_); + converter->RegisterBoolField(kAuthorized, &AppResource::authorized_); + converter->RegisterCustomField<GURL>(kProductUrl, + &AppResource::product_url_, + GetGURLFromString); + converter->RegisterRepeatedString(kPrimaryMimeTypes, + &AppResource::primary_mimetypes_); + converter->RegisterRepeatedString(kSecondaryMimeTypes, + &AppResource::secondary_mimetypes_); + converter->RegisterRepeatedString(kPrimaryFileExtensions, + &AppResource::primary_file_extensions_); + converter->RegisterRepeatedString(kSecondaryFileExtensions, + &AppResource::secondary_file_extensions_); + converter->RegisterRepeatedMessage(kIcons, &AppResource::icons_); +} + +// static +scoped_ptr<AppResource> AppResource::CreateFrom(const base::Value& value) { + scoped_ptr<AppResource> resource(new AppResource()); + if (!IsResourceKindExpected(value, kAppKind) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid AppResource JSON!"; + return scoped_ptr<AppResource>(); + } + return resource.Pass(); +} + +bool AppResource::Parse(const base::Value& value) { + base::JSONValueConverter<AppResource> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid AppResource"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// AppList implementation + +AppList::AppList() {} + +AppList::~AppList() {} + +// static +void AppList::RegisterJSONConverter( + base::JSONValueConverter<AppList>* converter) { + converter->RegisterStringField(kETag, &AppList::etag_); + converter->RegisterRepeatedMessage<AppResource>(kItems, + &AppList::items_); +} + +// static +scoped_ptr<AppList> AppList::CreateFrom(const base::Value& value) { + scoped_ptr<AppList> resource(new AppList()); + if (!IsResourceKindExpected(value, kAppListKind) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid AppList JSON!"; + return scoped_ptr<AppList>(); + } + return resource.Pass(); +} + +bool AppList::Parse(const base::Value& value) { + base::JSONValueConverter<AppList> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid AppList"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// ParentReference implementation + +ParentReference::ParentReference() : is_root_(false) {} + +ParentReference::~ParentReference() {} + +// static +void ParentReference::RegisterJSONConverter( + base::JSONValueConverter<ParentReference>* converter) { + converter->RegisterStringField(kId, &ParentReference::file_id_); + converter->RegisterCustomField<GURL>(kParentLink, + &ParentReference::parent_link_, + GetGURLFromString); + converter->RegisterBoolField(kIsRoot, &ParentReference::is_root_); +} + +// static +scoped_ptr<ParentReference> +ParentReference::CreateFrom(const base::Value& value) { + scoped_ptr<ParentReference> reference(new ParentReference()); + if (!IsResourceKindExpected(value, kParentReferenceKind) || + !reference->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid ParentRefernce JSON!"; + return scoped_ptr<ParentReference>(); + } + return reference.Pass(); +} + +bool ParentReference::Parse(const base::Value& value) { + base::JSONValueConverter<ParentReference> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid ParentReference"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// FileResource implementation + +FileResource::FileResource() : shared_(false), file_size_(0) {} + +FileResource::~FileResource() {} + +// static +void FileResource::RegisterJSONConverter( + base::JSONValueConverter<FileResource>* converter) { + converter->RegisterStringField(kId, &FileResource::file_id_); + converter->RegisterStringField(kETag, &FileResource::etag_); + converter->RegisterCustomField<GURL>(kSelfLink, + &FileResource::self_link_, + GetGURLFromString); + converter->RegisterStringField(kTitle, &FileResource::title_); + converter->RegisterStringField(kMimeType, &FileResource::mime_type_); + converter->RegisterNestedField(kLabels, &FileResource::labels_); + converter->RegisterNestedField(kImageMediaMetadata, + &FileResource::image_media_metadata_); + converter->RegisterCustomField<base::Time>( + kCreatedDate, + &FileResource::created_date_, + &util::GetTimeFromString); + converter->RegisterCustomField<base::Time>( + kModifiedDate, + &FileResource::modified_date_, + &util::GetTimeFromString); + converter->RegisterCustomField<base::Time>( + kModifiedByMeDate, + &FileResource::modified_by_me_date_, + &util::GetTimeFromString); + converter->RegisterCustomField<base::Time>( + kLastViewedByMeDate, + &FileResource::last_viewed_by_me_date_, + &util::GetTimeFromString); + converter->RegisterCustomField<base::Time>( + kSharedWithMeDate, + &FileResource::shared_with_me_date_, + &util::GetTimeFromString); + converter->RegisterBoolField(kShared, &FileResource::shared_); + converter->RegisterCustomField<GURL>(kDownloadUrl, + &FileResource::download_url_, + GetGURLFromString); + converter->RegisterStringField(kFileExtension, + &FileResource::file_extension_); + converter->RegisterStringField(kMd5Checksum, &FileResource::md5_checksum_); + converter->RegisterCustomField<int64>(kFileSize, + &FileResource::file_size_, + &base::StringToInt64); + converter->RegisterCustomField<GURL>(kAlternateLink, + &FileResource::alternate_link_, + GetGURLFromString); + converter->RegisterCustomField<GURL>(kEmbedLink, + &FileResource::embed_link_, + GetGURLFromString); + converter->RegisterRepeatedMessage<ParentReference>(kParents, + &FileResource::parents_); + converter->RegisterCustomField<GURL>(kThumbnailLink, + &FileResource::thumbnail_link_, + GetGURLFromString); + converter->RegisterCustomField<GURL>(kWebContentLink, + &FileResource::web_content_link_, + GetGURLFromString); + converter->RegisterCustomValueField<std::vector<OpenWithLink> >( + kOpenWithLinks, + &FileResource::open_with_links_, + GetOpenWithLinksFromDictionaryValue); +} + +// static +scoped_ptr<FileResource> FileResource::CreateFrom(const base::Value& value) { + scoped_ptr<FileResource> resource(new FileResource()); + if (!IsResourceKindExpected(value, kFileKind) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid FileResource JSON!"; + return scoped_ptr<FileResource>(); + } + return resource.Pass(); +} + +bool FileResource::IsDirectory() const { + return mime_type_ == kDriveFolderMimeType; +} + +bool FileResource::Parse(const base::Value& value) { + base::JSONValueConverter<FileResource> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid FileResource"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// FileList implementation + +FileList::FileList() {} + +FileList::~FileList() {} + +// static +void FileList::RegisterJSONConverter( + base::JSONValueConverter<FileList>* converter) { + converter->RegisterStringField(kETag, &FileList::etag_); + converter->RegisterStringField(kNextPageToken, &FileList::next_page_token_); + converter->RegisterCustomField<GURL>(kNextLink, + &FileList::next_link_, + GetGURLFromString); + converter->RegisterRepeatedMessage<FileResource>(kItems, + &FileList::items_); +} + +// static +bool FileList::HasFileListKind(const base::Value& value) { + return IsResourceKindExpected(value, kFileListKind); +} + +// static +scoped_ptr<FileList> FileList::CreateFrom(const base::Value& value) { + scoped_ptr<FileList> resource(new FileList()); + if (!HasFileListKind(value) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid FileList JSON!"; + return scoped_ptr<FileList>(); + } + return resource.Pass(); +} + +bool FileList::Parse(const base::Value& value) { + base::JSONValueConverter<FileList> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid FileList"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// ChangeResource implementation + +ChangeResource::ChangeResource() : change_id_(0), deleted_(false) {} + +ChangeResource::~ChangeResource() {} + +// static +void ChangeResource::RegisterJSONConverter( + base::JSONValueConverter<ChangeResource>* converter) { + converter->RegisterCustomField<int64>(kId, + &ChangeResource::change_id_, + &base::StringToInt64); + converter->RegisterStringField(kFileId, &ChangeResource::file_id_); + converter->RegisterBoolField(kDeleted, &ChangeResource::deleted_); + converter->RegisterCustomValueField(kFile, &ChangeResource::file_, + &CreateFileResourceFromValue); +} + +// static +scoped_ptr<ChangeResource> +ChangeResource::CreateFrom(const base::Value& value) { + scoped_ptr<ChangeResource> resource(new ChangeResource()); + if (!IsResourceKindExpected(value, kChangeKind) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid ChangeResource JSON!"; + return scoped_ptr<ChangeResource>(); + } + return resource.Pass(); +} + +bool ChangeResource::Parse(const base::Value& value) { + base::JSONValueConverter<ChangeResource> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid ChangeResource"; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// ChangeList implementation + +ChangeList::ChangeList() : largest_change_id_(0) {} + +ChangeList::~ChangeList() {} + +// static +void ChangeList::RegisterJSONConverter( + base::JSONValueConverter<ChangeList>* converter) { + converter->RegisterStringField(kETag, &ChangeList::etag_); + converter->RegisterStringField(kNextPageToken, &ChangeList::next_page_token_); + converter->RegisterCustomField<GURL>(kNextLink, + &ChangeList::next_link_, + GetGURLFromString); + converter->RegisterCustomField<int64>(kLargestChangeId, + &ChangeList::largest_change_id_, + &base::StringToInt64); + converter->RegisterRepeatedMessage<ChangeResource>(kItems, + &ChangeList::items_); +} + +// static +bool ChangeList::HasChangeListKind(const base::Value& value) { + return IsResourceKindExpected(value, kChangeListKind); +} + +// static +scoped_ptr<ChangeList> ChangeList::CreateFrom(const base::Value& value) { + scoped_ptr<ChangeList> resource(new ChangeList()); + if (!HasChangeListKind(value) || !resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid ChangeList JSON!"; + return scoped_ptr<ChangeList>(); + } + return resource.Pass(); +} + +bool ChangeList::Parse(const base::Value& value) { + base::JSONValueConverter<ChangeList> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid ChangeList"; + return false; + } + return true; +} + + +//////////////////////////////////////////////////////////////////////////////// +// FileLabels implementation + +FileLabels::FileLabels() + : starred_(false), + hidden_(false), + trashed_(false), + restricted_(false), + viewed_(false) {} + +FileLabels::~FileLabels() {} + +// static +void FileLabels::RegisterJSONConverter( + base::JSONValueConverter<FileLabels>* converter) { + converter->RegisterBoolField(kLabelStarred, &FileLabels::starred_); + converter->RegisterBoolField(kLabelHidden, &FileLabels::hidden_); + converter->RegisterBoolField(kLabelTrashed, &FileLabels::trashed_); + converter->RegisterBoolField(kLabelRestricted, &FileLabels::restricted_); + converter->RegisterBoolField(kLabelViewed, &FileLabels::viewed_); +} + +// static +scoped_ptr<FileLabels> FileLabels::CreateFrom(const base::Value& value) { + scoped_ptr<FileLabels> resource(new FileLabels()); + if (!resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid FileLabels JSON!"; + return scoped_ptr<FileLabels>(); + } + return resource.Pass(); +} + +bool FileLabels::Parse(const base::Value& value) { + base::JSONValueConverter<FileLabels> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid FileLabels."; + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// ImageMediaMetadata implementation + +ImageMediaMetadata::ImageMediaMetadata() + : width_(-1), + height_(-1), + rotation_(-1) {} + +ImageMediaMetadata::~ImageMediaMetadata() {} + +// static +void ImageMediaMetadata::RegisterJSONConverter( + base::JSONValueConverter<ImageMediaMetadata>* converter) { + converter->RegisterIntField(kImageMediaMetadataWidth, + &ImageMediaMetadata::width_); + converter->RegisterIntField(kImageMediaMetadataHeight, + &ImageMediaMetadata::height_); + converter->RegisterIntField(kImageMediaMetadataRotation, + &ImageMediaMetadata::rotation_); +} + +// static +scoped_ptr<ImageMediaMetadata> ImageMediaMetadata::CreateFrom( + const base::Value& value) { + scoped_ptr<ImageMediaMetadata> resource(new ImageMediaMetadata()); + if (!resource->Parse(value)) { + LOG(ERROR) << "Unable to create: Invalid ImageMediaMetadata JSON!"; + return scoped_ptr<ImageMediaMetadata>(); + } + return resource.Pass(); +} + +bool ImageMediaMetadata::Parse(const base::Value& value) { + return true; + base::JSONValueConverter<ImageMediaMetadata> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid ImageMediaMetadata."; + return false; + } + return true; +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_parser.h b/chromium/google_apis/drive/drive_api_parser.h new file mode 100644 index 00000000000..ef02035d595 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_parser.h @@ -0,0 +1,857 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_ +#define GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string_piece.h" +#include "base/time/time.h" +#include "url/gurl.h" + +namespace base { +class Value; +template <class StructType> +class JSONValueConverter; + +namespace internal { +template <class NestedType> +class RepeatedMessageConverter; +} // namespace internal +} // namespace base + +namespace google_apis { + +// About resource represents the account information about the current user. +// https://developers.google.com/drive/v2/reference/about +class AboutResource { + public: + AboutResource(); + ~AboutResource(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<AboutResource>* converter); + + // Creates about resource from parsed JSON. + static scoped_ptr<AboutResource> CreateFrom(const base::Value& value); + + // Returns the largest change ID number. + int64 largest_change_id() const { return largest_change_id_; } + // Returns total number of quota bytes. + int64 quota_bytes_total() const { return quota_bytes_total_; } + // Returns the number of quota bytes used. + int64 quota_bytes_used() const { return quota_bytes_used_; } + // Returns root folder ID. + const std::string& root_folder_id() const { return root_folder_id_; } + + void set_largest_change_id(int64 largest_change_id) { + largest_change_id_ = largest_change_id; + } + void set_quota_bytes_total(int64 quota_bytes_total) { + quota_bytes_total_ = quota_bytes_total; + } + void set_quota_bytes_used(int64 quota_bytes_used) { + quota_bytes_used_ = quota_bytes_used; + } + void set_root_folder_id(const std::string& root_folder_id) { + root_folder_id_ = root_folder_id; + } + + private: + friend class DriveAPIParserTest; + FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, AboutResourceParser); + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + int64 largest_change_id_; + int64 quota_bytes_total_; + int64 quota_bytes_used_; + std::string root_folder_id_; + + // This class is copyable on purpose. +}; + +// DriveAppIcon represents an icon for Drive Application. +// https://developers.google.com/drive/v2/reference/apps +class DriveAppIcon { + public: + enum IconCategory { + UNKNOWN, // Uninitialized state. + DOCUMENT, // Icon for a file associated with the app. + APPLICATION, // Icon for the application. + SHARED_DOCUMENT, // Icon for a shared file associated with the app. + }; + + DriveAppIcon(); + ~DriveAppIcon(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<DriveAppIcon>* converter); + + // Creates drive app icon instance from parsed JSON. + static scoped_ptr<DriveAppIcon> CreateFrom(const base::Value& value); + + // Category of the icon. + IconCategory category() const { return category_; } + + // Size in pixels of one side of the icon (icons are always square). + int icon_side_length() const { return icon_side_length_; } + + // Returns URL for this icon. + const GURL& icon_url() const { return icon_url_; } + + void set_category(IconCategory category) { + category_ = category; + } + void set_icon_side_length(int icon_side_length) { + icon_side_length_ = icon_side_length; + } + void set_icon_url(const GURL& icon_url) { + icon_url_ = icon_url; + } + + private: + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + // Extracts the icon category from the given string. Returns false and does + // not change |result| when |scheme| has an unrecognizable value. + static bool GetIconCategory(const base::StringPiece& category, + IconCategory* result); + + friend class base::internal::RepeatedMessageConverter<DriveAppIcon>; + friend class AppResource; + + IconCategory category_; + int icon_side_length_; + GURL icon_url_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppIcon); +}; + +// AppResource represents a Drive Application. +// https://developers.google.com/drive/v2/reference/apps +class AppResource { + public: + ~AppResource(); + AppResource(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<AppResource>* converter); + + // Creates app resource from parsed JSON. + static scoped_ptr<AppResource> CreateFrom(const base::Value& value); + + // Returns application ID, which is 12-digit decimals (e.g. "123456780123"). + const std::string& application_id() const { return application_id_; } + + // Returns application name. + const std::string& name() const { return name_; } + + // Returns the name of the type of object this application creates. + // This is used for displaying in "Create" menu item for this app. + // If empty, application name is used instead. + const std::string& object_type() const { return object_type_; } + + // Returns whether this application supports creating new objects. + bool supports_create() const { return supports_create_; } + + // Returns whether this application supports importing Google Docs. + bool supports_import() const { return supports_import_; } + + // Returns whether this application is installed. + bool is_installed() const { return installed_; } + + // Returns whether this application is authorized to access data on the + // user's Drive. + bool is_authorized() const { return authorized_; } + + // Returns the product URL, e.g. at Chrome Web Store. + const GURL& product_url() const { return product_url_; } + + // List of primary mime types supported by this WebApp. Primary status should + // trigger this WebApp becoming the default handler of file instances that + // have these mime types. + const ScopedVector<std::string>& primary_mimetypes() const { + return primary_mimetypes_; + } + + // List of secondary mime types supported by this WebApp. Secondary status + // should make this WebApp show up in "Open with..." pop-up menu of the + // default action menu for file with matching mime types. + const ScopedVector<std::string>& secondary_mimetypes() const { + return secondary_mimetypes_; + } + + // List of primary file extensions supported by this WebApp. Primary status + // should trigger this WebApp becoming the default handler of file instances + // that match these extensions. + const ScopedVector<std::string>& primary_file_extensions() const { + return primary_file_extensions_; + } + + // List of secondary file extensions supported by this WebApp. Secondary + // status should make this WebApp show up in "Open with..." pop-up menu of the + // default action menu for file with matching extensions. + const ScopedVector<std::string>& secondary_file_extensions() const { + return secondary_file_extensions_; + } + + // Returns Icons for this application. An application can have multiple + // icons for different purpose (application, document, shared document) + // in several sizes. + const ScopedVector<DriveAppIcon>& icons() const { + return icons_; + } + + void set_application_id(const std::string& application_id) { + application_id_ = application_id; + } + void set_name(const std::string& name) { name_ = name; } + void set_object_type(const std::string& object_type) { + object_type_ = object_type; + } + void set_supports_create(bool supports_create) { + supports_create_ = supports_create; + } + void set_supports_import(bool supports_import) { + supports_import_ = supports_import; + } + void set_installed(bool installed) { installed_ = installed; } + void set_authorized(bool authorized) { authorized_ = authorized; } + void set_product_url(const GURL& product_url) { + product_url_ = product_url; + } + void set_primary_mimetypes( + ScopedVector<std::string> primary_mimetypes) { + primary_mimetypes_ = primary_mimetypes.Pass(); + } + void set_secondary_mimetypes( + ScopedVector<std::string> secondary_mimetypes) { + secondary_mimetypes_ = secondary_mimetypes.Pass(); + } + void set_primary_file_extensions( + ScopedVector<std::string> primary_file_extensions) { + primary_file_extensions_ = primary_file_extensions.Pass(); + } + void set_secondary_file_extensions( + ScopedVector<std::string> secondary_file_extensions) { + secondary_file_extensions_ = secondary_file_extensions.Pass(); + } + void set_icons(ScopedVector<DriveAppIcon> icons) { + icons_ = icons.Pass(); + } + + private: + friend class base::internal::RepeatedMessageConverter<AppResource>; + friend class AppList; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string application_id_; + std::string name_; + std::string object_type_; + bool supports_create_; + bool supports_import_; + bool installed_; + bool authorized_; + GURL product_url_; + ScopedVector<std::string> primary_mimetypes_; + ScopedVector<std::string> secondary_mimetypes_; + ScopedVector<std::string> primary_file_extensions_; + ScopedVector<std::string> secondary_file_extensions_; + ScopedVector<DriveAppIcon> icons_; + + DISALLOW_COPY_AND_ASSIGN(AppResource); +}; + +// AppList represents a list of Drive Applications. +// https://developers.google.com/drive/v2/reference/apps/list +class AppList { + public: + AppList(); + ~AppList(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<AppList>* converter); + + // Creates app list from parsed JSON. + static scoped_ptr<AppList> CreateFrom(const base::Value& value); + + // ETag for this resource. + const std::string& etag() const { return etag_; } + + // Returns a vector of applications. + const ScopedVector<AppResource>& items() const { return items_; } + + void set_etag(const std::string& etag) { + etag_ = etag; + } + void set_items(ScopedVector<AppResource> items) { + items_ = items.Pass(); + } + + private: + friend class DriveAPIParserTest; + FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, AppListParser); + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string etag_; + ScopedVector<AppResource> items_; + + DISALLOW_COPY_AND_ASSIGN(AppList); +}; + +// ParentReference represents a directory. +// https://developers.google.com/drive/v2/reference/parents +class ParentReference { + public: + ParentReference(); + ~ParentReference(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<ParentReference>* converter); + + // Creates parent reference from parsed JSON. + static scoped_ptr<ParentReference> CreateFrom(const base::Value& value); + + // Returns the file id of the reference. + const std::string& file_id() const { return file_id_; } + + // Returns the URL for the parent in Drive. + const GURL& parent_link() const { return parent_link_; } + + // Returns true if the reference is root directory. + bool is_root() const { return is_root_; } + + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + void set_parent_link(const GURL& parent_link) { + parent_link_ = parent_link; + } + void set_is_root(bool is_root) { is_root_ = is_root; } + + private: + friend class base::internal::RepeatedMessageConverter<ParentReference>; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string file_id_; + GURL parent_link_; + bool is_root_; + + DISALLOW_COPY_AND_ASSIGN(ParentReference); +}; + +// FileLabels represents labels for file or folder. +// https://developers.google.com/drive/v2/reference/files +class FileLabels { + public: + FileLabels(); + ~FileLabels(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<FileLabels>* converter); + + // Creates about resource from parsed JSON. + static scoped_ptr<FileLabels> CreateFrom(const base::Value& value); + + // Whether this file is starred by the user. + bool is_starred() const { return starred_; } + // Whether this file is hidden from the user. + bool is_hidden() const { return hidden_; } + // Whether this file has been trashed. + bool is_trashed() const { return trashed_; } + // Whether viewers are prevented from downloading this file. + bool is_restricted() const { return restricted_; } + // Whether this file has been viewed by this user. + bool is_viewed() const { return viewed_; } + + void set_starred(bool starred) { starred_ = starred; } + void set_hidden(bool hidden) { hidden_ = hidden; } + void set_trashed(bool trashed) { trashed_ = trashed; } + void set_restricted(bool restricted) { restricted_ = restricted; } + void set_viewed(bool viewed) { viewed_ = viewed; } + + private: + friend class FileResource; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + bool starred_; + bool hidden_; + bool trashed_; + bool restricted_; + bool viewed_; + + DISALLOW_COPY_AND_ASSIGN(FileLabels); +}; + +// ImageMediaMetadata represents image metadata for a file. +// https://developers.google.com/drive/v2/reference/files +class ImageMediaMetadata { + public: + ImageMediaMetadata(); + ~ImageMediaMetadata(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<ImageMediaMetadata>* converter); + + // Creates about resource from parsed JSON. + static scoped_ptr<ImageMediaMetadata> CreateFrom(const base::Value& value); + + // Width of the image in pixels. + int width() const { return width_; } + // Height of the image in pixels. + int height() const { return height_; } + // Rotation of the image in clockwise degrees. + int rotation() const { return rotation_; } + + void set_width(int width) { width_ = width; } + void set_height(int height) { height_ = height; } + void set_rotation(int rotation) { rotation_ = rotation; } + + private: + friend class FileResource; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + int width_; + int height_; + int rotation_; + + DISALLOW_COPY_AND_ASSIGN(ImageMediaMetadata); +}; + + +// FileResource represents a file or folder metadata in Drive. +// https://developers.google.com/drive/v2/reference/files +class FileResource { + public: + // Link to open a file resource on a web app with |app_id|. + struct OpenWithLink { + std::string app_id; + GURL open_url; + }; + + FileResource(); + ~FileResource(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<FileResource>* converter); + + // Creates file resource from parsed JSON. + static scoped_ptr<FileResource> CreateFrom(const base::Value& value); + + // Returns true if this is a directory. + // Note: "folder" is used elsewhere in this file to match Drive API reference, + // but outside this file we use "directory" to match HTML5 filesystem API. + bool IsDirectory() const; + + // Returns file ID. This is unique in all files in Google Drive. + const std::string& file_id() const { return file_id_; } + + // Returns ETag for this file. + const std::string& etag() const { return etag_; } + + // Returns the link to JSON of this file itself. + const GURL& self_link() const { return self_link_; } + + // Returns the title of this file. + const std::string& title() const { return title_; } + + // Returns MIME type of this file. + const std::string& mime_type() const { return mime_type_; } + + // Returns labels for this file. + const FileLabels& labels() const { return labels_; } + + // Returns image media metadata for this file. + const ImageMediaMetadata& image_media_metadata() const { + return image_media_metadata_; + } + + // Returns created time of this file. + const base::Time& created_date() const { return created_date_; } + + // Returns modified time of this file. + const base::Time& modified_date() const { return modified_date_; } + + // Returns modification time by the user. + const base::Time& modified_by_me_date() const { return modified_by_me_date_; } + + // Returns last access time by the user. + const base::Time& last_viewed_by_me_date() const { + return last_viewed_by_me_date_; + } + + // Returns time when the file was shared with the user. + const base::Time& shared_with_me_date() const { + return shared_with_me_date_; + } + + // Returns the 'shared' attribute of the file. + bool shared() const { return shared_; } + + // Returns the short-lived download URL for the file. This field exists + // only when the file content is stored in Drive. + const GURL& download_url() const { return download_url_; } + + // Returns the extension part of the filename. + const std::string& file_extension() const { return file_extension_; } + + // Returns MD5 checksum of this file. + const std::string& md5_checksum() const { return md5_checksum_; } + + // Returns the size of this file in bytes. + int64 file_size() const { return file_size_; } + + // Return the link to open the file in Google editor or viewer. + // E.g. Google Document, Google Spreadsheet. + const GURL& alternate_link() const { return alternate_link_; } + + // Returns the link for embedding the file. + const GURL& embed_link() const { return embed_link_; } + + // Returns parent references (directories) of this file. + const ScopedVector<ParentReference>& parents() const { return parents_; } + + // Returns the link to the file's thumbnail. + const GURL& thumbnail_link() const { return thumbnail_link_; } + + // Returns the link to open its downloadable content, using cookie based + // authentication. + const GURL& web_content_link() const { return web_content_link_; } + + // Returns the list of links to open the resource with a web app. + const std::vector<OpenWithLink>& open_with_links() const { + return open_with_links_; + } + + void set_file_id(const std::string& file_id) { + file_id_ = file_id; + } + void set_etag(const std::string& etag) { + etag_ = etag; + } + void set_self_link(const GURL& self_link) { + self_link_ = self_link; + } + void set_title(const std::string& title) { + title_ = title; + } + void set_mime_type(const std::string& mime_type) { + mime_type_ = mime_type; + } + FileLabels* mutable_labels() { + return &labels_; + } + ImageMediaMetadata* mutable_image_media_metadata() { + return &image_media_metadata_; + } + void set_created_date(const base::Time& created_date) { + created_date_ = created_date; + } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + void set_modified_by_me_date(const base::Time& modified_by_me_date) { + modified_by_me_date_ = modified_by_me_date; + } + void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { + last_viewed_by_me_date_ = last_viewed_by_me_date; + } + void set_shared_with_me_date(const base::Time& shared_with_me_date) { + shared_with_me_date_ = shared_with_me_date; + } + void set_shared(bool shared) { + shared_ = shared; + } + void set_download_url(const GURL& download_url) { + download_url_ = download_url; + } + void set_file_extension(const std::string& file_extension) { + file_extension_ = file_extension; + } + void set_md5_checksum(const std::string& md5_checksum) { + md5_checksum_ = md5_checksum; + } + void set_file_size(int64 file_size) { + file_size_ = file_size; + } + void set_alternate_link(const GURL& alternate_link) { + alternate_link_ = alternate_link; + } + void set_embed_link(const GURL& embed_link) { + embed_link_ = embed_link; + } + void set_parents(ScopedVector<ParentReference> parents) { + parents_ = parents.Pass(); + } + void set_thumbnail_link(const GURL& thumbnail_link) { + thumbnail_link_ = thumbnail_link; + } + void set_web_content_link(const GURL& web_content_link) { + web_content_link_ = web_content_link; + } + + private: + friend class base::internal::RepeatedMessageConverter<FileResource>; + friend class ChangeResource; + friend class FileList; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string file_id_; + std::string etag_; + GURL self_link_; + std::string title_; + std::string mime_type_; + FileLabels labels_; + ImageMediaMetadata image_media_metadata_; + base::Time created_date_; + base::Time modified_date_; + base::Time modified_by_me_date_; + base::Time last_viewed_by_me_date_; + base::Time shared_with_me_date_; + bool shared_; + GURL download_url_; + std::string file_extension_; + std::string md5_checksum_; + int64 file_size_; + GURL alternate_link_; + GURL embed_link_; + ScopedVector<ParentReference> parents_; + GURL thumbnail_link_; + GURL web_content_link_; + std::vector<OpenWithLink> open_with_links_; + + DISALLOW_COPY_AND_ASSIGN(FileResource); +}; + +// FileList represents a collection of files and folders. +// https://developers.google.com/drive/v2/reference/files/list +class FileList { + public: + FileList(); + ~FileList(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<FileList>* converter); + + // Returns true if the |value| has kind field for FileList. + static bool HasFileListKind(const base::Value& value); + + // Creates file list from parsed JSON. + static scoped_ptr<FileList> CreateFrom(const base::Value& value); + + // Returns the ETag of the list. + const std::string& etag() const { return etag_; } + + // Returns the page token for the next page of files, if the list is large + // to fit in one response. If this is empty, there is no more file lists. + const std::string& next_page_token() const { return next_page_token_; } + + // Returns a link to the next page of files. The URL includes the next page + // token. + const GURL& next_link() const { return next_link_; } + + // Returns a set of files in this list. + const ScopedVector<FileResource>& items() const { return items_; } + + void set_etag(const std::string& etag) { + etag_ = etag; + } + void set_next_page_token(const std::string& next_page_token) { + next_page_token_ = next_page_token; + } + void set_next_link(const GURL& next_link) { + next_link_ = next_link; + } + void set_items(ScopedVector<FileResource> items) { + items_ = items.Pass(); + } + + private: + friend class DriveAPIParserTest; + FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, FileListParser); + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string etag_; + std::string next_page_token_; + GURL next_link_; + ScopedVector<FileResource> items_; + + DISALLOW_COPY_AND_ASSIGN(FileList); +}; + +// ChangeResource represents a change in a file. +// https://developers.google.com/drive/v2/reference/changes +class ChangeResource { + public: + ChangeResource(); + ~ChangeResource(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<ChangeResource>* converter); + + // Creates change resource from parsed JSON. + static scoped_ptr<ChangeResource> CreateFrom(const base::Value& value); + + // Returns change ID for this change. This is a monotonically increasing + // number. + int64 change_id() const { return change_id_; } + + // Returns a string file ID for corresponding file of the change. + const std::string& file_id() const { return file_id_; } + + // Returns true if this file is deleted in the change. + bool is_deleted() const { return deleted_; } + + // Returns FileResource of the file which the change refers to. + const FileResource* file() const { return file_.get(); } + + void set_change_id(int64 change_id) { + change_id_ = change_id; + } + void set_file_id(const std::string& file_id) { + file_id_ = file_id; + } + void set_deleted(bool deleted) { + deleted_ = deleted; + } + void set_file(scoped_ptr<FileResource> file) { + file_ = file.Pass(); + } + + private: + friend class base::internal::RepeatedMessageConverter<ChangeResource>; + friend class ChangeList; + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + int64 change_id_; + std::string file_id_; + bool deleted_; + scoped_ptr<FileResource> file_; + + DISALLOW_COPY_AND_ASSIGN(ChangeResource); +}; + +// ChangeList represents a set of changes in the drive. +// https://developers.google.com/drive/v2/reference/changes/list +class ChangeList { + public: + ChangeList(); + ~ChangeList(); + + // Registers the mapping between JSON field names and the members in this + // class. + static void RegisterJSONConverter( + base::JSONValueConverter<ChangeList>* converter); + + // Returns true if the |value| has kind field for ChangeList. + static bool HasChangeListKind(const base::Value& value); + + // Creates change list from parsed JSON. + static scoped_ptr<ChangeList> CreateFrom(const base::Value& value); + + // Returns the ETag of the list. + const std::string& etag() const { return etag_; } + + // Returns the page token for the next page of files, if the list is large + // to fit in one response. If this is empty, there is no more file lists. + const std::string& next_page_token() const { return next_page_token_; } + + // Returns a link to the next page of files. The URL includes the next page + // token. + const GURL& next_link() const { return next_link_; } + + // Returns the largest change ID number. + int64 largest_change_id() const { return largest_change_id_; } + + // Returns a set of changes in this list. + const ScopedVector<ChangeResource>& items() const { return items_; } + + void set_etag(const std::string& etag) { + etag_ = etag; + } + void set_next_page_token(const std::string& next_page_token) { + next_page_token_ = next_page_token; + } + void set_next_link(const GURL& next_link) { + next_link_ = next_link; + } + void set_largest_change_id(int64 largest_change_id) { + largest_change_id_ = largest_change_id; + } + void set_items(ScopedVector<ChangeResource> items) { + items_ = items.Pass(); + } + + private: + friend class DriveAPIParserTest; + FRIEND_TEST_ALL_PREFIXES(DriveAPIParserTest, ChangeListParser); + + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + std::string etag_; + std::string next_page_token_; + GURL next_link_; + int64 largest_change_id_; + ScopedVector<ChangeResource> items_; + + DISALLOW_COPY_AND_ASSIGN(ChangeList); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DRIVE_API_PARSER_H_ diff --git a/chromium/google_apis/drive/drive_api_parser_unittest.cc b/chromium/google_apis/drive/drive_api_parser_unittest.cc new file mode 100644 index 00000000000..0960b275bf5 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_parser_unittest.cc @@ -0,0 +1,304 @@ +// Copyright (c) 2012 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 "google_apis/drive/drive_api_parser.h" + +#include "base/time/time.h" +#include "base/values.h" +#include "google_apis/drive/gdata_wapi_parser.h" +#include "google_apis/drive/test_util.h" +#include "google_apis/drive/time_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +// Test about resource parsing. +TEST(DriveAPIParserTest, AboutResourceParser) { + std::string error; + scoped_ptr<base::Value> document = test_util::LoadJSONFile( + "drive/about.json"); + ASSERT_TRUE(document.get()); + + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<AboutResource> resource(new AboutResource()); + EXPECT_TRUE(resource->Parse(*document)); + + EXPECT_EQ("0AIv7G8yEYAWHUk9123", resource->root_folder_id()); + EXPECT_EQ(5368709120LL, resource->quota_bytes_total()); + EXPECT_EQ(1073741824LL, resource->quota_bytes_used()); + EXPECT_EQ(8177LL, resource->largest_change_id()); +} + +// Test app list parsing. +TEST(DriveAPIParserTest, AppListParser) { + std::string error; + scoped_ptr<base::Value> document = test_util::LoadJSONFile( + "drive/applist.json"); + ASSERT_TRUE(document.get()); + + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<AppList> applist(new AppList); + EXPECT_TRUE(applist->Parse(*document)); + + EXPECT_EQ("\"Jm4BaSnCWNND-noZsHINRqj4ABC/tuqRBw0lvjUdPtc_2msA1tN4XYZ\"", + applist->etag()); + ASSERT_EQ(2U, applist->items().size()); + // Check Drive app 1 + const AppResource& app1 = *applist->items()[0]; + EXPECT_EQ("123456788192", app1.application_id()); + EXPECT_EQ("Drive app 1", app1.name()); + EXPECT_EQ("", app1.object_type()); + EXPECT_TRUE(app1.supports_create()); + EXPECT_TRUE(app1.supports_import()); + EXPECT_TRUE(app1.is_installed()); + EXPECT_FALSE(app1.is_authorized()); + EXPECT_EQ("https://chrome.google.com/webstore/detail/" + "abcdefghabcdefghabcdefghabcdefgh", + app1.product_url().spec()); + + ASSERT_EQ(1U, app1.primary_mimetypes().size()); + EXPECT_EQ("application/vnd.google-apps.drive-sdk.123456788192", + *app1.primary_mimetypes()[0]); + + ASSERT_EQ(2U, app1.secondary_mimetypes().size()); + EXPECT_EQ("text/html", *app1.secondary_mimetypes()[0]); + EXPECT_EQ("text/plain", *app1.secondary_mimetypes()[1]); + + ASSERT_EQ(2U, app1.primary_file_extensions().size()); + EXPECT_EQ("exe", *app1.primary_file_extensions()[0]); + EXPECT_EQ("com", *app1.primary_file_extensions()[1]); + + EXPECT_EQ(0U, app1.secondary_file_extensions().size()); + + ASSERT_EQ(6U, app1.icons().size()); + const DriveAppIcon& icon1 = *app1.icons()[0]; + EXPECT_EQ(DriveAppIcon::APPLICATION, icon1.category()); + EXPECT_EQ(10, icon1.icon_side_length()); + EXPECT_EQ("http://www.example.com/10.png", icon1.icon_url().spec()); + + const DriveAppIcon& icon6 = *app1.icons()[5]; + EXPECT_EQ(DriveAppIcon::SHARED_DOCUMENT, icon6.category()); + EXPECT_EQ(16, icon6.icon_side_length()); + EXPECT_EQ("http://www.example.com/ds16.png", icon6.icon_url().spec()); + + // Check Drive app 2 + const AppResource& app2 = *applist->items()[1]; + EXPECT_EQ("876543210000", app2.application_id()); + EXPECT_EQ("Drive app 2", app2.name()); + EXPECT_EQ("", app2.object_type()); + EXPECT_FALSE(app2.supports_create()); + EXPECT_FALSE(app2.supports_import()); + EXPECT_TRUE(app2.is_installed()); + EXPECT_FALSE(app2.is_authorized()); + EXPECT_EQ("https://chrome.google.com/webstore/detail/" + "hgfedcbahgfedcbahgfedcbahgfedcba", + app2.product_url().spec()); + + ASSERT_EQ(3U, app2.primary_mimetypes().size()); + EXPECT_EQ("image/jpeg", *app2.primary_mimetypes()[0]); + EXPECT_EQ("image/png", *app2.primary_mimetypes()[1]); + EXPECT_EQ("application/vnd.google-apps.drive-sdk.876543210000", + *app2.primary_mimetypes()[2]); + + EXPECT_EQ(0U, app2.secondary_mimetypes().size()); + EXPECT_EQ(0U, app2.primary_file_extensions().size()); + EXPECT_EQ(0U, app2.secondary_file_extensions().size()); + + ASSERT_EQ(3U, app2.icons().size()); + const DriveAppIcon& icon2 = *app2.icons()[1]; + EXPECT_EQ(DriveAppIcon::DOCUMENT, icon2.category()); + EXPECT_EQ(10, icon2.icon_side_length()); + EXPECT_EQ("http://www.example.com/d10.png", icon2.icon_url().spec()); +} + +// Test file list parsing. +TEST(DriveAPIParserTest, FileListParser) { + std::string error; + scoped_ptr<base::Value> document = test_util::LoadJSONFile( + "drive/filelist.json"); + ASSERT_TRUE(document.get()); + + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<FileList> filelist(new FileList); + EXPECT_TRUE(filelist->Parse(*document)); + + EXPECT_EQ("\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/zyHTfoHpnRHovyi8bWpwK0DXABC\"", + filelist->etag()); + EXPECT_EQ("EAIaggELEgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_Ry" + "NGJnridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJnr" + "idyrbHs7u9tv8A__4QZCEiXPTi_wtIgTkAAAAAngnSXUgCDEAAIgsJPgart10AAAA" + "ABC", filelist->next_page_token()); + EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files?pageToken=EAIaggEL" + "EgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_RyNGJ" + "nridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJ" + "nridyrbHs7u9tv8A__4QZCEiXPTi_wtIgTkAAAAAngnSXUgCDEAAIgsJPgar" + "t10AAAAABC"), filelist->next_link()); + + ASSERT_EQ(3U, filelist->items().size()); + // Check file 1 (a regular file) + const FileResource& file1 = *filelist->items()[0]; + EXPECT_EQ("0B4v7G8yEYAWHUmRrU2lMS2hLABC", file1.file_id()); + EXPECT_EQ("\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/MTM0MzM2NzgwMDIXYZ\"", + file1.etag()); + EXPECT_EQ("My first file data", file1.title()); + EXPECT_EQ("application/octet-stream", file1.mime_type()); + + EXPECT_FALSE(file1.labels().is_starred()); + EXPECT_FALSE(file1.labels().is_hidden()); + EXPECT_FALSE(file1.labels().is_trashed()); + EXPECT_FALSE(file1.labels().is_restricted()); + EXPECT_TRUE(file1.labels().is_viewed()); + EXPECT_FALSE(file1.shared()); + + EXPECT_EQ(640, file1.image_media_metadata().width()); + EXPECT_EQ(480, file1.image_media_metadata().height()); + EXPECT_EQ(90, file1.image_media_metadata().rotation()); + + base::Time created_time; + ASSERT_TRUE( + util::GetTimeFromString("2012-07-24T08:51:16.570Z", &created_time)); + EXPECT_EQ(created_time, file1.created_date()); + + base::Time modified_time; + ASSERT_TRUE( + util::GetTimeFromString("2012-07-27T05:43:20.269Z", &modified_time)); + EXPECT_EQ(modified_time, file1.modified_date()); + EXPECT_EQ(modified_time, file1.modified_by_me_date()); + + ASSERT_EQ(1U, file1.parents().size()); + EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", file1.parents()[0]->file_id()); + EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/" + "0B4v7G8yEYAWHYW1OcExsUVZLABC"), + file1.parents()[0]->parent_link()); + EXPECT_FALSE(file1.parents()[0]->is_root()); + + EXPECT_EQ(GURL("https://www.example.com/download"), file1.download_url()); + EXPECT_EQ("ext", file1.file_extension()); + EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", file1.md5_checksum()); + EXPECT_EQ(1000U, file1.file_size()); + + EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/" + "0B4v7G8yEYAWHUmRrU2lMS2hLABC"), + file1.self_link()); + EXPECT_EQ(GURL("https://docs.google.com/file/d/" + "0B4v7G8yEYAWHUmRrU2lMS2hLABC/edit"), + file1.alternate_link()); + EXPECT_EQ(GURL("https://docs.google.com/uc?" + "id=0B4v7G8yEYAWHUmRrU2lMS2hLABC&export=download"), + file1.web_content_link()); + ASSERT_EQ(1U, file1.open_with_links().size()); + EXPECT_EQ("1234567890", file1.open_with_links()[0].app_id); + EXPECT_EQ(GURL("http://open_with_link/url"), + file1.open_with_links()[0].open_url); + + // Check file 2 (a Google Document) + const FileResource& file2 = *filelist->items()[1]; + EXPECT_EQ("Test Google Document", file2.title()); + EXPECT_EQ("application/vnd.google-apps.document", file2.mime_type()); + + EXPECT_TRUE(file2.labels().is_starred()); + EXPECT_TRUE(file2.labels().is_hidden()); + EXPECT_TRUE(file2.labels().is_trashed()); + EXPECT_TRUE(file2.labels().is_restricted()); + EXPECT_TRUE(file2.labels().is_viewed()); + EXPECT_TRUE(file2.shared()); + + EXPECT_EQ(-1, file2.image_media_metadata().width()); + EXPECT_EQ(-1, file2.image_media_metadata().height()); + EXPECT_EQ(-1, file2.image_media_metadata().rotation()); + + base::Time shared_with_me_time; + ASSERT_TRUE(util::GetTimeFromString("2012-07-27T04:54:11.030Z", + &shared_with_me_time)); + EXPECT_EQ(shared_with_me_time, file2.shared_with_me_date()); + + EXPECT_EQ(0U, file2.file_size()); + + ASSERT_EQ(0U, file2.parents().size()); + + EXPECT_EQ(GURL("https://docs.google.com/a/chromium.org/document/d/" + "1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC/preview"), + file2.embed_link()); + EXPECT_EQ(GURL("https://docs.google.com/feeds/vt?gd=true&" + "id=1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC&" + "v=3&s=AMedNnoAAAAAUBJyB0g8HbxZaLRnlztxefZPS24LiXYZ&sz=s220"), + file2.thumbnail_link()); + EXPECT_EQ(0U, file2.open_with_links().size()); + + // Check file 3 (a folder) + const FileResource& file3 = *filelist->items()[2]; + EXPECT_EQ(0U, file3.file_size()); + EXPECT_EQ("TestFolder", file3.title()); + EXPECT_EQ("application/vnd.google-apps.folder", file3.mime_type()); + ASSERT_TRUE(file3.IsDirectory()); + EXPECT_FALSE(file3.shared()); + + ASSERT_EQ(1U, file3.parents().size()); + EXPECT_EQ("0AIv7G8yEYAWHUk9ABC", file3.parents()[0]->file_id()); + EXPECT_TRUE(file3.parents()[0]->is_root()); + EXPECT_EQ(0U, file3.open_with_links().size()); +} + +// Test change list parsing. +TEST(DriveAPIParserTest, ChangeListParser) { + std::string error; + scoped_ptr<base::Value> document = + test_util::LoadJSONFile("drive/changelist.json"); + ASSERT_TRUE(document.get()); + + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<ChangeList> changelist(new ChangeList); + EXPECT_TRUE(changelist->Parse(*document)); + + EXPECT_EQ("\"Lp2bjAtLP341hvGmYHhxjYyBPJ8/BWbu_eylt5f_aGtCN6mGRv9hABC\"", + changelist->etag()); + EXPECT_EQ("8929", changelist->next_page_token()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/changes?pageToken=8929", + changelist->next_link().spec()); + EXPECT_EQ(13664, changelist->largest_change_id()); + + ASSERT_EQ(4U, changelist->items().size()); + + const ChangeResource& change1 = *changelist->items()[0]; + EXPECT_EQ(8421, change1.change_id()); + EXPECT_FALSE(change1.is_deleted()); + EXPECT_EQ("1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC", change1.file_id()); + EXPECT_EQ(change1.file_id(), change1.file()->file_id()); + EXPECT_FALSE(change1.file()->shared()); + + const ChangeResource& change2 = *changelist->items()[1]; + EXPECT_EQ(8424, change2.change_id()); + EXPECT_FALSE(change2.is_deleted()); + EXPECT_EQ("0B4v7G8yEYAWHUmRrU2lMS2hLABC", change2.file_id()); + EXPECT_EQ(change2.file_id(), change2.file()->file_id()); + EXPECT_TRUE(change2.file()->shared()); + + const ChangeResource& change3 = *changelist->items()[2]; + EXPECT_EQ(8429, change3.change_id()); + EXPECT_FALSE(change3.is_deleted()); + EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", change3.file_id()); + EXPECT_EQ(change3.file_id(), change3.file()->file_id()); + EXPECT_FALSE(change3.file()->shared()); + + // Deleted entry. + const ChangeResource& change4 = *changelist->items()[3]; + EXPECT_EQ(8430, change4.change_id()); + EXPECT_EQ("ABCv7G8yEYAWHc3Y5X0hMSkJYXYZ", change4.file_id()); + EXPECT_TRUE(change4.is_deleted()); +} + +TEST(DriveAPIParserTest, HasKind) { + scoped_ptr<base::Value> change_list_json( + test_util::LoadJSONFile("drive/changelist.json")); + scoped_ptr<base::Value> file_list_json( + test_util::LoadJSONFile("drive/filelist.json")); + + EXPECT_TRUE(ChangeList::HasChangeListKind(*change_list_json)); + EXPECT_FALSE(ChangeList::HasChangeListKind(*file_list_json)); + + EXPECT_FALSE(FileList::HasFileListKind(*change_list_json)); + EXPECT_TRUE(FileList::HasFileListKind(*file_list_json)); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_requests.cc b/chromium/google_apis/drive/drive_api_requests.cc new file mode 100644 index 00000000000..f0960e6f237 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_requests.cc @@ -0,0 +1,743 @@ +// Copyright (c) 2012 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 "google_apis/drive/drive_api_requests.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/json/json_writer.h" +#include "base/location.h" +#include "base/sequenced_task_runner.h" +#include "base/task_runner_util.h" +#include "base/values.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/request_util.h" +#include "google_apis/drive/time_util.h" +#include "net/base/url_util.h" + +namespace google_apis { +namespace { + +const char kContentTypeApplicationJson[] = "application/json"; +const char kParentLinkKind[] = "drive#fileLink"; + +// Parses the JSON value to a resource typed |T| and runs |callback| on the UI +// thread once parsing is done. +template<typename T> +void ParseJsonAndRun( + const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback, + GDataErrorCode error, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + scoped_ptr<T> resource; + if (value) { + resource = T::CreateFrom(*value); + if (!resource) { + // Failed to parse the JSON value, although the JSON value is available, + // so let the callback know the parsing error. + error = GDATA_PARSE_ERROR; + } + } + + callback.Run(error, resource.Pass()); +} + +// Thin adapter of T::CreateFrom. +template<typename T> +scoped_ptr<T> ParseJsonOnBlockingPool(scoped_ptr<base::Value> value) { + return T::CreateFrom(*value); +} + +// Runs |callback| with given |error| and |value|. If |value| is null, +// overwrites |error| to GDATA_PARSE_ERROR. +template<typename T> +void ParseJsonOnBlockingPoolAndRunAfterBlockingPoolTask( + const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback, + GDataErrorCode error, scoped_ptr<T> value) { + if (!value) + error = GDATA_PARSE_ERROR; + callback.Run(error, value.Pass()); +} + +// Parses the JSON value to a resource typed |T| and runs |callback| on +// blocking pool, and then run on the current thread. +// TODO(hidehiko): Move this and ParseJsonAndRun defined above into base with +// merging the tasks running on blocking pool into one. +template<typename T> +void ParseJsonOnBlockingPoolAndRun( + scoped_refptr<base::TaskRunner> blocking_task_runner, + const base::Callback<void(GDataErrorCode, scoped_ptr<T>)>& callback, + GDataErrorCode error, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(error, scoped_ptr<T>()); + return; + } + + base::PostTaskAndReplyWithResult( + blocking_task_runner, + FROM_HERE, + base::Bind(&ParseJsonOnBlockingPool<T>, base::Passed(&value)), + base::Bind(&ParseJsonOnBlockingPoolAndRunAfterBlockingPoolTask<T>, + callback, error)); +} + +// Parses the JSON value to FileResource instance and runs |callback| on the +// UI thread once parsing is done. +// This is customized version of ParseJsonAndRun defined above to adapt the +// remaining response type. +void ParseFileResourceWithUploadRangeAndRun( + const drive::UploadRangeCallback& callback, + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + scoped_ptr<FileResource> file_resource; + if (value) { + file_resource = FileResource::CreateFrom(*value); + if (!file_resource) { + callback.Run( + UploadRangeResponse(GDATA_PARSE_ERROR, + response.start_position_received, + response.end_position_received), + scoped_ptr<FileResource>()); + return; + } + } + + callback.Run(response, file_resource.Pass()); +} + +} // namespace + +namespace drive { + +//============================ DriveApiDataRequest =========================== + +DriveApiDataRequest::DriveApiDataRequest(RequestSender* sender, + const GetDataCallback& callback) + : GetDataRequest(sender, callback) { +} + +DriveApiDataRequest::~DriveApiDataRequest() { +} + +GURL DriveApiDataRequest::GetURL() const { + GURL url = GetURLInternal(); + if (!fields_.empty()) + url = net::AppendOrReplaceQueryParameter(url, "fields", fields_); + return url; +} + +//=============================== FilesGetRequest ============================= + +FilesGetRequest::FilesGetRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesGetRequest::~FilesGetRequest() {} + +GURL FilesGetRequest::GetURLInternal() const { + return url_generator_.GetFilesGetUrl(file_id_); +} + +//============================ FilesInsertRequest ============================ + +FilesInsertRequest::FilesInsertRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesInsertRequest::~FilesInsertRequest() {} + +net::URLFetcher::RequestType FilesInsertRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +bool FilesInsertRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + + if (!mime_type_.empty()) + root.SetString("mimeType", mime_type_); + + if (!parents_.empty()) { + base::ListValue* parents_value = new base::ListValue; + for (size_t i = 0; i < parents_.size(); ++i) { + base::DictionaryValue* parent = new base::DictionaryValue; + parent->SetString("id", parents_[i]); + parents_value->Append(parent); + } + root.Set("parents", parents_value); + } + + if (!title_.empty()) + root.SetString("title", title_); + + base::JSONWriter::Write(&root, upload_content); + DVLOG(1) << "FilesInsert data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +GURL FilesInsertRequest::GetURLInternal() const { + return url_generator_.GetFilesInsertUrl(); +} + +//============================== FilesPatchRequest ============================ + +FilesPatchRequest::FilesPatchRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator), + set_modified_date_(false), + update_viewed_date_(true) { + DCHECK(!callback.is_null()); +} + +FilesPatchRequest::~FilesPatchRequest() {} + +net::URLFetcher::RequestType FilesPatchRequest::GetRequestType() const { + return net::URLFetcher::PATCH; +} + +std::vector<std::string> FilesPatchRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(util::kIfMatchAllHeader); + return headers; +} + +GURL FilesPatchRequest::GetURLInternal() const { + return url_generator_.GetFilesPatchUrl( + file_id_, set_modified_date_, update_viewed_date_); +} + +bool FilesPatchRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + if (title_.empty() && + modified_date_.is_null() && + last_viewed_by_me_date_.is_null() && + parents_.empty()) + return false; + + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + if (!title_.empty()) + root.SetString("title", title_); + + if (!modified_date_.is_null()) + root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_)); + + if (!last_viewed_by_me_date_.is_null()) { + root.SetString("lastViewedByMeDate", + util::FormatTimeAsString(last_viewed_by_me_date_)); + } + + if (!parents_.empty()) { + base::ListValue* parents_value = new base::ListValue; + for (size_t i = 0; i < parents_.size(); ++i) { + base::DictionaryValue* parent = new base::DictionaryValue; + parent->SetString("id", parents_[i]); + parents_value->Append(parent); + } + root.Set("parents", parents_value); + } + + base::JSONWriter::Write(&root, upload_content); + DVLOG(1) << "FilesPatch data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//============================= FilesCopyRequest ============================== + +FilesCopyRequest::FilesCopyRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesCopyRequest::~FilesCopyRequest() { +} + +net::URLFetcher::RequestType FilesCopyRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +GURL FilesCopyRequest::GetURLInternal() const { + return url_generator_.GetFilesCopyUrl(file_id_); +} + +bool FilesCopyRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + if (parents_.empty() && title_.empty()) + return false; + + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + + if (!modified_date_.is_null()) + root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_)); + + if (!parents_.empty()) { + base::ListValue* parents_value = new base::ListValue; + for (size_t i = 0; i < parents_.size(); ++i) { + base::DictionaryValue* parent = new base::DictionaryValue; + parent->SetString("id", parents_[i]); + parents_value->Append(parent); + } + root.Set("parents", parents_value); + } + + if (!title_.empty()) + root.SetString("title", title_); + + base::JSONWriter::Write(&root, upload_content); + DVLOG(1) << "FilesCopy data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//============================= FilesListRequest ============================= + +FilesListRequest::FilesListRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileListCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonOnBlockingPoolAndRun<FileList>, + make_scoped_refptr(sender->blocking_task_runner()), + callback)), + url_generator_(url_generator), + max_results_(100) { + DCHECK(!callback.is_null()); +} + +FilesListRequest::~FilesListRequest() {} + +GURL FilesListRequest::GetURLInternal() const { + return url_generator_.GetFilesListUrl(max_results_, page_token_, q_); +} + +//======================== FilesListNextPageRequest ========================= + +FilesListNextPageRequest::FilesListNextPageRequest( + RequestSender* sender, + const FileListCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonOnBlockingPoolAndRun<FileList>, + make_scoped_refptr(sender->blocking_task_runner()), + callback)) { + DCHECK(!callback.is_null()); +} + +FilesListNextPageRequest::~FilesListNextPageRequest() { +} + +GURL FilesListNextPageRequest::GetURLInternal() const { + return next_link_; +} + +//============================ FilesDeleteRequest ============================= + +FilesDeleteRequest::FilesDeleteRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback) + : EntryActionRequest(sender, callback), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesDeleteRequest::~FilesDeleteRequest() {} + +net::URLFetcher::RequestType FilesDeleteRequest::GetRequestType() const { + return net::URLFetcher::DELETE_REQUEST; +} + +GURL FilesDeleteRequest::GetURL() const { + return url_generator_.GetFilesDeleteUrl(file_id_); +} + +std::vector<std::string> FilesDeleteRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers( + EntryActionRequest::GetExtraRequestHeaders()); + headers.push_back(util::GenerateIfMatchHeader(etag_)); + return headers; +} + +//============================ FilesTrashRequest ============================= + +FilesTrashRequest::FilesTrashRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesTrashRequest::~FilesTrashRequest() {} + +net::URLFetcher::RequestType FilesTrashRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +GURL FilesTrashRequest::GetURLInternal() const { + return url_generator_.GetFilesTrashUrl(file_id_); +} + +//============================== AboutGetRequest ============================= + +AboutGetRequest::AboutGetRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const AboutResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<AboutResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +AboutGetRequest::~AboutGetRequest() {} + +GURL AboutGetRequest::GetURLInternal() const { + return url_generator_.GetAboutGetUrl(); +} + +//============================ ChangesListRequest =========================== + +ChangesListRequest::ChangesListRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const ChangeListCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonOnBlockingPoolAndRun<ChangeList>, + make_scoped_refptr(sender->blocking_task_runner()), + callback)), + url_generator_(url_generator), + include_deleted_(true), + max_results_(100), + start_change_id_(0) { + DCHECK(!callback.is_null()); +} + +ChangesListRequest::~ChangesListRequest() {} + +GURL ChangesListRequest::GetURLInternal() const { + return url_generator_.GetChangesListUrl( + include_deleted_, max_results_, page_token_, start_change_id_); +} + +//======================== ChangesListNextPageRequest ========================= + +ChangesListNextPageRequest::ChangesListNextPageRequest( + RequestSender* sender, + const ChangeListCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonOnBlockingPoolAndRun<ChangeList>, + make_scoped_refptr(sender->blocking_task_runner()), + callback)) { + DCHECK(!callback.is_null()); +} + +ChangesListNextPageRequest::~ChangesListNextPageRequest() { +} + +GURL ChangesListNextPageRequest::GetURLInternal() const { + return next_link_; +} + +//============================== AppsListRequest =========================== + +AppsListRequest::AppsListRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const AppListCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<AppList>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +AppsListRequest::~AppsListRequest() {} + +GURL AppsListRequest::GetURLInternal() const { + return url_generator_.GetAppsListUrl(); +} + +//========================== ChildrenInsertRequest ============================ + +ChildrenInsertRequest::ChildrenInsertRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback) + : EntryActionRequest(sender, callback), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +ChildrenInsertRequest::~ChildrenInsertRequest() {} + +net::URLFetcher::RequestType ChildrenInsertRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +GURL ChildrenInsertRequest::GetURL() const { + return url_generator_.GetChildrenInsertUrl(folder_id_); +} + +bool ChildrenInsertRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + root.SetString("id", id_); + + base::JSONWriter::Write(&root, upload_content); + DVLOG(1) << "InsertResource data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//========================== ChildrenDeleteRequest ============================ + +ChildrenDeleteRequest::ChildrenDeleteRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback) + : EntryActionRequest(sender, callback), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +ChildrenDeleteRequest::~ChildrenDeleteRequest() {} + +net::URLFetcher::RequestType ChildrenDeleteRequest::GetRequestType() const { + return net::URLFetcher::DELETE_REQUEST; +} + +GURL ChildrenDeleteRequest::GetURL() const { + return url_generator_.GetChildrenDeleteUrl(child_id_, folder_id_); +} + +//======================= InitiateUploadNewFileRequest ======================= + +InitiateUploadNewFileRequest::InitiateUploadNewFileRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const InitiateUploadCallback& callback) + : InitiateUploadRequestBase(sender, + callback, + content_type, + content_length), + url_generator_(url_generator), + parent_resource_id_(parent_resource_id), + title_(title) { +} + +InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {} + +GURL InitiateUploadNewFileRequest::GetURL() const { + return url_generator_.GetInitiateUploadNewFileUrl(); +} + +net::URLFetcher::RequestType +InitiateUploadNewFileRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +bool InitiateUploadNewFileRequest::GetContentData( + std::string* upload_content_type, + std::string* upload_content) { + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + root.SetString("title", title_); + + // Fill parent link. + { + scoped_ptr<base::DictionaryValue> parent(new base::DictionaryValue); + parent->SetString("kind", kParentLinkKind); + parent->SetString("id", parent_resource_id_); + + scoped_ptr<base::ListValue> parents(new base::ListValue); + parents->Append(parent.release()); + + root.Set("parents", parents.release()); + } + + base::JSONWriter::Write(&root, upload_content); + + DVLOG(1) << "InitiateUploadNewFile data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//===================== InitiateUploadExistingFileRequest ==================== + +InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const std::string& etag, + const InitiateUploadCallback& callback) + : InitiateUploadRequestBase(sender, + callback, + content_type, + content_length), + url_generator_(url_generator), + resource_id_(resource_id), + etag_(etag) { +} + +InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {} + +GURL InitiateUploadExistingFileRequest::GetURL() const { + return url_generator_.GetInitiateUploadExistingFileUrl(resource_id_); +} + +net::URLFetcher::RequestType +InitiateUploadExistingFileRequest::GetRequestType() const { + return net::URLFetcher::PUT; +} + +std::vector<std::string> +InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers( + InitiateUploadRequestBase::GetExtraRequestHeaders()); + headers.push_back(util::GenerateIfMatchHeader(etag_)); + return headers; +} + +//============================ ResumeUploadRequest =========================== + +ResumeUploadRequest::ResumeUploadRequest( + RequestSender* sender, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) + : ResumeUploadRequestBase(sender, + upload_location, + start_position, + end_position, + content_length, + content_type, + local_file_path), + callback_(callback), + progress_callback_(progress_callback) { + DCHECK(!callback_.is_null()); +} + +ResumeUploadRequest::~ResumeUploadRequest() {} + +void ResumeUploadRequest::OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) { + DCHECK(CalledOnValidThread()); + ParseFileResourceWithUploadRangeAndRun(callback_, response, value.Pass()); +} + +void ResumeUploadRequest::OnURLFetchUploadProgress( + const net::URLFetcher* source, int64 current, int64 total) { + if (!progress_callback_.is_null()) + progress_callback_.Run(current, total); +} + +//========================== GetUploadStatusRequest ========================== + +GetUploadStatusRequest::GetUploadStatusRequest( + RequestSender* sender, + const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback) + : GetUploadStatusRequestBase(sender, + upload_url, + content_length), + callback_(callback) { + DCHECK(!callback.is_null()); +} + +GetUploadStatusRequest::~GetUploadStatusRequest() {} + +void GetUploadStatusRequest::OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) { + DCHECK(CalledOnValidThread()); + ParseFileResourceWithUploadRangeAndRun(callback_, response, value.Pass()); +} + +//========================== DownloadFileRequest ========================== + +DownloadFileRequest::DownloadFileRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& resource_id, + const base::FilePath& output_file_path, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback) + : DownloadFileRequestBase( + sender, + download_action_callback, + get_content_callback, + progress_callback, + url_generator.GenerateDownloadFileUrl(resource_id), + output_file_path) { +} + +DownloadFileRequest::~DownloadFileRequest() { +} + +} // namespace drive +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_requests.h b/chromium/google_apis/drive/drive_api_requests.h new file mode 100644 index 00000000000..6f10aa66d95 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_requests.h @@ -0,0 +1,733 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_ +#define GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_ + +#include <string> + +#include "base/callback_forward.h" +#include "base/time/time.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/drive_api_url_generator.h" +#include "google_apis/drive/drive_common_callbacks.h" + +namespace google_apis { + +class ChangeList; +class FileResource; +class FileList; + +// Callback used for requests that the server returns FileResource data +// formatted into JSON value. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<FileResource> entry)> + FileResourceCallback; + +// Callback used for requests that the server returns FileList data +// formatted into JSON value. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<FileList> entry)> FileListCallback; + +// Callback used for requests that the server returns ChangeList data +// formatted into JSON value. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<ChangeList> entry)> ChangeListCallback; + +namespace drive { + +//============================ DriveApiDataRequest =========================== + +// This is base class of the Drive API related requests. All Drive API requests +// support partial request (to improve the performance). The function can be +// shared among the Drive API requests. +// See also https://developers.google.com/drive/performance +class DriveApiDataRequest : public GetDataRequest { + public: + DriveApiDataRequest(RequestSender* sender, const GetDataCallback& callback); + virtual ~DriveApiDataRequest(); + + // Optional parameter. + const std::string& fields() const { return fields_; } + void set_fields(const std::string& fields) { fields_ = fields; } + + protected: + // Overridden from GetDataRequest. + virtual GURL GetURL() const OVERRIDE; + + // Derived classes should override GetURLInternal instead of GetURL() + // directly. + virtual GURL GetURLInternal() const = 0; + + private: + std::string fields_; + + DISALLOW_COPY_AND_ASSIGN(DriveApiDataRequest); +}; + +//=============================== FilesGetRequest ============================= + +// This class performs the request for fetching a file. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/get +class FilesGetRequest : public DriveApiDataRequest { + public: + FilesGetRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesGetRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string file_id_; + + DISALLOW_COPY_AND_ASSIGN(FilesGetRequest); +}; + +//============================ FilesInsertRequest ============================= + +// This class performs the request for creating a resource. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/insert +// See also https://developers.google.com/drive/manage-uploads and +// https://developers.google.com/drive/folder +class FilesInsertRequest : public DriveApiDataRequest { + public: + FilesInsertRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesInsertRequest(); + + // Optional request body. + const std::string& mime_type() const { return mime_type_; } + void set_mime_type(const std::string& mime_type) { + mime_type_ = mime_type; + } + + const std::vector<std::string>& parents() const { return parents_; } + void add_parent(const std::string& parent) { parents_.push_back(parent); } + + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + + protected: + // Overridden from GetDataRequest. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + + std::string mime_type_; + std::vector<std::string> parents_; + std::string title_; + + DISALLOW_COPY_AND_ASSIGN(FilesInsertRequest); +}; + +//============================== FilesPatchRequest ============================ + +// This class performs the request for patching file metadata. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/patch +class FilesPatchRequest : public DriveApiDataRequest { + public: + FilesPatchRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesPatchRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + + // Optional parameter. + bool set_modified_date() const { return set_modified_date_; } + void set_set_modified_date(bool set_modified_date) { + set_modified_date_ = set_modified_date; + } + + bool update_viewed_date() const { return update_viewed_date_; } + void set_update_viewed_date(bool update_viewed_date) { + update_viewed_date_ = update_viewed_date; + } + + // Optional request body. + // Note: "Files: patch" accepts any "Files resource" data, but this class + // only supports limited members of it for now. We can extend it upon + // requirments. + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + + const base::Time& modified_date() const { return modified_date_; } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + + const base::Time& last_viewed_by_me_date() const { + return last_viewed_by_me_date_; + } + void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { + last_viewed_by_me_date_ = last_viewed_by_me_date; + } + + const std::vector<std::string>& parents() const { return parents_; } + void add_parent(const std::string& parent) { parents_.push_back(parent); } + + protected: + // Overridden from URLFetchRequestBase. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + + std::string file_id_; + bool set_modified_date_; + bool update_viewed_date_; + + std::string title_; + base::Time modified_date_; + base::Time last_viewed_by_me_date_; + std::vector<std::string> parents_; + + DISALLOW_COPY_AND_ASSIGN(FilesPatchRequest); +}; + +//============================= FilesCopyRequest ============================== + +// This class performs the request for copying a resource. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/copy +class FilesCopyRequest : public DriveApiDataRequest { + public: + // Upon completion, |callback| will be called. |callback| must not be null. + FilesCopyRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesCopyRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + + // Optional request body. + const std::vector<std::string>& parents() const { return parents_; } + void add_parent(const std::string& parent) { parents_.push_back(parent); } + + const base::Time& modified_date() const { return modified_date_; } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + + protected: + // Overridden from URLFetchRequestBase. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + + std::string file_id_; + base::Time modified_date_; + std::vector<std::string> parents_; + std::string title_; + + DISALLOW_COPY_AND_ASSIGN(FilesCopyRequest); +}; + +//============================= FilesListRequest ============================= + +// This class performs the request for fetching FileList. +// The result may contain only first part of the result. The remaining result +// should be able to be fetched by ContinueGetFileListRequest defined below, +// or by FilesListRequest with setting page token. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/list +class FilesListRequest : public DriveApiDataRequest { + public: + FilesListRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileListCallback& callback); + virtual ~FilesListRequest(); + + // Optional parameter + int max_results() const { return max_results_; } + void set_max_results(int max_results) { max_results_ = max_results; } + + const std::string& page_token() const { return page_token_; } + void set_page_token(const std::string& page_token) { + page_token_ = page_token; + } + + const std::string& q() const { return q_; } + void set_q(const std::string& q) { q_ = q; } + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + int max_results_; + std::string page_token_; + std::string q_; + + DISALLOW_COPY_AND_ASSIGN(FilesListRequest); +}; + +//========================= FilesListNextPageRequest ========================== + +// There are two ways to obtain next pages of "Files: list" result (if paged). +// 1) Set pageToken and all params used for the initial request. +// 2) Use URL in the nextLink field in the previous response. +// This class implements 2)'s request. +class FilesListNextPageRequest : public DriveApiDataRequest { + public: + FilesListNextPageRequest(RequestSender* sender, + const FileListCallback& callback); + virtual ~FilesListNextPageRequest(); + + const GURL& next_link() const { return next_link_; } + void set_next_link(const GURL& next_link) { next_link_ = next_link; } + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + GURL next_link_; + + DISALLOW_COPY_AND_ASSIGN(FilesListNextPageRequest); +}; + +//============================= FilesDeleteRequest ============================= + +// This class performs the request for deleting a resource. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/delete +class FilesDeleteRequest : public EntryActionRequest { + public: + FilesDeleteRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback); + virtual ~FilesDeleteRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + void set_etag(const std::string& etag) { etag_ = etag; } + + protected: + // Overridden from UrlFetchRequestBase. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string file_id_; + std::string etag_; + + DISALLOW_COPY_AND_ASSIGN(FilesDeleteRequest); +}; + +//============================= FilesTrashRequest ============================== + +// This class performs the request for trashing a resource. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/trash +class FilesTrashRequest : public DriveApiDataRequest { + public: + FilesTrashRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesTrashRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + + protected: + // Overridden from UrlFetchRequestBase. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string file_id_; + + DISALLOW_COPY_AND_ASSIGN(FilesTrashRequest); +}; + +//============================== AboutGetRequest ============================= + +// This class performs the request for fetching About data. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/about/get +class AboutGetRequest : public DriveApiDataRequest { + public: + AboutGetRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const AboutResourceCallback& callback); + virtual ~AboutGetRequest(); + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + + DISALLOW_COPY_AND_ASSIGN(AboutGetRequest); +}; + +//============================ ChangesListRequest ============================ + +// This class performs the request for fetching ChangeList. +// The result may contain only first part of the result. The remaining result +// should be able to be fetched by ContinueGetFileListRequest defined below. +// or by ChangesListRequest with setting page token. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/changes/list +class ChangesListRequest : public DriveApiDataRequest { + public: + ChangesListRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const ChangeListCallback& callback); + virtual ~ChangesListRequest(); + + // Optional parameter + bool include_deleted() const { return include_deleted_; } + void set_include_deleted(bool include_deleted) { + include_deleted_ = include_deleted; + } + + int max_results() const { return max_results_; } + void set_max_results(int max_results) { max_results_ = max_results; } + + const std::string& page_token() const { return page_token_; } + void set_page_token(const std::string& page_token) { + page_token_ = page_token; + } + + int64 start_change_id() const { return start_change_id_; } + void set_start_change_id(int64 start_change_id) { + start_change_id_ = start_change_id; + } + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + bool include_deleted_; + int max_results_; + std::string page_token_; + int64 start_change_id_; + + DISALLOW_COPY_AND_ASSIGN(ChangesListRequest); +}; + +//======================== ChangesListNextPageRequest ========================= + +// There are two ways to obtain next pages of "Changes: list" result (if paged). +// 1) Set pageToken and all params used for the initial request. +// 2) Use URL in the nextLink field in the previous response. +// This class implements 2)'s request. +class ChangesListNextPageRequest : public DriveApiDataRequest { + public: + ChangesListNextPageRequest(RequestSender* sender, + const ChangeListCallback& callback); + virtual ~ChangesListNextPageRequest(); + + const GURL& next_link() const { return next_link_; } + void set_next_link(const GURL& next_link) { next_link_ = next_link; } + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + GURL next_link_; + + DISALLOW_COPY_AND_ASSIGN(ChangesListNextPageRequest); +}; + +//============================= AppsListRequest ============================ + +// This class performs the request for fetching AppList. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/apps/list +class AppsListRequest : public DriveApiDataRequest { + public: + AppsListRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const AppListCallback& callback); + virtual ~AppsListRequest(); + + protected: + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + + DISALLOW_COPY_AND_ASSIGN(AppsListRequest); +}; + +//========================== ChildrenInsertRequest ============================ + +// This class performs the request for inserting a resource to a directory. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/children/insert +class ChildrenInsertRequest : public EntryActionRequest { + public: + ChildrenInsertRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback); + virtual ~ChildrenInsertRequest(); + + // Required parameter. + const std::string& folder_id() const { return folder_id_; } + void set_folder_id(const std::string& folder_id) { + folder_id_ = folder_id; + } + + // Required body. + const std::string& id() const { return id_; } + void set_id(const std::string& id) { id_ = id; } + + protected: + // UrlFetchRequestBase overrides. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string folder_id_; + std::string id_; + + DISALLOW_COPY_AND_ASSIGN(ChildrenInsertRequest); +}; + +//========================== ChildrenDeleteRequest ============================ + +// This class performs the request for removing a resource from a directory. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/children/delete +class ChildrenDeleteRequest : public EntryActionRequest { + public: + // |callback| must not be null. + ChildrenDeleteRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback); + virtual ~ChildrenDeleteRequest(); + + // Required parameter. + const std::string& child_id() const { return child_id_; } + void set_child_id(const std::string& child_id) { + child_id_ = child_id; + } + + const std::string& folder_id() const { return folder_id_; } + void set_folder_id(const std::string& folder_id) { + folder_id_ = folder_id; + } + + protected: + // UrlFetchRequestBase overrides. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string child_id_; + std::string folder_id_; + + DISALLOW_COPY_AND_ASSIGN(ChildrenDeleteRequest); +}; + +//======================= InitiateUploadNewFileRequest ======================= + +// This class performs the request for initiating the upload of a new file. +class InitiateUploadNewFileRequest : public InitiateUploadRequestBase { + public: + // |parent_resource_id| should be the resource id of the parent directory. + // |title| should be set. + // See also the comments of InitiateUploadRequestBase for more details + // about the other parameters. + InitiateUploadNewFileRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const InitiateUploadCallback& callback); + virtual ~InitiateUploadNewFileRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + const std::string parent_resource_id_; + const std::string title_; + + DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest); +}; + +//==================== InitiateUploadExistingFileRequest ===================== + +// This class performs the request for initiating the upload of an existing +// file. +class InitiateUploadExistingFileRequest : public InitiateUploadRequestBase { + public: + // |upload_url| should be the upload_url() of the file + // (resumable-create-media URL) + // |etag| should be set if it is available to detect the upload confliction. + // See also the comments of InitiateUploadRequestBase for more details + // about the other parameters. + InitiateUploadExistingFileRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const std::string& etag, + const InitiateUploadCallback& callback); + virtual ~InitiateUploadExistingFileRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string etag_; + + DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest); +}; + +// Callback used for ResumeUpload() and GetUploadStatus(). +typedef base::Callback<void( + const UploadRangeResponse& response, + scoped_ptr<FileResource> new_resource)> UploadRangeCallback; + +//============================ ResumeUploadRequest =========================== + +// Performs the request for resuming the upload of a file. +class ResumeUploadRequest : public ResumeUploadRequestBase { + public: + // See also ResumeUploadRequestBase's comment for parameters meaning. + // |callback| must not be null. |progress_callback| may be null. + ResumeUploadRequest(RequestSender* sender, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback); + virtual ~ResumeUploadRequest(); + + protected: + // UploadRangeRequestBase overrides. + virtual void OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) OVERRIDE; + // content::UrlFetcherDelegate overrides. + virtual void OnURLFetchUploadProgress(const net::URLFetcher* source, + int64 current, int64 total) OVERRIDE; + + private: + const UploadRangeCallback callback_; + const ProgressCallback progress_callback_; + + DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequest); +}; + +//========================== GetUploadStatusRequest ========================== + +// Performs the request to fetch the current upload status of a file. +class GetUploadStatusRequest : public GetUploadStatusRequestBase { + public: + // See also GetUploadStatusRequestBase's comment for parameters meaning. + // |callback| must not be null. + GetUploadStatusRequest(RequestSender* sender, + const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback); + virtual ~GetUploadStatusRequest(); + + protected: + // UploadRangeRequestBase overrides. + virtual void OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) OVERRIDE; + + private: + const UploadRangeCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequest); +}; + +//========================== DownloadFileRequest ========================== + +// This class performs the request for downloading of a specified file. +class DownloadFileRequest : public DownloadFileRequestBase { + public: + // See also DownloadFileRequestBase's comment for parameters meaning. + DownloadFileRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const std::string& resource_id, + const base::FilePath& output_file_path, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback); + virtual ~DownloadFileRequest(); + + DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest); +}; + +} // namespace drive +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DRIVE_API_REQUESTS_H_ diff --git a/chromium/google_apis/drive/drive_api_requests_unittest.cc b/chromium/google_apis/drive/drive_api_requests_unittest.cc new file mode 100644 index 00000000000..466240cd369 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_requests_unittest.cc @@ -0,0 +1,1647 @@ +// Copyright (c) 2013 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 "base/bind.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/values.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/drive_api_requests.h" +#include "google_apis/drive/drive_api_url_generator.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/test_util.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +namespace { + +const char kTestETag[] = "test_etag"; +const char kTestUserAgent[] = "test-user-agent"; + +const char kTestChildrenResponse[] = + "{\n" + "\"kind\": \"drive#childReference\",\n" + "\"id\": \"resource_id\",\n" + "\"selfLink\": \"self_link\",\n" + "\"childLink\": \"child_link\",\n" + "}\n"; + +const char kTestUploadExistingFilePath[] = "/upload/existingfile/path"; +const char kTestUploadNewFilePath[] = "/upload/newfile/path"; +const char kTestDownloadPathPrefix[] = "/download/"; + +// Used as a GetContentCallback. +void AppendContent(std::string* out, + GDataErrorCode error, + scoped_ptr<std::string> content) { + EXPECT_EQ(HTTP_SUCCESS, error); + out->append(*content); +} + +} // namespace + +class DriveApiRequestsTest : public testing::Test { + public: + DriveApiRequestsTest() { + } + + virtual void SetUp() OVERRIDE { + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + + request_sender_.reset(new RequestSender(new DummyAuthService, + request_context_getter_.get(), + message_loop_.message_loop_proxy(), + kTestUserAgent)); + + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleChildrenDeleteRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleDataFileRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleDeleteRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandlePreconditionFailedRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleResumeUploadRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleInitiateUploadRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleContentResponse, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&DriveApiRequestsTest::HandleDownloadRequest, + base::Unretained(this))); + + GURL test_base_url = test_util::GetBaseUrlForTesting(test_server_.port()); + url_generator_.reset(new DriveApiUrlGenerator( + test_base_url, test_base_url.Resolve(kTestDownloadPathPrefix))); + + // Reset the server's expected behavior just in case. + ResetExpectedResponse(); + received_bytes_ = 0; + content_length_ = 0; + } + + base::MessageLoopForIO message_loop_; // Test server needs IO thread. + net::test_server::EmbeddedTestServer test_server_; + scoped_ptr<RequestSender> request_sender_; + scoped_ptr<DriveApiUrlGenerator> url_generator_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + base::ScopedTempDir temp_dir_; + + // This is a path to the file which contains expected response from + // the server. See also HandleDataFileRequest below. + base::FilePath expected_data_file_path_; + + // This is a path string in the expected response header from the server + // for initiating file uploading. + std::string expected_upload_path_; + + // This is a path to the file which contains expected response for + // PRECONDITION_FAILED response. + base::FilePath expected_precondition_failed_file_path_; + + // These are content and its type in the expected response from the server. + // See also HandleContentResponse below. + std::string expected_content_type_; + std::string expected_content_; + + // The incoming HTTP request is saved so tests can verify the request + // parameters like HTTP method (ex. some requests should use DELETE + // instead of GET). + net::test_server::HttpRequest http_request_; + + private: + void ResetExpectedResponse() { + expected_data_file_path_.clear(); + expected_upload_path_.clear(); + expected_content_type_.clear(); + expected_content_.clear(); + } + + // For "Children: delete" request, the server will return "204 No Content" + // response meaning "success". + scoped_ptr<net::test_server::HttpResponse> HandleChildrenDeleteRequest( + const net::test_server::HttpRequest& request) { + if (request.method != net::test_server::METHOD_DELETE || + request.relative_url.find("/children/") == string::npos) { + // The request is not the "Children: delete" request. Delegate the + // processing to the next handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + // Return the response with just "204 No Content" status code. + scoped_ptr<net::test_server::BasicHttpResponse> http_response( + new net::test_server::BasicHttpResponse); + http_response->set_code(net::HTTP_NO_CONTENT); + return http_response.PassAs<net::test_server::HttpResponse>(); + } + + // Reads the data file of |expected_data_file_path_| and returns its content + // for the request. + // To use this method, it is necessary to set |expected_data_file_path_| + // to the appropriate file path before sending the request to the server. + scoped_ptr<net::test_server::HttpResponse> HandleDataFileRequest( + const net::test_server::HttpRequest& request) { + if (expected_data_file_path_.empty()) { + // The file is not specified. Delegate the processing to the next + // handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + // Return the response from the data file. + return test_util::CreateHttpResponseFromFile( + expected_data_file_path_).PassAs<net::test_server::HttpResponse>(); + } + + // Deletes the resource and returns no content with HTTP_NO_CONTENT status + // code. + scoped_ptr<net::test_server::HttpResponse> HandleDeleteRequest( + const net::test_server::HttpRequest& request) { + if (request.method != net::test_server::METHOD_DELETE || + request.relative_url.find("/files/") == string::npos) { + // The file is not file deletion request. Delegate the processing to the + // next handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(net::HTTP_NO_CONTENT); + + return response.PassAs<net::test_server::HttpResponse>(); + } + + // Returns PRECONDITION_FAILED response for ETag mismatching with error JSON + // content specified by |expected_precondition_failed_file_path_|. + // To use this method, it is necessary to set the variable to the appropriate + // file path before sending the request to the server. + scoped_ptr<net::test_server::HttpResponse> HandlePreconditionFailedRequest( + const net::test_server::HttpRequest& request) { + if (expected_precondition_failed_file_path_.empty()) { + // The file is not specified. Delegate the process to the next handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(net::HTTP_PRECONDITION_FAILED); + + std::string content; + if (base::ReadFileToString(expected_precondition_failed_file_path_, + &content)) { + response->set_content(content); + response->set_content_type("application/json"); + } + + return response.PassAs<net::test_server::HttpResponse>(); + } + + // Returns the response based on set expected upload url. + // The response contains the url in its "Location: " header. Also, it doesn't + // have any content. + // To use this method, it is necessary to set |expected_upload_path_| + // to the string representation of the url to be returned. + scoped_ptr<net::test_server::HttpResponse> HandleInitiateUploadRequest( + const net::test_server::HttpRequest& request) { + if (request.relative_url == expected_upload_path_ || + expected_upload_path_.empty()) { + // The request is for resume uploading or the expected upload url is not + // set. Delegate the processing to the next handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + + // Check if the X-Upload-Content-Length is present. If yes, store the + // length of the file. + std::map<std::string, std::string>::const_iterator found = + request.headers.find("X-Upload-Content-Length"); + if (found == request.headers.end() || + !base::StringToInt64(found->second, &content_length_)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + received_bytes_ = 0; + + response->set_code(net::HTTP_OK); + response->AddCustomHeader( + "Location", + test_server_.base_url().Resolve(expected_upload_path_).spec()); + return response.PassAs<net::test_server::HttpResponse>(); + } + + scoped_ptr<net::test_server::HttpResponse> HandleResumeUploadRequest( + const net::test_server::HttpRequest& request) { + if (request.relative_url != expected_upload_path_) { + // The request path is different from the expected path for uploading. + // Delegate the processing to the next handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + if (!request.content.empty()) { + std::map<std::string, std::string>::const_iterator iter = + request.headers.find("Content-Range"); + if (iter == request.headers.end()) { + // The range must be set. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + int64 length = 0; + int64 start_position = 0; + int64 end_position = 0; + if (!test_util::ParseContentRangeHeader( + iter->second, &start_position, &end_position, &length)) { + // Invalid "Content-Range" value. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + EXPECT_EQ(start_position, received_bytes_); + EXPECT_EQ(length, content_length_); + + // end_position is inclusive, but so +1 to change the range to byte size. + received_bytes_ = end_position + 1; + } + + if (received_bytes_ < content_length_) { + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + // Set RESUME INCOMPLETE (308) status code. + response->set_code(static_cast<net::HttpStatusCode>(308)); + + // Add Range header to the response, based on the values of + // Content-Range header in the request. + // The header is annotated only when at least one byte is received. + if (received_bytes_ > 0) { + response->AddCustomHeader( + "Range", "bytes=0-" + base::Int64ToString(received_bytes_ - 1)); + } + + return response.PassAs<net::test_server::HttpResponse>(); + } + + // All bytes are received. Return the "success" response with the file's + // (dummy) metadata. + scoped_ptr<net::test_server::BasicHttpResponse> response = + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("drive/file_entry.json")); + + // The response code is CREATED if it is new file uploading. + if (http_request_.relative_url == kTestUploadNewFilePath) { + response->set_code(net::HTTP_CREATED); + } + + return response.PassAs<net::test_server::HttpResponse>(); + } + + // Returns the response based on set expected content and its type. + // To use this method, both |expected_content_type_| and |expected_content_| + // must be set in advance. + scoped_ptr<net::test_server::HttpResponse> HandleContentResponse( + const net::test_server::HttpRequest& request) { + if (expected_content_type_.empty() || expected_content_.empty()) { + // Expected content is not set. Delegate the processing to the next + // handler. + return scoped_ptr<net::test_server::HttpResponse>(); + } + + http_request_ = request; + + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(net::HTTP_OK); + response->set_content_type(expected_content_type_); + response->set_content(expected_content_); + return response.PassAs<net::test_server::HttpResponse>(); + } + + // Handles a request for downloading a file. + scoped_ptr<net::test_server::HttpResponse> HandleDownloadRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + std::string id; + if (!test_util::RemovePrefix(absolute_url.path(), + kTestDownloadPathPrefix, + &id)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + + // For testing, returns a text with |id| repeated 3 times. + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(net::HTTP_OK); + response->set_content(id + id + id); + response->set_content_type("text/plain"); + return response.PassAs<net::test_server::HttpResponse>(); + } + + // These are for the current upload file status. + int64 received_bytes_; + int64 content_length_; +}; + +TEST_F(DriveApiRequestsTest, DriveApiDataRequest_Fields) { + // Make sure that "fields" query param is supported by using its subclass, + // AboutGetRequest. + + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/about.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<AboutResource> about_resource; + + { + base::RunLoop run_loop; + drive::AboutGetRequest* request = new drive::AboutGetRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &about_resource))); + request->set_fields( + "kind,quotaBytesTotal,quotaBytesUsed,largestChangeId,rootFolderId"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/about?" + "fields=kind%2CquotaBytesTotal%2CquotaBytesUsed%2C" + "largestChangeId%2CrootFolderId", + http_request_.relative_url); + + scoped_ptr<AboutResource> expected( + AboutResource::CreateFrom( + *test_util::LoadJSONFile("drive/about.json"))); + ASSERT_TRUE(about_resource.get()); + EXPECT_EQ(expected->largest_change_id(), about_resource->largest_change_id()); + EXPECT_EQ(expected->quota_bytes_total(), about_resource->quota_bytes_total()); + EXPECT_EQ(expected->quota_bytes_used(), about_resource->quota_bytes_used()); + EXPECT_EQ(expected->root_folder_id(), about_resource->root_folder_id()); +} + +TEST_F(DriveApiRequestsTest, FilesInsertRequest) { + // Set an expected data file containing the directory's entry data. + expected_data_file_path_ = + test_util::GetTestFilePath("drive/directory_entry.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileResource> file_resource; + + // Create "new directory" in the root directory. + { + base::RunLoop run_loop; + drive::FilesInsertRequest* request = new drive::FilesInsertRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_mime_type("application/vnd.google-apps.folder"); + request->add_parent("root"); + request->set_title("new directory"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files", http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + + scoped_ptr<FileResource> expected( + FileResource::CreateFrom( + *test_util::LoadJSONFile("drive/directory_entry.json"))); + + // Sanity check. + ASSERT_TRUE(file_resource.get()); + + EXPECT_EQ(expected->file_id(), file_resource->file_id()); + EXPECT_EQ(expected->title(), file_resource->title()); + EXPECT_EQ(expected->mime_type(), file_resource->mime_type()); + EXPECT_EQ(expected->parents().size(), file_resource->parents().size()); +} + +TEST_F(DriveApiRequestsTest, FilesPatchRequest) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + const base::Time::Exploded kLastViewedByMeDate = + {2013, 7, 0, 19, 15, 59, 13, 123}; + + // Set an expected data file containing valid result. + expected_data_file_path_ = + test_util::GetTestFilePath("drive/file_entry.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileResource> file_resource; + + { + base::RunLoop run_loop; + drive::FilesPatchRequest* request = new drive::FilesPatchRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_file_id("resource_id"); + request->set_set_modified_date(true); + request->set_update_viewed_date(false); + + request->set_title("new title"); + request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate)); + request->set_last_viewed_by_me_date( + base::Time::FromUTCExploded(kLastViewedByMeDate)); + request->add_parent("parent_resource_id"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_PATCH, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id" + "?setModifiedDate=true&updateViewedDate=false", + http_request_.relative_url); + + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"lastViewedByMeDate\":\"2013-07-19T15:59:13.123Z\"," + "\"modifiedDate\":\"2012-07-19T15:59:13.123Z\"," + "\"parents\":[{\"id\":\"parent_resource_id\"}]," + "\"title\":\"new title\"}", + http_request_.content); + EXPECT_TRUE(file_resource); +} + +TEST_F(DriveApiRequestsTest, AboutGetRequest_ValidJson) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/about.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<AboutResource> about_resource; + + { + base::RunLoop run_loop; + drive::AboutGetRequest* request = new drive::AboutGetRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &about_resource))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/about", http_request_.relative_url); + + scoped_ptr<AboutResource> expected( + AboutResource::CreateFrom( + *test_util::LoadJSONFile("drive/about.json"))); + ASSERT_TRUE(about_resource.get()); + EXPECT_EQ(expected->largest_change_id(), about_resource->largest_change_id()); + EXPECT_EQ(expected->quota_bytes_total(), about_resource->quota_bytes_total()); + EXPECT_EQ(expected->quota_bytes_used(), about_resource->quota_bytes_used()); + EXPECT_EQ(expected->root_folder_id(), about_resource->root_folder_id()); +} + +TEST_F(DriveApiRequestsTest, AboutGetRequest_InvalidJson) { + // Set an expected data file containing invalid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "gdata/testfile.txt"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<AboutResource> about_resource; + + { + base::RunLoop run_loop; + drive::AboutGetRequest* request = new drive::AboutGetRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &about_resource))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + // "parse error" should be returned, and the about resource should be NULL. + EXPECT_EQ(GDATA_PARSE_ERROR, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/about", http_request_.relative_url); + EXPECT_FALSE(about_resource); +} + +TEST_F(DriveApiRequestsTest, AppsListRequest) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/applist.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<AppList> app_list; + + { + base::RunLoop run_loop; + drive::AppsListRequest* request = new drive::AppsListRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &app_list))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/apps", http_request_.relative_url); + EXPECT_TRUE(app_list); +} + +TEST_F(DriveApiRequestsTest, ChangesListRequest) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/changelist.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<ChangeList> result; + + { + base::RunLoop run_loop; + drive::ChangesListRequest* request = new drive::ChangesListRequest( + request_sender_.get(), *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &result))); + request->set_include_deleted(true); + request->set_start_change_id(100); + request->set_max_results(500); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/changes?maxResults=500&startChangeId=100", + http_request_.relative_url); + EXPECT_TRUE(result); +} + +TEST_F(DriveApiRequestsTest, ChangesListNextPageRequest) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/changelist.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<ChangeList> result; + + { + base::RunLoop run_loop; + drive::ChangesListNextPageRequest* request = + new drive::ChangesListNextPageRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &result))); + request->set_next_link(test_server_.GetURL("/continue/get/change/list")); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/continue/get/change/list", http_request_.relative_url); + EXPECT_TRUE(result); +} + +TEST_F(DriveApiRequestsTest, FilesCopyRequest) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + + // Set an expected data file containing the dummy file entry data. + // It'd be returned if we copy a file. + expected_data_file_path_ = + test_util::GetTestFilePath("drive/file_entry.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileResource> file_resource; + + // Copy the file to a new file named "new title". + { + base::RunLoop run_loop; + drive::FilesCopyRequest* request = new drive::FilesCopyRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_file_id("resource_id"); + request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate)); + request->add_parent("parent_resource_id"); + request->set_title("new title"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id/copy", http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ( + "{\"modifiedDate\":\"2012-07-19T15:59:13.123Z\"," + "\"parents\":[{\"id\":\"parent_resource_id\"}],\"title\":\"new title\"}", + http_request_.content); + EXPECT_TRUE(file_resource); +} + +TEST_F(DriveApiRequestsTest, FilesCopyRequest_EmptyParentResourceId) { + // Set an expected data file containing the dummy file entry data. + // It'd be returned if we copy a file. + expected_data_file_path_ = + test_util::GetTestFilePath("drive/file_entry.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileResource> file_resource; + + // Copy the file to a new file named "new title". + { + base::RunLoop run_loop; + drive::FilesCopyRequest* request = new drive::FilesCopyRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_file_id("resource_id"); + request->set_title("new title"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id/copy", http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"title\":\"new title\"}", http_request_.content); + EXPECT_TRUE(file_resource); +} + +TEST_F(DriveApiRequestsTest, FilesListRequest) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/filelist.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileList> result; + + { + base::RunLoop run_loop; + drive::FilesListRequest* request = new drive::FilesListRequest( + request_sender_.get(), *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &result))); + request->set_max_results(50); + request->set_q("\"abcde\" in parents"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/drive/v2/files?maxResults=50&q=%22abcde%22+in+parents", + http_request_.relative_url); + EXPECT_TRUE(result); +} + +TEST_F(DriveApiRequestsTest, FilesListNextPageRequest) { + // Set an expected data file containing valid result. + expected_data_file_path_ = test_util::GetTestFilePath( + "drive/filelist.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileList> result; + + { + base::RunLoop run_loop; + drive::FilesListNextPageRequest* request = + new drive::FilesListNextPageRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &result))); + request->set_next_link(test_server_.GetURL("/continue/get/file/list")); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/continue/get/file/list", http_request_.relative_url); + EXPECT_TRUE(result); +} + +TEST_F(DriveApiRequestsTest, FilesDeleteRequest) { + GDataErrorCode error = GDATA_OTHER_ERROR; + + // Delete a resource with the given resource id. + { + base::RunLoop run_loop; + drive::FilesDeleteRequest* request = new drive::FilesDeleteRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error))); + request->set_file_id("resource_id"); + request->set_etag(kTestETag); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_NO_CONTENT, error); + EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + EXPECT_EQ("/drive/v2/files/resource_id", http_request_.relative_url); + EXPECT_FALSE(http_request_.has_content); +} + +TEST_F(DriveApiRequestsTest, FilesTrashRequest) { + // Set data for the expected result. Directory entry should be returned + // if the trashing entry is a directory, so using it here should be fine. + expected_data_file_path_ = + test_util::GetTestFilePath("drive/directory_entry.json"); + + GDataErrorCode error = GDATA_OTHER_ERROR; + scoped_ptr<FileResource> file_resource; + + // Trash a resource with the given resource id. + { + base::RunLoop run_loop; + drive::FilesTrashRequest* request = new drive::FilesTrashRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_file_id("resource_id"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id/trash", http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); +} + +TEST_F(DriveApiRequestsTest, ChildrenInsertRequest) { + // Set an expected data file containing the children entry. + expected_content_type_ = "application/json"; + expected_content_ = kTestChildrenResponse; + + GDataErrorCode error = GDATA_OTHER_ERROR; + + // Add a resource with "resource_id" to a directory with + // "parent_resource_id". + { + base::RunLoop run_loop; + drive::ChildrenInsertRequest* request = new drive::ChildrenInsertRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error))); + request->set_folder_id("parent_resource_id"); + request->set_id("resource_id"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/parent_resource_id/children", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"id\":\"resource_id\"}", http_request_.content); +} + +TEST_F(DriveApiRequestsTest, ChildrenDeleteRequest) { + GDataErrorCode error = GDATA_OTHER_ERROR; + + // Remove a resource with "resource_id" from a directory with + // "parent_resource_id". + { + base::RunLoop run_loop; + drive::ChildrenDeleteRequest* request = new drive::ChildrenDeleteRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error))); + request->set_child_id("resource_id"); + request->set_folder_id("parent_resource_id"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_NO_CONTENT, error); + EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); + EXPECT_EQ("/drive/v2/files/parent_resource_id/children/resource_id", + http_request_.relative_url); + EXPECT_FALSE(http_request_.has_content); +} + +TEST_F(DriveApiRequestsTest, UploadNewFileRequest) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadNewFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with + // "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadNewFileRequest* request = + new drive::InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"parents\":[{" + "\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"" + "}]," + "\"title\":\"new file title\"}", + http_request_.content); + + // Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiRequestsTest, UploadNewEmptyFileRequest) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadNewFilePath; + + const char kTestContentType[] = "text/plain"; + const char kTestContent[] = ""; + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("empty_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadNewFileRequest* request = + new drive::InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + 0, + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ("0", http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"parents\":[{" + "\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"" + "}]," + "\"title\":\"new file title\"}", + http_request_.content); + + // Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + 0, // start_position + 0, // end_position (exclusive) + 0, // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should NOT be added. + EXPECT_EQ(0U, http_request_.headers.count("Content-Range")); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiRequestsTest, UploadNewLargeFileRequest) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadNewFilePath; + + const char kTestContentType[] = "text/plain"; + const size_t kNumChunkBytes = 10; // Num bytes in a chunk. + const std::string kTestContent(100, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadNewFileRequest* request = + new drive::InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"parents\":[{" + "\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"" + "}]," + "\"title\":\"new file title\"}", + http_request_.content); + + // Before sending any data, check the current status. + // This is an edge case test for GetUploadStatusRequest. + { + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + // Check the response by GetUploadStatusRequest. + { + base::RunLoop run_loop; + drive::GetUploadStatusRequest* get_upload_status_request = + new drive::GetUploadStatusRequest( + request_sender_.get(), + upload_url, + kTestContent.size(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry))); + request_sender_->StartRequestWithRetry(get_upload_status_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes */" + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(0, response.end_position_received); + } + + // Upload the content to the upload URL. + for (size_t start_position = 0; start_position < kTestContent.size(); + start_position += kNumChunkBytes) { + const std::string payload = kTestContent.substr( + start_position, + std::min(kNumChunkBytes, kTestContent.size() - start_position)); + const size_t end_position = start_position + payload.size(); + + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + start_position, + end_position, + kTestContent.size(), // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes " + + base::Int64ToString(start_position) + "-" + + base::Int64ToString(end_position - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(payload, http_request_.content); + + if (end_position == kTestContent.size()) { + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is + // complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); + break; + } + + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(static_cast<int64>(end_position), response.end_position_received); + + // Check the response by GetUploadStatusRequest. + { + base::RunLoop run_loop; + drive::GetUploadStatusRequest* get_upload_status_request = + new drive::GetUploadStatusRequest( + request_sender_.get(), + upload_url, + kTestContent.size(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry))); + request_sender_->StartRequestWithRetry(get_upload_status_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes */" + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(static_cast<int64>(end_position), + response.end_position_received); + } +} + +TEST_F(DriveApiRequestsTest, UploadExistingFileRequest) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadExistingFileRequest* request = + new drive::InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + std::string(), // No etag. + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiRequestsTest, UploadExistingFileRequestWithETag) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadExistingFileRequest* request = + new drive::InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + kTestETag, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +TEST_F(DriveApiRequestsTest, UploadExistingFileRequestWithETagConflicting) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + // If it turned out that the etag is conflicting, PRECONDITION_FAILED should + // be returned. + expected_precondition_failed_file_path_ = + test_util::GetTestFilePath("drive/error.json"); + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadExistingFileRequest* request = + new drive::InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + "Conflicting-etag", + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_PRECONDITION, error); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ("Conflicting-etag", http_request_.headers["If-Match"]); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); +} + +TEST_F(DriveApiRequestsTest, + UploadExistingFileRequestWithETagConflictOnResumeUpload) { + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kTestContent)); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadExistingFileRequest* request = + new drive::InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + kTestETag, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?uploadType=resumable", + http_request_.relative_url); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Set PRECONDITION_FAILED to the server. This is the emulation of the + // confliction during uploading. + expected_precondition_failed_file_path_ = + test_util::GetTestFilePath("drive/error.json"); + + // Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<FileResource> new_entry; + + { + base::RunLoop run_loop; + drive::ResumeUploadRequest* resume_request = + new drive::ResumeUploadRequest( + request_sender_.get(), + upload_url, + 0, // start_position + kTestContent.size(), // end_position (exclusive) + kTestContent.size(), // content_length, + kTestContentType, + kTestFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback()); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kTestContent.size() - 1) + "/" + + base::Int64ToString(kTestContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kTestContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_PRECONDITION, response.code); + // The start and end positions should be set to -1 for error. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); + + // New entry should be NULL. + EXPECT_FALSE(new_entry.get()); +} + +TEST_F(DriveApiRequestsTest, DownloadFileRequest) { + const base::FilePath kDownloadedFilePath = + temp_dir_.path().AppendASCII("cache_file"); + const std::string kTestId("dummyId"); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + base::FilePath temp_file; + { + base::RunLoop run_loop; + drive::DownloadFileRequest* request = new drive::DownloadFileRequest( + request_sender_.get(), + *url_generator_, + kTestId, + kDownloadedFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &temp_file)), + GetContentCallback(), + ProgressCallback()); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + std::string contents; + base::ReadFileToString(temp_file, &contents); + base::DeleteFile(temp_file, false); + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url); + EXPECT_EQ(kDownloadedFilePath, temp_file); + + const std::string expected_contents = kTestId + kTestId + kTestId; + EXPECT_EQ(expected_contents, contents); +} + +TEST_F(DriveApiRequestsTest, DownloadFileRequest_GetContentCallback) { + const base::FilePath kDownloadedFilePath = + temp_dir_.path().AppendASCII("cache_file"); + const std::string kTestId("dummyId"); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + base::FilePath temp_file; + std::string contents; + { + base::RunLoop run_loop; + drive::DownloadFileRequest* request = new drive::DownloadFileRequest( + request_sender_.get(), + *url_generator_, + kTestId, + kDownloadedFilePath, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &temp_file)), + base::Bind(&AppendContent, &contents), + ProgressCallback()); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + base::DeleteFile(temp_file, false); + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url); + EXPECT_EQ(kDownloadedFilePath, temp_file); + + const std::string expected_contents = kTestId + kTestId + kTestId; + EXPECT_EQ(expected_contents, contents); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_url_generator.cc b/chromium/google_apis/drive/drive_api_url_generator.cc new file mode 100644 index 00000000000..c12b947d3dc --- /dev/null +++ b/chromium/google_apis/drive/drive_api_url_generator.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2012 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 "google_apis/drive/drive_api_url_generator.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "net/base/escape.h" +#include "net/base/url_util.h" + +namespace google_apis { + +namespace { + +// Hard coded URLs for communication with a google drive server. +const char kDriveV2AboutUrl[] = "/drive/v2/about"; +const char kDriveV2AppsUrl[] = "/drive/v2/apps"; +const char kDriveV2ChangelistUrl[] = "/drive/v2/changes"; +const char kDriveV2FilesUrl[] = "/drive/v2/files"; +const char kDriveV2FileUrlPrefix[] = "/drive/v2/files/"; +const char kDriveV2ChildrenUrlFormat[] = "/drive/v2/files/%s/children"; +const char kDriveV2ChildrenUrlForRemovalFormat[] = + "/drive/v2/files/%s/children/%s"; +const char kDriveV2FileCopyUrlFormat[] = "/drive/v2/files/%s/copy"; +const char kDriveV2FileDeleteUrlFormat[] = "/drive/v2/files/%s"; +const char kDriveV2FileTrashUrlFormat[] = "/drive/v2/files/%s/trash"; +const char kDriveV2InitiateUploadNewFileUrl[] = "/upload/drive/v2/files"; +const char kDriveV2InitiateUploadExistingFileUrlPrefix[] = + "/upload/drive/v2/files/"; + +GURL AddResumableUploadParam(const GURL& url) { + return net::AppendOrReplaceQueryParameter(url, "uploadType", "resumable"); +} + +} // namespace + +DriveApiUrlGenerator::DriveApiUrlGenerator(const GURL& base_url, + const GURL& base_download_url) + : base_url_(base_url), + base_download_url_(base_download_url) { + // Do nothing. +} + +DriveApiUrlGenerator::~DriveApiUrlGenerator() { + // Do nothing. +} + +const char DriveApiUrlGenerator::kBaseUrlForProduction[] = + "https://www.googleapis.com"; +const char DriveApiUrlGenerator::kBaseDownloadUrlForProduction[] = + "https://www.googledrive.com/host/"; + +GURL DriveApiUrlGenerator::GetAboutGetUrl() const { + return base_url_.Resolve(kDriveV2AboutUrl); +} + +GURL DriveApiUrlGenerator::GetAppsListUrl() const { + return base_url_.Resolve(kDriveV2AppsUrl); +} + +GURL DriveApiUrlGenerator::GetFilesGetUrl(const std::string& file_id) const { + return base_url_.Resolve(kDriveV2FileUrlPrefix + net::EscapePath(file_id)); +} + +GURL DriveApiUrlGenerator::GetFilesInsertUrl() const { + return base_url_.Resolve(kDriveV2FilesUrl); +} + +GURL DriveApiUrlGenerator::GetFilesPatchUrl(const std::string& file_id, + bool set_modified_date, + bool update_viewed_date) const { + GURL url = + base_url_.Resolve(kDriveV2FileUrlPrefix + net::EscapePath(file_id)); + + // setModifiedDate is "false" by default. + if (set_modified_date) + url = net::AppendOrReplaceQueryParameter(url, "setModifiedDate", "true"); + + // updateViewedDate is "true" by default. + if (!update_viewed_date) + url = net::AppendOrReplaceQueryParameter(url, "updateViewedDate", "false"); + + return url; +} + +GURL DriveApiUrlGenerator::GetFilesCopyUrl(const std::string& file_id) const { + return base_url_.Resolve(base::StringPrintf( + kDriveV2FileCopyUrlFormat, net::EscapePath(file_id).c_str())); +} + +GURL DriveApiUrlGenerator::GetFilesListUrl(int max_results, + const std::string& page_token, + const std::string& q) const { + GURL url = base_url_.Resolve(kDriveV2FilesUrl); + + // maxResults is 100 by default. + if (max_results != 100) { + url = net::AppendOrReplaceQueryParameter( + url, "maxResults", base::IntToString(max_results)); + } + + if (!page_token.empty()) + url = net::AppendOrReplaceQueryParameter(url, "pageToken", page_token); + + if (!q.empty()) + url = net::AppendOrReplaceQueryParameter(url, "q", q); + + return url; +} + +GURL DriveApiUrlGenerator::GetFilesDeleteUrl(const std::string& file_id) const { + return base_url_.Resolve(base::StringPrintf( + kDriveV2FileDeleteUrlFormat, net::EscapePath(file_id).c_str())); +} + +GURL DriveApiUrlGenerator::GetFilesTrashUrl(const std::string& file_id) const { + return base_url_.Resolve(base::StringPrintf( + kDriveV2FileTrashUrlFormat, net::EscapePath(file_id).c_str())); +} + +GURL DriveApiUrlGenerator::GetChangesListUrl(bool include_deleted, + int max_results, + const std::string& page_token, + int64 start_change_id) const { + DCHECK_GE(start_change_id, 0); + + GURL url = base_url_.Resolve(kDriveV2ChangelistUrl); + + // includeDeleted is "true" by default. + if (!include_deleted) + url = net::AppendOrReplaceQueryParameter(url, "includeDeleted", "false"); + + // maxResults is "100" by default. + if (max_results != 100) { + url = net::AppendOrReplaceQueryParameter( + url, "maxResults", base::IntToString(max_results)); + } + + if (!page_token.empty()) + url = net::AppendOrReplaceQueryParameter(url, "pageToken", page_token); + + if (start_change_id > 0) + url = net::AppendOrReplaceQueryParameter( + url, "startChangeId", base::Int64ToString(start_change_id)); + + return url; +} + +GURL DriveApiUrlGenerator::GetChildrenInsertUrl( + const std::string& file_id) const { + return base_url_.Resolve(base::StringPrintf( + kDriveV2ChildrenUrlFormat, net::EscapePath(file_id).c_str())); +} + +GURL DriveApiUrlGenerator::GetChildrenDeleteUrl( + const std::string& child_id, const std::string& folder_id) const { + return base_url_.Resolve( + base::StringPrintf(kDriveV2ChildrenUrlForRemovalFormat, + net::EscapePath(folder_id).c_str(), + net::EscapePath(child_id).c_str())); +} + +GURL DriveApiUrlGenerator::GetInitiateUploadNewFileUrl() const { + return AddResumableUploadParam( + base_url_.Resolve(kDriveV2InitiateUploadNewFileUrl)); +} + +GURL DriveApiUrlGenerator::GetInitiateUploadExistingFileUrl( + const std::string& resource_id) const { + const GURL& url = base_url_.Resolve( + kDriveV2InitiateUploadExistingFileUrlPrefix + + net::EscapePath(resource_id)); + return AddResumableUploadParam(url); +} + +GURL DriveApiUrlGenerator::GenerateDownloadFileUrl( + const std::string& resource_id) const { + return base_download_url_.Resolve(net::EscapePath(resource_id)); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_url_generator.h b/chromium/google_apis/drive/drive_api_url_generator.h new file mode 100644 index 00000000000..cf93edd3255 --- /dev/null +++ b/chromium/google_apis/drive/drive_api_url_generator.h @@ -0,0 +1,93 @@ +// Copyright (c) 2013 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. + +#ifndef GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_ +#define GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_ + +#include <string> + +#include "url/gurl.h" + +namespace google_apis { + +// This class is used to generate URLs for communicating with drive api +// servers for production, and a local server for testing. +class DriveApiUrlGenerator { + public: + // |base_url| is the path to the target drive api server. + // Note that this is an injecting point for a testing server. + DriveApiUrlGenerator(const GURL& base_url, const GURL& base_download_url); + ~DriveApiUrlGenerator(); + + // The base URL for communicating with the production drive api server. + static const char kBaseUrlForProduction[]; + + // The base URL for the file download server for production. + static const char kBaseDownloadUrlForProduction[]; + + // Returns a URL to invoke "About: get" method. + GURL GetAboutGetUrl() const; + + // Returns a URL to invoke "Apps: list" method. + GURL GetAppsListUrl() const; + + // Returns a URL to fetch a file metadata. + GURL GetFilesGetUrl(const std::string& file_id) const; + + // Returns a URL to create a resource. + GURL GetFilesInsertUrl() const; + + // Returns a URL to patch file metadata. + GURL GetFilesPatchUrl(const std::string& file_id, + bool set_modified_date, + bool update_viewed_date) const; + + // Returns a URL to copy a resource specified by |file_id|. + GURL GetFilesCopyUrl(const std::string& file_id) const; + + // Returns a URL to fetch file list. + GURL GetFilesListUrl(int max_results, + const std::string& page_token, + const std::string& q) const; + + // Returns a URL to delete a resource with the given |file_id|. + GURL GetFilesDeleteUrl(const std::string& file_id) const; + + // Returns a URL to trash a resource with the given |file_id|. + GURL GetFilesTrashUrl(const std::string& file_id) const; + + // Returns a URL to fetch a list of changes. + GURL GetChangesListUrl(bool include_deleted, + int max_results, + const std::string& page_token, + int64 start_change_id) const; + + // Returns a URL to add a resource to a directory with |folder_id|. + GURL GetChildrenInsertUrl(const std::string& folder_id) const; + + // Returns a URL to remove a resource with |child_id| from a directory + // with |folder_id|. + GURL GetChildrenDeleteUrl(const std::string& child_id, + const std::string& folder_id) const; + + // Returns a URL to initiate uploading a new file. + GURL GetInitiateUploadNewFileUrl() const; + + // Returns a URL to initiate uploading an existing file specified by + // |resource_id|. + GURL GetInitiateUploadExistingFileUrl(const std::string& resource_id) const; + + // Generates a URL for downloading a file. + GURL GenerateDownloadFileUrl(const std::string& resource_id) const; + + private: + const GURL base_url_; + const GURL base_download_url_; + + // This class is copyable hence no DISALLOW_COPY_AND_ASSIGN here. +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DRIVE_API_URL_GENERATOR_H_ diff --git a/chromium/google_apis/drive/drive_api_url_generator_unittest.cc b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc new file mode 100644 index 00000000000..343b2791c4e --- /dev/null +++ b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc @@ -0,0 +1,394 @@ +// Copyright (c) 2013 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 "google_apis/drive/drive_api_url_generator.h" + +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace google_apis { + +class DriveApiUrlGeneratorTest : public testing::Test { + public: + DriveApiUrlGeneratorTest() + : url_generator_( + GURL(DriveApiUrlGenerator::kBaseUrlForProduction), + GURL(DriveApiUrlGenerator::kBaseDownloadUrlForProduction)), + test_url_generator_( + test_util::GetBaseUrlForTesting(12345), + test_util::GetBaseUrlForTesting(12345).Resolve("download/")) { + } + + protected: + DriveApiUrlGenerator url_generator_; + DriveApiUrlGenerator test_url_generator_; +}; + +// Make sure the hard-coded urls are returned. +TEST_F(DriveApiUrlGeneratorTest, GetAboutGetUrl) { + EXPECT_EQ("https://www.googleapis.com/drive/v2/about", + url_generator_.GetAboutGetUrl().spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/about", + test_url_generator_.GetAboutGetUrl().spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetAppsListUrl) { + EXPECT_EQ("https://www.googleapis.com/drive/v2/apps", + url_generator_.GetAppsListUrl().spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/apps", + test_url_generator_.GetAppsListUrl().spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesGetUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg", + url_generator_.GetFilesGetUrl("0ADK06pfg").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074", + url_generator_.GetFilesGetUrl("0Bz0bd074").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id", + url_generator_.GetFilesGetUrl("file:file_id").spec()); + + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg", + test_url_generator_.GetFilesGetUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074", + test_url_generator_.GetFilesGetUrl("0Bz0bd074").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id", + test_url_generator_.GetFilesGetUrl("file:file_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesInsertUrl) { + EXPECT_EQ("https://www.googleapis.com/drive/v2/files", + url_generator_.GetFilesInsertUrl().spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files", + test_url_generator_.GetFilesInsertUrl().spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilePatchUrl) { + struct TestPattern { + bool set_modified_date; + bool update_viewed_date; + const std::string expected_query; + }; + const TestPattern kTestPatterns[] = { + { false, true, "" }, + { true, true, "?setModifiedDate=true" }, + { false, false, "?updateViewedDate=false" }, + { true, false, "?setModifiedDate=true&updateViewedDate=false" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) { + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/0ADK06pfg" + + kTestPatterns[i].expected_query, + url_generator_.GetFilesPatchUrl( + "0ADK06pfg", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/0Bz0bd074" + + kTestPatterns[i].expected_query, + url_generator_.GetFilesPatchUrl( + "0Bz0bd074", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/file%3Afile_id" + + kTestPatterns[i].expected_query, + url_generator_.GetFilesPatchUrl( + "file:file_id", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/0ADK06pfg" + + kTestPatterns[i].expected_query, + test_url_generator_.GetFilesPatchUrl( + "0ADK06pfg", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/0Bz0bd074" + + kTestPatterns[i].expected_query, + test_url_generator_.GetFilesPatchUrl( + "0Bz0bd074", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/file%3Afile_id" + + kTestPatterns[i].expected_query, + test_url_generator_.GetFilesPatchUrl( + "file:file_id", + kTestPatterns[i].set_modified_date, + kTestPatterns[i].update_viewed_date).spec()); + } +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesCopyUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/copy", + url_generator_.GetFilesCopyUrl("0ADK06pfg").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/copy", + url_generator_.GetFilesCopyUrl("0Bz0bd074").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id/copy", + url_generator_.GetFilesCopyUrl("file:file_id").spec()); + + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/copy", + test_url_generator_.GetFilesCopyUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/copy", + test_url_generator_.GetFilesCopyUrl("0Bz0bd074").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id/copy", + test_url_generator_.GetFilesCopyUrl("file:file_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesListUrl) { + struct TestPattern { + int max_results; + const std::string page_token; + const std::string q; + const std::string expected_query; + }; + const TestPattern kTestPatterns[] = { + { 100, "", "", "" }, + { 150, "", "", "?maxResults=150" }, + { 10, "", "", "?maxResults=10" }, + { 100, "token", "", "?pageToken=token" }, + { 150, "token", "", "?maxResults=150&pageToken=token" }, + { 10, "token", "", "?maxResults=10&pageToken=token" }, + { 100, "", "query", "?q=query" }, + { 150, "", "query", "?maxResults=150&q=query" }, + { 10, "", "query", "?maxResults=10&q=query" }, + { 100, "token", "query", "?pageToken=token&q=query" }, + { 150, "token", "query", "?maxResults=150&pageToken=token&q=query" }, + { 10, "token", "query", "?maxResults=10&pageToken=token&q=query" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) { + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files" + + kTestPatterns[i].expected_query, + url_generator_.GetFilesListUrl( + kTestPatterns[i].max_results, kTestPatterns[i].page_token, + kTestPatterns[i].q).spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files" + + kTestPatterns[i].expected_query, + test_url_generator_.GetFilesListUrl( + kTestPatterns[i].max_results, kTestPatterns[i].page_token, + kTestPatterns[i].q).spec()); + } +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesDeleteUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg", + url_generator_.GetFilesDeleteUrl("0ADK06pfg").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074", + url_generator_.GetFilesDeleteUrl("0Bz0bd074").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id", + url_generator_.GetFilesDeleteUrl("file:file_id").spec()); + + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg", + test_url_generator_.GetFilesDeleteUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074", + test_url_generator_.GetFilesDeleteUrl("0Bz0bd074").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id", + test_url_generator_.GetFilesDeleteUrl("file:file_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetFilesTrashUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/trash", + url_generator_.GetFilesTrashUrl("0ADK06pfg").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/trash", + url_generator_.GetFilesTrashUrl("0Bz0bd074").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/file%3Afile_id/trash", + url_generator_.GetFilesTrashUrl("file:file_id").spec()); + + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/trash", + test_url_generator_.GetFilesTrashUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/trash", + test_url_generator_.GetFilesTrashUrl("0Bz0bd074").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afile_id/trash", + test_url_generator_.GetFilesTrashUrl("file:file_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetChangesListUrl) { + struct TestPattern { + bool include_deleted; + int max_results; + const std::string page_token; + int64 start_change_id; + const std::string expected_query; + }; + const TestPattern kTestPatterns[] = { + { true, 100, "", 0, "" }, + { false, 100, "", 0, "?includeDeleted=false" }, + { true, 150, "", 0, "?maxResults=150" }, + { false, 150, "", 0, "?includeDeleted=false&maxResults=150" }, + { true, 10, "", 0, "?maxResults=10" }, + { false, 10, "", 0, "?includeDeleted=false&maxResults=10" }, + + { true, 100, "token", 0, "?pageToken=token" }, + { false, 100, "token", 0, "?includeDeleted=false&pageToken=token" }, + { true, 150, "token", 0, "?maxResults=150&pageToken=token" }, + { false, 150, "token", 0, + "?includeDeleted=false&maxResults=150&pageToken=token" }, + { true, 10, "token", 0, "?maxResults=10&pageToken=token" }, + { false, 10, "token", 0, + "?includeDeleted=false&maxResults=10&pageToken=token" }, + + { true, 100, "", 12345, "?startChangeId=12345" }, + { false, 100, "", 12345, "?includeDeleted=false&startChangeId=12345" }, + { true, 150, "", 12345, "?maxResults=150&startChangeId=12345" }, + { false, 150, "", 12345, + "?includeDeleted=false&maxResults=150&startChangeId=12345" }, + { true, 10, "", 12345, "?maxResults=10&startChangeId=12345" }, + { false, 10, "", 12345, + "?includeDeleted=false&maxResults=10&startChangeId=12345" }, + + { true, 100, "token", 12345, "?pageToken=token&startChangeId=12345" }, + { false, 100, "token", 12345, + "?includeDeleted=false&pageToken=token&startChangeId=12345" }, + { true, 150, "token", 12345, + "?maxResults=150&pageToken=token&startChangeId=12345" }, + { false, 150, "token", 12345, + "?includeDeleted=false&maxResults=150&pageToken=token" + "&startChangeId=12345" }, + { true, 10, "token", 12345, + "?maxResults=10&pageToken=token&startChangeId=12345" }, + { false, 10, "token", 12345, + "?includeDeleted=false&maxResults=10&pageToken=token" + "&startChangeId=12345" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestPatterns); ++i) { + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/changes" + + kTestPatterns[i].expected_query, + url_generator_.GetChangesListUrl( + kTestPatterns[i].include_deleted, + kTestPatterns[i].max_results, + kTestPatterns[i].page_token, + kTestPatterns[i].start_change_id).spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/changes" + + kTestPatterns[i].expected_query, + test_url_generator_.GetChangesListUrl( + kTestPatterns[i].include_deleted, + kTestPatterns[i].max_results, + kTestPatterns[i].page_token, + kTestPatterns[i].start_change_id).spec()); + } +} + +TEST_F(DriveApiUrlGeneratorTest, GetChildrenInsertUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/children", + url_generator_.GetChildrenInsertUrl("0ADK06pfg").spec()); + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0Bz0bd074/children", + url_generator_.GetChildrenInsertUrl("0Bz0bd074").spec()); + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/file%3Afolder_id/children", + url_generator_.GetChildrenInsertUrl("file:folder_id").spec()); + + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/children", + test_url_generator_.GetChildrenInsertUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/children", + test_url_generator_.GetChildrenInsertUrl("0Bz0bd074").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Afolder_id/children", + test_url_generator_.GetChildrenInsertUrl("file:folder_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetChildrenDeleteUrl) { + // |file_id| should be embedded into the url. + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/0ADK06pfg/children/0Bz0bd074", + url_generator_.GetChildrenDeleteUrl("0Bz0bd074", "0ADK06pfg").spec()); + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/0Bz0bd074/children/0ADK06pfg", + url_generator_.GetChildrenDeleteUrl("0ADK06pfg", "0Bz0bd074").spec()); + EXPECT_EQ( + "https://www.googleapis.com/drive/v2/files/file%3Afolder_id/children" + "/file%3Achild_id", + url_generator_.GetChildrenDeleteUrl( + "file:child_id", "file:folder_id").spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/0ADK06pfg/children/0Bz0bd074", + test_url_generator_.GetChildrenDeleteUrl( + "0Bz0bd074", "0ADK06pfg").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/0Bz0bd074/children/0ADK06pfg", + test_url_generator_.GetChildrenDeleteUrl( + "0ADK06pfg", "0Bz0bd074").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2/files/file%3Afolder_id/children/" + "file%3Achild_id", + test_url_generator_.GetChildrenDeleteUrl( + "file:child_id", "file:folder_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadNewFileUrl) { + EXPECT_EQ( + "https://www.googleapis.com/upload/drive/v2/files?uploadType=resumable", + url_generator_.GetInitiateUploadNewFileUrl().spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files?uploadType=resumable", + test_url_generator_.GetInitiateUploadNewFileUrl().spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadExistingFileUrl) { + // |resource_id| should be embedded into the url. + EXPECT_EQ( + "https://www.googleapis.com/upload/drive/v2/files/0ADK06pfg" + "?uploadType=resumable", + url_generator_.GetInitiateUploadExistingFileUrl("0ADK06pfg").spec()); + EXPECT_EQ( + "https://www.googleapis.com/upload/drive/v2/files/0Bz0bd074" + "?uploadType=resumable", + url_generator_.GetInitiateUploadExistingFileUrl("0Bz0bd074").spec()); + EXPECT_EQ( + "https://www.googleapis.com/upload/drive/v2/files/file%3Afile_id" + "?uploadType=resumable", + url_generator_.GetInitiateUploadExistingFileUrl("file:file_id").spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files/0ADK06pfg" + "?uploadType=resumable", + test_url_generator_.GetInitiateUploadExistingFileUrl( + "0ADK06pfg").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files/0Bz0bd074" + "?uploadType=resumable", + test_url_generator_.GetInitiateUploadExistingFileUrl( + "0Bz0bd074").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files/file%3Afile_id" + "?uploadType=resumable", + test_url_generator_.GetInitiateUploadExistingFileUrl( + "file:file_id").spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GenerateDownloadFileUrl) { + EXPECT_EQ( + "https://www.googledrive.com/host/resourceId", + url_generator_.GenerateDownloadFileUrl("resourceId").spec()); + EXPECT_EQ( + "https://www.googledrive.com/host/file%3AresourceId", + url_generator_.GenerateDownloadFileUrl("file:resourceId").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/download/resourceId", + test_url_generator_.GenerateDownloadFileUrl("resourceId").spec()); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/drive_common_callbacks.h b/chromium/google_apis/drive/drive_common_callbacks.h new file mode 100644 index 00000000000..c31bea0358e --- /dev/null +++ b/chromium/google_apis/drive/drive_common_callbacks.h @@ -0,0 +1,62 @@ +// Copyright 2013 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. +// +// This file contains callback types used for communicating with the Drive +// server via WAPI (Documents List API) and Drive API. + +#ifndef GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_ +#define GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_ + +#include "google_apis/drive/base_requests.h" + +namespace google_apis { + +class AboutResource; +class AppList; +class ResourceEntry; +class ResourceList; + +// Callback used for getting ResourceList. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<ResourceList> resource_list)> + GetResourceListCallback; + +// Callback used for getting ResourceEntry. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<ResourceEntry> entry)> + GetResourceEntryCallback; + +// Callback used for getting AboutResource. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<AboutResource> about_resource)> + AboutResourceCallback; + +// Callback used for getting ShareUrl. +typedef base::Callback<void(GDataErrorCode error, + const GURL& share_url)> GetShareUrlCallback; + +// Callback used for getting AppList. +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<AppList> app_list)> AppListCallback; + +// Callback used for handling UploadRangeResponse. +typedef base::Callback<void( + const UploadRangeResponse& response, + scoped_ptr<ResourceEntry> new_entry)> UploadRangeCallback; + +// Callback used for authorizing an app. |open_url| is used to open the target +// file with the authorized app. +typedef base::Callback<void(GDataErrorCode error, + const GURL& open_url)> + AuthorizeAppCallback; + +// Closure for canceling a certain request. Each request-issuing method returns +// this type of closure. If it is called during the request is in-flight, the +// callback passed with the request is invoked with GDATA_CANCELLED. If the +// request is already finished, nothing happens. +typedef base::Closure CancelCallback; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DRIVE_COMMON_CALLBACKS_H_ diff --git a/chromium/google_apis/drive/drive_entry_kinds.h b/chromium/google_apis/drive/drive_entry_kinds.h new file mode 100644 index 00000000000..27e0f69d046 --- /dev/null +++ b/chromium/google_apis/drive/drive_entry_kinds.h @@ -0,0 +1,40 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_ +#define GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_ + +namespace google_apis { + +// DriveEntryKind specifies the kind of a Drive entry. +// +// kEntryKindMap in gdata_wapi_parser.cc should also be updated if you modify +// DriveEntryKind. The compiler will catch if they are not in sync. +enum DriveEntryKind { + ENTRY_KIND_UNKNOWN, + // Special entries. + ENTRY_KIND_ITEM, + ENTRY_KIND_SITE, + // Hosted Google document. + ENTRY_KIND_DOCUMENT, + ENTRY_KIND_SPREADSHEET, + ENTRY_KIND_PRESENTATION, + ENTRY_KIND_DRAWING, + ENTRY_KIND_TABLE, + ENTRY_KIND_FORM, + // Hosted external application document. + ENTRY_KIND_EXTERNAL_APP, + // Folders; collections. + ENTRY_KIND_FOLDER, + // Regular files. + ENTRY_KIND_FILE, + ENTRY_KIND_PDF, + + // This should be the last item. + ENTRY_KIND_MAX_VALUE, +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DRIVE_ENTRY_KINDS_H_ diff --git a/chromium/google_apis/drive/dummy_auth_service.cc b/chromium/google_apis/drive/dummy_auth_service.cc new file mode 100644 index 00000000000..e1b6891f969 --- /dev/null +++ b/chromium/google_apis/drive/dummy_auth_service.cc @@ -0,0 +1,43 @@ +// Copyright 2013 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 "google_apis/drive/dummy_auth_service.h" + +namespace google_apis { + +DummyAuthService::DummyAuthService() { + set_access_token("dummy"); + set_refresh_token("dummy"); +} + +void DummyAuthService::AddObserver(AuthServiceObserver* observer) { +} + +void DummyAuthService::RemoveObserver(AuthServiceObserver* observer) { +} + +void DummyAuthService::StartAuthentication(const AuthStatusCallback& callback) { +} + +bool DummyAuthService::HasAccessToken() const { + return !access_token_.empty(); +} + +bool DummyAuthService::HasRefreshToken() const { + return !refresh_token_.empty(); +} + +const std::string& DummyAuthService::access_token() const { + return access_token_; +} + +void DummyAuthService::ClearAccessToken() { + access_token_.clear(); +} + +void DummyAuthService::ClearRefreshToken() { + refresh_token_.clear(); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/dummy_auth_service.h b/chromium/google_apis/drive/dummy_auth_service.h new file mode 100644 index 00000000000..a69da6b9a5c --- /dev/null +++ b/chromium/google_apis/drive/dummy_auth_service.h @@ -0,0 +1,43 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_ +#define GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_ + +#include "base/compiler_specific.h" +#include "google_apis/drive/auth_service_interface.h" + +namespace google_apis { + +// Dummy implementation of AuthServiceInterface that always return a dummy +// access token. +class DummyAuthService : public AuthServiceInterface { + public: + // The constructor presets non-empty tokens. When a test for checking auth + // failure case (i.e., empty tokens) is needed, explicitly clear them by the + // Clear{Access, Refresh}Token methods. + DummyAuthService(); + + void set_access_token(const std::string& token) { access_token_ = token; } + void set_refresh_token(const std::string& token) { refresh_token_ = token; } + const std::string& refresh_token() const { return refresh_token_; } + + // AuthServiceInterface overrides. + virtual void AddObserver(AuthServiceObserver* observer) OVERRIDE; + virtual void RemoveObserver(AuthServiceObserver* observer) OVERRIDE; + virtual void StartAuthentication(const AuthStatusCallback& callback) OVERRIDE; + virtual bool HasAccessToken() const OVERRIDE; + virtual bool HasRefreshToken() const OVERRIDE; + virtual const std::string& access_token() const OVERRIDE; + virtual void ClearAccessToken() OVERRIDE; + virtual void ClearRefreshToken() OVERRIDE; + + private: + std::string access_token_; + std::string refresh_token_; +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_DUMMY_AUTH_SERVICE_H_ diff --git a/chromium/google_apis/drive/gdata_contacts_requests.cc b/chromium/google_apis/drive/gdata_contacts_requests.cc new file mode 100644 index 00000000000..11419af449b --- /dev/null +++ b/chromium/google_apis/drive/gdata_contacts_requests.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_contacts_requests.h" + +#include "google_apis/drive/time_util.h" +#include "net/base/url_util.h" +#include "url/gurl.h" + +namespace google_apis { + +namespace { + +// URL requesting all contact groups. +const char kGetContactGroupsURL[] = + "https://www.google.com/m8/feeds/groups/default/full?alt=json"; + +// URL requesting all contacts. +// TODO(derat): Per https://goo.gl/AufHP, "The feed may not contain all of the +// user's contacts, because there's a default limit on the number of results +// returned." Decide if 10000 is reasonable or not. +const char kGetContactsURL[] = + "https://www.google.com/m8/feeds/contacts/default/full" + "?alt=json&showdeleted=true&max-results=10000"; + +// Query parameter optionally appended to |kGetContactsURL| to return contacts +// from a specific group (as opposed to all contacts). +const char kGetContactsGroupParam[] = "group"; + +// Query parameter optionally appended to |kGetContactsURL| to return only +// recently-updated contacts. +const char kGetContactsUpdatedMinParam[] = "updated-min"; + +} // namespace + +//========================== GetContactGroupsRequest ========================= + +GetContactGroupsRequest::GetContactGroupsRequest( + RequestSender* runner, + const GetDataCallback& callback) + : GetDataRequest(runner, callback) { +} + +GetContactGroupsRequest::~GetContactGroupsRequest() {} + +GURL GetContactGroupsRequest::GetURL() const { + return !feed_url_for_testing_.is_empty() ? + feed_url_for_testing_ : + GURL(kGetContactGroupsURL); +} + +//============================ GetContactsRequest ============================ + +GetContactsRequest::GetContactsRequest( + RequestSender* runner, + const std::string& group_id, + const base::Time& min_update_time, + const GetDataCallback& callback) + : GetDataRequest(runner, callback), + group_id_(group_id), + min_update_time_(min_update_time) { +} + +GetContactsRequest::~GetContactsRequest() {} + +GURL GetContactsRequest::GetURL() const { + if (!feed_url_for_testing_.is_empty()) + return GURL(feed_url_for_testing_); + + GURL url(kGetContactsURL); + + if (!group_id_.empty()) { + url = net::AppendQueryParameter(url, kGetContactsGroupParam, group_id_); + } + if (!min_update_time_.is_null()) { + std::string time_rfc3339 = util::FormatTimeAsString(min_update_time_); + url = net::AppendQueryParameter( + url, kGetContactsUpdatedMinParam, time_rfc3339); + } + return url; +} + +//========================== GetContactPhotoRequest ========================== + +GetContactPhotoRequest::GetContactPhotoRequest( + RequestSender* runner, + const GURL& photo_url, + const GetContentCallback& callback) + : UrlFetchRequestBase(runner), + photo_url_(photo_url), + callback_(callback) { +} + +GetContactPhotoRequest::~GetContactPhotoRequest() {} + +GURL GetContactPhotoRequest::GetURL() const { + return photo_url_; +} + +void GetContactPhotoRequest::ProcessURLFetchResults( + const net::URLFetcher* source) { + GDataErrorCode code = GetErrorCode(); + scoped_ptr<std::string> data(new std::string(response_writer()->data())); + callback_.Run(code, data.Pass()); + OnProcessURLFetchResultsComplete(); +} + +void GetContactPhotoRequest::RunCallbackOnPrematureFailure( + GDataErrorCode code) { + scoped_ptr<std::string> data(new std::string); + callback_.Run(code, data.Pass()); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_contacts_requests.h b/chromium/google_apis/drive/gdata_contacts_requests.h new file mode 100644 index 00000000000..05ce693242d --- /dev/null +++ b/chromium/google_apis/drive/gdata_contacts_requests.h @@ -0,0 +1,102 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_ +#define GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_ + +#include <string> + +#include "base/time/time.h" +#include "google_apis/drive/base_requests.h" + +namespace google_apis { + +//========================== GetContactGroupsRequest ========================= + +// This class fetches a JSON feed containing a user's contact groups. +class GetContactGroupsRequest : public GetDataRequest { + public: + GetContactGroupsRequest(RequestSender* runner, + const GetDataCallback& callback); + virtual ~GetContactGroupsRequest(); + + void set_feed_url_for_testing(const GURL& url) { + feed_url_for_testing_ = url; + } + + protected: + // Overridden from GetDataRequest. + virtual GURL GetURL() const OVERRIDE; + + private: + // If non-empty, URL of the feed to fetch. + GURL feed_url_for_testing_; + + DISALLOW_COPY_AND_ASSIGN(GetContactGroupsRequest); +}; + +//============================ GetContactsRequest ============================ + +// This class fetches a JSON feed containing a user's contacts. +class GetContactsRequest : public GetDataRequest { + public: + GetContactsRequest(RequestSender* runner, + const std::string& group_id, + const base::Time& min_update_time, + const GetDataCallback& callback); + virtual ~GetContactsRequest(); + + void set_feed_url_for_testing(const GURL& url) { + feed_url_for_testing_ = url; + } + + protected: + // Overridden from GetDataRequest. + virtual GURL GetURL() const OVERRIDE; + + private: + // If non-empty, URL of the feed to fetch. + GURL feed_url_for_testing_; + + // If non-empty, contains the ID of the group whose contacts should be + // returned. Group IDs generally look like this: + // http://www.google.com/m8/feeds/groups/user%40gmail.com/base/6 + std::string group_id_; + + // If is_null() is false, contains a minimum last-updated time that will be + // used to filter contacts. + base::Time min_update_time_; + + DISALLOW_COPY_AND_ASSIGN(GetContactsRequest); +}; + +//========================== GetContactPhotoRequest ========================== + +// This class fetches a contact's photo. +class GetContactPhotoRequest : public UrlFetchRequestBase { + public: + GetContactPhotoRequest(RequestSender* runner, + const GURL& photo_url, + const GetContentCallback& callback); + virtual ~GetContactPhotoRequest(); + + protected: + // Overridden from UrlFetchRequestBase. + virtual GURL GetURL() const OVERRIDE; + virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; + virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; + + private: + // Location of the photo to fetch. + GURL photo_url_; + + // Callback to which the photo data is passed. + GetContentCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetContactPhotoRequest); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_ diff --git a/chromium/google_apis/drive/gdata_errorcode.cc b/chromium/google_apis/drive/gdata_errorcode.cc new file mode 100644 index 00000000000..89fa3f35054 --- /dev/null +++ b/chromium/google_apis/drive/gdata_errorcode.cc @@ -0,0 +1,93 @@ +// Copyright 2013 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 "google_apis/drive/gdata_errorcode.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" + +namespace google_apis { + +std::string GDataErrorCodeToString(GDataErrorCode error) { + switch (error) { + case HTTP_SUCCESS: + return"HTTP_SUCCESS"; + + case HTTP_CREATED: + return"HTTP_CREATED"; + + case HTTP_NO_CONTENT: + return"HTTP_NO_CONTENT"; + + case HTTP_FOUND: + return"HTTP_FOUND"; + + case HTTP_NOT_MODIFIED: + return"HTTP_NOT_MODIFIED"; + + case HTTP_RESUME_INCOMPLETE: + return"HTTP_RESUME_INCOMPLETE"; + + case HTTP_BAD_REQUEST: + return"HTTP_BAD_REQUEST"; + + case HTTP_UNAUTHORIZED: + return"HTTP_UNAUTHORIZED"; + + case HTTP_FORBIDDEN: + return"HTTP_FORBIDDEN"; + + case HTTP_NOT_FOUND: + return"HTTP_NOT_FOUND"; + + case HTTP_CONFLICT: + return"HTTP_CONFLICT"; + + case HTTP_GONE: + return "HTTP_GONE"; + + case HTTP_LENGTH_REQUIRED: + return"HTTP_LENGTH_REQUIRED"; + + case HTTP_PRECONDITION: + return"HTTP_PRECONDITION"; + + case HTTP_INTERNAL_SERVER_ERROR: + return"HTTP_INTERNAL_SERVER_ERROR"; + + case HTTP_NOT_IMPLEMENTED: + return "HTTP_NOT_IMPLEMENTED"; + + case HTTP_BAD_GATEWAY: + return"HTTP_BAD_GATEWAY"; + + case HTTP_SERVICE_UNAVAILABLE: + return"HTTP_SERVICE_UNAVAILABLE"; + + case GDATA_PARSE_ERROR: + return"GDATA_PARSE_ERROR"; + + case GDATA_FILE_ERROR: + return"GDATA_FILE_ERROR"; + + case GDATA_CANCELLED: + return"GDATA_CANCELLED"; + + case GDATA_OTHER_ERROR: + return"GDATA_OTHER_ERROR"; + + case GDATA_NO_CONNECTION: + return"GDATA_NO_CONNECTION"; + + case GDATA_NOT_READY: + return"GDATA_NOT_READY"; + + case GDATA_NO_SPACE: + return"GDATA_NO_SPACE"; + } + + return "UNKNOWN_ERROR_" + base::IntToString(error); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_errorcode.h b/chromium/google_apis/drive/gdata_errorcode.h new file mode 100644 index 00000000000..a275cb39164 --- /dev/null +++ b/chromium/google_apis/drive/gdata_errorcode.h @@ -0,0 +1,46 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_ +#define GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_ + +#include <string> + +namespace google_apis { + +// HTTP errors that can be returned by GData service. +enum GDataErrorCode { + HTTP_SUCCESS = 200, + HTTP_CREATED = 201, + HTTP_NO_CONTENT = 204, + HTTP_FOUND = 302, + HTTP_NOT_MODIFIED = 304, + HTTP_RESUME_INCOMPLETE = 308, + HTTP_BAD_REQUEST = 400, + HTTP_UNAUTHORIZED = 401, + HTTP_FORBIDDEN = 403, + HTTP_NOT_FOUND = 404, + HTTP_CONFLICT = 409, + HTTP_GONE = 410, + HTTP_LENGTH_REQUIRED = 411, + HTTP_PRECONDITION = 412, + HTTP_INTERNAL_SERVER_ERROR = 500, + HTTP_NOT_IMPLEMENTED = 501, + HTTP_BAD_GATEWAY = 502, + HTTP_SERVICE_UNAVAILABLE = 503, + GDATA_PARSE_ERROR = -100, + GDATA_FILE_ERROR = -101, + GDATA_CANCELLED = -102, + GDATA_OTHER_ERROR = -103, + GDATA_NO_CONNECTION = -104, + GDATA_NOT_READY = -105, + GDATA_NO_SPACE = -106, +}; + +// Returns a string representation of GDataErrorCode. +std::string GDataErrorCodeToString(GDataErrorCode error); + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_GDATA_ERRORCODE_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_parser.cc b/chromium/google_apis/drive/gdata_wapi_parser.cc new file mode 100644 index 00000000000..82f9d43151f --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_parser.cc @@ -0,0 +1,883 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_wapi_parser.h" + +#include <algorithm> +#include <string> + +#include "base/basictypes.h" +#include "base/files/file_path.h" +#include "base/json/json_value_converter.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "google_apis/drive/time_util.h" + +using base::Value; +using base::DictionaryValue; +using base::ListValue; + +namespace google_apis { + +namespace { + +// Term values for kSchemeKind category: +const char kTermPrefix[] = "http://schemas.google.com/docs/2007#"; + +// Node names. +const char kEntryNode[] = "entry"; + +// Field names. +const char kAuthorField[] = "author"; +const char kCategoryField[] = "category"; +const char kChangestampField[] = "docs$changestamp.value"; +const char kContentField[] = "content"; +const char kDeletedField[] = "gd$deleted"; +const char kETagField[] = "gd$etag"; +const char kEmailField[] = "email.$t"; +const char kEntryField[] = "entry"; +const char kFeedField[] = "feed"; +const char kFeedLinkField[] = "gd$feedLink"; +const char kFileNameField[] = "docs$filename.$t"; +const char kHrefField[] = "href"; +const char kIDField[] = "id.$t"; +const char kInstalledAppField[] = "docs$installedApp"; +const char kInstalledAppNameField[] = "docs$installedAppName"; +const char kInstalledAppIdField[] = "docs$installedAppId"; +const char kInstalledAppIconField[] = "docs$installedAppIcon"; +const char kInstalledAppIconCategoryField[] = "docs$installedAppIconCategory"; +const char kInstalledAppIconSizeField[] = "docs$installedAppIconSize"; +const char kInstalledAppObjectTypeField[] = "docs$installedAppObjectType"; +const char kInstalledAppPrimaryFileExtensionField[] = + "docs$installedAppPrimaryFileExtension"; +const char kInstalledAppPrimaryMimeTypeField[] = + "docs$installedAppPrimaryMimeType"; +const char kInstalledAppSecondaryFileExtensionField[] = + "docs$installedAppSecondaryFileExtension"; +const char kInstalledAppSecondaryMimeTypeField[] = + "docs$installedAppSecondaryMimeType"; +const char kInstalledAppSupportsCreateField[] = + "docs$installedAppSupportsCreate"; +const char kItemsPerPageField[] = "openSearch$itemsPerPage.$t"; +const char kLabelField[] = "label"; +const char kLargestChangestampField[] = "docs$largestChangestamp.value"; +const char kLastViewedField[] = "gd$lastViewed.$t"; +const char kLinkField[] = "link"; +const char kMD5Field[] = "docs$md5Checksum.$t"; +const char kNameField[] = "name.$t"; +const char kPublishedField[] = "published.$t"; +const char kQuotaBytesTotalField[] = "gd$quotaBytesTotal.$t"; +const char kQuotaBytesUsedField[] = "gd$quotaBytesUsed.$t"; +const char kRelField[] = "rel"; +const char kRemovedField[] = "docs$removed"; +const char kResourceIdField[] = "gd$resourceId.$t"; +const char kSchemeField[] = "scheme"; +const char kSizeField[] = "docs$size.$t"; +const char kSrcField[] = "src"; +const char kStartIndexField[] = "openSearch$startIndex.$t"; +const char kSuggestedFileNameField[] = "docs$suggestedFilename.$t"; +const char kTField[] = "$t"; +const char kTermField[] = "term"; +const char kTitleField[] = "title"; +const char kTitleTField[] = "title.$t"; +const char kTypeField[] = "type"; +const char kUpdatedField[] = "updated.$t"; + +// Link Prefixes +const char kOpenWithPrefix[] = "http://schemas.google.com/docs/2007#open-with-"; +const size_t kOpenWithPrefixSize = arraysize(kOpenWithPrefix) - 1; + +struct EntryKindMap { + DriveEntryKind kind; + const char* entry; + const char* extension; +}; + +const EntryKindMap kEntryKindMap[] = { + { ENTRY_KIND_UNKNOWN, "unknown", NULL}, + { ENTRY_KIND_ITEM, "item", NULL}, + { ENTRY_KIND_DOCUMENT, "document", ".gdoc"}, + { ENTRY_KIND_SPREADSHEET, "spreadsheet", ".gsheet"}, + { ENTRY_KIND_PRESENTATION, "presentation", ".gslides" }, + { ENTRY_KIND_DRAWING, "drawing", ".gdraw"}, + { ENTRY_KIND_TABLE, "table", ".gtable"}, + { ENTRY_KIND_FORM, "form", ".gform"}, + { ENTRY_KIND_EXTERNAL_APP, "externalapp", ".glink"}, + { ENTRY_KIND_SITE, "site", NULL}, + { ENTRY_KIND_FOLDER, "folder", NULL}, + { ENTRY_KIND_FILE, "file", NULL}, + { ENTRY_KIND_PDF, "pdf", NULL}, +}; +COMPILE_ASSERT(arraysize(kEntryKindMap) == ENTRY_KIND_MAX_VALUE, + EntryKindMap_and_DriveEntryKind_are_not_in_sync); + +struct LinkTypeMap { + Link::LinkType type; + const char* rel; +}; + +const LinkTypeMap kLinkTypeMap[] = { + { Link::LINK_SELF, + "self" }, + { Link::LINK_NEXT, + "next" }, + { Link::LINK_PARENT, + "http://schemas.google.com/docs/2007#parent" }, + { Link::LINK_ALTERNATE, + "alternate"}, + { Link::LINK_EDIT, + "edit" }, + { Link::LINK_EDIT_MEDIA, + "edit-media" }, + { Link::LINK_ALT_EDIT_MEDIA, + "http://schemas.google.com/docs/2007#alt-edit-media" }, + { Link::LINK_ALT_POST, + "http://schemas.google.com/docs/2007#alt-post" }, + { Link::LINK_FEED, + "http://schemas.google.com/g/2005#feed"}, + { Link::LINK_POST, + "http://schemas.google.com/g/2005#post"}, + { Link::LINK_BATCH, + "http://schemas.google.com/g/2005#batch"}, + { Link::LINK_THUMBNAIL, + "http://schemas.google.com/docs/2007/thumbnail"}, + { Link::LINK_RESUMABLE_EDIT_MEDIA, + "http://schemas.google.com/g/2005#resumable-edit-media"}, + { Link::LINK_RESUMABLE_CREATE_MEDIA, + "http://schemas.google.com/g/2005#resumable-create-media"}, + { Link::LINK_TABLES_FEED, + "http://schemas.google.com/spreadsheets/2006#tablesfeed"}, + { Link::LINK_WORKSHEET_FEED, + "http://schemas.google.com/spreadsheets/2006#worksheetsfeed"}, + { Link::LINK_EMBED, + "http://schemas.google.com/docs/2007#embed"}, + { Link::LINK_PRODUCT, + "http://schemas.google.com/docs/2007#product"}, + { Link::LINK_ICON, + "http://schemas.google.com/docs/2007#icon"}, + { Link::LINK_SHARE, + "http://schemas.google.com/docs/2007#share"}, +}; + +struct ResourceLinkTypeMap { + ResourceLink::ResourceLinkType type; + const char* rel; +}; + +const ResourceLinkTypeMap kFeedLinkTypeMap[] = { + { ResourceLink::FEED_LINK_ACL, + "http://schemas.google.com/acl/2007#accessControlList" }, + { ResourceLink::FEED_LINK_REVISIONS, + "http://schemas.google.com/docs/2007/revisions" }, +}; + +struct CategoryTypeMap { + Category::CategoryType type; + const char* scheme; +}; + +const CategoryTypeMap kCategoryTypeMap[] = { + { Category::CATEGORY_KIND, "http://schemas.google.com/g/2005#kind" }, + { Category::CATEGORY_LABEL, "http://schemas.google.com/g/2005/labels" }, +}; + +struct AppIconCategoryMap { + AppIcon::IconCategory category; + const char* category_name; +}; + +const AppIconCategoryMap kAppIconCategoryMap[] = { + { AppIcon::ICON_DOCUMENT, "document" }, + { AppIcon::ICON_APPLICATION, "application" }, + { AppIcon::ICON_SHARED_DOCUMENT, "documentShared" }, +}; + +// Converts |url_string| to |result|. Always returns true to be used +// for JSONValueConverter::RegisterCustomField method. +// TODO(mukai): make it return false in case of invalid |url_string|. +bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) { + *result = GURL(url_string.as_string()); + return true; +} + +// Converts boolean string values like "true" into bool. +bool GetBoolFromString(const base::StringPiece& value, bool* result) { + *result = (value == "true"); + return true; +} + +bool SortBySize(const InstalledApp::IconList::value_type& a, + const InstalledApp::IconList::value_type& b) { + return a.first < b.first; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// Author implementation + +Author::Author() { +} + +// static +void Author::RegisterJSONConverter( + base::JSONValueConverter<Author>* converter) { + converter->RegisterStringField(kNameField, &Author::name_); + converter->RegisterStringField(kEmailField, &Author::email_); +} + +//////////////////////////////////////////////////////////////////////////////// +// Link implementation + +Link::Link() : type_(Link::LINK_UNKNOWN) { +} + +Link::~Link() { +} + +// static +bool Link::GetAppID(const base::StringPiece& rel, std::string* app_id) { + DCHECK(app_id); + // Fast return path if the link clearly isn't an OPEN_WITH link. + if (rel.size() < kOpenWithPrefixSize) { + app_id->clear(); + return true; + } + + const std::string kOpenWithPrefixStr(kOpenWithPrefix); + if (StartsWithASCII(rel.as_string(), kOpenWithPrefixStr, false)) { + *app_id = rel.as_string().substr(kOpenWithPrefixStr.size()); + return true; + } + + app_id->clear(); + return true; +} + +// static. +bool Link::GetLinkType(const base::StringPiece& rel, Link::LinkType* type) { + DCHECK(type); + for (size_t i = 0; i < arraysize(kLinkTypeMap); i++) { + if (rel == kLinkTypeMap[i].rel) { + *type = kLinkTypeMap[i].type; + return true; + } + } + + // OPEN_WITH links have extra information at the end of the rel that is unique + // for each one, so we can't just check the usual map. This check is slightly + // redundant to provide a quick skip if it's obviously not an OPEN_WITH url. + if (rel.size() >= kOpenWithPrefixSize && + StartsWithASCII(rel.as_string(), kOpenWithPrefix, false)) { + *type = LINK_OPEN_WITH; + return true; + } + + // Let unknown link types through, just report it; if the link type is needed + // in the future, add it into LinkType and kLinkTypeMap. + DVLOG(1) << "Ignoring unknown link type for rel " << rel; + *type = LINK_UNKNOWN; + return true; +} + +// static +void Link::RegisterJSONConverter(base::JSONValueConverter<Link>* converter) { + converter->RegisterCustomField<Link::LinkType>(kRelField, + &Link::type_, + &Link::GetLinkType); + // We have to register kRelField twice because we extract two different pieces + // of data from the same rel field. + converter->RegisterCustomField<std::string>(kRelField, + &Link::app_id_, + &Link::GetAppID); + converter->RegisterCustomField(kHrefField, &Link::href_, &GetGURLFromString); + converter->RegisterStringField(kTitleField, &Link::title_); + converter->RegisterStringField(kTypeField, &Link::mime_type_); +} + +//////////////////////////////////////////////////////////////////////////////// +// ResourceLink implementation + +ResourceLink::ResourceLink() : type_(ResourceLink::FEED_LINK_UNKNOWN) { +} + +// static. +bool ResourceLink::GetFeedLinkType( + const base::StringPiece& rel, ResourceLink::ResourceLinkType* result) { + for (size_t i = 0; i < arraysize(kFeedLinkTypeMap); i++) { + if (rel == kFeedLinkTypeMap[i].rel) { + *result = kFeedLinkTypeMap[i].type; + return true; + } + } + DVLOG(1) << "Unknown feed link type for rel " << rel; + return false; +} + +// static +void ResourceLink::RegisterJSONConverter( + base::JSONValueConverter<ResourceLink>* converter) { + converter->RegisterCustomField<ResourceLink::ResourceLinkType>( + kRelField, &ResourceLink::type_, &ResourceLink::GetFeedLinkType); + converter->RegisterCustomField( + kHrefField, &ResourceLink::href_, &GetGURLFromString); +} + +//////////////////////////////////////////////////////////////////////////////// +// Category implementation + +Category::Category() : type_(CATEGORY_UNKNOWN) { +} + +// Converts category.scheme into CategoryType enum. +bool Category::GetCategoryTypeFromScheme( + const base::StringPiece& scheme, Category::CategoryType* result) { + for (size_t i = 0; i < arraysize(kCategoryTypeMap); i++) { + if (scheme == kCategoryTypeMap[i].scheme) { + *result = kCategoryTypeMap[i].type; + return true; + } + } + DVLOG(1) << "Unknown feed link type for scheme " << scheme; + return false; +} + +// static +void Category::RegisterJSONConverter( + base::JSONValueConverter<Category>* converter) { + converter->RegisterStringField(kLabelField, &Category::label_); + converter->RegisterCustomField<Category::CategoryType>( + kSchemeField, &Category::type_, &Category::GetCategoryTypeFromScheme); + converter->RegisterStringField(kTermField, &Category::term_); +} + +const Link* CommonMetadata::GetLinkByType(Link::LinkType type) const { + for (size_t i = 0; i < links_.size(); ++i) { + if (links_[i]->type() == type) + return links_[i]; + } + return NULL; +} + +//////////////////////////////////////////////////////////////////////////////// +// Content implementation + +Content::Content() { +} + +// static +void Content::RegisterJSONConverter( + base::JSONValueConverter<Content>* converter) { + converter->RegisterCustomField(kSrcField, &Content::url_, &GetGURLFromString); + converter->RegisterStringField(kTypeField, &Content::mime_type_); +} + +//////////////////////////////////////////////////////////////////////////////// +// AppIcon implementation + +AppIcon::AppIcon() : category_(AppIcon::ICON_UNKNOWN), icon_side_length_(0) { +} + +AppIcon::~AppIcon() { +} + +// static +void AppIcon::RegisterJSONConverter( + base::JSONValueConverter<AppIcon>* converter) { + converter->RegisterCustomField<AppIcon::IconCategory>( + kInstalledAppIconCategoryField, + &AppIcon::category_, + &AppIcon::GetIconCategory); + converter->RegisterCustomField<int>(kInstalledAppIconSizeField, + &AppIcon::icon_side_length_, + base::StringToInt); + converter->RegisterRepeatedMessage(kLinkField, &AppIcon::links_); +} + +GURL AppIcon::GetIconURL() const { + for (size_t i = 0; i < links_.size(); ++i) { + if (links_[i]->type() == Link::LINK_ICON) + return links_[i]->href(); + } + return GURL(); +} + +// static +bool AppIcon::GetIconCategory(const base::StringPiece& category, + AppIcon::IconCategory* result) { + for (size_t i = 0; i < arraysize(kAppIconCategoryMap); i++) { + if (category == kAppIconCategoryMap[i].category_name) { + *result = kAppIconCategoryMap[i].category; + return true; + } + } + DVLOG(1) << "Unknown icon category " << category; + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// CommonMetadata implementation + +CommonMetadata::CommonMetadata() { +} + +CommonMetadata::~CommonMetadata() { +} + +// static +template<typename CommonMetadataDescendant> +void CommonMetadata::RegisterJSONConverter( + base::JSONValueConverter<CommonMetadataDescendant>* converter) { + converter->RegisterStringField(kETagField, &CommonMetadata::etag_); + converter->template RegisterRepeatedMessage<Author>( + kAuthorField, &CommonMetadata::authors_); + converter->template RegisterRepeatedMessage<Link>( + kLinkField, &CommonMetadata::links_); + converter->template RegisterRepeatedMessage<Category>( + kCategoryField, &CommonMetadata::categories_); + converter->template RegisterCustomField<base::Time>( + kUpdatedField, &CommonMetadata::updated_time_, &util::GetTimeFromString); +} + +//////////////////////////////////////////////////////////////////////////////// +// ResourceEntry implementation + +ResourceEntry::ResourceEntry() + : kind_(ENTRY_KIND_UNKNOWN), + file_size_(0), + deleted_(false), + removed_(false), + changestamp_(0), + image_width_(-1), + image_height_(-1), + image_rotation_(-1) { +} + +ResourceEntry::~ResourceEntry() { +} + +bool ResourceEntry::HasFieldPresent(const base::Value* value, + bool* result) { + *result = (value != NULL); + return true; +} + +bool ResourceEntry::ParseChangestamp(const base::Value* value, + int64* result) { + DCHECK(result); + if (!value) { + *result = 0; + return true; + } + + std::string string_value; + if (value->GetAsString(&string_value) && + base::StringToInt64(string_value, result)) + return true; + + return false; +} + +// static +void ResourceEntry::RegisterJSONConverter( + base::JSONValueConverter<ResourceEntry>* converter) { + // Inherit the parent registrations. + CommonMetadata::RegisterJSONConverter(converter); + converter->RegisterStringField( + kResourceIdField, &ResourceEntry::resource_id_); + converter->RegisterStringField(kIDField, &ResourceEntry::id_); + converter->RegisterStringField(kTitleTField, &ResourceEntry::title_); + converter->RegisterCustomField<base::Time>( + kPublishedField, &ResourceEntry::published_time_, + &util::GetTimeFromString); + converter->RegisterCustomField<base::Time>( + kLastViewedField, &ResourceEntry::last_viewed_time_, + &util::GetTimeFromString); + converter->RegisterRepeatedMessage( + kFeedLinkField, &ResourceEntry::resource_links_); + converter->RegisterNestedField(kContentField, &ResourceEntry::content_); + + // File properties. If the resource type is not a normal file, then + // that's no problem because those feed must not have these fields + // themselves, which does not report errors. + converter->RegisterStringField(kFileNameField, &ResourceEntry::filename_); + converter->RegisterStringField(kMD5Field, &ResourceEntry::file_md5_); + converter->RegisterCustomField<int64>( + kSizeField, &ResourceEntry::file_size_, &base::StringToInt64); + converter->RegisterStringField( + kSuggestedFileNameField, &ResourceEntry::suggested_filename_); + // Deleted are treated as 'trashed' items on web client side. Removed files + // are gone for good. We treat both cases as 'deleted' for this client. + converter->RegisterCustomValueField<bool>( + kDeletedField, &ResourceEntry::deleted_, &ResourceEntry::HasFieldPresent); + converter->RegisterCustomValueField<bool>( + kRemovedField, &ResourceEntry::removed_, &ResourceEntry::HasFieldPresent); + converter->RegisterCustomValueField<int64>( + kChangestampField, &ResourceEntry::changestamp_, + &ResourceEntry::ParseChangestamp); + // ImageMediaMetadata fields are not supported by WAPI. +} + +std::string ResourceEntry::GetHostedDocumentExtension() const { + for (size_t i = 0; i < arraysize(kEntryKindMap); i++) { + if (kEntryKindMap[i].kind == kind_) { + if (kEntryKindMap[i].extension) + return std::string(kEntryKindMap[i].extension); + else + return std::string(); + } + } + return std::string(); +} + +// static +int ResourceEntry::ClassifyEntryKindByFileExtension( + const base::FilePath& file_path) { +#if defined(OS_WIN) + std::string file_extension = WideToUTF8(file_path.Extension()); +#else + std::string file_extension = file_path.Extension(); +#endif + for (size_t i = 0; i < arraysize(kEntryKindMap); ++i) { + const char* document_extension = kEntryKindMap[i].extension; + if (document_extension && file_extension == document_extension) + return ClassifyEntryKind(kEntryKindMap[i].kind); + } + return 0; +} + +// static +DriveEntryKind ResourceEntry::GetEntryKindFromTerm( + const std::string& term) { + if (!StartsWithASCII(term, kTermPrefix, false)) { + DVLOG(1) << "Unexpected term prefix term " << term; + return ENTRY_KIND_UNKNOWN; + } + + std::string type = term.substr(strlen(kTermPrefix)); + for (size_t i = 0; i < arraysize(kEntryKindMap); i++) { + if (type == kEntryKindMap[i].entry) + return kEntryKindMap[i].kind; + } + DVLOG(1) << "Unknown entry type for term " << term << ", type " << type; + return ENTRY_KIND_UNKNOWN; +} + +// static +int ResourceEntry::ClassifyEntryKind(DriveEntryKind kind) { + int classes = 0; + + // All DriveEntryKind members are listed here, so the compiler catches if a + // newly added member is missing here. + switch (kind) { + case ENTRY_KIND_UNKNOWN: + // Special entries. + case ENTRY_KIND_ITEM: + case ENTRY_KIND_SITE: + break; + + // Hosted Google document. + case ENTRY_KIND_DOCUMENT: + case ENTRY_KIND_SPREADSHEET: + case ENTRY_KIND_PRESENTATION: + case ENTRY_KIND_DRAWING: + case ENTRY_KIND_TABLE: + case ENTRY_KIND_FORM: + classes = KIND_OF_GOOGLE_DOCUMENT | KIND_OF_HOSTED_DOCUMENT; + break; + + // Hosted external application document. + case ENTRY_KIND_EXTERNAL_APP: + classes = KIND_OF_EXTERNAL_DOCUMENT | KIND_OF_HOSTED_DOCUMENT; + break; + + // Folders, collections. + case ENTRY_KIND_FOLDER: + classes = KIND_OF_FOLDER; + break; + + // Regular files. + case ENTRY_KIND_FILE: + case ENTRY_KIND_PDF: + classes = KIND_OF_FILE; + break; + + case ENTRY_KIND_MAX_VALUE: + NOTREACHED(); + } + + return classes; +} + +void ResourceEntry::FillRemainingFields() { + // Set |kind_| and |labels_| based on the |categories_| in the class. + // JSONValueConverter does not have the ability to catch an element in a list + // based on a predicate. Thus we need to iterate over |categories_| and + // find the elements to set these fields as a post-process. + for (size_t i = 0; i < categories_.size(); ++i) { + const Category* category = categories_[i]; + if (category->type() == Category::CATEGORY_KIND) + kind_ = GetEntryKindFromTerm(category->term()); + else if (category->type() == Category::CATEGORY_LABEL) + labels_.push_back(category->label()); + } +} + +// static +scoped_ptr<ResourceEntry> ResourceEntry::ExtractAndParse( + const base::Value& value) { + const base::DictionaryValue* as_dict = NULL; + const base::DictionaryValue* entry_dict = NULL; + if (value.GetAsDictionary(&as_dict) && + as_dict->GetDictionary(kEntryField, &entry_dict)) { + return ResourceEntry::CreateFrom(*entry_dict); + } + return scoped_ptr<ResourceEntry>(); +} + +// static +scoped_ptr<ResourceEntry> ResourceEntry::CreateFrom(const base::Value& value) { + base::JSONValueConverter<ResourceEntry> converter; + scoped_ptr<ResourceEntry> entry(new ResourceEntry()); + if (!converter.Convert(value, entry.get())) { + DVLOG(1) << "Invalid resource entry!"; + return scoped_ptr<ResourceEntry>(); + } + + entry->FillRemainingFields(); + return entry.Pass(); +} + +// static +std::string ResourceEntry::GetEntryNodeName() { + return kEntryNode; +} + +//////////////////////////////////////////////////////////////////////////////// +// ResourceList implementation + +ResourceList::ResourceList() + : start_index_(0), + items_per_page_(0), + largest_changestamp_(0) { +} + +ResourceList::~ResourceList() { +} + +// static +void ResourceList::RegisterJSONConverter( + base::JSONValueConverter<ResourceList>* converter) { + // inheritance + CommonMetadata::RegisterJSONConverter(converter); + // TODO(zelidrag): Once we figure out where these will be used, we should + // check for valid start_index_ and items_per_page_ values. + converter->RegisterCustomField<int>( + kStartIndexField, &ResourceList::start_index_, &base::StringToInt); + converter->RegisterCustomField<int>( + kItemsPerPageField, &ResourceList::items_per_page_, &base::StringToInt); + converter->RegisterStringField(kTitleTField, &ResourceList::title_); + converter->RegisterRepeatedMessage(kEntryField, &ResourceList::entries_); + converter->RegisterCustomField<int64>( + kLargestChangestampField, &ResourceList::largest_changestamp_, + &base::StringToInt64); +} + +bool ResourceList::Parse(const base::Value& value) { + base::JSONValueConverter<ResourceList> converter; + if (!converter.Convert(value, this)) { + DVLOG(1) << "Invalid resource list!"; + return false; + } + + ScopedVector<ResourceEntry>::iterator iter = entries_.begin(); + while (iter != entries_.end()) { + ResourceEntry* entry = (*iter); + entry->FillRemainingFields(); + ++iter; + } + return true; +} + +// static +scoped_ptr<ResourceList> ResourceList::ExtractAndParse( + const base::Value& value) { + const base::DictionaryValue* as_dict = NULL; + const base::DictionaryValue* feed_dict = NULL; + if (value.GetAsDictionary(&as_dict) && + as_dict->GetDictionary(kFeedField, &feed_dict)) { + return ResourceList::CreateFrom(*feed_dict); + } + return scoped_ptr<ResourceList>(); +} + +// static +scoped_ptr<ResourceList> ResourceList::CreateFrom(const base::Value& value) { + scoped_ptr<ResourceList> feed(new ResourceList()); + if (!feed->Parse(value)) { + DVLOG(1) << "Invalid resource list!"; + return scoped_ptr<ResourceList>(); + } + + return feed.Pass(); +} + +bool ResourceList::GetNextFeedURL(GURL* url) const { + DCHECK(url); + for (size_t i = 0; i < links_.size(); ++i) { + if (links_[i]->type() == Link::LINK_NEXT) { + *url = links_[i]->href(); + return true; + } + } + return false; +} + +void ResourceList::ReleaseEntries(std::vector<ResourceEntry*>* entries) { + entries_.release(entries); +} + +//////////////////////////////////////////////////////////////////////////////// +// InstalledApp implementation + +InstalledApp::InstalledApp() : supports_create_(false) { +} + +InstalledApp::~InstalledApp() { +} + +InstalledApp::IconList InstalledApp::GetIconsForCategory( + AppIcon::IconCategory category) const { + IconList result; + + for (ScopedVector<AppIcon>::const_iterator icon_iter = app_icons_.begin(); + icon_iter != app_icons_.end(); ++icon_iter) { + if ((*icon_iter)->category() != category) + continue; + GURL icon_url = (*icon_iter)->GetIconURL(); + if (icon_url.is_empty()) + continue; + result.push_back(std::make_pair((*icon_iter)->icon_side_length(), + icon_url)); + } + + // Return a sorted list, smallest to largest. + std::sort(result.begin(), result.end(), SortBySize); + return result; +} + +GURL InstalledApp::GetProductUrl() const { + for (ScopedVector<Link>::const_iterator it = links_.begin(); + it != links_.end(); ++it) { + const Link* link = *it; + if (link->type() == Link::LINK_PRODUCT) + return link->href(); + } + return GURL(); +} + +// static +bool InstalledApp::GetValueString(const base::Value* value, + std::string* result) { + const base::DictionaryValue* dict = NULL; + if (!value->GetAsDictionary(&dict)) + return false; + + if (!dict->GetString(kTField, result)) + return false; + + return true; +} + +// static +void InstalledApp::RegisterJSONConverter( + base::JSONValueConverter<InstalledApp>* converter) { + converter->RegisterRepeatedMessage(kInstalledAppIconField, + &InstalledApp::app_icons_); + converter->RegisterStringField(kInstalledAppIdField, + &InstalledApp::app_id_); + converter->RegisterStringField(kInstalledAppNameField, + &InstalledApp::app_name_); + converter->RegisterStringField(kInstalledAppObjectTypeField, + &InstalledApp::object_type_); + converter->RegisterCustomField<bool>(kInstalledAppSupportsCreateField, + &InstalledApp::supports_create_, + &GetBoolFromString); + converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryMimeTypeField, + &InstalledApp::primary_mimetypes_, + &GetValueString); + converter->RegisterRepeatedCustomValue(kInstalledAppSecondaryMimeTypeField, + &InstalledApp::secondary_mimetypes_, + &GetValueString); + converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryFileExtensionField, + &InstalledApp::primary_extensions_, + &GetValueString); + converter->RegisterRepeatedCustomValue( + kInstalledAppSecondaryFileExtensionField, + &InstalledApp::secondary_extensions_, + &GetValueString); + converter->RegisterRepeatedMessage(kLinkField, &InstalledApp::links_); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccountMetadata implementation + +AccountMetadata::AccountMetadata() + : quota_bytes_total_(0), + quota_bytes_used_(0), + largest_changestamp_(0) { +} + +AccountMetadata::~AccountMetadata() { +} + +// static +void AccountMetadata::RegisterJSONConverter( + base::JSONValueConverter<AccountMetadata>* converter) { + converter->RegisterCustomField<int64>( + kQuotaBytesTotalField, + &AccountMetadata::quota_bytes_total_, + &base::StringToInt64); + converter->RegisterCustomField<int64>( + kQuotaBytesUsedField, + &AccountMetadata::quota_bytes_used_, + &base::StringToInt64); + converter->RegisterCustomField<int64>( + kLargestChangestampField, + &AccountMetadata::largest_changestamp_, + &base::StringToInt64); + converter->RegisterRepeatedMessage(kInstalledAppField, + &AccountMetadata::installed_apps_); +} + +// static +scoped_ptr<AccountMetadata> AccountMetadata::CreateFrom( + const base::Value& value) { + scoped_ptr<AccountMetadata> metadata(new AccountMetadata()); + const base::DictionaryValue* dictionary = NULL; + const base::Value* entry = NULL; + if (!value.GetAsDictionary(&dictionary) || + !dictionary->Get(kEntryField, &entry) || + !metadata->Parse(*entry)) { + LOG(ERROR) << "Unable to create: Invalid account metadata feed!"; + return scoped_ptr<AccountMetadata>(); + } + + return metadata.Pass(); +} + +bool AccountMetadata::Parse(const base::Value& value) { + base::JSONValueConverter<AccountMetadata> converter; + if (!converter.Convert(value, this)) { + LOG(ERROR) << "Unable to parse: Invalid account metadata feed!"; + return false; + } + return true; +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_parser.h b/chromium/google_apis/drive/gdata_wapi_parser.h new file mode 100644 index 00000000000..0c89c11b09e --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_parser.h @@ -0,0 +1,866 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_ +#define GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_ + +#include <string> +#include <utility> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string_piece.h" +#include "base/time/time.h" +#include "google_apis/drive/drive_entry_kinds.h" +#include "url/gurl.h" + +namespace base { +class FilePath; +class DictionaryValue; +class Value; + +template <class StructType> +class JSONValueConverter; + +namespace internal { +template <class NestedType> +class RepeatedMessageConverter; +} // namespace internal + +} // namespace base + +// Defines data elements of Google Documents API as described in +// http://code.google.com/apis/documents/. +namespace google_apis { + +// Defines link (URL) of an entity (document, file, feed...). Each entity could +// have more than one link representing it. +class Link { + public: + enum LinkType { + LINK_UNKNOWN, + LINK_SELF, + LINK_NEXT, + LINK_PARENT, + LINK_ALTERNATE, + LINK_EDIT, + LINK_EDIT_MEDIA, + LINK_ALT_EDIT_MEDIA, + LINK_ALT_POST, + LINK_FEED, + LINK_POST, + LINK_BATCH, + LINK_RESUMABLE_EDIT_MEDIA, + LINK_RESUMABLE_CREATE_MEDIA, + LINK_TABLES_FEED, + LINK_WORKSHEET_FEED, + LINK_THUMBNAIL, + LINK_EMBED, + LINK_PRODUCT, + LINK_ICON, + LINK_OPEN_WITH, + LINK_SHARE, + }; + Link(); + ~Link(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter(base::JSONValueConverter<Link>* converter); + + // Type of the link. + LinkType type() const { return type_; } + + // URL of the link. + const GURL& href() const { return href_; } + + // Title of the link. + const std::string& title() const { return title_; } + + // For OPEN_WITH links, this contains the application ID. For all other link + // types, it is the empty string. + const std::string& app_id() const { return app_id_; } + + // Link MIME type. + const std::string& mime_type() const { return mime_type_; } + + void set_type(LinkType type) { type_ = type; } + void set_href(const GURL& href) { href_ = href; } + void set_title(const std::string& title) { title_ = title; } + void set_app_id(const std::string& app_id) { app_id_ = app_id; } + void set_mime_type(const std::string& mime_type) { mime_type_ = mime_type; } + + private: + friend class ResourceEntry; + // Converts value of link.rel into LinkType. Outputs to |type| and returns + // true when |rel| has a valid value. Otherwise does nothing and returns + // false. + static bool GetLinkType(const base::StringPiece& rel, LinkType* type); + + // Converts value of link.rel to application ID, if there is one embedded in + // the link.rel field. Outputs to |app_id| and returns true when |rel| has a + // valid value. Otherwise does nothing and returns false. + static bool GetAppID(const base::StringPiece& rel, std::string* app_id); + + LinkType type_; + GURL href_; + std::string title_; + std::string app_id_; + std::string mime_type_; + + DISALLOW_COPY_AND_ASSIGN(Link); +}; + +// Feed links define links (URLs) to special list of entries (i.e. list of +// previous document revisions). +class ResourceLink { + public: + enum ResourceLinkType { + FEED_LINK_UNKNOWN, + FEED_LINK_ACL, + FEED_LINK_REVISIONS, + }; + ResourceLink(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<ResourceLink>* converter); + + // MIME type of the feed. + ResourceLinkType type() const { return type_; } + + // URL of the feed. + const GURL& href() const { return href_; } + + void set_type(ResourceLinkType type) { type_ = type; } + void set_href(const GURL& href) { href_ = href; } + + private: + friend class ResourceEntry; + // Converts value of gd$feedLink.rel into ResourceLinkType enum. + // Outputs to |result| and returns true when |rel| has a valid + // value. Otherwise does nothing and returns false. + static bool GetFeedLinkType( + const base::StringPiece& rel, ResourceLinkType* result); + + ResourceLinkType type_; + GURL href_; + + DISALLOW_COPY_AND_ASSIGN(ResourceLink); +}; + +// Author represents an author of an entity. +class Author { + public: + Author(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<Author>* converter); + + // Getters. + const std::string& name() const { return name_; } + const std::string& email() const { return email_; } + + void set_name(const std::string& name) { name_ = name; } + void set_email(const std::string& email) { email_ = email; } + + private: + friend class ResourceEntry; + + std::string name_; + std::string email_; + + DISALLOW_COPY_AND_ASSIGN(Author); +}; + +// Entry category. +class Category { + public: + enum CategoryType { + CATEGORY_UNKNOWN, + CATEGORY_ITEM, + CATEGORY_KIND, + CATEGORY_LABEL, + }; + + Category(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<Category>* converter); + + // Category label. + const std::string& label() const { return label_; } + + // Category type. + CategoryType type() const { return type_; } + + // Category term. + const std::string& term() const { return term_; } + + void set_label(const std::string& label) { label_ = label; } + void set_type(CategoryType type) { type_ = type; } + void set_term(const std::string& term) { term_ = term; } + + private: + friend class ResourceEntry; + // Converts category scheme into CategoryType enum. For example, + // http://schemas.google.com/g/2005#kind => Category::CATEGORY_KIND + // Returns false and does not change |result| when |scheme| has an + // unrecognizable value. + static bool GetCategoryTypeFromScheme( + const base::StringPiece& scheme, CategoryType* result); + + std::string label_; + CategoryType type_; + std::string term_; + + DISALLOW_COPY_AND_ASSIGN(Category); +}; + +// Content details of a resource: mime-type, url, and so on. +class Content { + public: + Content(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<Content>* converter); + + // The URL to download the file content. + // Note that the url can expire, so we'll fetch the latest resource + // entry before starting a download to get the download URL. + const GURL& url() const { return url_; } + const std::string& mime_type() const { return mime_type_; } + + void set_url(const GURL& url) { url_ = url; } + void set_mime_type(const std::string& mime_type) { mime_type_ = mime_type; } + + private: + friend class ResourceEntry; + + GURL url_; + std::string mime_type_; +}; + +// This stores a representation of an application icon as registered with the +// installed applications section of the account metadata feed. There can be +// multiple icons registered for each application, differing in size, category +// and MIME type. +class AppIcon { + public: + enum IconCategory { + ICON_UNKNOWN, // Uninitialized state + ICON_DOCUMENT, // Document icon for various MIME types + ICON_APPLICATION, // Application icon for various MIME types + ICON_SHARED_DOCUMENT, // Icon for documents that are shared from other + // users. + }; + + AppIcon(); + ~AppIcon(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<AppIcon>* converter); + + // Category of the icon. + IconCategory category() const { return category_; } + + // Size in pixels of one side of the icon (icons are always square). + int icon_side_length() const { return icon_side_length_; } + + // Get a list of links available for this AppIcon. + const ScopedVector<Link>& links() const { return links_; } + + // Get the icon URL from the internal list of links. Returns the first + // icon URL found in the list. + GURL GetIconURL() const; + + void set_category(IconCategory category) { category_ = category; } + void set_icon_side_length(int icon_side_length) { + icon_side_length_ = icon_side_length; + } + void set_links(ScopedVector<Link> links) { links_ = links.Pass(); } + + private: + // Extracts the icon category from the given string. Returns false and does + // not change |result| when |scheme| has an unrecognizable value. + static bool GetIconCategory(const base::StringPiece& category, + IconCategory* result); + + IconCategory category_; + int icon_side_length_; + ScopedVector<Link> links_; + + DISALLOW_COPY_AND_ASSIGN(AppIcon); +}; + +// Base class for feed entries. This class defines fields commonly used by +// various feeds. +class CommonMetadata { + public: + CommonMetadata(); + virtual ~CommonMetadata(); + + // Returns a link of a given |type| for this entry. If not found, it returns + // NULL. + const Link* GetLinkByType(Link::LinkType type) const; + + // Entry update time. + base::Time updated_time() const { return updated_time_; } + + // Entry ETag. + const std::string& etag() const { return etag_; } + + // List of entry authors. + const ScopedVector<Author>& authors() const { return authors_; } + + // List of entry links. + const ScopedVector<Link>& links() const { return links_; } + ScopedVector<Link>* mutable_links() { return &links_; } + + // List of entry categories. + const ScopedVector<Category>& categories() const { return categories_; } + + void set_etag(const std::string& etag) { etag_ = etag; } + void set_authors(ScopedVector<Author> authors) { + authors_ = authors.Pass(); + } + void set_links(ScopedVector<Link> links) { + links_ = links.Pass(); + } + void set_categories(ScopedVector<Category> categories) { + categories_ = categories.Pass(); + } + void set_updated_time(const base::Time& updated_time) { + updated_time_ = updated_time; + } + + protected: + // Registers the mapping between JSON field names and the members in + // this class. + template<typename CommonMetadataDescendant> + static void RegisterJSONConverter( + base::JSONValueConverter<CommonMetadataDescendant>* converter); + + std::string etag_; + ScopedVector<Author> authors_; + ScopedVector<Link> links_; + ScopedVector<Category> categories_; + base::Time updated_time_; + + DISALLOW_COPY_AND_ASSIGN(CommonMetadata); +}; + +// This class represents a resource entry. A resource is a generic term which +// refers to a file and a directory. +class ResourceEntry : public CommonMetadata { + public: + ResourceEntry(); + virtual ~ResourceEntry(); + + // Extracts "entry" dictionary from the JSON value, and parse the contents, + // using CreateFrom(). Returns NULL on failure. The input JSON data, coming + // from the gdata server, looks like: + // + // { + // "encoding": "UTF-8", + // "entry": { ... }, // This function will extract this and parse. + // "version": "1.0" + // } + // + // The caller should delete the returned object. + static scoped_ptr<ResourceEntry> ExtractAndParse(const base::Value& value); + + // Creates resource entry from parsed JSON Value. You should call + // this instead of instantiating JSONValueConverter by yourself + // because this method does some post-process for some fields. See + // FillRemainingFields comment and implementation for the details. + static scoped_ptr<ResourceEntry> CreateFrom(const base::Value& value); + + // Returns name of entry node. + static std::string GetEntryNodeName(); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<ResourceEntry>* converter); + + // Sets true to |result| if the field exists. + // Always returns true even when the field does not exist. + static bool HasFieldPresent(const base::Value* value, bool* result); + + // Parses |value| as int64 and sets it to |result|. If the field does not + // exist, sets 0 to |result| as default value. + // Returns true if |value| is NULL or it is parsed as int64 successfully. + static bool ParseChangestamp(const base::Value* value, int64* result); + + // The resource ID is used to identify a resource, which looks like: + // file:d41d8cd98f00b204e9800998ecf8 + const std::string& resource_id() const { return resource_id_; } + + // This is a URL looks like: + // https://docs.google.com/feeds/id/file%3Ad41d8cd98f00b204e9800998ecf8. + // The URL is currently not used. + const std::string& id() const { return id_; } + + DriveEntryKind kind() const { return kind_; } + const std::string& title() const { return title_; } + base::Time published_time() const { return published_time_; } + base::Time last_viewed_time() const { return last_viewed_time_; } + const std::vector<std::string>& labels() const { return labels_; } + + // The URL to download a file content. + // Search for 'download_url' in gdata_wapi_requests.h for details. + const GURL& download_url() const { return content_.url(); } + + const std::string& content_mime_type() const { return content_.mime_type(); } + + // The resource links contain extra links for revisions and access control, + // etc. Note that links() contain more basic links like edit URL, + // alternative URL, etc. + const ScopedVector<ResourceLink>& resource_links() const { + return resource_links_; + } + + // File name (exists only for kinds FILE and PDF). + const std::string& filename() const { return filename_; } + + // Suggested file name (exists only for kinds FILE and PDF). + const std::string& suggested_filename() const { return suggested_filename_; } + + // File content MD5 (exists only for kinds FILE and PDF). + const std::string& file_md5() const { return file_md5_; } + + // File size (exists only for kinds FILE and PDF). + int64 file_size() const { return file_size_; } + + // True if the file or directory is deleted (applicable to change list only). + bool deleted() const { return deleted_ || removed_; } + + // Changestamp (exists only for change query results). + // If not exists, defaults to 0. + int64 changestamp() const { return changestamp_; } + + // Image width (exists only for images). + // If doesn't exist, then equals -1. + int64 image_width() const { return image_width_; } + + // Image height (exists only for images). + // If doesn't exist, then equals -1. + int64 image_height() const { return image_height_; } + + // Image rotation in clockwise degrees (exists only for images). + // If doesn't exist, then equals -1. + int64 image_rotation() const { return image_rotation_; } + + // Text version of resource entry kind. Returns an empty string for + // unknown entry kind. + std::string GetEntryKindText() const; + + // Returns preferred file extension for hosted documents. If entry is not + // a hosted document, this call returns an empty string. + std::string GetHostedDocumentExtension() const; + + // True if resource entry is remotely hosted. + bool is_hosted_document() const { + return (ClassifyEntryKind(kind_) & KIND_OF_HOSTED_DOCUMENT) > 0; + } + // True if resource entry hosted by Google Documents. + bool is_google_document() const { + return (ClassifyEntryKind(kind_) & KIND_OF_GOOGLE_DOCUMENT) > 0; + } + // True if resource entry is hosted by an external application. + bool is_external_document() const { + return (ClassifyEntryKind(kind_) & KIND_OF_EXTERNAL_DOCUMENT) > 0; + } + // True if resource entry is a folder (collection). + bool is_folder() const { + return (ClassifyEntryKind(kind_) & KIND_OF_FOLDER) > 0; + } + // True if resource entry is regular file. + bool is_file() const { + return (ClassifyEntryKind(kind_) & KIND_OF_FILE) > 0; + } + // True if resource entry can't be mapped to the file system. + bool is_special() const { + return !is_file() && !is_folder() && !is_hosted_document(); + } + + // The following constructs are exposed for unit tests. + + // Classes of EntryKind. Used for ClassifyEntryKind(). + enum EntryKindClass { + KIND_OF_NONE = 0, + KIND_OF_HOSTED_DOCUMENT = 1, + KIND_OF_GOOGLE_DOCUMENT = 1 << 1, + KIND_OF_EXTERNAL_DOCUMENT = 1 << 2, + KIND_OF_FOLDER = 1 << 3, + KIND_OF_FILE = 1 << 4, + }; + + // Classifies the EntryKind. The returned value is a bitmask of + // EntryKindClass. For example, DOCUMENT is classified as + // KIND_OF_HOSTED_DOCUMENT and KIND_OF_GOOGLE_DOCUMENT, hence the returned + // value is KIND_OF_HOSTED_DOCUMENT | KIND_OF_GOOGLE_DOCUMENT. + static int ClassifyEntryKind(DriveEntryKind kind); + + // Classifies the EntryKind by the file extension of specific path. The + // returned value is a bitmask of EntryKindClass. See also ClassifyEntryKind. + static int ClassifyEntryKindByFileExtension(const base::FilePath& file); + + void set_resource_id(const std::string& resource_id) { + resource_id_ = resource_id; + } + void set_id(const std::string& id) { id_ = id; } + void set_kind(DriveEntryKind kind) { kind_ = kind; } + void set_title(const std::string& title) { title_ = title; } + void set_published_time(const base::Time& published_time) { + published_time_ = published_time; + } + void set_last_viewed_time(const base::Time& last_viewed_time) { + last_viewed_time_ = last_viewed_time; + } + void set_labels(const std::vector<std::string>& labels) { + labels_ = labels; + } + void set_content(const Content& content) { + content_ = content; + } + void set_resource_links(ScopedVector<ResourceLink> resource_links) { + resource_links_ = resource_links.Pass(); + } + void set_filename(const std::string& filename) { filename_ = filename; } + void set_suggested_filename(const std::string& suggested_filename) { + suggested_filename_ = suggested_filename; + } + void set_file_md5(const std::string& file_md5) { file_md5_ = file_md5; } + void set_file_size(int64 file_size) { file_size_ = file_size; } + void set_deleted(bool deleted) { deleted_ = deleted; } + void set_removed(bool removed) { removed_ = removed; } + void set_changestamp(int64 changestamp) { changestamp_ = changestamp; } + void set_image_width(int64 image_width) { image_width_ = image_width; } + void set_image_height(int64 image_height) { image_height_ = image_height; } + void set_image_rotation(int64 image_rotation) { + image_rotation_ = image_rotation; + } + + // Fills the remaining fields where JSONValueConverter cannot catch. + // Currently, sets |kind_| and |labels_| based on the |categories_| in the + // class. + void FillRemainingFields(); + + private: + friend class base::internal::RepeatedMessageConverter<ResourceEntry>; + friend class ResourceList; + friend class ResumeUploadRequest; + + // Converts categories.term into DriveEntryKind enum. + static DriveEntryKind GetEntryKindFromTerm(const std::string& term); + // Converts |kind| into its text identifier equivalent. + static const char* GetEntryKindDescription(DriveEntryKind kind); + + std::string resource_id_; + std::string id_; + DriveEntryKind kind_; + std::string title_; + base::Time published_time_; + // Last viewed value may be unreliable. See: crbug.com/152628. + base::Time last_viewed_time_; + std::vector<std::string> labels_; + Content content_; + ScopedVector<ResourceLink> resource_links_; + // Optional fields for files only. + std::string filename_; + std::string suggested_filename_; + std::string file_md5_; + int64 file_size_; + bool deleted_; + bool removed_; + int64 changestamp_; + int64 image_width_; + int64 image_height_; + int64 image_rotation_; + + DISALLOW_COPY_AND_ASSIGN(ResourceEntry); +}; + +// This class represents a list of resource entries with some extra metadata +// such as the root upload URL. The feed is paginated and the rest of the +// feed can be fetched by retrieving the remaining parts of the feed from +// URLs provided by GetNextFeedURL() method. +class ResourceList : public CommonMetadata { + public: + ResourceList(); + virtual ~ResourceList(); + + // Extracts "feed" dictionary from the JSON value, and parse the contents, + // using CreateFrom(). Returns NULL on failure. The input JSON data, coming + // from the gdata server, looks like: + // + // { + // "encoding": "UTF-8", + // "feed": { ... }, // This function will extract this and parse. + // "version": "1.0" + // } + static scoped_ptr<ResourceList> ExtractAndParse(const base::Value& value); + + // Creates feed from parsed JSON Value. You should call this + // instead of instantiating JSONValueConverter by yourself because + // this method does some post-process for some fields. See + // FillRemainingFields comment and implementation in ResourceEntry + // class for the details. + static scoped_ptr<ResourceList> CreateFrom(const base::Value& value); + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<ResourceList>* converter); + + // Returns true and passes|url| of the next feed if the current entry list + // does not completed this feed. + bool GetNextFeedURL(GURL* url) const; + + // List of resource entries. + const ScopedVector<ResourceEntry>& entries() const { return entries_; } + ScopedVector<ResourceEntry>* mutable_entries() { return &entries_; } + + // Releases entries_ into |entries|. This is a transfer of ownership, so the + // caller is responsible for deleting the elements of |entries|. + void ReleaseEntries(std::vector<ResourceEntry*>* entries); + + // Start index of the resource entry list. + int start_index() const { return start_index_; } + + // Number of items per feed of the resource entry list. + int items_per_page() const { return items_per_page_; } + + // The largest changestamp. Next time the resource list should be fetched + // from this changestamp. + int64 largest_changestamp() const { return largest_changestamp_; } + + // Resource entry list title. + const std::string& title() { return title_; } + + void set_entries(ScopedVector<ResourceEntry> entries) { + entries_ = entries.Pass(); + } + void set_start_index(int start_index) { + start_index_ = start_index; + } + void set_items_per_page(int items_per_page) { + items_per_page_ = items_per_page; + } + void set_title(const std::string& title) { + title_ = title; + } + void set_largest_changestamp(int64 largest_changestamp) { + largest_changestamp_ = largest_changestamp; + } + + private: + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + ScopedVector<ResourceEntry> entries_; + int start_index_; + int items_per_page_; + std::string title_; + int64 largest_changestamp_; + + DISALLOW_COPY_AND_ASSIGN(ResourceList); +}; + +// Metadata representing installed Google Drive application. +class InstalledApp { + public: + typedef std::vector<std::pair<int, GURL> > IconList; + + InstalledApp(); + virtual ~InstalledApp(); + + // WebApp name. + const std::string& app_name() const { return app_name_; } + + // Drive app id + const std::string& app_id() const { return app_id_; } + + // Object (file) type name that is generated by this WebApp. + const std::string& object_type() const { return object_type_; } + + // True if WebApp supports creation of new file instances. + bool supports_create() const { return supports_create_; } + + // List of primary mime types supported by this WebApp. Primary status should + // trigger this WebApp becoming the default handler of file instances that + // have these mime types. + const ScopedVector<std::string>& primary_mimetypes() const { + return primary_mimetypes_; + } + + // List of secondary mime types supported by this WebApp. Secondary status + // should make this WebApp show up in "Open with..." pop-up menu of the + // default action menu for file with matching mime types. + const ScopedVector<std::string>& secondary_mimetypes() const { + return secondary_mimetypes_; + } + + // List of primary file extensions supported by this WebApp. Primary status + // should trigger this WebApp becoming the default handler of file instances + // that match these extensions. + const ScopedVector<std::string>& primary_extensions() const { + return primary_extensions_; + } + + // List of secondary file extensions supported by this WebApp. Secondary + // status should make this WebApp show up in "Open with..." pop-up menu of the + // default action menu for file with matching extensions. + const ScopedVector<std::string>& secondary_extensions() const { + return secondary_extensions_; + } + + // List of entry links. + const ScopedVector<Link>& links() const { return links_; } + + // Returns a list of icons associated with this installed application. + const ScopedVector<AppIcon>& app_icons() const { + return app_icons_; + } + + // Convenience function for getting the icon URLs for a particular |category| + // of icon. Icons are returned in a sorted list, from smallest to largest. + IconList GetIconsForCategory(AppIcon::IconCategory category) const; + + // Retrieves product URL from the link collection. + GURL GetProductUrl() const; + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<InstalledApp>* converter); + + void set_app_id(const std::string& app_id) { app_id_ = app_id; } + void set_app_name(const std::string& app_name) { app_name_ = app_name; } + void set_object_type(const std::string& object_type) { + object_type_ = object_type; + } + void set_supports_create(bool supports_create) { + supports_create_ = supports_create; + } + void set_primary_mimetypes( + ScopedVector<std::string> primary_mimetypes) { + primary_mimetypes_ = primary_mimetypes.Pass(); + } + void set_secondary_mimetypes( + ScopedVector<std::string> secondary_mimetypes) { + secondary_mimetypes_ = secondary_mimetypes.Pass(); + } + void set_primary_extensions( + ScopedVector<std::string> primary_extensions) { + primary_extensions_ = primary_extensions.Pass(); + } + void set_secondary_extensions( + ScopedVector<std::string> secondary_extensions) { + secondary_extensions_ = secondary_extensions.Pass(); + } + void set_links(ScopedVector<Link> links) { + links_ = links.Pass(); + } + void set_app_icons(ScopedVector<AppIcon> app_icons) { + app_icons_ = app_icons.Pass(); + } + + private: + // Extracts "$t" value from the dictionary |value| and returns it in |result|. + // If the string value can't be found, it returns false. + static bool GetValueString(const base::Value* value, + std::string* result); + + std::string app_id_; + std::string app_name_; + std::string object_type_; + bool supports_create_; + ScopedVector<std::string> primary_mimetypes_; + ScopedVector<std::string> secondary_mimetypes_; + ScopedVector<std::string> primary_extensions_; + ScopedVector<std::string> secondary_extensions_; + ScopedVector<Link> links_; + ScopedVector<AppIcon> app_icons_; +}; + +// Account metadata feed represents the metadata object attached to the user's +// account. +class AccountMetadata { + public: + AccountMetadata(); + virtual ~AccountMetadata(); + + // Creates feed from parsed JSON Value. You should call this + // instead of instantiating JSONValueConverter by yourself because + // this method does some post-process for some fields. See + // FillRemainingFields comment and implementation in ResourceEntry + // class for the details. + static scoped_ptr<AccountMetadata> CreateFrom(const base::Value& value); + + int64 quota_bytes_total() const { + return quota_bytes_total_; + } + + int64 quota_bytes_used() const { + return quota_bytes_used_; + } + + int64 largest_changestamp() const { + return largest_changestamp_; + } + + const ScopedVector<InstalledApp>& installed_apps() const { + return installed_apps_; + } + + void set_quota_bytes_total(int64 quota_bytes_total) { + quota_bytes_total_ = quota_bytes_total; + } + void set_quota_bytes_used(int64 quota_bytes_used) { + quota_bytes_used_ = quota_bytes_used; + } + void set_largest_changestamp(int64 largest_changestamp) { + largest_changestamp_ = largest_changestamp; + } + void set_installed_apps(ScopedVector<InstalledApp> installed_apps) { + installed_apps_ = installed_apps.Pass(); + } + + // Registers the mapping between JSON field names and the members in + // this class. + static void RegisterJSONConverter( + base::JSONValueConverter<AccountMetadata>* converter); + + private: + // Parses and initializes data members from content of |value|. + // Return false if parsing fails. + bool Parse(const base::Value& value); + + int64 quota_bytes_total_; + int64 quota_bytes_used_; + int64 largest_changestamp_; + ScopedVector<InstalledApp> installed_apps_; + + DISALLOW_COPY_AND_ASSIGN(AccountMetadata); +}; + + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc new file mode 100644 index 00000000000..58728d129ba --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc @@ -0,0 +1,389 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_wapi_parser.h" + +#include <string> + +#include "base/files/file_path.h" +#include "base/json/json_file_value_serializer.h" +#include "base/logging.h" +#include "base/time/time.h" +#include "base/values.h" +#include "google_apis/drive/test_util.h" +#include "google_apis/drive/time_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +// TODO(nhiroki): Move json files to out of 'chromeos' directory +// (http://crbug.com/149788). +// Test document feed parsing. +TEST(GDataWAPIParserTest, ResourceListJsonParser) { + std::string error; + scoped_ptr<base::Value> document = + test_util::LoadJSONFile("gdata/basic_feed.json"); + ASSERT_TRUE(document.get()); + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<ResourceList> feed(ResourceList::ExtractAndParse(*document)); + ASSERT_TRUE(feed.get()); + + base::Time update_time; + ASSERT_TRUE(util::GetTimeFromString("2011-12-14T01:03:21.151Z", + &update_time)); + + EXPECT_EQ(1, feed->start_index()); + EXPECT_EQ(1000, feed->items_per_page()); + EXPECT_EQ(update_time, feed->updated_time()); + + // Check authors. + ASSERT_EQ(1U, feed->authors().size()); + EXPECT_EQ("tester", feed->authors()[0]->name()); + EXPECT_EQ("tester@testing.com", feed->authors()[0]->email()); + + // Check links. + ASSERT_EQ(6U, feed->links().size()); + const Link* self_link = feed->GetLinkByType(Link::LINK_SELF); + ASSERT_TRUE(self_link); + EXPECT_EQ("https://self_link/", self_link->href().spec()); + EXPECT_EQ("application/atom+xml", self_link->mime_type()); + + const Link* resumable_link = + feed->GetLinkByType(Link::LINK_RESUMABLE_CREATE_MEDIA); + ASSERT_TRUE(resumable_link); + EXPECT_EQ("https://resumable_create_media_link/", + resumable_link->href().spec()); + EXPECT_EQ("application/atom+xml", resumable_link->mime_type()); + + // Check entries. + ASSERT_EQ(4U, feed->entries().size()); + + // Check a folder entry. + const ResourceEntry* folder_entry = feed->entries()[0]; + ASSERT_TRUE(folder_entry); + EXPECT_EQ(ENTRY_KIND_FOLDER, folder_entry->kind()); + EXPECT_EQ("\"HhMOFgcNHSt7ImBr\"", folder_entry->etag()); + EXPECT_EQ("folder:sub_sub_directory_folder_id", folder_entry->resource_id()); + EXPECT_EQ("https://1_folder_id", folder_entry->id()); + EXPECT_EQ("Entry 1 Title", folder_entry->title()); + base::Time entry1_update_time; + base::Time entry1_publish_time; + ASSERT_TRUE(util::GetTimeFromString("2011-04-01T18:34:08.234Z", + &entry1_update_time)); + ASSERT_TRUE(util::GetTimeFromString("2010-11-07T05:03:54.719Z", + &entry1_publish_time)); + EXPECT_EQ(entry1_update_time, folder_entry->updated_time()); + EXPECT_EQ(entry1_publish_time, folder_entry->published_time()); + + ASSERT_EQ(1U, folder_entry->authors().size()); + EXPECT_EQ("entry_tester", folder_entry->authors()[0]->name()); + EXPECT_EQ("entry_tester@testing.com", folder_entry->authors()[0]->email()); + EXPECT_EQ("https://1_folder_content_url/", + folder_entry->download_url().spec()); + EXPECT_EQ("application/atom+xml;type=feed", + folder_entry->content_mime_type()); + + ASSERT_EQ(1U, folder_entry->resource_links().size()); + const ResourceLink* feed_link = folder_entry->resource_links()[0]; + ASSERT_TRUE(feed_link); + ASSERT_EQ(ResourceLink::FEED_LINK_ACL, feed_link->type()); + + const Link* entry1_alternate_link = + folder_entry->GetLinkByType(Link::LINK_ALTERNATE); + ASSERT_TRUE(entry1_alternate_link); + EXPECT_EQ("https://1_folder_alternate_link/", + entry1_alternate_link->href().spec()); + EXPECT_EQ("text/html", entry1_alternate_link->mime_type()); + + const Link* entry1_edit_link = folder_entry->GetLinkByType(Link::LINK_EDIT); + ASSERT_TRUE(entry1_edit_link); + EXPECT_EQ("https://1_edit_link/", entry1_edit_link->href().spec()); + EXPECT_EQ("application/atom+xml", entry1_edit_link->mime_type()); + + // Check a file entry. + const ResourceEntry* file_entry = feed->entries()[1]; + ASSERT_TRUE(file_entry); + EXPECT_EQ(ENTRY_KIND_FILE, file_entry->kind()); + EXPECT_EQ("filename.m4a", file_entry->filename()); + EXPECT_EQ("sugg_file_name.m4a", file_entry->suggested_filename()); + EXPECT_EQ("3b4382ebefec6e743578c76bbd0575ce", file_entry->file_md5()); + EXPECT_EQ(892721, file_entry->file_size()); + const Link* file_parent_link = file_entry->GetLinkByType(Link::LINK_PARENT); + ASSERT_TRUE(file_parent_link); + EXPECT_EQ("https://file_link_parent/", file_parent_link->href().spec()); + EXPECT_EQ("application/atom+xml", file_parent_link->mime_type()); + EXPECT_EQ("Medical", file_parent_link->title()); + const Link* file_open_with_link = + file_entry->GetLinkByType(Link::LINK_OPEN_WITH); + ASSERT_TRUE(file_open_with_link); + EXPECT_EQ("https://xml_file_entry_open_with_link/", + file_open_with_link->href().spec()); + EXPECT_EQ("application/atom+xml", file_open_with_link->mime_type()); + EXPECT_EQ("the_app_id", file_open_with_link->app_id()); + EXPECT_EQ(654321, file_entry->changestamp()); + + const Link* file_unknown_link = file_entry->GetLinkByType(Link::LINK_UNKNOWN); + ASSERT_TRUE(file_unknown_link); + EXPECT_EQ("https://xml_file_fake_entry_open_with_link/", + file_unknown_link->href().spec()); + EXPECT_EQ("application/atom+xml", file_unknown_link->mime_type()); + EXPECT_EQ("", file_unknown_link->app_id()); + + // Check a file entry. + const ResourceEntry* resource_entry = feed->entries()[2]; + ASSERT_TRUE(resource_entry); + EXPECT_EQ(ENTRY_KIND_DOCUMENT, resource_entry->kind()); + EXPECT_TRUE(resource_entry->is_hosted_document()); + EXPECT_TRUE(resource_entry->is_google_document()); + EXPECT_FALSE(resource_entry->is_external_document()); + + // Check an external document entry. + const ResourceEntry* app_entry = feed->entries()[3]; + ASSERT_TRUE(app_entry); + EXPECT_EQ(ENTRY_KIND_EXTERNAL_APP, app_entry->kind()); + EXPECT_TRUE(app_entry->is_hosted_document()); + EXPECT_TRUE(app_entry->is_external_document()); + EXPECT_FALSE(app_entry->is_google_document()); +} + + +// Test document feed parsing. +TEST(GDataWAPIParserTest, ResourceEntryJsonParser) { + std::string error; + scoped_ptr<base::Value> document = + test_util::LoadJSONFile("gdata/file_entry.json"); + ASSERT_TRUE(document.get()); + ASSERT_EQ(base::Value::TYPE_DICTIONARY, document->GetType()); + scoped_ptr<ResourceEntry> entry(ResourceEntry::ExtractAndParse(*document)); + ASSERT_TRUE(entry.get()); + + EXPECT_EQ(ENTRY_KIND_FILE, entry->kind()); + EXPECT_EQ("\"HhMOFgxXHit7ImBr\"", entry->etag()); + EXPECT_EQ("file:2_file_resource_id", entry->resource_id()); + EXPECT_EQ("2_file_id", entry->id()); + EXPECT_EQ("File 1.mp3", entry->title()); + base::Time entry1_update_time; + base::Time entry1_publish_time; + ASSERT_TRUE(util::GetTimeFromString("2011-12-14T00:40:47.330Z", + &entry1_update_time)); + ASSERT_TRUE(util::GetTimeFromString("2011-12-13T00:40:47.330Z", + &entry1_publish_time)); + EXPECT_EQ(entry1_update_time, entry->updated_time()); + EXPECT_EQ(entry1_publish_time, entry->published_time()); + + EXPECT_EQ(1U, entry->authors().size()); + EXPECT_EQ("tester", entry->authors()[0]->name()); + EXPECT_EQ("tester@testing.com", entry->authors()[0]->email()); + EXPECT_EQ("https://file_content_url/", + entry->download_url().spec()); + EXPECT_EQ("audio/mpeg", + entry->content_mime_type()); + + // Check feed links. + ASSERT_EQ(1U, entry->resource_links().size()); + const ResourceLink* feed_link_1 = entry->resource_links()[0]; + ASSERT_TRUE(feed_link_1); + EXPECT_EQ(ResourceLink::FEED_LINK_REVISIONS, feed_link_1->type()); + + // Check links. + ASSERT_EQ(8U, entry->links().size()); + const Link* entry1_alternate_link = + entry->GetLinkByType(Link::LINK_ALTERNATE); + ASSERT_TRUE(entry1_alternate_link); + EXPECT_EQ("https://file_link_alternate/", + entry1_alternate_link->href().spec()); + EXPECT_EQ("text/html", entry1_alternate_link->mime_type()); + + const Link* entry1_edit_link = entry->GetLinkByType(Link::LINK_EDIT_MEDIA); + ASSERT_TRUE(entry1_edit_link); + EXPECT_EQ("https://file_edit_media/", + entry1_edit_link->href().spec()); + EXPECT_EQ("audio/mpeg", entry1_edit_link->mime_type()); + + const Link* entry1_self_link = entry->GetLinkByType(Link::LINK_SELF); + ASSERT_TRUE(entry1_self_link); + EXPECT_EQ("https://file1_link_self/file%3A2_file_resource_id", + entry1_self_link->href().spec()); + EXPECT_EQ("application/atom+xml", entry1_self_link->mime_type()); + EXPECT_EQ("", entry1_self_link->app_id()); + + const Link* entry1_open_with_link = + entry->GetLinkByType(Link::LINK_OPEN_WITH); + ASSERT_TRUE(entry1_open_with_link); + EXPECT_EQ("https://entry1_open_with_link/", + entry1_open_with_link->href().spec()); + EXPECT_EQ("application/atom+xml", entry1_open_with_link->mime_type()); + EXPECT_EQ("the_app_id", entry1_open_with_link->app_id()); + + const Link* entry1_unknown_link = entry->GetLinkByType(Link::LINK_UNKNOWN); + ASSERT_TRUE(entry1_unknown_link); + EXPECT_EQ("https://entry1_fake_entry_open_with_link/", + entry1_unknown_link->href().spec()); + EXPECT_EQ("application/atom+xml", entry1_unknown_link->mime_type()); + EXPECT_EQ("", entry1_unknown_link->app_id()); + + // Check a file properties. + EXPECT_EQ(ENTRY_KIND_FILE, entry->kind()); + EXPECT_EQ("File 1.mp3", entry->filename()); + EXPECT_EQ("File 1.mp3", entry->suggested_filename()); + EXPECT_EQ("3b4382ebefec6e743578c76bbd0575ce", entry->file_md5()); + EXPECT_EQ(892721, entry->file_size()); + + // WAPI doesn't provide image metadata, but these fields are available + // since this class can wrap data received from Drive API (via a converter). + EXPECT_EQ(-1, entry->image_width()); + EXPECT_EQ(-1, entry->image_height()); + EXPECT_EQ(-1, entry->image_rotation()); +} + +TEST(GDataWAPIParserTest, AccountMetadataParser) { + scoped_ptr<base::Value> document = + test_util::LoadJSONFile("gdata/account_metadata.json"); + ASSERT_TRUE(document.get()); + base::DictionaryValue* document_dict = NULL; + base::DictionaryValue* entry_value = NULL; + ASSERT_TRUE(document->GetAsDictionary(&document_dict)); + ASSERT_TRUE(document_dict->GetDictionary(std::string("entry"), &entry_value)); + ASSERT_TRUE(entry_value); + + scoped_ptr<AccountMetadata> metadata( + AccountMetadata::CreateFrom(*document)); + ASSERT_TRUE(metadata.get()); + EXPECT_EQ(GG_LONGLONG(6789012345), metadata->quota_bytes_used()); + EXPECT_EQ(GG_LONGLONG(9876543210), metadata->quota_bytes_total()); + EXPECT_EQ(654321, metadata->largest_changestamp()); + EXPECT_EQ(2U, metadata->installed_apps().size()); + const InstalledApp* first_app = metadata->installed_apps()[0]; + const InstalledApp* second_app = metadata->installed_apps()[1]; + + ASSERT_TRUE(first_app); + EXPECT_EQ("Drive App 1", first_app->app_name()); + EXPECT_EQ("Drive App Object 1", first_app->object_type()); + EXPECT_TRUE(first_app->supports_create()); + EXPECT_EQ("https://chrome.google.com/webstore/detail/abcdefabcdef", + first_app->GetProductUrl().spec()); + + ASSERT_EQ(2U, first_app->primary_mimetypes().size()); + EXPECT_EQ("application/test_type_1", + *first_app->primary_mimetypes()[0]); + EXPECT_EQ("application/vnd.google-apps.drive-sdk.11111111", + *first_app->primary_mimetypes()[1]); + + ASSERT_EQ(1U, first_app->secondary_mimetypes().size()); + EXPECT_EQ("image/jpeg", *first_app->secondary_mimetypes()[0]); + + ASSERT_EQ(2U, first_app->primary_extensions().size()); + EXPECT_EQ("ext_1", *first_app->primary_extensions()[0]); + EXPECT_EQ("ext_2", *first_app->primary_extensions()[1]); + + ASSERT_EQ(1U, first_app->secondary_extensions().size()); + EXPECT_EQ("ext_3", *first_app->secondary_extensions()[0]); + + ASSERT_EQ(1U, first_app->app_icons().size()); + EXPECT_EQ(AppIcon::ICON_DOCUMENT, first_app->app_icons()[0]->category()); + EXPECT_EQ(16, first_app->app_icons()[0]->icon_side_length()); + GURL icon_url = first_app->app_icons()[0]->GetIconURL(); + EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png", icon_url.spec()); + InstalledApp::IconList icons = + first_app->GetIconsForCategory(AppIcon::ICON_DOCUMENT); + EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png", + icons[0].second.spec()); + icons = first_app->GetIconsForCategory(AppIcon::ICON_SHARED_DOCUMENT); + EXPECT_TRUE(icons.empty()); + + ASSERT_TRUE(second_app); + EXPECT_EQ("Drive App 2", second_app->app_name()); + EXPECT_EQ("Drive App Object 2", second_app->object_type()); + EXPECT_EQ("https://chrome.google.com/webstore/detail/deadbeefdeadbeef", + second_app->GetProductUrl().spec()); + EXPECT_FALSE(second_app->supports_create()); + EXPECT_EQ(2U, second_app->primary_mimetypes().size()); + EXPECT_EQ(0U, second_app->secondary_mimetypes().size()); + EXPECT_EQ(1U, second_app->primary_extensions().size()); + EXPECT_EQ(0U, second_app->secondary_extensions().size()); +} + +TEST(GDataWAPIParserTest, ClassifyEntryKindByFileExtension) { + EXPECT_EQ( + ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.gdoc")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.gsheet")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.gslides")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.gdraw")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.gtable")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_EXTERNAL_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.glink")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.tar.gz")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test.txt")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath(FILE_PATH_LITERAL("Test")))); + EXPECT_EQ( + ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKindByFileExtension( + base::FilePath())); +} + +TEST(GDataWAPIParserTest, ResourceEntryClassifyEntryKind) { + EXPECT_EQ(ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_UNKNOWN)); + EXPECT_EQ(ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_ITEM)); + EXPECT_EQ(ResourceEntry::KIND_OF_NONE, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_SITE)); + EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_DOCUMENT)); + EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_SPREADSHEET)); + EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_PRESENTATION)); + EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_DRAWING)); + EXPECT_EQ(ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_TABLE)); + EXPECT_EQ(ResourceEntry::KIND_OF_EXTERNAL_DOCUMENT | + ResourceEntry::KIND_OF_HOSTED_DOCUMENT, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_EXTERNAL_APP)); + EXPECT_EQ(ResourceEntry::KIND_OF_FOLDER, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_FOLDER)); + EXPECT_EQ(ResourceEntry::KIND_OF_FILE, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_FILE)); + EXPECT_EQ(ResourceEntry::KIND_OF_FILE, + ResourceEntry::ClassifyEntryKind(ENTRY_KIND_PDF)); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_requests.cc b/chromium/google_apis/drive/gdata_wapi_requests.cc new file mode 100644 index 00000000000..1ae8a95ef08 --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_requests.cc @@ -0,0 +1,680 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_wapi_requests.h" + +#include "base/location.h" +#include "base/sequenced_task_runner.h" +#include "base/task_runner_util.h" +#include "base/values.h" +#include "google_apis/drive/gdata_wapi_parser.h" +#include "google_apis/drive/gdata_wapi_url_generator.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/request_util.h" +#include "third_party/libxml/chromium/libxml_utils.h" + +using net::URLFetcher; + +namespace google_apis { + +namespace { + +// Parses the JSON value to ResourceList. +scoped_ptr<ResourceList> ParseResourceListOnBlockingPool( + scoped_ptr<base::Value> value) { + DCHECK(value); + + return ResourceList::ExtractAndParse(*value); +} + +// Runs |callback| with |error| and |resource_list|, but replace the error code +// with GDATA_PARSE_ERROR, if there was a parsing error. +void DidParseResourceListOnBlockingPool( + const GetResourceListCallback& callback, + GDataErrorCode error, + scoped_ptr<ResourceList> resource_list) { + DCHECK(!callback.is_null()); + + // resource_list being NULL indicates there was a parsing error. + if (!resource_list) + error = GDATA_PARSE_ERROR; + + callback.Run(error, resource_list.Pass()); +} + +// Parses the JSON value to ResourceList on the blocking pool and runs +// |callback| on the UI thread once parsing is done. +void ParseResourceListAndRun( + scoped_refptr<base::TaskRunner> blocking_task_runner, + const GetResourceListCallback& callback, + GDataErrorCode error, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(error, scoped_ptr<ResourceList>()); + return; + } + + base::PostTaskAndReplyWithResult( + blocking_task_runner, + FROM_HERE, + base::Bind(&ParseResourceListOnBlockingPool, base::Passed(&value)), + base::Bind(&DidParseResourceListOnBlockingPool, callback, error)); +} + +// Parses the JSON value to AccountMetadata and runs |callback| on the UI +// thread once parsing is done. +void ParseAccounetMetadataAndRun(const GetAccountMetadataCallback& callback, + GDataErrorCode error, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(error, scoped_ptr<AccountMetadata>()); + return; + } + + // Parsing AccountMetadata is cheap enough to do on UI thread. + scoped_ptr<AccountMetadata> entry = + google_apis::AccountMetadata::CreateFrom(*value); + if (!entry) { + callback.Run(GDATA_PARSE_ERROR, scoped_ptr<AccountMetadata>()); + return; + } + + callback.Run(error, entry.Pass()); +} + +// Parses the |value| to ResourceEntry with error handling. +// This is designed to be used for ResumeUploadRequest and +// GetUploadStatusRequest. +scoped_ptr<ResourceEntry> ParseResourceEntry(scoped_ptr<base::Value> value) { + scoped_ptr<ResourceEntry> entry; + if (value.get()) { + entry = ResourceEntry::ExtractAndParse(*value); + + // Note: |value| may be NULL, in particular if the callback is for a + // failure. + if (!entry.get()) + LOG(WARNING) << "Invalid entry received on upload."; + } + + return entry.Pass(); +} + +// Extracts the open link url from the JSON Feed. Used by AuthorizeApp(). +void ParseOpenLinkAndRun(const std::string& app_id, + const AuthorizeAppCallback& callback, + GDataErrorCode error, + scoped_ptr<base::Value> value) { + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(error, GURL()); + return; + } + + // Parsing ResourceEntry is cheap enough to do on UI thread. + scoped_ptr<ResourceEntry> resource_entry = ParseResourceEntry(value.Pass()); + if (!resource_entry) { + callback.Run(GDATA_PARSE_ERROR, GURL()); + return; + } + + // Look for the link to open the file with the app with |app_id|. + const ScopedVector<Link>& resource_links = resource_entry->links(); + GURL open_link; + for (size_t i = 0; i < resource_links.size(); ++i) { + const Link& link = *resource_links[i]; + if (link.type() == Link::LINK_OPEN_WITH && link.app_id() == app_id) { + open_link = link.href(); + break; + } + } + + if (open_link.is_empty()) + error = GDATA_OTHER_ERROR; + + callback.Run(error, open_link); +} + +} // namespace + +//============================ GetResourceListRequest ======================== + +GetResourceListRequest::GetResourceListRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GURL& override_url, + int64 start_changestamp, + const std::string& search_string, + const std::string& directory_resource_id, + const GetResourceListCallback& callback) + : GetDataRequest( + sender, + base::Bind(&ParseResourceListAndRun, + make_scoped_refptr(sender->blocking_task_runner()), + callback)), + url_generator_(url_generator), + override_url_(override_url), + start_changestamp_(start_changestamp), + search_string_(search_string), + directory_resource_id_(directory_resource_id) { + DCHECK(!callback.is_null()); +} + +GetResourceListRequest::~GetResourceListRequest() {} + +GURL GetResourceListRequest::GetURL() const { + return url_generator_.GenerateResourceListUrl(override_url_, + start_changestamp_, + search_string_, + directory_resource_id_); +} + +//============================ SearchByTitleRequest ========================== + +SearchByTitleRequest::SearchByTitleRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const std::string& title, + const std::string& directory_resource_id, + const GetResourceListCallback& callback) + : GetDataRequest( + sender, + base::Bind(&ParseResourceListAndRun, + make_scoped_refptr(sender->blocking_task_runner()), + callback)), + url_generator_(url_generator), + title_(title), + directory_resource_id_(directory_resource_id) { + DCHECK(!callback.is_null()); +} + +SearchByTitleRequest::~SearchByTitleRequest() {} + +GURL SearchByTitleRequest::GetURL() const { + return url_generator_.GenerateSearchByTitleUrl( + title_, directory_resource_id_); +} + +//============================ GetResourceEntryRequest ======================= + +GetResourceEntryRequest::GetResourceEntryRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const std::string& resource_id, + const GURL& embed_origin, + const GetDataCallback& callback) + : GetDataRequest(sender, callback), + url_generator_(url_generator), + resource_id_(resource_id), + embed_origin_(embed_origin) { + DCHECK(!callback.is_null()); +} + +GetResourceEntryRequest::~GetResourceEntryRequest() {} + +GURL GetResourceEntryRequest::GetURL() const { + return url_generator_.GenerateEditUrlWithEmbedOrigin( + resource_id_, embed_origin_); +} + +//========================= GetAccountMetadataRequest ======================== + +GetAccountMetadataRequest::GetAccountMetadataRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GetAccountMetadataCallback& callback, + bool include_installed_apps) + : GetDataRequest(sender, + base::Bind(&ParseAccounetMetadataAndRun, callback)), + url_generator_(url_generator), + include_installed_apps_(include_installed_apps) { + DCHECK(!callback.is_null()); +} + +GetAccountMetadataRequest::~GetAccountMetadataRequest() {} + +GURL GetAccountMetadataRequest::GetURL() const { + return url_generator_.GenerateAccountMetadataUrl(include_installed_apps_); +} + +//=========================== DeleteResourceRequest ========================== + +DeleteResourceRequest::DeleteResourceRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& resource_id, + const std::string& etag) + : EntryActionRequest(sender, callback), + url_generator_(url_generator), + resource_id_(resource_id), + etag_(etag) { + DCHECK(!callback.is_null()); +} + +DeleteResourceRequest::~DeleteResourceRequest() {} + +GURL DeleteResourceRequest::GetURL() const { + return url_generator_.GenerateEditUrl(resource_id_); +} + +URLFetcher::RequestType DeleteResourceRequest::GetRequestType() const { + return URLFetcher::DELETE_REQUEST; +} + +std::vector<std::string> +DeleteResourceRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(util::GenerateIfMatchHeader(etag_)); + return headers; +} + +//========================== CreateDirectoryRequest ========================== + +CreateDirectoryRequest::CreateDirectoryRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GetDataCallback& callback, + const std::string& parent_resource_id, + const std::string& directory_title) + : GetDataRequest(sender, callback), + url_generator_(url_generator), + parent_resource_id_(parent_resource_id), + directory_title_(directory_title) { + DCHECK(!callback.is_null()); +} + +CreateDirectoryRequest::~CreateDirectoryRequest() {} + +GURL CreateDirectoryRequest::GetURL() const { + return url_generator_.GenerateContentUrl(parent_resource_id_); +} + +URLFetcher::RequestType +CreateDirectoryRequest::GetRequestType() const { + return URLFetcher::POST; +} + +bool CreateDirectoryRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + upload_content_type->assign("application/atom+xml"); + XmlWriter xml_writer; + xml_writer.StartWriting(); + xml_writer.StartElement("entry"); + xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); + + xml_writer.StartElement("category"); + xml_writer.AddAttribute("scheme", + "http://schemas.google.com/g/2005#kind"); + xml_writer.AddAttribute("term", + "http://schemas.google.com/docs/2007#folder"); + xml_writer.EndElement(); // Ends "category" element. + + xml_writer.WriteElement("title", directory_title_); + + xml_writer.EndElement(); // Ends "entry" element. + xml_writer.StopWriting(); + upload_content->assign(xml_writer.GetWrittenString()); + DVLOG(1) << "CreateDirectory data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//=========================== RenameResourceRequest ========================== + +RenameResourceRequest::RenameResourceRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& resource_id, + const std::string& new_title) + : EntryActionRequest(sender, callback), + url_generator_(url_generator), + resource_id_(resource_id), + new_title_(new_title) { + DCHECK(!callback.is_null()); +} + +RenameResourceRequest::~RenameResourceRequest() {} + +URLFetcher::RequestType RenameResourceRequest::GetRequestType() const { + return URLFetcher::PUT; +} + +std::vector<std::string> +RenameResourceRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(util::kIfMatchAllHeader); + return headers; +} + +GURL RenameResourceRequest::GetURL() const { + return url_generator_.GenerateEditUrl(resource_id_); +} + +bool RenameResourceRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + upload_content_type->assign("application/atom+xml"); + XmlWriter xml_writer; + xml_writer.StartWriting(); + xml_writer.StartElement("entry"); + xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); + + xml_writer.WriteElement("title", new_title_); + + xml_writer.EndElement(); // Ends "entry" element. + xml_writer.StopWriting(); + upload_content->assign(xml_writer.GetWrittenString()); + DVLOG(1) << "RenameResourceRequest data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//=========================== AuthorizeAppRequest ========================== + +AuthorizeAppRequest::AuthorizeAppRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const AuthorizeAppCallback& callback, + const std::string& resource_id, + const std::string& app_id) + : GetDataRequest(sender, + base::Bind(&ParseOpenLinkAndRun, app_id, callback)), + url_generator_(url_generator), + resource_id_(resource_id), + app_id_(app_id) { + DCHECK(!callback.is_null()); +} + +AuthorizeAppRequest::~AuthorizeAppRequest() {} + +URLFetcher::RequestType AuthorizeAppRequest::GetRequestType() const { + return URLFetcher::PUT; +} + +std::vector<std::string> +AuthorizeAppRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(util::kIfMatchAllHeader); + return headers; +} + +bool AuthorizeAppRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + upload_content_type->assign("application/atom+xml"); + XmlWriter xml_writer; + xml_writer.StartWriting(); + xml_writer.StartElement("entry"); + xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); + xml_writer.AddAttribute("xmlns:docs", "http://schemas.google.com/docs/2007"); + xml_writer.WriteElement("docs:authorizedApp", app_id_); + + xml_writer.EndElement(); // Ends "entry" element. + xml_writer.StopWriting(); + upload_content->assign(xml_writer.GetWrittenString()); + DVLOG(1) << "AuthorizeAppRequest data: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +GURL AuthorizeAppRequest::GetURL() const { + return url_generator_.GenerateEditUrl(resource_id_); +} + +//======================= AddResourceToDirectoryRequest ====================== + +AddResourceToDirectoryRequest::AddResourceToDirectoryRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& parent_resource_id, + const std::string& resource_id) + : EntryActionRequest(sender, callback), + url_generator_(url_generator), + parent_resource_id_(parent_resource_id), + resource_id_(resource_id) { + DCHECK(!callback.is_null()); +} + +AddResourceToDirectoryRequest::~AddResourceToDirectoryRequest() {} + +GURL AddResourceToDirectoryRequest::GetURL() const { + return url_generator_.GenerateContentUrl(parent_resource_id_); +} + +URLFetcher::RequestType +AddResourceToDirectoryRequest::GetRequestType() const { + return URLFetcher::POST; +} + +bool AddResourceToDirectoryRequest::GetContentData( + std::string* upload_content_type, std::string* upload_content) { + upload_content_type->assign("application/atom+xml"); + XmlWriter xml_writer; + xml_writer.StartWriting(); + xml_writer.StartElement("entry"); + xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); + + xml_writer.WriteElement( + "id", url_generator_.GenerateEditUrlWithoutParams(resource_id_).spec()); + + xml_writer.EndElement(); // Ends "entry" element. + xml_writer.StopWriting(); + upload_content->assign(xml_writer.GetWrittenString()); + DVLOG(1) << "AddResourceToDirectoryRequest data: " << *upload_content_type + << ", [" << *upload_content << "]"; + return true; +} + +//==================== RemoveResourceFromDirectoryRequest ==================== + +RemoveResourceFromDirectoryRequest::RemoveResourceFromDirectoryRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& parent_resource_id, + const std::string& document_resource_id) + : EntryActionRequest(sender, callback), + url_generator_(url_generator), + resource_id_(document_resource_id), + parent_resource_id_(parent_resource_id) { + DCHECK(!callback.is_null()); +} + +RemoveResourceFromDirectoryRequest::~RemoveResourceFromDirectoryRequest() { +} + +GURL RemoveResourceFromDirectoryRequest::GetURL() const { + return url_generator_.GenerateResourceUrlForRemoval( + parent_resource_id_, resource_id_); +} + +URLFetcher::RequestType +RemoveResourceFromDirectoryRequest::GetRequestType() const { + return URLFetcher::DELETE_REQUEST; +} + +std::vector<std::string> +RemoveResourceFromDirectoryRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers; + headers.push_back(util::kIfMatchAllHeader); + return headers; +} + +//======================= InitiateUploadNewFileRequest ======================= + +InitiateUploadNewFileRequest::InitiateUploadNewFileRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title) + : InitiateUploadRequestBase(sender, callback, content_type, content_length), + url_generator_(url_generator), + parent_resource_id_(parent_resource_id), + title_(title) { +} + +InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {} + +GURL InitiateUploadNewFileRequest::GetURL() const { + return url_generator_.GenerateInitiateUploadNewFileUrl(parent_resource_id_); +} + +net::URLFetcher::RequestType +InitiateUploadNewFileRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +bool InitiateUploadNewFileRequest::GetContentData( + std::string* upload_content_type, + std::string* upload_content) { + upload_content_type->assign("application/atom+xml"); + XmlWriter xml_writer; + xml_writer.StartWriting(); + xml_writer.StartElement("entry"); + xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); + xml_writer.AddAttribute("xmlns:docs", + "http://schemas.google.com/docs/2007"); + xml_writer.WriteElement("title", title_); + xml_writer.EndElement(); // Ends "entry" element. + xml_writer.StopWriting(); + upload_content->assign(xml_writer.GetWrittenString()); + DVLOG(1) << "InitiateUploadNewFile: " << *upload_content_type << ", [" + << *upload_content << "]"; + return true; +} + +//===================== InitiateUploadExistingFileRequest ==================== + +InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const std::string& etag) + : InitiateUploadRequestBase(sender, callback, content_type, content_length), + url_generator_(url_generator), + resource_id_(resource_id), + etag_(etag) { +} + +InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {} + +GURL InitiateUploadExistingFileRequest::GetURL() const { + return url_generator_.GenerateInitiateUploadExistingFileUrl(resource_id_); +} + +net::URLFetcher::RequestType +InitiateUploadExistingFileRequest::GetRequestType() const { + return net::URLFetcher::PUT; +} + +bool InitiateUploadExistingFileRequest::GetContentData( + std::string* upload_content_type, + std::string* upload_content) { + // According to the document there is no need to send the content-type. + // However, the server would return 500 server error without the + // content-type. + // As its workaround, send "text/plain" content-type here. + *upload_content_type = "text/plain"; + *upload_content = ""; + return true; +} + +std::vector<std::string> +InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const { + std::vector<std::string> headers( + InitiateUploadRequestBase::GetExtraRequestHeaders()); + headers.push_back(util::GenerateIfMatchHeader(etag_)); + return headers; +} + +//============================ ResumeUploadRequest =========================== + +ResumeUploadRequest::ResumeUploadRequest( + RequestSender* sender, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path) + : ResumeUploadRequestBase(sender, + upload_location, + start_position, + end_position, + content_length, + content_type, + local_file_path), + callback_(callback), + progress_callback_(progress_callback) { + DCHECK(!callback_.is_null()); +} + +ResumeUploadRequest::~ResumeUploadRequest() {} + +void ResumeUploadRequest::OnRangeRequestComplete( + const UploadRangeResponse& response, scoped_ptr<base::Value> value) { + callback_.Run(response, ParseResourceEntry(value.Pass())); +} + +void ResumeUploadRequest::OnURLFetchUploadProgress( + const URLFetcher* source, int64 current, int64 total) { + if (!progress_callback_.is_null()) + progress_callback_.Run(current, total); +} + +//========================== GetUploadStatusRequest ========================== + +GetUploadStatusRequest::GetUploadStatusRequest( + RequestSender* sender, + const UploadRangeCallback& callback, + const GURL& upload_url, + int64 content_length) + : GetUploadStatusRequestBase(sender, upload_url, content_length), + callback_(callback) { + DCHECK(!callback.is_null()); +} + +GetUploadStatusRequest::~GetUploadStatusRequest() {} + +void GetUploadStatusRequest::OnRangeRequestComplete( + const UploadRangeResponse& response, scoped_ptr<base::Value> value) { + callback_.Run(response, ParseResourceEntry(value.Pass())); +} + +//========================== DownloadFileRequest ========================== + +DownloadFileRequest::DownloadFileRequest( + RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback, + const std::string& resource_id, + const base::FilePath& output_file_path) + : DownloadFileRequestBase( + sender, + download_action_callback, + get_content_callback, + progress_callback, + url_generator.GenerateDownloadFileUrl(resource_id), + output_file_path) { +} + +DownloadFileRequest::~DownloadFileRequest() { +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_requests.h b/chromium/google_apis/drive/gdata_wapi_requests.h new file mode 100644 index 00000000000..a4587d7628b --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_requests.h @@ -0,0 +1,486 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_ +#define GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_ + +#include <string> +#include <vector> + +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/drive_common_callbacks.h" +#include "google_apis/drive/gdata_wapi_url_generator.h" + +namespace google_apis { + +class AccountMetadata; +class GDataWapiUrlGenerator; +class ResourceEntry; + +//============================ GetResourceListRequest ======================== + +// This class performs the request for fetching a resource list. +class GetResourceListRequest : public GetDataRequest { + public: + // override_url: + // If empty, a hard-coded base URL of the WAPI server is used to fetch + // the first page of the feed. This parameter is used for fetching 2nd + // page and onward. + // + // start_changestamp: + // This parameter specifies the starting point of a delta feed or 0 if a + // full feed is necessary. + // + // search_string: + // If non-empty, fetches a list of resources that match the search + // string. + // + // directory_resource_id: + // If non-empty, fetches a list of resources in a particular directory. + // + // callback: + // Called once the feed is fetched. Must not be null. + GetResourceListRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GURL& override_url, + int64 start_changestamp, + const std::string& search_string, + const std::string& directory_resource_id, + const GetResourceListCallback& callback); + virtual ~GetResourceListRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const GURL override_url_; + const int64 start_changestamp_; + const std::string search_string_; + const std::string directory_resource_id_; + + DISALLOW_COPY_AND_ASSIGN(GetResourceListRequest); +}; + +//============================ SearchByTitleRequest ========================== + +// This class performs the request for searching resources by title. +class SearchByTitleRequest : public GetDataRequest { + public: + // title: the search query. + // + // directory_resource_id: If given (non-empty), the search target is + // directly under the directory with the |directory_resource_id|. + // If empty, the search target is all the existing resources. + // + // callback: + // Called once the feed is fetched. Must not be null. + SearchByTitleRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const std::string& title, + const std::string& directory_resource_id, + const GetResourceListCallback& callback); + virtual ~SearchByTitleRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string title_; + const std::string directory_resource_id_; + + DISALLOW_COPY_AND_ASSIGN(SearchByTitleRequest); +}; + +//========================= GetResourceEntryRequest ========================== + +// This class performs the request for fetching a single resource entry. +class GetResourceEntryRequest : public GetDataRequest { + public: + // |callback| must not be null. + GetResourceEntryRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const std::string& resource_id, + const GURL& embed_origin, + const GetDataCallback& callback); + virtual ~GetResourceEntryRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + // Resource id of the requested entry. + const std::string resource_id_; + // Embed origin for an url to the sharing dialog. Can be empty. + const GURL& embed_origin_; + + DISALLOW_COPY_AND_ASSIGN(GetResourceEntryRequest); +}; + +//========================= GetAccountMetadataRequest ======================== + +// Callback used for GetAccountMetadata(). +typedef base::Callback<void(GDataErrorCode error, + scoped_ptr<AccountMetadata> account_metadata)> + GetAccountMetadataCallback; + +// This class performs the request for fetching account metadata. +class GetAccountMetadataRequest : public GetDataRequest { + public: + // If |include_installed_apps| is set to true, the result should include + // the list of installed third party applications. + // |callback| must not be null. + GetAccountMetadataRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GetAccountMetadataCallback& callback, + bool include_installed_apps); + virtual ~GetAccountMetadataRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const bool include_installed_apps_; + + DISALLOW_COPY_AND_ASSIGN(GetAccountMetadataRequest); +}; + +//=========================== DeleteResourceRequest ========================== + +// This class performs the request for deleting a resource. +// +// In WAPI, "gd:deleted" means that the resource was put in the trash, and +// "docs:removed" means its permanently gone. Since what the class does is to +// put the resource into trash, we have chosen "Delete" in the name, even though +// we are preferring the term "Remove" in drive/google_api code. +class DeleteResourceRequest : public EntryActionRequest { + public: + // |callback| must not be null. + DeleteResourceRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& resource_id, + const std::string& etag); + virtual ~DeleteResourceRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string etag_; + + DISALLOW_COPY_AND_ASSIGN(DeleteResourceRequest); +}; + +//========================== CreateDirectoryRequest ========================== + +// This class performs the request for creating a directory. +class CreateDirectoryRequest : public GetDataRequest { + public: + // A new directory will be created under a directory specified by + // |parent_resource_id|. If this parameter is empty, a new directory will + // be created in the root directory. + // |callback| must not be null. + CreateDirectoryRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const GetDataCallback& callback, + const std::string& parent_resource_id, + const std::string& directory_title); + virtual ~CreateDirectoryRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string parent_resource_id_; + const std::string directory_title_; + + DISALLOW_COPY_AND_ASSIGN(CreateDirectoryRequest); +}; + +//=========================== RenameResourceRequest ========================== + +// This class performs the request for renaming a document/file/directory. +class RenameResourceRequest : public EntryActionRequest { + public: + // |callback| must not be null. + RenameResourceRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& resource_id, + const std::string& new_title); + virtual ~RenameResourceRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string new_title_; + + DISALLOW_COPY_AND_ASSIGN(RenameResourceRequest); +}; + +//=========================== AuthorizeAppRequest ========================== + +// This class performs the request for authorizing an application specified +// by |app_id| to access a document specified by |resource_id|. +class AuthorizeAppRequest : public GetDataRequest { + public: + // |callback| must not be null. + AuthorizeAppRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const AuthorizeAppCallback& callback, + const std::string& resource_id, + const std::string& app_id); + virtual ~AuthorizeAppRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string app_id_; + + DISALLOW_COPY_AND_ASSIGN(AuthorizeAppRequest); +}; + +//======================= AddResourceToDirectoryRequest ====================== + +// This class performs the request for adding a document/file/directory +// to a directory. +class AddResourceToDirectoryRequest : public EntryActionRequest { + public: + // |callback| must not be null. + AddResourceToDirectoryRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& parent_resource_id, + const std::string& resource_id); + virtual ~AddResourceToDirectoryRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string parent_resource_id_; + const std::string resource_id_; + + DISALLOW_COPY_AND_ASSIGN(AddResourceToDirectoryRequest); +}; + +//==================== RemoveResourceFromDirectoryRequest ==================== + +// This class performs the request for removing a document/file/directory +// from a directory. +class RemoveResourceFromDirectoryRequest : public EntryActionRequest { + public: + // |callback| must not be null. + RemoveResourceFromDirectoryRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const EntryActionCallback& callback, + const std::string& parent_resource_id, + const std::string& resource_id); + virtual ~RemoveResourceFromDirectoryRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string parent_resource_id_; + + DISALLOW_COPY_AND_ASSIGN(RemoveResourceFromDirectoryRequest); +}; + +//======================= InitiateUploadNewFileRequest ======================= + +// This class performs the request for initiating the upload of a new file. +class InitiateUploadNewFileRequest : public InitiateUploadRequestBase { + public: + // |title| should be set. + // |parent_upload_url| should be the upload_url() of the parent directory. + // (resumable-create-media URL) + // See also the comments of InitiateUploadRequestBase for more details + // about the other parameters. + InitiateUploadNewFileRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title); + virtual ~InitiateUploadNewFileRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string parent_resource_id_; + const std::string title_; + + DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest); +}; + +//==================== InitiateUploadExistingFileRequest ===================== + +// This class performs the request for initiating the upload of an existing +// file. +class InitiateUploadExistingFileRequest + : public InitiateUploadRequestBase { + public: + // |upload_url| should be the upload_url() of the file + // (resumable-create-media URL) + // |etag| should be set if it is available to detect the upload confliction. + // See also the comments of InitiateUploadRequestBase for more details + // about the other parameters. + InitiateUploadExistingFileRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const InitiateUploadCallback& callback, + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const std::string& etag); + virtual ~InitiateUploadExistingFileRequest(); + + protected: + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const GDataWapiUrlGenerator url_generator_; + const std::string resource_id_; + const std::string etag_; + + DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest); +}; + +//============================ ResumeUploadRequest =========================== + +// Performs the request for resuming the upload of a file. +class ResumeUploadRequest : public ResumeUploadRequestBase { + public: + // See also ResumeUploadRequestBase's comment for parameters meaning. + // |callback| must not be null. + ResumeUploadRequest(RequestSender* sender, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback, + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path); + virtual ~ResumeUploadRequest(); + + protected: + // UploadRangeRequestBase overrides. + virtual void OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) OVERRIDE; + // content::UrlFetcherDelegate overrides. + virtual void OnURLFetchUploadProgress(const net::URLFetcher* source, + int64 current, int64 total) OVERRIDE; + + private: + const UploadRangeCallback callback_; + const ProgressCallback progress_callback_; + + DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequest); +}; + +//========================== GetUploadStatusRequest ========================== + +// Performs the request to request the current upload status of a file. +class GetUploadStatusRequest : public GetUploadStatusRequestBase { + public: + // See also GetUploadStatusRequestBase's comment for parameters meaning. + // |callback| must not be null. + GetUploadStatusRequest(RequestSender* sender, + const UploadRangeCallback& callback, + const GURL& upload_url, + int64 content_length); + virtual ~GetUploadStatusRequest(); + + protected: + // UploadRangeRequestBase overrides. + virtual void OnRangeRequestComplete( + const UploadRangeResponse& response, + scoped_ptr<base::Value> value) OVERRIDE; + + private: + const UploadRangeCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequest); +}; + + +//========================== DownloadFileRequest ========================== + +// This class performs the request for downloading of a specified file. +class DownloadFileRequest : public DownloadFileRequestBase { + public: + // See also DownloadFileRequestBase's comment for parameters meaning. + DownloadFileRequest(RequestSender* sender, + const GDataWapiUrlGenerator& url_generator, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback, + const std::string& resource_id, + const base::FilePath& output_file_path); + virtual ~DownloadFileRequest(); + + DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc new file mode 100644 index 00000000000..e302f23f1e8 --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc @@ -0,0 +1,1561 @@ +// Copyright (c) 2012 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 <algorithm> +#include <map> + +#include "base/bind.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "google_apis/drive/gdata_wapi_parser.h" +#include "google_apis/drive/gdata_wapi_requests.h" +#include "google_apis/drive/gdata_wapi_url_generator.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/test_util.h" +#include "net/base/escape.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +namespace { + +const char kTestUserAgent[] = "test-user-agent"; +const char kTestETag[] = "test_etag"; +const char kTestDownloadPathPrefix[] = "/download/"; + +class GDataWapiRequestsTest : public testing::Test { + public: + GDataWapiRequestsTest() { + } + + virtual void SetUp() OVERRIDE { + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + + request_sender_.reset(new RequestSender(new DummyAuthService, + request_context_getter_.get(), + message_loop_.message_loop_proxy(), + kTestUserAgent)); + + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); + test_server_.RegisterRequestHandler( + base::Bind(&test_util::HandleDownloadFileRequest, + test_server_.base_url(), + base::Unretained(&http_request_))); + test_server_.RegisterRequestHandler( + base::Bind(&GDataWapiRequestsTest::HandleResourceFeedRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&GDataWapiRequestsTest::HandleMetadataRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&GDataWapiRequestsTest::HandleCreateSessionRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&GDataWapiRequestsTest::HandleUploadRequest, + base::Unretained(this))); + test_server_.RegisterRequestHandler( + base::Bind(&GDataWapiRequestsTest::HandleDownloadRequest, + base::Unretained(this))); + + GURL test_base_url = test_util::GetBaseUrlForTesting(test_server_.port()); + url_generator_.reset(new GDataWapiUrlGenerator( + test_base_url, test_base_url.Resolve(kTestDownloadPathPrefix))); + + received_bytes_ = 0; + content_length_ = 0; + } + + protected: + // Handles a request for fetching a resource feed. + scoped_ptr<net::test_server::HttpResponse> HandleResourceFeedRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + std::string remaining_path; + if (absolute_url.path() == "/feeds/default/private/full" && + request.method == net::test_server::METHOD_POST) { + // This is a request for copying a document. + // TODO(satorux): we should generate valid JSON data for the newly + // copied document but for now, just return "file_entry.json" + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/file_entry.json"))); + return result.PassAs<net::test_server::HttpResponse>(); + } + + if (!test_util::RemovePrefix(absolute_url.path(), + "/feeds/default/private/full", + &remaining_path)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + + if (remaining_path.empty()) { + // Process the default feed. + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/root_feed.json"))); + return result.PassAs<net::test_server::HttpResponse>(); + } else { + // Process a feed for a single resource ID. + const std::string resource_id = net::UnescapeURLComponent( + remaining_path.substr(1), net::UnescapeRule::URL_SPECIAL_CHARS); + if (resource_id == "file:2_file_resource_id") { + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/file_entry.json"))); + return result.PassAs<net::test_server::HttpResponse>(); + } else if (resource_id == "folder:root/contents" && + request.method == net::test_server::METHOD_POST) { + // This is a request for creating a directory in the root directory. + // TODO(satorux): we should generate valid JSON data for the newly + // created directory but for now, just return "directory_entry.json" + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath( + "gdata/directory_entry.json"))); + return result.PassAs<net::test_server::HttpResponse>(); + } else if (resource_id == + "folder:root/contents/file:2_file_resource_id" && + request.method == net::test_server::METHOD_DELETE) { + // This is a request for deleting a file from the root directory. + // TODO(satorux): Investigate what's returned from the server, and + // copy it. For now, just return a random file, as the contents don't + // matter. + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/testfile.txt"))); + return result.PassAs<net::test_server::HttpResponse>(); + } else if (resource_id == "invalid_resource_id") { + // Check if this is an authorization request for an app. + // This emulates to return invalid formatted result from the server. + if (request.method == net::test_server::METHOD_PUT && + request.content.find("<docs:authorizedApp>") != std::string::npos) { + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/testfile.txt"))); + return result.PassAs<net::test_server::HttpResponse>(); + } + } + } + + return scoped_ptr<net::test_server::HttpResponse>(); + } + + // Handles a request for fetching a metadata feed. + scoped_ptr<net::test_server::HttpResponse> HandleMetadataRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + if (absolute_url.path() != "/feeds/metadata/default") + return scoped_ptr<net::test_server::HttpResponse>(); + + scoped_ptr<net::test_server::BasicHttpResponse> result( + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath( + "gdata/account_metadata.json"))); + if (absolute_url.query().find("include-installed-apps=true") == + string::npos) { + // Exclude the list of installed apps. + scoped_ptr<base::Value> parsed_content( + base::JSONReader::Read(result->content(), base::JSON_PARSE_RFC)); + CHECK(parsed_content); + + // Remove the install apps node. + base::DictionaryValue* dictionary_value; + CHECK(parsed_content->GetAsDictionary(&dictionary_value)); + dictionary_value->Remove("entry.docs$installedApp", NULL); + + // Write back it as the content of the result. + std::string content; + base::JSONWriter::Write(parsed_content.get(), &content); + result->set_content(content); + } + + return result.PassAs<net::test_server::HttpResponse>(); + } + + // Handles a request for creating a session for uploading. + scoped_ptr<net::test_server::HttpResponse> HandleCreateSessionRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + if (StartsWithASCII(absolute_url.path(), + "/feeds/upload/create-session/default/private/full", + true)) { // case sensitive + // This is an initiating upload URL. + scoped_ptr<net::test_server::BasicHttpResponse> http_response( + new net::test_server::BasicHttpResponse); + + // Check an ETag. + std::map<std::string, std::string>::const_iterator found = + request.headers.find("If-Match"); + if (found != request.headers.end() && + found->second != "*" && + found->second != kTestETag) { + http_response->set_code(net::HTTP_PRECONDITION_FAILED); + return http_response.PassAs<net::test_server::HttpResponse>(); + } + + // Check if the X-Upload-Content-Length is present. If yes, store the + // length of the file. + found = request.headers.find("X-Upload-Content-Length"); + if (found == request.headers.end() || + !base::StringToInt64(found->second, &content_length_)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + received_bytes_ = 0; + + http_response->set_code(net::HTTP_OK); + GURL upload_url; + // POST is used for a new file, and PUT is used for an existing file. + if (request.method == net::test_server::METHOD_POST) { + upload_url = test_server_.GetURL("/upload_new_file"); + } else if (request.method == net::test_server::METHOD_PUT) { + upload_url = test_server_.GetURL("/upload_existing_file"); + } else { + return scoped_ptr<net::test_server::HttpResponse>(); + } + http_response->AddCustomHeader("Location", upload_url.spec()); + return http_response.PassAs<net::test_server::HttpResponse>(); + } + + return scoped_ptr<net::test_server::HttpResponse>(); + } + + // Handles a request for uploading content. + scoped_ptr<net::test_server::HttpResponse> HandleUploadRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + if (absolute_url.path() != "/upload_new_file" && + absolute_url.path() != "/upload_existing_file") { + return scoped_ptr<net::test_server::HttpResponse>(); + } + + // TODO(satorux): We should create a correct JSON data for the uploaded + // file, but for now, just return file_entry.json. + scoped_ptr<net::test_server::BasicHttpResponse> response = + test_util::CreateHttpResponseFromFile( + test_util::GetTestFilePath("gdata/file_entry.json")); + // response.code() is set to SUCCESS. Change it to CREATED if it's a new + // file. + if (absolute_url.path() == "/upload_new_file") + response->set_code(net::HTTP_CREATED); + + // Check if the Content-Range header is present. This must be present if + // the request body is not empty. + if (!request.content.empty()) { + std::map<std::string, std::string>::const_iterator iter = + request.headers.find("Content-Range"); + if (iter == request.headers.end()) + return scoped_ptr<net::test_server::HttpResponse>(); + int64 length = 0; + int64 start_position = 0; + int64 end_position = 0; + if (!test_util::ParseContentRangeHeader(iter->second, + &start_position, + &end_position, + &length)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + EXPECT_EQ(start_position, received_bytes_); + EXPECT_EQ(length, content_length_); + // end_position is inclusive, but so +1 to change the range to byte size. + received_bytes_ = end_position + 1; + } + + // Add Range header to the response, based on the values of + // Content-Range header in the request. + // The header is annotated only when at least one byte is received. + if (received_bytes_ > 0) { + response->AddCustomHeader( + "Range", + "bytes=0-" + base::Int64ToString(received_bytes_ - 1)); + } + + // Change the code to RESUME_INCOMPLETE if upload is not complete. + if (received_bytes_ < content_length_) + response->set_code(static_cast<net::HttpStatusCode>(308)); + + return response.PassAs<net::test_server::HttpResponse>(); + } + + // Handles a request for downloading a file. + scoped_ptr<net::test_server::HttpResponse> HandleDownloadRequest( + const net::test_server::HttpRequest& request) { + http_request_ = request; + + const GURL absolute_url = test_server_.GetURL(request.relative_url); + std::string id; + if (!test_util::RemovePrefix(absolute_url.path(), + kTestDownloadPathPrefix, + &id)) { + return scoped_ptr<net::test_server::HttpResponse>(); + } + + // For testing, returns a text with |id| repeated 3 times. + scoped_ptr<net::test_server::BasicHttpResponse> response( + new net::test_server::BasicHttpResponse); + response->set_code(net::HTTP_OK); + response->set_content(id + id + id); + response->set_content_type("text/plain"); + return response.PassAs<net::test_server::HttpResponse>(); + } + + base::MessageLoopForIO message_loop_; // Test server needs IO thread. + net::test_server::EmbeddedTestServer test_server_; + scoped_ptr<RequestSender> request_sender_; + scoped_ptr<GDataWapiUrlGenerator> url_generator_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + base::ScopedTempDir temp_dir_; + + // These fields are used to keep the current upload state during a + // test case. These values are updated by the request from + // ResumeUploadRequest, and used to construct the response for + // both ResumeUploadRequest and GetUploadStatusRequest, to emulate + // the WAPI server. + int64 received_bytes_; + int64 content_length_; + + // The incoming HTTP request is saved so tests can verify the request + // parameters like HTTP method (ex. some requests should use DELETE + // instead of GET). + net::test_server::HttpRequest http_request_; +}; + +} // namespace + +TEST_F(GDataWapiRequestsTest, GetResourceListRequest_DefaultFeed) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<ResourceList> result_data; + + { + base::RunLoop run_loop; + GetResourceListRequest* request = new GetResourceListRequest( + request_sender_.get(), + *url_generator_, + GURL(), // Pass an empty URL to use the default feed + 0, // start changestamp + std::string(), // search string + std::string(), // directory resource ID + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&" + "showfolders=true&include-shared=true&max-results=500", + http_request_.relative_url); + + // Sanity check of the result. + scoped_ptr<ResourceList> expected( + ResourceList::ExtractAndParse( + *test_util::LoadJSONFile("gdata/root_feed.json"))); + ASSERT_TRUE(result_data); + EXPECT_EQ(expected->title(), result_data->title()); +} + +TEST_F(GDataWapiRequestsTest, GetResourceListRequest_ValidFeed) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<ResourceList> result_data; + + { + base::RunLoop run_loop; + GetResourceListRequest* request = new GetResourceListRequest( + request_sender_.get(), + *url_generator_, + test_server_.GetURL("/files/gdata/root_feed.json"), + 0, // start changestamp + std::string(), // search string + std::string(), // directory resource ID + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/files/gdata/root_feed.json?v=3&alt=json&showroot=true&" + "showfolders=true&include-shared=true&max-results=500", + http_request_.relative_url); + + scoped_ptr<ResourceList> expected( + ResourceList::ExtractAndParse( + *test_util::LoadJSONFile("gdata/root_feed.json"))); + ASSERT_TRUE(result_data); + EXPECT_EQ(expected->title(), result_data->title()); +} + +TEST_F(GDataWapiRequestsTest, GetResourceListRequest_InvalidFeed) { + // testfile.txt exists but the response is not JSON, so it should + // emit a parse error instead. + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<ResourceList> result_data; + + { + base::RunLoop run_loop; + GetResourceListRequest* request = new GetResourceListRequest( + request_sender_.get(), + *url_generator_, + test_server_.GetURL("/files/gdata/testfile.txt"), + 0, // start changestamp + std::string(), // search string + std::string(), // directory resource ID + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(GDATA_PARSE_ERROR, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/files/gdata/testfile.txt?v=3&alt=json&showroot=true&" + "showfolders=true&include-shared=true&max-results=500", + http_request_.relative_url); + EXPECT_FALSE(result_data); +} + +TEST_F(GDataWapiRequestsTest, SearchByTitleRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<ResourceList> result_data; + + { + base::RunLoop run_loop; + SearchByTitleRequest* request = new SearchByTitleRequest( + request_sender_.get(), + *url_generator_, + "search-title", + std::string(), // directory resource id + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&" + "showfolders=true&include-shared=true&max-results=500" + "&title=search-title&title-exact=true", + http_request_.relative_url); + EXPECT_TRUE(result_data); +} + +TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_ValidResourceId) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<base::Value> result_data; + + { + base::RunLoop run_loop; + GetResourceEntryRequest* request = new GetResourceEntryRequest( + request_sender_.get(), + *url_generator_, + "file:2_file_resource_id", // resource ID + GURL(), // embed origin + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id" + "?v=3&alt=json&showroot=true", + http_request_.relative_url); + scoped_ptr<base::Value> expected_json = + test_util::LoadJSONFile("gdata/file_entry.json"); + ASSERT_TRUE(expected_json); + EXPECT_TRUE(result_data); + EXPECT_TRUE(base::Value::Equals(expected_json.get(), result_data.get())); +} + +TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_InvalidResourceId) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<base::Value> result_data; + + { + base::RunLoop run_loop; + GetResourceEntryRequest* request = new GetResourceEntryRequest( + request_sender_.get(), + *url_generator_, + "<invalid>", // resource ID + GURL(), // embed origin + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data))); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_NOT_FOUND, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/%3Cinvalid%3E?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + ASSERT_FALSE(result_data); +} + +TEST_F(GDataWapiRequestsTest, GetAccountMetadataRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<AccountMetadata> result_data; + + { + base::RunLoop run_loop; + GetAccountMetadataRequest* request = new GetAccountMetadataRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + true); // Include installed apps. + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true" + "&include-installed-apps=true", + http_request_.relative_url); + + scoped_ptr<AccountMetadata> expected( + AccountMetadata::CreateFrom( + *test_util::LoadJSONFile("gdata/account_metadata.json"))); + + ASSERT_TRUE(result_data.get()); + EXPECT_EQ(expected->largest_changestamp(), + result_data->largest_changestamp()); + EXPECT_EQ(expected->quota_bytes_total(), + result_data->quota_bytes_total()); + EXPECT_EQ(expected->quota_bytes_used(), + result_data->quota_bytes_used()); + + // Sanity check for installed apps. + EXPECT_EQ(expected->installed_apps().size(), + result_data->installed_apps().size()); +} + +TEST_F(GDataWapiRequestsTest, + GetAccountMetadataRequestWithoutInstalledApps) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<AccountMetadata> result_data; + + { + base::RunLoop run_loop; + GetAccountMetadataRequest* request = new GetAccountMetadataRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + false); // Exclude installed apps. + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true", + http_request_.relative_url); + + scoped_ptr<AccountMetadata> expected( + AccountMetadata::CreateFrom( + *test_util::LoadJSONFile("gdata/account_metadata.json"))); + + ASSERT_TRUE(result_data.get()); + EXPECT_EQ(expected->largest_changestamp(), + result_data->largest_changestamp()); + EXPECT_EQ(expected->quota_bytes_total(), + result_data->quota_bytes_total()); + EXPECT_EQ(expected->quota_bytes_used(), + result_data->quota_bytes_used()); + + // Installed apps shouldn't be included. + EXPECT_EQ(0U, result_data->installed_apps().size()); +} + +TEST_F(GDataWapiRequestsTest, DeleteResourceRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + + { + base::RunLoop run_loop; + DeleteResourceRequest* request = new DeleteResourceRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code)), + "file:2_file_resource_id", + std::string()); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); + EXPECT_EQ( + "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + EXPECT_EQ("*", http_request_.headers["If-Match"]); +} + +TEST_F(GDataWapiRequestsTest, DeleteResourceRequestWithETag) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + + { + base::RunLoop run_loop; + DeleteResourceRequest* request = new DeleteResourceRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code)), + "file:2_file_resource_id", + "etag"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); + EXPECT_EQ( + "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + EXPECT_EQ("etag", http_request_.headers["If-Match"]); +} + +TEST_F(GDataWapiRequestsTest, CreateDirectoryRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + scoped_ptr<base::Value> result_data; + + // Create "new directory" in the root directory. + { + base::RunLoop run_loop; + CreateDirectoryRequest* request = new CreateDirectoryRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + "folder:root", + "new directory"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <category scheme=\"http://schemas.google.com/g/2005#kind\" " + "term=\"http://schemas.google.com/docs/2007#folder\"/>\n" + " <title>new directory</title>\n" + "</entry>\n", + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, RenameResourceRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + + // Rename a file with a new name "New File". + { + base::RunLoop run_loop; + RenameResourceRequest* request = new RenameResourceRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code)), + "file:2_file_resource_id", + "New File"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ( + "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <title>New File</title>\n" + "</entry>\n", + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_ValidFeed) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL result_data; + + // Authorize an app with APP_ID to access to a document. + { + base::RunLoop run_loop; + AuthorizeAppRequest* request = new AuthorizeAppRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + "file:2_file_resource_id", + "the_app_id"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(GURL("https://entry1_open_with_link/"), result_data); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id" + "?v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <docs:authorizedApp>the_app_id</docs:authorizedApp>\n" + "</entry>\n", + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_NotFound) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL result_data; + + // Authorize an app with APP_ID to access to a document. + { + base::RunLoop run_loop; + AuthorizeAppRequest* request = new AuthorizeAppRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + "file:2_file_resource_id", + "unauthorized_app_id"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(GDATA_OTHER_ERROR, result_code); + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id" + "?v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <docs:authorizedApp>unauthorized_app_id</docs:authorizedApp>\n" + "</entry>\n", + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_InvalidFeed) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL result_data; + + // Authorize an app with APP_ID to access to a document but an invalid feed. + { + base::RunLoop run_loop; + AuthorizeAppRequest* request = new AuthorizeAppRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &result_data)), + "invalid_resource_id", + "APP_ID"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(GDATA_PARSE_ERROR, result_code); + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/invalid_resource_id" + "?v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <docs:authorizedApp>APP_ID</docs:authorizedApp>\n" + "</entry>\n", + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, AddResourceToDirectoryRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + + // Add a file to the root directory. + { + base::RunLoop run_loop; + AddResourceToDirectoryRequest* request = + new AddResourceToDirectoryRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code)), + "folder:root", + "file:2_file_resource_id"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json" + "&showroot=true", + http_request_.relative_url); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(base::StringPrintf("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <id>%sfeeds/default/private/full/" + "file%%3A2_file_resource_id</id>\n" + "</entry>\n", + test_server_.base_url().spec().c_str()), + http_request_.content); +} + +TEST_F(GDataWapiRequestsTest, RemoveResourceFromDirectoryRequest) { + GDataErrorCode result_code = GDATA_OTHER_ERROR; + + // Remove a file from the root directory. + { + base::RunLoop run_loop; + RemoveResourceFromDirectoryRequest* request = + new RemoveResourceFromDirectoryRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code)), + "folder:root", + "file:2_file_resource_id"); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + // DELETE method should be used, without the body content. + EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); + EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents/" + "file%3A2_file_resource_id?v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + EXPECT_FALSE(http_request_.has_content); +} + +// This test exercises InitiateUploadNewFileRequest and +// ResumeUploadRequest for a scenario of uploading a new file. +TEST_F(GDataWapiRequestsTest, UploadNewFile) { + const std::string kUploadContent = "hello"; + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Get the upload URL for uploading a new file. + { + base::RunLoop run_loop; + InitiateUploadNewFileRequest* initiate_request = + new InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "folder:id", + "New file"); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ( + "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <title>New file</title>\n" + "</entry>\n", + http_request_.content); + + // 2) Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + { + base::RunLoop run_loop; + ResumeUploadRequest* resume_request = new ResumeUploadRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback(), + upload_url, + 0, // start_position + kUploadContent.size(), // end_position (exclusive) + kUploadContent.size(), // content_length, + "text/plain", // content_type + kTestFilePath); + + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kUploadContent.size() -1) + "/" + + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kUploadContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest +// for a scenario of uploading a new *large* file, which requires multiple +// requests of ResumeUploadRequest. GetUploadRequest is also tested in this +// test case. +TEST_F(GDataWapiRequestsTest, UploadNewLargeFile) { + const size_t kMaxNumBytes = 10; + // This is big enough to cause multiple requests of ResumeUploadRequest + // as we are going to send at most kMaxNumBytes at a time. + // So, sending "kMaxNumBytes * 2 + 1" bytes ensures three + // ResumeUploadRequests, which are start, middle and last requests. + const std::string kUploadContent(kMaxNumBytes * 2 + 1, 'a'); + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Get the upload URL for uploading a new file. + { + base::RunLoop run_loop; + InitiateUploadNewFileRequest* initiate_request = + new InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "folder:id", + "New file"); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ( + "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <title>New file</title>\n" + "</entry>\n", + http_request_.content); + + // 2) Before sending any data, check the current status. + // This is an edge case test for GetUploadStatusRequest + // (UploadRangeRequestBase). + { + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + // Check the response by GetUploadStatusRequest. + { + base::RunLoop run_loop; + GetUploadStatusRequest* get_upload_status_request = + new GetUploadStatusRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + upload_url, + kUploadContent.size()); + request_sender_->StartRequestWithRetry(get_upload_status_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(0, response.end_position_received); + } + + // 3) Upload the content to the upload URL with multiple requests. + size_t num_bytes_consumed = 0; + for (size_t start_position = 0; start_position < kUploadContent.size(); + start_position += kMaxNumBytes) { + SCOPED_TRACE(testing::Message("start_position: ") << start_position); + + // The payload is at most kMaxNumBytes. + const size_t remaining_size = kUploadContent.size() - start_position; + const std::string payload = kUploadContent.substr( + start_position, std::min(kMaxNumBytes, remaining_size)); + num_bytes_consumed += payload.size(); + // The end position is exclusive. + const size_t end_position = start_position + payload.size(); + + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + { + base::RunLoop run_loop; + ResumeUploadRequest* resume_request = new ResumeUploadRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback(), + upload_url, + start_position, + end_position, + kUploadContent.size(), // content_length, + "text/plain", // content_type + kTestFilePath); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes " + + base::Int64ToString(start_position) + "-" + + base::Int64ToString(end_position - 1) + "/" + + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(payload, http_request_.content); + + // Check the response. + if (payload.size() == remaining_size) { + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file. + // The start and end positions should be set to -1, if an upload is + // complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); + // The upload process is completed, so exit from the loop. + break; + } + + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(static_cast<int64>(end_position), + response.end_position_received); + + // Check the response by GetUploadStatusRequest. + { + base::RunLoop run_loop; + GetUploadStatusRequest* get_upload_status_request = + new GetUploadStatusRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + upload_url, + kUploadContent.size()); + request_sender_->StartRequestWithRetry(get_upload_status_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(http_request_.content.empty()); + + // Check the response. + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_EQ(0, response.start_position_received); + EXPECT_EQ(static_cast<int64>(end_position), + response.end_position_received); + } + + EXPECT_EQ(kUploadContent.size(), num_bytes_consumed); +} + +// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest +// for a scenario of uploading a new *empty* file. +// +// The test is almost identical to UploadNewFile. The only difference is the +// expectation for the Content-Range header. +TEST_F(GDataWapiRequestsTest, UploadNewEmptyFile) { + const std::string kUploadContent; + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("empty_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Get the upload URL for uploading a new file. + { + base::RunLoop run_loop; + InitiateUploadNewFileRequest* initiate_request = + new InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "folder:id", + "New file"); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ( + "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <title>New file</title>\n" + "</entry>\n", + http_request_.content); + + // 2) Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + { + base::RunLoop run_loop; + ResumeUploadRequest* resume_request = new ResumeUploadRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback(), + upload_url, + 0, // start_position + kUploadContent.size(), // end_position (exclusive) + kUploadContent.size(), // content_length, + "text/plain", // content_type + kTestFilePath); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should not exit if the content is empty. + // We should not generate the header with an invalid value "bytes 0--1/0". + EXPECT_EQ(0U, http_request_.headers.count("Content-Range")); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kUploadContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file. + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +// This test exercises InitiateUploadExistingFileRequest and +// ResumeUploadRequest for a scenario of updating an existing file. +TEST_F(GDataWapiRequestsTest, UploadExistingFile) { + const std::string kUploadContent = "hello"; + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Get the upload URL for uploading an existing file. + { + base::RunLoop run_loop; + InitiateUploadExistingFileRequest* initiate_request = + new InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "file:foo", + std::string() /* etag */); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url); + // For updating an existing file, METHOD_PUT should be used. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + // Even though the body is empty, the content type should be set to + // "text/plain". + EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + // For updating an existing file, an empty body should be attached (PUT + // requires a body) + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("", http_request_.content); + EXPECT_EQ("*", http_request_.headers["If-Match"]); + + // 2) Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + { + base::RunLoop run_loop; + ResumeUploadRequest* resume_request = new ResumeUploadRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback(), + upload_url, + 0, // start_position + kUploadContent.size(), // end_position (exclusive) + kUploadContent.size(), // content_length, + "text/plain", // content_type + kTestFilePath); + + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kUploadContent.size() -1) + "/" + + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kUploadContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file. + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +// This test exercises InitiateUploadExistingFileRequest and +// ResumeUploadRequest for a scenario of updating an existing file. +TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETag) { + const std::string kUploadContent = "hello"; + const base::FilePath kTestFilePath = + temp_dir_.path().AppendASCII("upload_file.txt"); + ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + // 1) Get the upload URL for uploading an existing file. + { + base::RunLoop run_loop; + InitiateUploadExistingFileRequest* initiate_request = + new InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "file:foo", + kTestETag); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url); + // For updating an existing file, METHOD_PUT should be used. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + // Even though the body is empty, the content type should be set to + // "text/plain". + EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + // For updating an existing file, an empty body should be attached (PUT + // requires a body) + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("", http_request_.content); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + + // 2) Upload the content to the upload URL. + UploadRangeResponse response; + scoped_ptr<ResourceEntry> new_entry; + + { + base::RunLoop run_loop; + ResumeUploadRequest* resume_request = new ResumeUploadRequest( + request_sender_.get(), + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&response, &new_entry)), + ProgressCallback(), + upload_url, + 0, // start_position + kUploadContent.size(), // end_position (exclusive) + kUploadContent.size(), // content_length, + "text/plain", // content_type + kTestFilePath); + request_sender_->StartRequestWithRetry(resume_request); + run_loop.Run(); + } + + // METHOD_PUT should be used to upload data. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // Request should go to the upload URL. + EXPECT_EQ(upload_url.path(), http_request_.relative_url); + // Content-Range header should be added. + EXPECT_EQ("bytes 0-" + + base::Int64ToString(kUploadContent.size() -1) + "/" + + base::Int64ToString(kUploadContent.size()), + http_request_.headers["Content-Range"]); + // The upload content should be set in the HTTP request. + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ(kUploadContent, http_request_.content); + + // Check the response. + EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file. + // The start and end positions should be set to -1, if an upload is complete. + EXPECT_EQ(-1, response.start_position_received); + EXPECT_EQ(-1, response.end_position_received); +} + +// This test exercises InitiateUploadExistingFileRequest for a scenario of +// confliction on updating an existing file. +TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETagConflict) { + const std::string kUploadContent = "hello"; + const std::string kWrongETag = "wrong_etag"; + GDataErrorCode result_code = GDATA_OTHER_ERROR; + GURL upload_url; + + { + base::RunLoop run_loop; + InitiateUploadExistingFileRequest* initiate_request = + new InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &upload_url)), + "text/plain", + kUploadContent.size(), + "file:foo", + kWrongETag); + request_sender_->StartRequestWithRetry(initiate_request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_PRECONDITION, result_code); + // For updating an existing file, METHOD_PUT should be used. + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + // convert=false should be passed as files should be uploaded as-is. + EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" + "?convert=false&v=3&alt=json&showroot=true", + http_request_.relative_url); + // Even though the body is empty, the content type should be set to + // "text/plain". + EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); + EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kUploadContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + // For updating an existing file, an empty body should be attached (PUT + // requires a body) + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("", http_request_.content); + EXPECT_EQ(kWrongETag, http_request_.headers["If-Match"]); +} + +TEST_F(GDataWapiRequestsTest, DownloadFileRequest) { + const base::FilePath kDownloadedFilePath = + temp_dir_.path().AppendASCII("cache_file"); + const std::string kTestIdWithTypeLabel("file:dummyId"); + const std::string kTestId("dummyId"); + + GDataErrorCode result_code = GDATA_OTHER_ERROR; + base::FilePath temp_file; + { + base::RunLoop run_loop; + DownloadFileRequest* request = new DownloadFileRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&result_code, &temp_file)), + GetContentCallback(), + ProgressCallback(), + kTestIdWithTypeLabel, + kDownloadedFilePath); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + std::string contents; + base::ReadFileToString(temp_file, &contents); + base::DeleteFile(temp_file, false); + + EXPECT_EQ(HTTP_SUCCESS, result_code); + EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); + EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url); + EXPECT_EQ(kDownloadedFilePath, temp_file); + + const std::string expected_contents = kTestId + kTestId + kTestId; + EXPECT_EQ(expected_contents, contents); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.cc b/chromium/google_apis/drive/gdata_wapi_url_generator.cc new file mode 100644 index 00000000000..c1263b71f34 --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_url_generator.cc @@ -0,0 +1,250 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_wapi_url_generator.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "net/base/escape.h" +#include "net/base/url_util.h" +#include "url/gurl.h" + +namespace google_apis { +namespace { + +// Content URL for modification or resource list retrieval in a particular +// directory specified by "%s" which will be replaced with its resource id. +const char kContentURLFormat[] = "/feeds/default/private/full/%s/contents"; + +// Content URL for removing a resource specified by the latter "%s" from the +// directory specified by the former "%s". +const char kResourceURLForRemovalFormat[] = + "/feeds/default/private/full/%s/contents/%s"; + +// URL requesting single resource entry whose resource id is followed by this +// prefix. +const char kGetEditURLPrefix[] = "/feeds/default/private/full/"; + +// Root resource list url. +const char kResourceListRootURL[] = "/feeds/default/private/full"; + +// Metadata feed with things like user quota. +const char kAccountMetadataURL[] = "/feeds/metadata/default"; + +// URL to upload a new file under a particular directory specified by "%s". +const char kInitiateUploadNewFileURLFormat[] = + "/feeds/upload/create-session/default/private/full/%s/contents"; + +// URL to upload a file content to overwrite a file whose resource id is +// followed by this prefix. +const char kInitiateUploadExistingFileURLPrefix[] = + "/feeds/upload/create-session/default/private/full/"; + +// Maximum number of resource entries to include in a feed. +// Be careful not to use something too small because it might overload the +// server. Be careful not to use something too large because it makes the +// "fetched N items" UI less responsive. +const int kMaxDocumentsPerFeed = 500; +const int kMaxDocumentsPerSearchFeed = 50; + +// URL requesting documents list of changes to documents collections. +const char kGetChangesListURL[] = "/feeds/default/private/changes"; + +} // namespace + +const char GDataWapiUrlGenerator::kBaseUrlForProduction[] = + "https://docs.google.com/"; + +const char GDataWapiUrlGenerator::kBaseDownloadUrlForProduction[] = + "https://www.googledrive.com/host/"; + +// static +GURL GDataWapiUrlGenerator::AddStandardUrlParams(const GURL& url) { + GURL result = net::AppendOrReplaceQueryParameter(url, "v", "3"); + result = net::AppendOrReplaceQueryParameter(result, "alt", "json"); + result = net::AppendOrReplaceQueryParameter(result, "showroot", "true"); + return result; +} + +// static +GURL GDataWapiUrlGenerator::AddInitiateUploadUrlParams(const GURL& url) { + GURL result = net::AppendOrReplaceQueryParameter(url, "convert", "false"); + return AddStandardUrlParams(result); +} + +// static +GURL GDataWapiUrlGenerator::AddFeedUrlParams( + const GURL& url, + int num_items_to_fetch) { + GURL result = AddStandardUrlParams(url); + result = net::AppendOrReplaceQueryParameter(result, "showfolders", "true"); + result = net::AppendOrReplaceQueryParameter(result, "include-shared", "true"); + result = net::AppendOrReplaceQueryParameter( + result, "max-results", base::IntToString(num_items_to_fetch)); + return result; +} + +GDataWapiUrlGenerator::GDataWapiUrlGenerator(const GURL& base_url, + const GURL& base_download_url) + : base_url_(base_url), + base_download_url_(base_download_url) { +} + +GDataWapiUrlGenerator::~GDataWapiUrlGenerator() { +} + +GURL GDataWapiUrlGenerator::GenerateResourceListUrl( + const GURL& override_url, + int64 start_changestamp, + const std::string& search_string, + const std::string& directory_resource_id) const { + DCHECK_LE(0, start_changestamp); + + int max_docs = search_string.empty() ? kMaxDocumentsPerFeed : + kMaxDocumentsPerSearchFeed; + GURL url; + if (!override_url.is_empty()) { + // |override_url| specifies the URL of the continuation feed when the feed + // is broken up to multiple chunks. In this case we must not add the + // |start_changestamp| that provides the original start point. + start_changestamp = 0; + url = override_url; + } else if (start_changestamp > 0) { + // The start changestamp shouldn't be used for a search. + DCHECK(search_string.empty()); + url = base_url_.Resolve(kGetChangesListURL); + } else if (!directory_resource_id.empty()) { + url = base_url_.Resolve( + base::StringPrintf(kContentURLFormat, + net::EscapePath( + directory_resource_id).c_str())); + } else { + url = base_url_.Resolve(kResourceListRootURL); + } + + url = AddFeedUrlParams(url, max_docs); + + if (start_changestamp) { + url = net::AppendOrReplaceQueryParameter( + url, "start-index", base::Int64ToString(start_changestamp)); + } + if (!search_string.empty()) { + url = net::AppendOrReplaceQueryParameter(url, "q", search_string); + } + + return url; +} + +GURL GDataWapiUrlGenerator::GenerateSearchByTitleUrl( + const std::string& title, + const std::string& directory_resource_id) const { + DCHECK(!title.empty()); + + GURL url = directory_resource_id.empty() ? + base_url_.Resolve(kResourceListRootURL) : + base_url_.Resolve(base::StringPrintf( + kContentURLFormat, net::EscapePath(directory_resource_id).c_str())); + url = AddFeedUrlParams(url, kMaxDocumentsPerFeed); + url = net::AppendOrReplaceQueryParameter(url, "title", title); + url = net::AppendOrReplaceQueryParameter(url, "title-exact", "true"); + return url; +} + +GURL GDataWapiUrlGenerator::GenerateEditUrl( + const std::string& resource_id) const { + return AddStandardUrlParams(GenerateEditUrlWithoutParams(resource_id)); +} + +GURL GDataWapiUrlGenerator::GenerateEditUrlWithoutParams( + const std::string& resource_id) const { + return base_url_.Resolve(kGetEditURLPrefix + net::EscapePath(resource_id)); +} + +GURL GDataWapiUrlGenerator::GenerateEditUrlWithEmbedOrigin( + const std::string& resource_id, const GURL& embed_origin) const { + GURL url = GenerateEditUrl(resource_id); + if (!embed_origin.is_empty()) { + // Construct a valid serialized embed origin from an url, according to + // WD-html5-20110525. Such string has to be built manually, since + // GURL::spec() always adds the trailing slash. Moreover, ports are + // currently not supported. + DCHECK(!embed_origin.has_port()); + DCHECK(!embed_origin.has_path() || embed_origin.path() == "/"); + const std::string serialized_embed_origin = + embed_origin.scheme() + "://" + embed_origin.host(); + url = net::AppendOrReplaceQueryParameter( + url, "embedOrigin", serialized_embed_origin); + } + return url; +} + +GURL GDataWapiUrlGenerator::GenerateContentUrl( + const std::string& resource_id) const { + if (resource_id.empty()) { + // |resource_id| must not be empty. Return an empty GURL as an error. + return GURL(); + } + + GURL result = base_url_.Resolve( + base::StringPrintf(kContentURLFormat, + net::EscapePath(resource_id).c_str())); + return AddStandardUrlParams(result); +} + +GURL GDataWapiUrlGenerator::GenerateResourceUrlForRemoval( + const std::string& parent_resource_id, + const std::string& resource_id) const { + if (resource_id.empty() || parent_resource_id.empty()) { + // Both |resource_id| and |parent_resource_id| must be non-empty. + // Return an empty GURL as an error. + return GURL(); + } + + GURL result = base_url_.Resolve( + base::StringPrintf(kResourceURLForRemovalFormat, + net::EscapePath(parent_resource_id).c_str(), + net::EscapePath(resource_id).c_str())); + return AddStandardUrlParams(result); +} + +GURL GDataWapiUrlGenerator::GenerateInitiateUploadNewFileUrl( + const std::string& parent_resource_id) const { + GURL result = base_url_.Resolve( + base::StringPrintf(kInitiateUploadNewFileURLFormat, + net::EscapePath(parent_resource_id).c_str())); + return AddInitiateUploadUrlParams(result); +} + +GURL GDataWapiUrlGenerator::GenerateInitiateUploadExistingFileUrl( + const std::string& resource_id) const { + GURL result = base_url_.Resolve( + kInitiateUploadExistingFileURLPrefix + net::EscapePath(resource_id)); + return AddInitiateUploadUrlParams(result); +} + +GURL GDataWapiUrlGenerator::GenerateResourceListRootUrl() const { + return AddStandardUrlParams(base_url_.Resolve(kResourceListRootURL)); +} + +GURL GDataWapiUrlGenerator::GenerateAccountMetadataUrl( + bool include_installed_apps) const { + GURL result = AddStandardUrlParams(base_url_.Resolve(kAccountMetadataURL)); + if (include_installed_apps) { + result = net::AppendOrReplaceQueryParameter( + result, "include-installed-apps", "true"); + } + return result; +} + +GURL GDataWapiUrlGenerator::GenerateDownloadFileUrl( + const std::string& resource_id) const { + // Strip the file type prefix before the colon character. + size_t colon = resource_id.find(':'); + return base_download_url_.Resolve(net::EscapePath( + colon == std::string::npos ? resource_id + : resource_id.substr(colon + 1))); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.h b/chromium/google_apis/drive/gdata_wapi_url_generator.h new file mode 100644 index 00000000000..05b565d404b --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_url_generator.h @@ -0,0 +1,140 @@ +// Copyright (c) 2012 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. +// +// URL utility functions for Google Documents List API (aka WAPI). + +#ifndef GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_ +#define GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_ + +#include <string> + +#include "url/gurl.h" + +namespace google_apis { + +// The class is used to generate URLs for communicating with the WAPI server. +// for production, and the local server for testing. +class GDataWapiUrlGenerator { + public: + // The + GDataWapiUrlGenerator(const GURL& base_url, const GURL& base_download_url); + ~GDataWapiUrlGenerator(); + + // The base URL for communicating with the WAPI server for production. + static const char kBaseUrlForProduction[]; + + // The base URL for the file download server for production. + static const char kBaseDownloadUrlForProduction[]; + + // Adds additional parameters for API version, output content type and to + // show folders in the feed are added to document feed URLs. + static GURL AddStandardUrlParams(const GURL& url); + + // Adds additional parameters for initiate uploading as well as standard + // url params (as AddStandardUrlParams above does). + static GURL AddInitiateUploadUrlParams(const GURL& url); + + // Adds additional parameters for API version, output content type and to + // show folders in the feed are added to document feed URLs. + static GURL AddFeedUrlParams(const GURL& url, + int num_items_to_fetch); + + // Generates a URL for getting the resource list feed. + // + // The parameters other than |search_string| are mutually exclusive. + // If |override_url| is non-empty, other parameters are ignored. Or if + // |override_url| is empty, others are not used. Besides, |search_string| + // cannot be set together with |start_changestamp|. + // + // override_url: + // By default, a hard-coded base URL of the WAPI server is used. + // The base URL can be overridden by |override_url|. + // This is used for handling continuation of feeds (2nd page and onward). + // + // start_changestamp + // If |start_changestamp| is 0, URL for a full feed is generated. + // If |start_changestamp| is non-zero, URL for a delta feed is generated. + // + // search_string + // If |search_string| is non-empty, q=... parameter is added, and + // max-results=... parameter is adjusted for a search. + // + // directory_resource_id: + // If |directory_resource_id| is non-empty, a URL for fetching documents in + // a particular directory is generated. + // + GURL GenerateResourceListUrl( + const GURL& override_url, + int64 start_changestamp, + const std::string& search_string, + const std::string& directory_resource_id) const; + + // Generates a URL for searching resources by title (exact-match). + // |directory_resource_id| is optional parameter. When it is empty + // all the existing resources are target of the search. Otherwise, + // the search target is just under the directory with it. + GURL GenerateSearchByTitleUrl( + const std::string& title, + const std::string& directory_resource_id) const; + + // Generates a URL for getting or editing the resource entry of + // the given resource ID. + GURL GenerateEditUrl(const std::string& resource_id) const; + + // Generates a URL for getting or editing the resource entry of the + // given resource ID without query params. + // Note that, in order to access to the WAPI server, it is necessary to + // append some query parameters to the URL. GenerateEditUrl declared above + // should be used in such cases. This method is designed for constructing + // the data, such as xml element/attributes in request body containing + // edit urls. + GURL GenerateEditUrlWithoutParams(const std::string& resource_id) const; + + // Generates a URL for getting or editing the resource entry of the given + // resource ID with additionally passed embed origin. This is used to fetch + // share urls for the sharing dialog to be embedded with the |embed_origin| + // origin. + GURL GenerateEditUrlWithEmbedOrigin(const std::string& resource_id, + const GURL& embed_origin) const; + + // Generates a URL for editing the contents in the directory specified + // by the given resource ID. + GURL GenerateContentUrl(const std::string& resource_id) const; + + // Generates a URL to remove an entry specified by |resource_id| from + // the directory specified by the given |parent_resource_id|. + GURL GenerateResourceUrlForRemoval(const std::string& parent_resource_id, + const std::string& resource_id) const; + + // Generates a URL to initiate uploading a new file to a directory + // specified by |parent_resource_id|. + GURL GenerateInitiateUploadNewFileUrl( + const std::string& parent_resource_id) const; + + // Generates a URL to initiate uploading file content to overwrite a + // file specified by |resource_id|. + GURL GenerateInitiateUploadExistingFileUrl( + const std::string& resource_id) const; + + // Generates a URL for getting the root resource list feed. + // Used to make changes in the root directory (ex. create a directory in the + // root directory) + GURL GenerateResourceListRootUrl() const; + + // Generates a URL for getting the account metadata feed. + // If |include_installed_apps| is set to true, the response will include the + // list of installed third party applications. + GURL GenerateAccountMetadataUrl(bool include_installed_apps) const; + + // Generates a URL for downloading a file. + GURL GenerateDownloadFileUrl(const std::string& resource_id) const; + + private: + const GURL base_url_; + const GURL base_download_url_; +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_URL_GENERATOR_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc new file mode 100644 index 00000000000..63db63d6e3e --- /dev/null +++ b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc @@ -0,0 +1,222 @@ +// Copyright (c) 2012 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 "google_apis/drive/gdata_wapi_url_generator.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace google_apis { + +class GDataWapiUrlGeneratorTest : public testing::Test { + public: + GDataWapiUrlGeneratorTest() + : url_generator_( + GURL(GDataWapiUrlGenerator::kBaseUrlForProduction), + GURL(GDataWapiUrlGenerator::kBaseDownloadUrlForProduction)) { + } + + protected: + GDataWapiUrlGenerator url_generator_; +}; + +TEST_F(GDataWapiUrlGeneratorTest, AddStandardUrlParams) { + EXPECT_EQ("http://www.example.com/?v=3&alt=json&showroot=true", + GDataWapiUrlGenerator::AddStandardUrlParams( + GURL("http://www.example.com")).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, AddInitiateUploadUrlParams) { + EXPECT_EQ("http://www.example.com/?convert=false&v=3&alt=json&showroot=true", + GDataWapiUrlGenerator::AddInitiateUploadUrlParams( + GURL("http://www.example.com")).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, AddFeedUrlParams) { + EXPECT_EQ( + "http://www.example.com/?v=3&alt=json&showroot=true&" + "showfolders=true" + "&include-shared=true" + "&max-results=100", + GDataWapiUrlGenerator::AddFeedUrlParams(GURL("http://www.example.com"), + 100 // num_items_to_fetch + ).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListUrl) { + // This is the very basic URL for the GetResourceList request. + EXPECT_EQ("https://docs.google.com/feeds/default/private/full" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500", + url_generator_.GenerateResourceListUrl( + GURL(), // override_url, + 0, // start_changestamp, + std::string(), // search_string, + std::string() // directory resource ID + ).spec()); + + // With an override URL provided, the base URL is changed, but the default + // parameters remain as-is. + EXPECT_EQ("http://localhost/" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500", + url_generator_.GenerateResourceListUrl( + GURL("http://localhost/"), // override_url, + 0, // start_changestamp, + std::string(), // search_string, + std::string() // directory resource ID + ).spec()); + + // With a non-zero start_changestamp provided, the base URL is changed from + // "full" to "changes", and "start-index" parameter is added. + EXPECT_EQ("https://docs.google.com/feeds/default/private/changes" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500&start-index=100", + url_generator_.GenerateResourceListUrl( + GURL(), // override_url, + 100, // start_changestamp, + std::string(), // search_string, + std::string() // directory resource ID + ).spec()); + + // With a non-empty search string provided, "max-results" value is changed, + // and "q" parameter is added. + EXPECT_EQ("https://docs.google.com/feeds/default/private/full" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=50&q=foo", + url_generator_.GenerateResourceListUrl( + GURL(), // override_url, + 0, // start_changestamp, + "foo", // search_string, + std::string() // directory resource ID + ).spec()); + + // With a non-empty directory resource ID provided, the base URL is + // changed, but the default parameters remain. + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX/contents" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500", + url_generator_.GenerateResourceListUrl(GURL(), // override_url, + 0, // start_changestamp, + std::string(), // search_string, + "XXX" // directory resource ID + ).spec()); + + // With a non-empty override_url provided, the base URL is changed, but + // the default parameters remain. Note that start-index should not be + // overridden. + EXPECT_EQ("http://example.com/" + "?start-index=123&v=3&alt=json&showroot=true&showfolders=true" + "&include-shared=true&max-results=500", + url_generator_.GenerateResourceListUrl( + GURL("http://example.com/?start-index=123"), // override_url, + 100, // start_changestamp, + std::string(), // search_string, + "XXX" // directory resource ID + ).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateSearchByTitleUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500&title=search-title&title-exact=true", + url_generator_.GenerateSearchByTitleUrl( + "search-title", std::string()).spec()); + + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX/contents" + "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" + "&max-results=500&title=search-title&title-exact=true", + url_generator_.GenerateSearchByTitleUrl( + "search-title", "XXX").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json" + "&showroot=true", + url_generator_.GenerateEditUrl("XXX").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithoutParams) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX", + url_generator_.GenerateEditUrlWithoutParams("XXX").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithEmbedOrigin) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json" + "&showroot=true&embedOrigin=chrome-extension%3A%2F%2Ftest", + url_generator_.GenerateEditUrlWithEmbedOrigin( + "XXX", + GURL("chrome-extension://test")).spec()); + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json" + "&showroot=true", + url_generator_.GenerateEditUrlWithEmbedOrigin( + "XXX", + GURL()).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateContentUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/" + "folder%3Aroot/contents?v=3&alt=json&showroot=true", + url_generator_.GenerateContentUrl("folder:root").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceUrlForRemoval) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full/" + "folder%3Aroot/contents/file%3AABCDE?v=3&alt=json&showroot=true", + url_generator_.GenerateResourceUrlForRemoval( + "folder:root", "file:ABCDE").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadNewFileUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/upload/create-session/default/private/" + "full/folder%3Aabcde/contents?convert=false&v=3&alt=json&showroot=true", + url_generator_.GenerateInitiateUploadNewFileUrl("folder:abcde").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadExistingFileUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/upload/create-session/default/private/" + "full/file%3Aresource_id?convert=false&v=3&alt=json&showroot=true", + url_generator_.GenerateInitiateUploadExistingFileUrl( + "file:resource_id").spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListRootUrl) { + EXPECT_EQ( + "https://docs.google.com/feeds/default/private/full?v=3&alt=json" + "&showroot=true", + url_generator_.GenerateResourceListRootUrl().spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateAccountMetadataUrl) { + // Include installed apps. + EXPECT_EQ( + "https://docs.google.com/feeds/metadata/default" + "?v=3&alt=json&showroot=true&include-installed-apps=true", + url_generator_.GenerateAccountMetadataUrl(true).spec()); + + // Exclude installed apps. + EXPECT_EQ( + "https://docs.google.com/feeds/metadata/default?v=3&alt=json" + "&showroot=true", + url_generator_.GenerateAccountMetadataUrl(false).spec()); +} + +TEST_F(GDataWapiUrlGeneratorTest, GenerateDownloadFileUrl) { + EXPECT_EQ( + "https://www.googledrive.com/host/resourceId", + url_generator_.GenerateDownloadFileUrl("file:resourceId").spec()); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/request_sender.cc b/chromium/google_apis/drive/request_sender.cc new file mode 100644 index 00000000000..dbf5d85860f --- /dev/null +++ b/chromium/google_apis/drive/request_sender.cc @@ -0,0 +1,105 @@ +// Copyright (c) 2012 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 "google_apis/drive/request_sender.h" + +#include "base/bind.h" +#include "base/sequenced_task_runner.h" +#include "base/stl_util.h" +#include "google_apis/drive/auth_service.h" +#include "google_apis/drive/base_requests.h" +#include "net/url_request/url_request_context_getter.h" + +namespace google_apis { + +RequestSender::RequestSender( + AuthServiceInterface* auth_service, + net::URLRequestContextGetter* url_request_context_getter, + base::SequencedTaskRunner* blocking_task_runner, + const std::string& custom_user_agent) + : auth_service_(auth_service), + url_request_context_getter_(url_request_context_getter), + blocking_task_runner_(blocking_task_runner), + custom_user_agent_(custom_user_agent), + weak_ptr_factory_(this) { +} + +RequestSender::~RequestSender() { + DCHECK(thread_checker_.CalledOnValidThread()); + STLDeleteContainerPointers(in_flight_requests_.begin(), + in_flight_requests_.end()); +} + +base::Closure RequestSender::StartRequestWithRetry( + AuthenticatedRequestInterface* request) { + DCHECK(thread_checker_.CalledOnValidThread()); + + in_flight_requests_.insert(request); + + // TODO(kinaba): Stop relying on weak pointers. Move lifetime management + // of the requests to request sender. + base::Closure cancel_closure = + base::Bind(&RequestSender::CancelRequest, + weak_ptr_factory_.GetWeakPtr(), + request->GetWeakPtr()); + + if (!auth_service_->HasAccessToken()) { + // Fetch OAuth2 access token from the refresh token first. + auth_service_->StartAuthentication( + base::Bind(&RequestSender::OnAccessTokenFetched, + weak_ptr_factory_.GetWeakPtr(), + request->GetWeakPtr())); + } else { + request->Start(auth_service_->access_token(), + custom_user_agent_, + base::Bind(&RequestSender::RetryRequest, + weak_ptr_factory_.GetWeakPtr())); + } + + return cancel_closure; +} + +void RequestSender::OnAccessTokenFetched( + const base::WeakPtr<AuthenticatedRequestInterface>& request, + GDataErrorCode code, + const std::string& /* access_token */) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Do nothing if the request is canceled during authentication. + if (!request.get()) + return; + + if (code == HTTP_SUCCESS) { + DCHECK(auth_service_->HasAccessToken()); + StartRequestWithRetry(request.get()); + } else { + request->OnAuthFailed(code); + } +} + +void RequestSender::RetryRequest(AuthenticatedRequestInterface* request) { + DCHECK(thread_checker_.CalledOnValidThread()); + + auth_service_->ClearAccessToken(); + // User authentication might have expired - rerun the request to force + // auth token refresh. + StartRequestWithRetry(request); +} + +void RequestSender::CancelRequest( + const base::WeakPtr<AuthenticatedRequestInterface>& request) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Do nothing if the request is already finished. + if (!request.get()) + return; + request->Cancel(); +} + +void RequestSender::RequestFinished(AuthenticatedRequestInterface* request) { + in_flight_requests_.erase(request); + delete request; +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/request_sender.h b/chromium/google_apis/drive/request_sender.h new file mode 100644 index 00000000000..dfc671c42bf --- /dev/null +++ b/chromium/google_apis/drive/request_sender.h @@ -0,0 +1,111 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_ +#define GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_ + +#include <set> +#include <string> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "google_apis/drive/gdata_errorcode.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace net { +class URLRequestContextGetter; +} + +namespace google_apis { + +class AuthenticatedRequestInterface; +class AuthServiceInterface; + +// Helper class that sends requests implementing +// AuthenticatedRequestInterface and handles retries and authentication. +class RequestSender { + public: + // |auth_service| is used for fetching OAuth tokens. It'll be owned by + // this RequestSender. + // + // |url_request_context_getter| is the context used to perform network + // requests from this RequestSender. + // + // |blocking_task_runner| is used for running blocking operation, e.g., + // parsing JSON response from the server. + // + // |custom_user_agent| will be used for the User-Agent header in HTTP + // requests issued through the request sender if the value is not empty. + RequestSender(AuthServiceInterface* auth_service, + net::URLRequestContextGetter* url_request_context_getter, + base::SequencedTaskRunner* blocking_task_runner, + const std::string& custom_user_agent); + ~RequestSender(); + + AuthServiceInterface* auth_service() { return auth_service_.get(); } + + net::URLRequestContextGetter* url_request_context_getter() const { + return url_request_context_getter_; + } + + base::SequencedTaskRunner* blocking_task_runner() const { + return blocking_task_runner_.get(); + } + + // Starts a request implementing the AuthenticatedRequestInterface + // interface, and makes the request retry upon authentication failures by + // calling back to RetryRequest. The |request| object is owned by this + // RequestSender. It will be deleted in RequestSender's destructor or + // in RequestFinished(). + // + // Returns a closure to cancel the request. The closure cancels the request + // if it is in-flight, and does nothing if it is already terminated. + base::Closure StartRequestWithRetry(AuthenticatedRequestInterface* request); + + // Notifies to this RequestSender that |request| has finished. + // TODO(kinaba): refactor the life time management and make this at private. + void RequestFinished(AuthenticatedRequestInterface* request); + + private: + // Called when the access token is fetched. + void OnAccessTokenFetched( + const base::WeakPtr<AuthenticatedRequestInterface>& request, + GDataErrorCode error, + const std::string& access_token); + + // Clears any authentication token and retries the request, which forces + // an authentication token refresh. + void RetryRequest(AuthenticatedRequestInterface* request); + + // Cancels the request. Used for implementing the returned closure of + // StartRequestWithRetry. + void CancelRequest( + const base::WeakPtr<AuthenticatedRequestInterface>& request); + + scoped_ptr<AuthServiceInterface> auth_service_; + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + + std::set<AuthenticatedRequestInterface*> in_flight_requests_; + const std::string custom_user_agent_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<RequestSender> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(RequestSender); +}; + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_REQUEST_SENDER_H_ diff --git a/chromium/google_apis/drive/request_sender_unittest.cc b/chromium/google_apis/drive/request_sender_unittest.cc new file mode 100644 index 00000000000..20f5402ef11 --- /dev/null +++ b/chromium/google_apis/drive/request_sender_unittest.cc @@ -0,0 +1,250 @@ +// Copyright 2013 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 "google_apis/drive/request_sender.h" + +#include "base/strings/string_number_conversions.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { + +namespace { + +const char kTestRefreshToken[] = "valid-refresh-token"; +const char kTestAccessToken[] = "valid-access-token"; + +// Enum for indicating the reason why a request is finished. +enum FinishReason { + NONE, + SUCCESS, + CANCEL, + AUTH_FAILURE, +}; + +// AuthService for testing purpose. It accepts kTestRefreshToken and returns +// kTestAccessToken + {"1", "2", "3", ...}. +class TestAuthService : public DummyAuthService { + public: + TestAuthService() : auth_try_count_(0) {} + + virtual void StartAuthentication( + const AuthStatusCallback& callback) OVERRIDE { + // RequestSender should clear the rejected access token before starting + // to request another one. + EXPECT_FALSE(HasAccessToken()); + + ++auth_try_count_; + + if (refresh_token() == kTestRefreshToken) { + const std::string token = + kTestAccessToken + base::IntToString(auth_try_count_); + set_access_token(token); + callback.Run(HTTP_SUCCESS, token); + } else { + set_access_token(""); + callback.Run(HTTP_UNAUTHORIZED, ""); + } + } + + private: + int auth_try_count_; +}; + +// The main test fixture class. +class RequestSenderTest : public testing::Test { + protected: + RequestSenderTest() + : auth_service_(new TestAuthService), + request_sender_(auth_service_, NULL, NULL, "dummy-user-agent") { + auth_service_->set_refresh_token(kTestRefreshToken); + auth_service_->set_access_token(kTestAccessToken); + } + + TestAuthService* auth_service_; // Owned by |request_sender_|. + RequestSender request_sender_; +}; + +// Minimal implementation for AuthenticatedRequestInterface that can interact +// with RequestSender correctly. +class TestRequest : public AuthenticatedRequestInterface { + public: + TestRequest(RequestSender* sender, + bool* start_called, + FinishReason* finish_reason) + : sender_(sender), + start_called_(start_called), + finish_reason_(finish_reason), + weak_ptr_factory_(this) { + } + + // Test the situation that the request has finished. + void FinishRequestWithSuccess() { + *finish_reason_ = SUCCESS; + sender_->RequestFinished(this); + } + + const std::string& passed_access_token() const { + return passed_access_token_; + } + + const ReAuthenticateCallback& passed_reauth_callback() const { + return passed_reauth_callback_; + } + + virtual void Start(const std::string& access_token, + const std::string& custom_user_agent, + const ReAuthenticateCallback& callback) OVERRIDE { + *start_called_ = true; + passed_access_token_ = access_token; + passed_reauth_callback_ = callback; + + // This request class itself does not return any response at this point. + // Each test case should respond properly by using the above methods. + } + + virtual void Cancel() OVERRIDE { + EXPECT_TRUE(*start_called_); + *finish_reason_ = CANCEL; + sender_->RequestFinished(this); + } + + virtual void OnAuthFailed(GDataErrorCode code) OVERRIDE { + *finish_reason_ = AUTH_FAILURE; + sender_->RequestFinished(this); + } + + virtual base::WeakPtr<AuthenticatedRequestInterface> GetWeakPtr() OVERRIDE { + return weak_ptr_factory_.GetWeakPtr(); + } + + private: + RequestSender* sender_; + bool* start_called_; + FinishReason* finish_reason_; + std::string passed_access_token_; + ReAuthenticateCallback passed_reauth_callback_; + base::WeakPtrFactory<TestRequest> weak_ptr_factory_; +}; + +} // namespace + +TEST_F(RequestSenderTest, StartAndFinishRequest) { + bool start_called = false; + FinishReason finish_reason = NONE; + TestRequest* request = new TestRequest(&request_sender_, + &start_called, + &finish_reason); + base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr(); + + base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request); + EXPECT_TRUE(!cancel_closure.is_null()); + + // Start is called with the specified access token. Let it succeed. + EXPECT_TRUE(start_called); + EXPECT_EQ(kTestAccessToken, request->passed_access_token()); + request->FinishRequestWithSuccess(); + EXPECT_FALSE(weak_ptr); // The request object is deleted. + + // It is safe to run the cancel closure even after the request is finished. + // It is just no-op. The TestRequest::Cancel method is not called. + cancel_closure.Run(); + EXPECT_EQ(SUCCESS, finish_reason); +} + +TEST_F(RequestSenderTest, StartAndCancelRequest) { + bool start_called = false; + FinishReason finish_reason = NONE; + TestRequest* request = new TestRequest(&request_sender_, + &start_called, + &finish_reason); + base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr(); + + base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request); + EXPECT_TRUE(!cancel_closure.is_null()); + EXPECT_TRUE(start_called); + + cancel_closure.Run(); + EXPECT_EQ(CANCEL, finish_reason); + EXPECT_FALSE(weak_ptr); // The request object is deleted. +} + +TEST_F(RequestSenderTest, NoRefreshToken) { + auth_service_->ClearRefreshToken(); + auth_service_->ClearAccessToken(); + + bool start_called = false; + FinishReason finish_reason = NONE; + TestRequest* request = new TestRequest(&request_sender_, + &start_called, + &finish_reason); + base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr(); + + base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request); + EXPECT_TRUE(!cancel_closure.is_null()); + + // The request is not started at all because no access token is obtained. + EXPECT_FALSE(start_called); + EXPECT_EQ(AUTH_FAILURE, finish_reason); + EXPECT_FALSE(weak_ptr); // The request object is deleted. +} + +TEST_F(RequestSenderTest, ValidRefreshTokenAndNoAccessToken) { + auth_service_->ClearAccessToken(); + + bool start_called = false; + FinishReason finish_reason = NONE; + TestRequest* request = new TestRequest(&request_sender_, + &start_called, + &finish_reason); + base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr(); + + base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request); + EXPECT_TRUE(!cancel_closure.is_null()); + + // Access token should indicate that this is the first retry. + EXPECT_TRUE(start_called); + EXPECT_EQ(kTestAccessToken + std::string("1"), + request->passed_access_token()); + request->FinishRequestWithSuccess(); + EXPECT_EQ(SUCCESS, finish_reason); + EXPECT_FALSE(weak_ptr); // The request object is deleted. +} + +TEST_F(RequestSenderTest, AccessTokenRejectedSeveralTimes) { + bool start_called = false; + FinishReason finish_reason = NONE; + TestRequest* request = new TestRequest(&request_sender_, + &start_called, + &finish_reason); + base::WeakPtr<AuthenticatedRequestInterface> weak_ptr = request->GetWeakPtr(); + + base::Closure cancel_closure = request_sender_.StartRequestWithRetry(request); + EXPECT_TRUE(!cancel_closure.is_null()); + + EXPECT_TRUE(start_called); + EXPECT_EQ(kTestAccessToken, request->passed_access_token()); + // Emulate the case that the access token was rejected by the remote service. + request->passed_reauth_callback().Run(request); + // New access token is fetched. Let it fail once again. + EXPECT_EQ(kTestAccessToken + std::string("1"), + request->passed_access_token()); + request->passed_reauth_callback().Run(request); + // Once more. + EXPECT_EQ(kTestAccessToken + std::string("2"), + request->passed_access_token()); + request->passed_reauth_callback().Run(request); + + // Currently, limit for the retry is controlled in each request object, not + // by the RequestSender. So with this TestRequest, RequestSender retries + // infinitely. Let it succeed/ + EXPECT_EQ(kTestAccessToken + std::string("3"), + request->passed_access_token()); + request->FinishRequestWithSuccess(); + EXPECT_EQ(SUCCESS, finish_reason); + EXPECT_FALSE(weak_ptr); +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/request_util.cc b/chromium/google_apis/drive/request_util.cc new file mode 100644 index 00000000000..9737a6f90d5 --- /dev/null +++ b/chromium/google_apis/drive/request_util.cc @@ -0,0 +1,26 @@ +// Copyright (c) 2013 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 "google_apis/drive/request_util.h" + +#include <string> + +namespace google_apis { +namespace util { + +namespace { + +// etag matching header. +const char kIfMatchHeaderPrefix[] = "If-Match: "; + +} // namespace + +const char kIfMatchAllHeader[] = "If-Match: *"; + +std::string GenerateIfMatchHeader(const std::string& etag) { + return etag.empty() ? kIfMatchAllHeader : (kIfMatchHeaderPrefix + etag); +} + +} // namespace util +} // namespace google_apis diff --git a/chromium/google_apis/drive/request_util.h b/chromium/google_apis/drive/request_util.h new file mode 100644 index 00000000000..f0672edaf7a --- /dev/null +++ b/chromium/google_apis/drive/request_util.h @@ -0,0 +1,23 @@ +// Copyright (c) 2013 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. + +#ifndef GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_ +#define GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_ + +#include <string> + +namespace google_apis { +namespace util { + +// If-Match header which matches to all etags. +extern const char kIfMatchAllHeader[]; + +// Returns If-Match header string for |etag|. +// If |etag| is empty, the returned header should match any etag. +std::string GenerateIfMatchHeader(const std::string& etag); + +} // namespace util +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_REQUEST_UTIL_H_ diff --git a/chromium/google_apis/drive/request_util_unittest.cc b/chromium/google_apis/drive/request_util_unittest.cc new file mode 100644 index 00000000000..f1e703c8e93 --- /dev/null +++ b/chromium/google_apis/drive/request_util_unittest.cc @@ -0,0 +1,22 @@ +// Copyright (c) 2013 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 "google_apis/drive/request_util.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { +namespace util { + +TEST(GenerateIfMatchHeaderTest, GenerateIfMatchHeader) { + // The header matched to all etag should be returned for empty etag. + EXPECT_EQ("If-Match: *", GenerateIfMatchHeader(std::string())); + + // Otherwise, the returned header should be matched to the given etag. + EXPECT_EQ("If-Match: abcde", GenerateIfMatchHeader("abcde")); + EXPECT_EQ("If-Match: fake_etag", GenerateIfMatchHeader("fake_etag")); +} + +} // namespace util +} // namespace google_apis diff --git a/chromium/google_apis/drive/task_util.cc b/chromium/google_apis/drive/task_util.cc new file mode 100644 index 00000000000..3f149e434ef --- /dev/null +++ b/chromium/google_apis/drive/task_util.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2012 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 "google_apis/drive/task_util.h" + +#include "base/location.h" + +namespace google_apis { + +void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner, + const base::Closure& task) { + if (task_runner->BelongsToCurrentThread()) { + task.Run(); + } else { + const bool posted = task_runner->PostTask(FROM_HERE, task); + DCHECK(posted); + } +} + +} // namespace google_apis diff --git a/chromium/google_apis/drive/task_util.h b/chromium/google_apis/drive/task_util.h new file mode 100644 index 00000000000..443f719694e --- /dev/null +++ b/chromium/google_apis/drive/task_util.h @@ -0,0 +1,136 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_TASK_UTIL_H_ +#define GOOGLE_APIS_DRIVE_TASK_UTIL_H_ + +#include "base/bind.h" +#include "base/message_loop/message_loop_proxy.h" + +namespace google_apis { + +// Runs task on the thread to which |task_runner| belongs. +void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner, + const base::Closure& task); + +namespace internal { + +// Implementation of the composed callback, whose signature is |Sig|. +template<typename Sig> struct ComposedCallback; + +// ComposedCallback with no argument. +template<> +struct ComposedCallback<void()> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Closure& callback) { + runner.Run(callback); + } +}; + +// ComposedCallback with one argument. +template<typename T1> +struct ComposedCallback<void(T1)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1)>& callback, + T1 arg1) { + runner.Run(base::Bind(callback, arg1)); + } +}; + +// ComposedCallback with two arguments. +template<typename T1, typename T2> +struct ComposedCallback<void(T1, T2)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, T2)>& callback, + T1 arg1, T2 arg2) { + runner.Run(base::Bind(callback, arg1, arg2)); + } +}; + +// ComposedCallback with two arguments, and the last one is scoped_ptr. +template<typename T1, typename T2, typename D2> +struct ComposedCallback<void(T1, scoped_ptr<T2, D2>)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, scoped_ptr<T2, D2>)>& callback, + T1 arg1, scoped_ptr<T2, D2> arg2) { + runner.Run(base::Bind(callback, arg1, base::Passed(&arg2))); + } +}; + +// ComposedCallback with three arguments. +template<typename T1, typename T2, typename T3> +struct ComposedCallback<void(T1, T2, T3)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, T2, T3)>& callback, + T1 arg1, T2 arg2, T3 arg3) { + runner.Run(base::Bind(callback, arg1, arg2, arg3)); + } +}; + +// ComposedCallback with three arguments, and the last one is scoped_ptr. +template<typename T1, typename T2, typename T3, typename D3> +struct ComposedCallback<void(T1, T2, scoped_ptr<T3, D3>)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, T2, scoped_ptr<T3, D3>)>& callback, + T1 arg1, T2 arg2, scoped_ptr<T3, D3> arg3) { + runner.Run(base::Bind(callback, arg1, arg2, base::Passed(&arg3))); + } +}; + +// ComposedCallback with four arguments. +template<typename T1, typename T2, typename T3, typename T4> +struct ComposedCallback<void(T1, T2, T3, T4)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, T2, T3, T4)>& callback, + T1 arg1, T2 arg2, T3 arg3, T4 arg4) { + runner.Run(base::Bind(callback, arg1, arg2, arg3, arg4)); + } +}; + +// ComposedCallback with four arguments, and the second one is scoped_ptr. +template<typename T1, typename T2, typename D2, typename T3, typename T4> +struct ComposedCallback<void(T1, scoped_ptr<T2, D2>, T3, T4)> { + static void Run( + const base::Callback<void(const base::Closure&)>& runner, + const base::Callback<void(T1, scoped_ptr<T2, D2>, T3, T4)>& callback, + T1 arg1, scoped_ptr<T2, D2> arg2, T3 arg3, T4 arg4) { + runner.Run(base::Bind(callback, arg1, base::Passed(&arg2), arg3, arg4)); + } +}; + +} // namespace internal + +// Returns callback that takes arguments (arg1, arg2, ...), create a closure +// by binding them to |callback|, and runs |runner| with the closure. +// I.e. the returned callback works as follows: +// runner.Run(Bind(callback, arg1, arg2, ...)) +template<typename CallbackType> +CallbackType CreateComposedCallback( + const base::Callback<void(const base::Closure&)>& runner, + const CallbackType& callback) { + DCHECK(!runner.is_null()); + DCHECK(!callback.is_null()); + return base::Bind( + &internal::ComposedCallback<typename CallbackType::RunType>::Run, + runner, callback); +} + +// Returns callback which runs the given |callback| on the current thread. +template<typename CallbackType> +CallbackType CreateRelayCallback(const CallbackType& callback) { + return CreateComposedCallback( + base::Bind(&RunTaskOnThread, base::MessageLoopProxy::current()), + callback); +} + +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_TASK_UTIL_H_ diff --git a/chromium/google_apis/drive/test_util.cc b/chromium/google_apis/drive/test_util.cc new file mode 100644 index 00000000000..20d8ad65a6f --- /dev/null +++ b/chromium/google_apis/drive/test_util.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2012 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 "google_apis/drive/test_util.h" + +#include "base/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_reader.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/rand_util.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/gdata_wapi_parser.h" +#include "google_apis/drive/gdata_wapi_requests.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "url/gurl.h" + +namespace google_apis { +namespace test_util { + +bool RemovePrefix(const std::string& input, + const std::string& prefix, + std::string* output) { + if (!StartsWithASCII(input, prefix, true /* case sensitive */)) + return false; + + *output = input.substr(prefix.size()); + return true; +} + +base::FilePath GetTestFilePath(const std::string& relative_path) { + base::FilePath path; + if (!PathService::Get(base::DIR_SOURCE_ROOT, &path)) + return base::FilePath(); + path = path.AppendASCII("chrome") + .AppendASCII("test") + .AppendASCII("data") + .Append(base::FilePath::FromUTF8Unsafe(relative_path)); + return path; +} + +GURL GetBaseUrlForTesting(int port) { + return GURL(base::StringPrintf("http://127.0.0.1:%d/", port)); +} + +void RunAndQuit(base::RunLoop* run_loop, const base::Closure& closure) { + closure.Run(); + run_loop->Quit(); +} + +bool WriteStringToFile(const base::FilePath& file_path, + const std::string& content) { + int result = file_util::WriteFile(file_path, content.data(), content.size()); + return content.size() == static_cast<size_t>(result); +} + +bool CreateFileOfSpecifiedSize(const base::FilePath& temp_dir, + size_t size, + base::FilePath* path, + std::string* data) { + if (!base::CreateTemporaryFileInDir(temp_dir, path)) + return false; + + if (size == 0) { + // Note: RandBytesAsString doesn't support generating an empty string. + data->clear(); + return true; + } + + *data = base::RandBytesAsString(size); + return WriteStringToFile(*path, *data); +} + +scoped_ptr<base::Value> LoadJSONFile(const std::string& relative_path) { + base::FilePath path = GetTestFilePath(relative_path); + + std::string error; + JSONFileValueSerializer serializer(path); + scoped_ptr<base::Value> value(serializer.Deserialize(NULL, &error)); + LOG_IF(WARNING, !value.get()) << "Failed to parse " << path.value() + << ": " << error; + return value.Pass(); +} + +// Returns a HttpResponse created from the given file path. +scoped_ptr<net::test_server::BasicHttpResponse> CreateHttpResponseFromFile( + const base::FilePath& file_path) { + std::string content; + if (!base::ReadFileToString(file_path, &content)) + return scoped_ptr<net::test_server::BasicHttpResponse>(); + + std::string content_type = "text/plain"; + if (EndsWith(file_path.AsUTF8Unsafe(), ".json", true /* case sensitive */)) + content_type = "application/json"; + + scoped_ptr<net::test_server::BasicHttpResponse> http_response( + new net::test_server::BasicHttpResponse); + http_response->set_code(net::HTTP_OK); + http_response->set_content(content); + http_response->set_content_type(content_type); + return http_response.Pass(); +} + +scoped_ptr<net::test_server::HttpResponse> HandleDownloadFileRequest( + const GURL& base_url, + net::test_server::HttpRequest* out_request, + const net::test_server::HttpRequest& request) { + *out_request = request; + + GURL absolute_url = base_url.Resolve(request.relative_url); + std::string remaining_path; + if (!RemovePrefix(absolute_url.path(), "/files/", &remaining_path)) + return scoped_ptr<net::test_server::HttpResponse>(); + return CreateHttpResponseFromFile( + GetTestFilePath(remaining_path)).PassAs<net::test_server::HttpResponse>(); +} + +bool ParseContentRangeHeader(const std::string& value, + int64* start_position, + int64* end_position, + int64* length) { + DCHECK(start_position); + DCHECK(end_position); + DCHECK(length); + + std::string remaining; + if (!RemovePrefix(value, "bytes ", &remaining)) + return false; + + std::vector<std::string> parts; + base::SplitString(remaining, '/', &parts); + if (parts.size() != 2U) + return false; + + const std::string range = parts[0]; + if (!base::StringToInt64(parts[1], length)) + return false; + + parts.clear(); + base::SplitString(range, '-', &parts); + if (parts.size() != 2U) + return false; + + return (base::StringToInt64(parts[0], start_position) && + base::StringToInt64(parts[1], end_position)); +} + +void AppendProgressCallbackResult(std::vector<ProgressInfo>* progress_values, + int64 progress, + int64 total) { + progress_values->push_back(ProgressInfo(progress, total)); +} + +TestGetContentCallback::TestGetContentCallback() + : callback_(base::Bind(&TestGetContentCallback::OnGetContent, + base::Unretained(this))) { +} + +TestGetContentCallback::~TestGetContentCallback() { +} + +std::string TestGetContentCallback::GetConcatenatedData() const { + std::string result; + for (size_t i = 0; i < data_.size(); ++i) { + result += *data_[i]; + } + return result; +} + +void TestGetContentCallback::OnGetContent(google_apis::GDataErrorCode error, + scoped_ptr<std::string> data) { + data_.push_back(data.release()); +} + +} // namespace test_util +} // namespace google_apis diff --git a/chromium/google_apis/drive/test_util.h b/chromium/google_apis/drive/test_util.h new file mode 100644 index 00000000000..2cd85b62dd3 --- /dev/null +++ b/chromium/google_apis/drive/test_util.h @@ -0,0 +1,299 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_TEST_UTIL_H_ +#define GOOGLE_APIS_DRIVE_TEST_UTIL_H_ + +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/template_util.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/gdata_errorcode.h" +#include "google_apis/drive/task_util.h" + +class GURL; + +namespace base { +class FilePath; +class RunLoop; +class Value; +} + +namespace net { +namespace test_server { +class BasicHttpResponse; +class HttpResponse; +struct HttpRequest; +} +} + +namespace google_apis { +namespace test_util { + +// Runs the closure, and then quits the |run_loop|. +void RunAndQuit(base::RunLoop* run_loop, const base::Closure& closure); + +// Returns callback which runs the given |callback| and then quits |run_loop|. +template<typename CallbackType> +CallbackType CreateQuitCallback(base::RunLoop* run_loop, + const CallbackType& callback) { + return CreateComposedCallback(base::Bind(&RunAndQuit, run_loop), callback); +} + +// Removes |prefix| from |input| and stores the result in |output|. Returns +// true if the prefix is removed. +bool RemovePrefix(const std::string& input, + const std::string& prefix, + std::string* output); + +// Returns the absolute path for a test file stored under +// chrome/test/data. +base::FilePath GetTestFilePath(const std::string& relative_path); + +// Returns the base URL for communicating with the local test server for +// testing, running at the specified port number. +GURL GetBaseUrlForTesting(int port); + +// Writes the |content| to the file at |file_path|. Returns true on success, +// otherwise false. +bool WriteStringToFile(const base::FilePath& file_path, + const std::string& content); + +// Creates a |size| byte file. The file is filled with random bytes so that +// the test assertions can identify correct portion/position of the file is +// used. +// Returns true on success with the created file's |path| and |data|, otherwise +// false. +bool CreateFileOfSpecifiedSize(const base::FilePath& temp_dir, + size_t size, + base::FilePath* path, + std::string* data); + +// Loads a test JSON file as a base::Value, from a test file stored under +// chrome/test/data. +scoped_ptr<base::Value> LoadJSONFile(const std::string& relative_path); + +// Returns a HttpResponse created from the given file path. +scoped_ptr<net::test_server::BasicHttpResponse> CreateHttpResponseFromFile( + const base::FilePath& file_path); + +// Handles a request for downloading a file. Reads a file from the test +// directory and returns the content. Also, copies the |request| to the memory +// pointed by |out_request|. +// |base_url| must be set to the server's base url. +scoped_ptr<net::test_server::HttpResponse> HandleDownloadFileRequest( + const GURL& base_url, + net::test_server::HttpRequest* out_request, + const net::test_server::HttpRequest& request); + +// Parses a value of Content-Range header, which looks like +// "bytes <start_position>-<end_position>/<length>". +// Returns true on success. +bool ParseContentRangeHeader(const std::string& value, + int64* start_position, + int64* end_position, + int64* length); + +// Google API related code and Drive File System code work on asynchronous +// architecture and return the results via callbacks. +// Following code implements a callback to copy such results. +// Here is how to use: +// +// // Prepare result storage. +// ResultType1 result1; +// ResultType2 result2; +// : +// +// PerformAsynchronousTask( +// param1, param2, ..., +// CreateCopyResultCallback(&result1, &result2, ...)); +// base::RunLoop().RunUntilIdle(); // Run message loop to complete +// // the async task. +// +// // Hereafter, we can write expectation with results. +// EXPECT_EQ(expected_result1, result1); +// EXPECT_EQ(expected_result2, result2); +// : +// +// Note: The max arity of the supported function is 4 based on the usage. +namespace internal { +// Following helper templates are to support Chrome's move semantics. +// Their goal is defining helper methods which are similar to: +// void CopyResultCallback1(T1* out1, T1&& in1) +// void CopyResultCallback2(T1* out1, T2* out2, T1&& in1, T2&& in2) +// : +// in C++11. + +// Declare if the type is movable or not. Currently limited to scoped_ptr only. +// We can add more types upon the usage. +template<typename T> struct IsMovable : base::false_type {}; +template<typename T, typename D> +struct IsMovable<scoped_ptr<T, D> > : base::true_type {}; + +// InType is const T& if |UseConstRef| is true, otherwise |T|. +template<bool UseConstRef, typename T> struct InTypeHelper { + typedef const T& InType; +}; +template<typename T> struct InTypeHelper<false, T> { + typedef T InType; +}; + +// Simulates the std::move function in C++11. We use pointer here for argument, +// instead of rvalue reference. +template<bool IsMovable, typename T> struct MoveHelper { + static const T& Move(const T* in) { return *in; } +}; +template<typename T> struct MoveHelper<true, T> { + static T Move(T* in) { return in->Pass(); } +}; + +// Helper to handle Chrome's move semantics correctly. +template<typename T> +struct CopyResultCallbackHelper + // It is necessary to calculate the exact signature of callbacks we want + // to create here. In our case, as we use value-parameters for primitive + // types and movable types in the callback declaration. + // Thus the incoming type is as follows: + // 1) If the argument type |T| is class type but doesn't movable, + // |InType| is const T&. + // 2) Otherwise, |T| as is. + : InTypeHelper< + base::is_class<T>::value && !IsMovable<T>::value, // UseConstRef + T>, + MoveHelper<IsMovable<T>::value, T> { +}; + +// Copies the |in|'s value to |out|. +template<typename T1> +void CopyResultCallback( + T1* out, + typename CopyResultCallbackHelper<T1>::InType in) { + *out = CopyResultCallbackHelper<T1>::Move(&in); +} + +// Copies the |in1|'s value to |out1|, and |in2|'s to |out2|. +template<typename T1, typename T2> +void CopyResultCallback( + T1* out1, + T2* out2, + typename CopyResultCallbackHelper<T1>::InType in1, + typename CopyResultCallbackHelper<T2>::InType in2) { + *out1 = CopyResultCallbackHelper<T1>::Move(&in1); + *out2 = CopyResultCallbackHelper<T2>::Move(&in2); +} + +// Copies the |in1|'s value to |out1|, |in2|'s to |out2|, and |in3|'s to |out3|. +template<typename T1, typename T2, typename T3> +void CopyResultCallback( + T1* out1, + T2* out2, + T3* out3, + typename CopyResultCallbackHelper<T1>::InType in1, + typename CopyResultCallbackHelper<T2>::InType in2, + typename CopyResultCallbackHelper<T3>::InType in3) { + *out1 = CopyResultCallbackHelper<T1>::Move(&in1); + *out2 = CopyResultCallbackHelper<T2>::Move(&in2); + *out3 = CopyResultCallbackHelper<T3>::Move(&in3); +} + +// Holds the pointers for output. This is introduced for the workaround of +// the arity limitation of Callback. +template<typename T1, typename T2, typename T3, typename T4> +struct OutputParams { + OutputParams(T1* out1, T2* out2, T3* out3, T4* out4) + : out1(out1), out2(out2), out3(out3), out4(out4) {} + T1* out1; + T2* out2; + T3* out3; + T4* out4; +}; + +// Copies the |in1|'s value to |output->out1|, |in2|'s to |output->out2|, +// and so on. +template<typename T1, typename T2, typename T3, typename T4> +void CopyResultCallback( + const OutputParams<T1, T2, T3, T4>& output, + typename CopyResultCallbackHelper<T1>::InType in1, + typename CopyResultCallbackHelper<T2>::InType in2, + typename CopyResultCallbackHelper<T3>::InType in3, + typename CopyResultCallbackHelper<T4>::InType in4) { + *output.out1 = CopyResultCallbackHelper<T1>::Move(&in1); + *output.out2 = CopyResultCallbackHelper<T2>::Move(&in2); + *output.out3 = CopyResultCallbackHelper<T3>::Move(&in3); + *output.out4 = CopyResultCallbackHelper<T4>::Move(&in4); +} + +} // namespace internal + +template<typename T1> +base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType)> +CreateCopyResultCallback(T1* out1) { + return base::Bind(&internal::CopyResultCallback<T1>, out1); +} + +template<typename T1, typename T2> +base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType, + typename internal::CopyResultCallbackHelper<T2>::InType)> +CreateCopyResultCallback(T1* out1, T2* out2) { + return base::Bind(&internal::CopyResultCallback<T1, T2>, out1, out2); +} + +template<typename T1, typename T2, typename T3> +base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType, + typename internal::CopyResultCallbackHelper<T2>::InType, + typename internal::CopyResultCallbackHelper<T3>::InType)> +CreateCopyResultCallback(T1* out1, T2* out2, T3* out3) { + return base::Bind( + &internal::CopyResultCallback<T1, T2, T3>, out1, out2, out3); +} + +template<typename T1, typename T2, typename T3, typename T4> +base::Callback<void(typename internal::CopyResultCallbackHelper<T1>::InType, + typename internal::CopyResultCallbackHelper<T2>::InType, + typename internal::CopyResultCallbackHelper<T3>::InType, + typename internal::CopyResultCallbackHelper<T4>::InType)> +CreateCopyResultCallback(T1* out1, T2* out2, T3* out3, T4* out4) { + return base::Bind( + &internal::CopyResultCallback<T1, T2, T3, T4>, + internal::OutputParams<T1, T2, T3, T4>(out1, out2, out3, out4)); +} + +typedef std::pair<int64, int64> ProgressInfo; + +// Helper utility for recording the results via ProgressCallback. +void AppendProgressCallbackResult(std::vector<ProgressInfo>* progress_values, + int64 progress, + int64 total); + +// Helper utility for recording the content via GetContentCallback. +class TestGetContentCallback { + public: + TestGetContentCallback(); + ~TestGetContentCallback(); + + const GetContentCallback& callback() const { return callback_; } + const ScopedVector<std::string>& data() const { return data_; } + ScopedVector<std::string>* mutable_data() { return &data_; } + std::string GetConcatenatedData() const; + + private: + void OnGetContent(google_apis::GDataErrorCode error, + scoped_ptr<std::string> data); + + const GetContentCallback callback_; + ScopedVector<std::string> data_; + + DISALLOW_COPY_AND_ASSIGN(TestGetContentCallback); +}; + +} // namespace test_util +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_TEST_UTIL_H_ diff --git a/chromium/google_apis/drive/time_util.cc b/chromium/google_apis/drive/time_util.cc new file mode 100644 index 00000000000..6ac55e944c6 --- /dev/null +++ b/chromium/google_apis/drive/time_util.cc @@ -0,0 +1,170 @@ +// Copyright (c) 2012 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 "google_apis/drive/time_util.h" + +#include <string> +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" + +namespace google_apis { +namespace util { + +namespace { + +const char kNullTimeString[] = "null"; + +bool ParseTimezone(const base::StringPiece& timezone, + bool ahead, + int* out_offset_to_utc_in_minutes) { + DCHECK(out_offset_to_utc_in_minutes); + + std::vector<base::StringPiece> parts; + int num_of_token = Tokenize(timezone, ":", &parts); + + int hour = 0; + if (!base::StringToInt(parts[0], &hour)) + return false; + + int minute = 0; + if (num_of_token > 1 && !base::StringToInt(parts[1], &minute)) + return false; + + *out_offset_to_utc_in_minutes = (hour * 60 + minute) * (ahead ? +1 : -1); + return true; +} + +} // namespace + +bool GetTimeFromString(const base::StringPiece& raw_value, + base::Time* parsed_time) { + base::StringPiece date; + base::StringPiece time_and_tz; + base::StringPiece time; + base::Time::Exploded exploded = {0}; + bool has_timezone = false; + int offset_to_utc_in_minutes = 0; + + // Splits the string into "date" part and "time" part. + { + std::vector<base::StringPiece> parts; + if (Tokenize(raw_value, "T", &parts) != 2) + return false; + date = parts[0]; + time_and_tz = parts[1]; + } + + // Parses timezone suffix on the time part if available. + { + std::vector<base::StringPiece> parts; + if (time_and_tz[time_and_tz.size() - 1] == 'Z') { + // Timezone is 'Z' (UTC) + has_timezone = true; + offset_to_utc_in_minutes = 0; + time = time_and_tz; + time.remove_suffix(1); + } else if (Tokenize(time_and_tz, "+", &parts) == 2) { + // Timezone is "+hh:mm" format + if (!ParseTimezone(parts[1], true, &offset_to_utc_in_minutes)) + return false; + has_timezone = true; + time = parts[0]; + } else if (Tokenize(time_and_tz, "-", &parts) == 2) { + // Timezone is "-hh:mm" format + if (!ParseTimezone(parts[1], false, &offset_to_utc_in_minutes)) + return false; + has_timezone = true; + time = parts[0]; + } else { + // No timezone (uses local timezone) + time = time_and_tz; + } + } + + // Parses the date part. + { + std::vector<base::StringPiece> parts; + if (Tokenize(date, "-", &parts) != 3) + return false; + + if (!base::StringToInt(parts[0], &exploded.year) || + !base::StringToInt(parts[1], &exploded.month) || + !base::StringToInt(parts[2], &exploded.day_of_month)) { + return false; + } + } + + // Parses the time part. + { + std::vector<base::StringPiece> parts; + int num_of_token = Tokenize(time, ":", &parts); + if (num_of_token != 3) + return false; + + if (!base::StringToInt(parts[0], &exploded.hour) || + !base::StringToInt(parts[1], &exploded.minute)) { + return false; + } + + std::vector<base::StringPiece> seconds_parts; + int num_of_seconds_token = Tokenize(parts[2], ".", &seconds_parts); + if (num_of_seconds_token >= 3) + return false; + + if (!base::StringToInt(seconds_parts[0], &exploded.second)) + return false; + + // Only accept milli-seconds (3-digits). + if (num_of_seconds_token > 1 && + seconds_parts[1].length() == 3 && + !base::StringToInt(seconds_parts[1], &exploded.millisecond)) { + return false; + } + } + + exploded.day_of_week = 0; + if (!exploded.HasValidValues()) + return false; + + if (has_timezone) { + *parsed_time = base::Time::FromUTCExploded(exploded); + if (offset_to_utc_in_minutes != 0) + *parsed_time -= base::TimeDelta::FromMinutes(offset_to_utc_in_minutes); + } else { + *parsed_time = base::Time::FromLocalExploded(exploded); + } + + return true; +} + +std::string FormatTimeAsString(const base::Time& time) { + if (time.is_null()) + return kNullTimeString; + + base::Time::Exploded exploded; + time.UTCExplode(&exploded); + return base::StringPrintf( + "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + exploded.year, exploded.month, exploded.day_of_month, + exploded.hour, exploded.minute, exploded.second, exploded.millisecond); +} + +std::string FormatTimeAsStringLocaltime(const base::Time& time) { + if (time.is_null()) + return kNullTimeString; + + base::Time::Exploded exploded; + time.LocalExplode(&exploded); + return base::StringPrintf( + "%04d-%02d-%02dT%02d:%02d:%02d.%03d", + exploded.year, exploded.month, exploded.day_of_month, + exploded.hour, exploded.minute, exploded.second, exploded.millisecond); +} + +} // namespace util +} // namespace google_apis diff --git a/chromium/google_apis/drive/time_util.h b/chromium/google_apis/drive/time_util.h new file mode 100644 index 00000000000..f281d800f83 --- /dev/null +++ b/chromium/google_apis/drive/time_util.h @@ -0,0 +1,35 @@ +// Copyright (c) 2012 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. + +#ifndef GOOGLE_APIS_DRIVE_TIME_UTIL_H_ +#define GOOGLE_APIS_DRIVE_TIME_UTIL_H_ + +#include <string> + +#include "base/strings/string_piece.h" + +namespace base { +class Time; +} // namespace base + +namespace google_apis { +namespace util { + +// Parses an RFC 3339 date/time into a base::Time, returning true on success. +// The time string must be in the format "yyyy-mm-ddThh:mm:ss.dddTZ" (TZ is +// either '+hh:mm', '-hh:mm', 'Z' (representing UTC), or an empty string). +bool GetTimeFromString(const base::StringPiece& raw_value, base::Time* time); + +// Formats a base::Time as an RFC 3339 date/time (in UTC). +// If |time| is null, returns "null". +std::string FormatTimeAsString(const base::Time& time); + +// Formats a base::Time as an RFC 3339 date/time (in localtime). +// If |time| is null, returns "null". +std::string FormatTimeAsStringLocaltime(const base::Time& time); + +} // namespace util +} // namespace google_apis + +#endif // GOOGLE_APIS_DRIVE_TIME_UTIL_H_ diff --git a/chromium/google_apis/drive/time_util_unittest.cc b/chromium/google_apis/drive/time_util_unittest.cc new file mode 100644 index 00000000000..6d3abf6717c --- /dev/null +++ b/chromium/google_apis/drive/time_util_unittest.cc @@ -0,0 +1,96 @@ +// Copyright (c) 2012 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 "google_apis/drive/time_util.h" + +#include "base/i18n/time_formatting.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { +namespace util { +namespace { + +std::string FormatTime(const base::Time& time) { + return base::UTF16ToUTF8(base::TimeFormatShortDateAndTime(time)); +} + +} // namespace + +TEST(TimeUtilTest, GetTimeFromStringLocalTimezone) { + // Creates local time objects from exploded structure. + base::Time::Exploded exploded = {2013, 1, 0, 15, 17, 11, 35, 374}; + base::Time local_time = base::Time::FromLocalExploded(exploded); + + // Creates local time object, parsing time string. Note that if there is + // not timezone suffix, GetTimeFromString() will handle this as local time + // with FromLocalExploded(). + base::Time test_time; + ASSERT_TRUE(GetTimeFromString("2013-01-15T17:11:35.374", &test_time)); + + // Compare the time objects. + EXPECT_EQ(local_time, test_time); +} + +TEST(TimeUtilTest, GetTimeFromStringNonTrivialTimezones) { + base::Time target_time; + base::Time test_time; + // Creates the target time. + EXPECT_TRUE(GetTimeFromString("2012-07-14T01:03:21.151Z", &target_time)); + + // Tests positive offset (hour only). + EXPECT_TRUE(GetTimeFromString("2012-07-14T02:03:21.151+01", &test_time)); + EXPECT_EQ(FormatTime(target_time), FormatTime(test_time)); + + // Tests positive offset (hour and minutes). + EXPECT_TRUE(GetTimeFromString("2012-07-14T07:33:21.151+06:30", &test_time)); + EXPECT_EQ(FormatTime(target_time), FormatTime(test_time)); + + // Tests negative offset. + EXPECT_TRUE(GetTimeFromString("2012-07-13T18:33:21.151-06:30", &test_time)); + EXPECT_EQ(FormatTime(target_time), FormatTime(test_time)); +} + +TEST(TimeUtilTest, GetTimeFromStringBasic) { + base::Time test_time; + + // Test that the special timezone "Z" (UTC) is handled. + base::Time::Exploded target_time1 = {2005, 1, 0, 7, 8, 2, 0, 0}; + EXPECT_TRUE(GetTimeFromString("2005-01-07T08:02:00Z", &test_time)); + EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time1)), + FormatTime(test_time)); + + // Test that a simple timezone "-08:00" is handled + // 17:57 - 8 hours = 09:57 + base::Time::Exploded target_time2 = {2005, 8, 0, 9, 17, 57, 0, 0}; + EXPECT_TRUE(GetTimeFromString("2005-08-09T09:57:00-08:00", &test_time)); + EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time2)), + FormatTime(test_time)); + + // Test that milliseconds (.123) are handled. + base::Time::Exploded target_time3 = {2005, 1, 0, 7, 8, 2, 0, 123}; + EXPECT_TRUE(GetTimeFromString("2005-01-07T08:02:00.123Z", &test_time)); + EXPECT_EQ(FormatTime(base::Time::FromUTCExploded(target_time3)), + FormatTime(test_time)); +} + +TEST(TimeUtilTest, FormatTimeAsString) { + base::Time::Exploded exploded_time = {2012, 7, 0, 19, 15, 59, 13, 123}; + base::Time time = base::Time::FromUTCExploded(exploded_time); + EXPECT_EQ("2012-07-19T15:59:13.123Z", FormatTimeAsString(time)); + + EXPECT_EQ("null", FormatTimeAsString(base::Time())); +} + +TEST(TimeUtilTest, FormatTimeAsStringLocalTime) { + base::Time::Exploded exploded_time = {2012, 7, 0, 19, 15, 59, 13, 123}; + base::Time time = base::Time::FromLocalExploded(exploded_time); + EXPECT_EQ("2012-07-19T15:59:13.123", FormatTimeAsStringLocaltime(time)); + + EXPECT_EQ("null", FormatTimeAsStringLocaltime(base::Time())); +} + +} // namespace util +} // namespace google_apis diff --git a/chromium/google_apis/gaia/fake_gaia.cc b/chromium/google_apis/gaia/fake_gaia.cc index 9f4a8122a22..322ed54ff9f 100644 --- a/chromium/google_apis/gaia/fake_gaia.cc +++ b/chromium/google_apis/gaia/fake_gaia.cc @@ -10,6 +10,8 @@ #include "base/json/json_writer.h" #include "base/logging.h" #include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/values.h" #include "google_apis/gaia/gaia_urls.h" @@ -65,9 +67,13 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { http_response->set_content_type("text/html"); } else if (request_path == gaia_urls->oauth2_token_url().path()) { std::string refresh_token; + std::string client_id; + std::string scope; const AccessTokenInfo* token_info = NULL; + GetQueryParameter(request.content, "scope", &scope); if (GetQueryParameter(request.content, "refresh_token", &refresh_token) && - (token_info = GetAccessTokenInfo(refresh_token))) { + GetQueryParameter(request.content, "client_id", &client_id) && + (token_info = GetAccessTokenInfo(refresh_token, client_id, scope))) { base::DictionaryValue response_dict; response_dict.SetString("access_token", token_info->token); response_dict.SetInteger("expires_in", 3600); @@ -104,6 +110,30 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { } else { http_response->set_code(net::HTTP_BAD_REQUEST); } + } else if (request_path == gaia_urls->oauth2_issue_token_url().path()) { + std::string access_token; + std::map<std::string, std::string>::const_iterator auth_header_entry = + request.headers.find("Authorization"); + if (auth_header_entry != request.headers.end()) { + if (StartsWithASCII(auth_header_entry->second, "Bearer ", true)) + access_token = auth_header_entry->second.substr(7); + } + + std::string scope; + std::string client_id; + const AccessTokenInfo* token_info = NULL; + if (GetQueryParameter(request.content, "scope", &scope) && + GetQueryParameter(request.content, "client_id", &client_id) && + (token_info = GetAccessTokenInfo(access_token, client_id, scope))) { + base::DictionaryValue response_dict; + response_dict.SetString("issueAdvice", "auto"); + response_dict.SetString("expiresIn", + base::IntToString(token_info->expires_in)); + response_dict.SetString("token", token_info->token); + FormatJSONResponse(response_dict, http_response.get()); + } else { + http_response->set_code(net::HTTP_BAD_REQUEST); + } } else { // Request not understood. return scoped_ptr<HttpResponse>(); @@ -112,9 +142,9 @@ scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { return http_response.PassAs<HttpResponse>(); } -void FakeGaia::IssueOAuthToken(const std::string& refresh_token, +void FakeGaia::IssueOAuthToken(const std::string& auth_token, const AccessTokenInfo& token_info) { - access_token_info_map_[refresh_token] = token_info; + access_token_info_map_.insert(std::make_pair(auth_token, token_info)); } void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, @@ -126,10 +156,27 @@ void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, } const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo( - const std::string& refresh_token) const { - AccessTokenInfoMap::const_iterator entry = - access_token_info_map_.find(refresh_token); - return entry == access_token_info_map_.end() ? NULL : &entry->second; + const std::string& auth_token, + const std::string& client_id, + const std::string& scope_string) const { + if (auth_token.empty() || client_id.empty()) + return NULL; + + std::vector<std::string> scope_list; + base::SplitString(scope_string, ' ', &scope_list); + ScopeSet scopes(scope_list.begin(), scope_list.end()); + + for (AccessTokenInfoMap::const_iterator entry( + access_token_info_map_.lower_bound(auth_token)); + entry != access_token_info_map_.upper_bound(auth_token); + ++entry) { + if (entry->second.audience == client_id && + (scope_string.empty() || entry->second.scopes == scopes)) { + return &(entry->second); + } + } + + return NULL; } // static diff --git a/chromium/google_apis/gaia/fake_gaia.h b/chromium/google_apis/gaia/fake_gaia.h index 24f6891c420..de4201f7337 100644 --- a/chromium/google_apis/gaia/fake_gaia.h +++ b/chromium/google_apis/gaia/fake_gaia.h @@ -55,21 +55,27 @@ class FakeGaia { const net::test_server::HttpRequest& request); // Configures an OAuth2 token that'll be returned when a client requests an - // access token for the given refresh token. - void IssueOAuthToken(const std::string& refresh_token, + // access token for the given auth token, which can be a refresh token or an + // login-scoped access token for the token minting endpoint. Note that the + // scope and audience requested by the client need to match the token_info. + void IssueOAuthToken(const std::string& auth_token, const AccessTokenInfo& token_info); private: - typedef std::map<std::string, AccessTokenInfo> AccessTokenInfoMap; + typedef std::multimap<std::string, AccessTokenInfo> AccessTokenInfoMap; // Formats a JSON response with the data in |response_dict|. void FormatJSONResponse(const base::DictionaryValue& response_dict, net::test_server::BasicHttpResponse* http_response); - // Returns the access token associated with |refresh_token| or NULL if not - // found. - const AccessTokenInfo* GetAccessTokenInfo( - const std::string& refresh_token) const; + // Returns the access token associated with |auth_token| that matches the + // given |client_id| and |scope_string|. If |scope_string| is empty, the first + // token satisfying the other criteria is returned. Returns NULL if no token + // matches. + const AccessTokenInfo* GetAccessTokenInfo(const std::string& auth_token, + const std::string& client_id, + const std::string& scope_string) + const; // Extracts the parameter named |key| from |query| and places it in |value|. // Returns false if no parameter is found. diff --git a/chromium/google_apis/gaia/gaia_auth_consumer.h b/chromium/google_apis/gaia/gaia_auth_consumer.h index 403033622b2..d5ab306c355 100644 --- a/chromium/google_apis/gaia/gaia_auth_consumer.h +++ b/chromium/google_apis/gaia/gaia_auth_consumer.h @@ -82,6 +82,9 @@ class GaiaAuthConsumer { virtual void OnMergeSessionSuccess(const std::string& data) {} virtual void OnMergeSessionFailure(const GoogleServiceAuthError& error) {} + + virtual void OnListAccountsSuccess(const std::string& data) {} + virtual void OnListAccountsFailure(const GoogleServiceAuthError& error) {} }; #endif // GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.cc b/chromium/google_apis/gaia/gaia_auth_fetcher.cc index f0e26445a52..c5254f4235e 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher.cc +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.cc @@ -182,6 +182,7 @@ GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer, uberauth_token_gurl_(GaiaUrls::GetInstance()->oauth1_login_url().Resolve( base::StringPrintf(kUberAuthTokenURLFormat, source.c_str()))), oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()), + list_accounts_gurl_(GaiaUrls::GetInstance()->list_accounts_url()), client_login_to_oauth2_gurl_( GaiaUrls::GetInstance()->client_login_to_oauth2_url()), fetch_pending_(false) {} @@ -219,8 +220,8 @@ net::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher( // The Gaia token exchange requests do not require any cookie-based // identification as part of requests. We suppress sending any cookies to // maintain a separation between the user's browsing and Chrome's internal - // services. Where such mixing is desired (MergeSession), it will be done - // explicitly. + // services. Where such mixing is desired (MergeSession or OAuthLogin), it + // will be done explicitly. to_return->SetLoadFlags(load_flags); // Fetchers are sometimes cancelled because a network change was detected, @@ -636,7 +637,7 @@ void GaiaAuthFetcher::StartTokenFetchForUberAuthExchange( std::string(), authentication_header, uberauth_token_gurl_, - kLoadFlagsIgnoreCookies, + net::LOAD_NORMAL, this)); fetch_pending_ = true; fetcher_->Start(); @@ -653,7 +654,20 @@ void GaiaAuthFetcher::StartOAuthLogin(const std::string& access_token, request_body_, authentication_header, oauth_login_gurl_, - kLoadFlagsIgnoreCookies, + net::LOAD_NORMAL, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartListAccounts() { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + fetcher_.reset(CreateGaiaFetcher(getter_, + " ", // To force an HTTP POST. + "Origin: https://www.google.com", + list_accounts_gurl_, + net::LOAD_NORMAL, this)); fetch_pending_ = true; fetcher_->Start(); @@ -844,6 +858,16 @@ void GaiaAuthFetcher::OnOAuth2RevokeTokenFetched( consumer_->OnOAuth2RevokeTokenCompleted(); } +void GaiaAuthFetcher::OnListAccountsFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + consumer_->OnListAccountsSuccess(data); + } else { + consumer_->OnListAccountsFailure(GenerateAuthError(data, status)); + } +} + void GaiaAuthFetcher::OnGetUserInfoFetched( const std::string& data, const net::URLRequestStatus& status, @@ -937,6 +961,8 @@ void GaiaAuthFetcher::OnURLFetchComplete(const net::URLFetcher* source) { OnOAuthLoginFetched(data, status, response_code); } else if (url == oauth2_revoke_gurl_) { OnOAuth2RevokeTokenFetched(data, status, response_code); + } else if (url == list_accounts_gurl_) { + OnListAccountsFetched(data, status, response_code); } else { NOTREACHED(); } diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.h b/chromium/google_apis/gaia/gaia_auth_fetcher.h index 5021a534ce3..0864f187cfb 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher.h +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.h @@ -158,6 +158,9 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { void StartOAuthLogin(const std::string& access_token, const std::string& service); + // Starts a request to list the accounts in the GAIA cookie. + void StartListAccounts(); + // Implementation of net::URLFetcherDelegate virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; @@ -253,6 +256,10 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { const net::URLRequestStatus& status, int response_code); + void OnListAccountsFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + void OnGetUserInfoFetched(const std::string& data, const net::URLRequestStatus& status, int response_code); @@ -359,6 +366,7 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { const GURL merge_session_gurl_; const GURL uberauth_token_gurl_; const GURL oauth_login_gurl_; + const GURL list_accounts_gurl_; // While a fetch is going on: scoped_ptr<net::URLFetcher> fetcher_; diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc index f554b5ed855..a12fb97b220 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc +++ b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc @@ -30,33 +30,21 @@ using ::testing::Invoke; using ::testing::_; -namespace { -static const char kGetAuthCodeValidCookie[] = +const char kGetAuthCodeValidCookie[] = "oauth_code=test-code; Path=/test; Secure; HttpOnly"; -static const char kGetAuthCodeCookieNoSecure[] = +const char kGetAuthCodeCookieNoSecure[] = "oauth_code=test-code; Path=/test; HttpOnly"; -static const char kGetAuthCodeCookieNoHttpOnly[] = +const char kGetAuthCodeCookieNoHttpOnly[] = "oauth_code=test-code; Path=/test; Secure"; -static const char kGetAuthCodeCookieNoOAuthCode[] = +const char kGetAuthCodeCookieNoOAuthCode[] = "Path=/test; Secure; HttpOnly"; -static const char kGetTokenPairValidResponse[] = +const char kGetTokenPairValidResponse[] = "{" " \"refresh_token\": \"rt1\"," " \"access_token\": \"at1\"," " \"expires_in\": 3600," " \"token_type\": \"Bearer\"" "}"; -static const char kClientOAuthValidResponse[] = - "{" - " \"oauth2\": {" - " \"refresh_token\": \"rt1\"," - " \"access_token\": \"at1\"," - " \"expires_in\": 3600," - " \"token_type\": \"Bearer\"" - " }" - "}"; - -} // namespace MockFetcher::MockFetcher(bool success, const GURL& url, @@ -199,6 +187,7 @@ class MockGaiaConsumer : public GaiaAuthConsumer { const GoogleServiceAuthError& error)); MOCK_METHOD1(OnUberAuthTokenFailure, void( const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnListAccountsSuccess, void(const std::string& data)); }; #if defined(OS_WIN) @@ -801,3 +790,17 @@ TEST_F(GaiaAuthFetcherTest, StartOAuthLogin) { net::URLFetcher::GET, &auth); auth.OnURLFetchComplete(&mock_fetcher); } + +TEST_F(GaiaAuthFetcherTest, ListAccounts) { + std::string data("[\"gaia.l.a.r\", [" + "[\"gaia.l.a\", 1, \"First Last\", \"user@gmail.com\", " + "\"//googleusercontent.com/A/B/C/D/photo.jpg\", 1, 1, 0]]]"); + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnListAccountsSuccess(data)).Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher(GaiaUrls::GetInstance()->list_accounts_url(), + status, net::HTTP_OK, cookies_, data, net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} diff --git a/chromium/google_apis/gaia/gaia_auth_util.cc b/chromium/google_apis/gaia/gaia_auth_util.cc index a8cad86408c..f8f95c14f5b 100644 --- a/chromium/google_apis/gaia/gaia_auth_util.cc +++ b/chromium/google_apis/gaia/gaia_auth_util.cc @@ -4,11 +4,11 @@ #include "google_apis/gaia/gaia_auth_util.h" -#include <vector> - +#include "base/json/json_reader.h" #include "base/logging.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" +#include "base/values.h" #include "google_apis/gaia/gaia_urls.h" #include "url/gurl.h" @@ -25,7 +25,7 @@ std::string CanonicalizeEmail(const std::string& email_address) { if (parts.size() != 2U) NOTREACHED() << "expecting exactly one @, but got " << parts.size(); else if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts. - RemoveChars(parts[0], ".", &parts[0]); + base::RemoveChars(parts[0], ".", &parts[0]); std::string new_email = StringToLowerASCII(JoinString(parts, at)); VLOG(1) << "Canonicalized " << email_address << " to " << new_email; return new_email; @@ -72,4 +72,36 @@ bool IsGaiaSignonRealm(const GURL& url) { return url == GaiaUrls::GetInstance()->gaia_url(); } + +std::vector<std::string> ParseListAccountsData(const std::string& data) { + std::vector<std::string> account_ids; + + // Parse returned data and make sure we have data. + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value) + return account_ids; + + base::ListValue* list; + if (!value->GetAsList(&list) || list->GetSize() < 2) + return account_ids; + + // Get list of account info. + base::ListValue* accounts; + if (!list->GetList(1, &accounts) || accounts == NULL) + return account_ids; + + // Build a vector of accounts from the cookie. Order is important: the first + // account in the list is the primary account. + for (size_t i = 0; i < accounts->GetSize(); ++i) { + base::ListValue* account; + if (accounts->GetList(i, &account) && account != NULL) { + std::string email; + if (account->GetString(3, &email) && !email.empty()) + account_ids.push_back(email); + } + } + + return account_ids; +} + } // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_auth_util.h b/chromium/google_apis/gaia/gaia_auth_util.h index 35e834b3629..354d116e7a5 100644 --- a/chromium/google_apis/gaia/gaia_auth_util.h +++ b/chromium/google_apis/gaia/gaia_auth_util.h @@ -6,6 +6,7 @@ #define GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ #include <string> +#include <vector> class GURL; @@ -34,6 +35,10 @@ std::string ExtractDomainName(const std::string& email); bool IsGaiaSignonRealm(const GURL& url); +// Parses JSON data returned by /ListAccounts call, returns vector of +// accounts (email addresses). +std::vector<std::string> ParseListAccountsData(const std::string& data); + } // namespace gaia #endif // GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc index 4fe58a3bba4..ebe8f876abe 100644 --- a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc +++ b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc @@ -108,4 +108,38 @@ TEST(GaiaAuthUtilTest, IsGaiaSignonRealm) { EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://www.example.com/"))); } +TEST(GaiaAuthUtilTest, ParseListAccountsData) { + std::vector<std::string> accounts; + accounts = ParseListAccountsData(""); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData("1"); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData("[]"); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData("[\"foo\", \"bar\"]"); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData("[\"foo\", []]"); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData( + "[\"foo\", [[\"bar\", 0, \"name\", 0, \"photo\", 0, 0, 0]]]"); + ASSERT_EQ(0u, accounts.size()); + + accounts = ParseListAccountsData( + "[\"foo\", [[\"bar\", 0, \"name\", \"email\", \"photo\", 0, 0, 0]]]"); + ASSERT_EQ(1u, accounts.size()); + ASSERT_EQ("email", accounts[0]); + + accounts = ParseListAccountsData( + "[\"foo\", [[\"bar1\", 0, \"name1\", \"email1\", \"photo1\", 0, 0, 0], " + "[\"bar2\", 0, \"name2\", \"email2\", \"photo2\", 0, 0, 0]]]"); + ASSERT_EQ(2u, accounts.size()); + ASSERT_EQ("email1", accounts[0]); + ASSERT_EQ("email2", accounts[1]); +} + } // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_oauth_client.cc b/chromium/google_apis/gaia/gaia_oauth_client.cc index 86064d9bc62..8c6e1807369 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client.cc +++ b/chromium/google_apis/gaia/gaia_oauth_client.cc @@ -11,6 +11,7 @@ #include "base/values.h" #include "google_apis/gaia/gaia_urls.h" #include "net/base/escape.h" +#include "net/base/load_flags.h" #include "net/http/http_status_code.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_fetcher_delegate.h" @@ -52,6 +53,9 @@ class GaiaOAuthClient::Core void GetUserEmail(const std::string& oauth_access_token, int max_retries, Delegate* delegate); + void GetUserId(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); void GetTokenInfo(const std::string& oauth_access_token, int max_retries, Delegate* delegate); @@ -67,11 +71,15 @@ class GaiaOAuthClient::Core TOKENS_FROM_AUTH_CODE, REFRESH_TOKEN, TOKEN_INFO, - USER_INFO, + USER_EMAIL, + USER_ID, }; virtual ~Core() {} + void GetUserInfo(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); void MakeGaiaRequest(const GURL& url, const std::string& post_body, int max_retries, @@ -136,7 +144,22 @@ void GaiaOAuthClient::Core::GetUserEmail(const std::string& oauth_access_token, Delegate* delegate) { DCHECK_EQ(request_type_, NO_PENDING_REQUEST); DCHECK(!request_.get()); - request_type_ = USER_INFO; + request_type_ = USER_EMAIL; + GetUserInfo(oauth_access_token, max_retries, delegate); +} + +void GaiaOAuthClient::Core::GetUserId(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate) { + DCHECK_EQ(request_type_, NO_PENDING_REQUEST); + DCHECK(!request_.get()); + request_type_ = USER_ID; + GetUserInfo(oauth_access_token, max_retries, delegate); +} + +void GaiaOAuthClient::Core::GetUserInfo(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate) { delegate_ = delegate; num_retries_ = 0; request_.reset(net::URLFetcher::Create( @@ -145,6 +168,9 @@ void GaiaOAuthClient::Core::GetUserEmail(const std::string& oauth_access_token, request_->SetRequestContext(request_context_getter_.get()); request_->AddExtraRequestHeader("Authorization: OAuth " + oauth_access_token); request_->SetMaxRetriesOn5xx(max_retries); + request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + // Fetchers are sometimes cancelled because a network change was detected, // especially at startup and after sign-in on ChromeOS. Retrying once should // be enough in those cases; let the fetcher retry up to 3 times just in case. @@ -180,6 +206,8 @@ void GaiaOAuthClient::Core::MakeGaiaRequest( request_->SetRequestContext(request_context_getter_.get()); request_->SetUploadData("application/x-www-form-urlencoded", post_body); request_->SetMaxRetriesOn5xx(max_retries); + request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); // See comment on SetAutomaticallyRetryOnNetworkChanges() above. request_->SetAutomaticallyRetryOnNetworkChanges(3); request_->Start(); @@ -250,13 +278,20 @@ void GaiaOAuthClient::Core::HandleResponse( request_type_ = NO_PENDING_REQUEST; switch (type) { - case USER_INFO: { + case USER_EMAIL: { std::string email; response_dict->GetString("email", &email); delegate_->OnGetUserEmailResponse(email); break; } + case USER_ID: { + std::string id; + response_dict->GetString("id", &id); + delegate_->OnGetUserIdResponse(id); + break; + } + case TOKEN_INFO: { delegate_->OnGetTokenInfoResponse(response_dict.Pass()); break; @@ -328,6 +363,12 @@ void GaiaOAuthClient::GetUserEmail(const std::string& access_token, return core_->GetUserEmail(access_token, max_retries, delegate); } +void GaiaOAuthClient::GetUserId(const std::string& access_token, + int max_retries, + Delegate* delegate) { + return core_->GetUserId(access_token, max_retries, delegate); +} + void GaiaOAuthClient::GetTokenInfo(const std::string& access_token, int max_retries, Delegate* delegate) { diff --git a/chromium/google_apis/gaia/gaia_oauth_client.h b/chromium/google_apis/gaia/gaia_oauth_client.h index 94d33137e84..14a26a63d7e 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client.h +++ b/chromium/google_apis/gaia/gaia_oauth_client.h @@ -45,6 +45,8 @@ class GaiaOAuthClient { int expires_in_seconds) {} // Invoked on a successful response to the GetUserInfo request. virtual void OnGetUserEmailResponse(const std::string& user_email) {} + // Invoked on a successful response to the GetUserId request. + virtual void OnGetUserIdResponse(const std::string& user_id) {} // Invoked on a successful response to the GetTokenInfo request. virtual void OnGetTokenInfoResponse( scoped_ptr<DictionaryValue> token_info) {} @@ -94,6 +96,14 @@ class GaiaOAuthClient { int max_retries, Delegate* delegate); + // Call the userinfo API, returning the user gaia ID associated + // with the given access token. The provided access token must have + // https://www.googleapis.com/auth/userinfo as one of its scopes. + // See |max_retries| docs above. + void GetUserId(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); + // Call the tokeninfo API, returning a dictionary of response values. The // provided access token may have any scope, and basic results will be // returned: issued_to, audience, scope, expires_in, access_type. In diff --git a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc index cdeb4831154..32e7c6620b4 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc +++ b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc @@ -130,6 +130,7 @@ const std::string kTestAccessToken = "1/fFAGRNJru1FTz70BzhT3Zg"; const std::string kTestRefreshToken = "1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ"; const std::string kTestUserEmail = "a_user@gmail.com"; +const std::string kTestUserId = "8675309"; const int kTestExpiresIn = 3920; const std::string kDummyGetTokensResult = @@ -144,6 +145,9 @@ const std::string kDummyRefreshTokenResult = const std::string kDummyUserInfoResult = "{\"email\":\"" + kTestUserEmail + "\"}"; +const std::string kDummyUserIdResult = + "{\"id\":\"" + kTestUserId + "\"}"; + const std::string kDummyTokenInfoResult = "{\"issued_to\": \"1234567890.apps.googleusercontent.com\"," "\"audience\": \"1234567890.apps.googleusercontent.com\"," @@ -186,6 +190,7 @@ class MockGaiaOAuthClientDelegate : public gaia::GaiaOAuthClient::Delegate { MOCK_METHOD2(OnRefreshTokenResponse, void(const std::string& access_token, int expires_in_seconds)); MOCK_METHOD1(OnGetUserEmailResponse, void(const std::string& user_email)); + MOCK_METHOD1(OnGetUserIdResponse, void(const std::string& user_id)); MOCK_METHOD0(OnOAuthError, void()); MOCK_METHOD1(OnNetworkError, void(int response_code)); @@ -311,6 +316,17 @@ TEST_F(GaiaOAuthClientTest, GetUserEmail) { auth.GetUserEmail("access_token", 1, &delegate); } +TEST_F(GaiaOAuthClientTest, GetUserId) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetUserIdResponse(kTestUserId)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyUserIdResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetUserId("access_token", 1, &delegate); +} + TEST_F(GaiaOAuthClientTest, GetTokenInfo) { const DictionaryValue* captured_result; diff --git a/chromium/google_apis/gaia/gaia_urls.cc b/chromium/google_apis/gaia/gaia_urls.cc index e5e5b8f32d0..053f7a37e29 100644 --- a/chromium/google_apis/gaia/gaia_urls.cc +++ b/chromium/google_apis/gaia/gaia_urls.cc @@ -28,6 +28,9 @@ const char kOAuthGetAccessTokenUrlSuffix[] = "OAuthGetAccessToken"; const char kOAuthWrapBridgeUrlSuffix[] = "OAuthWrapBridge"; const char kOAuth1LoginUrlSuffix[] = "OAuthLogin"; const char kOAuthRevokeTokenUrlSuffix[] = "AuthSubRevokeToken"; +const char kListAccountsSuffix[] = "ListAccounts"; +const char kEmbeddedSigninSuffix[] = "EmbeddedSignIn"; +const char kAddAccountSuffix[] = "AddSession"; // OAuth scopes const char kOAuth1LoginScope[] = "https://www.google.com/accounts/OAuthLogin"; @@ -40,7 +43,6 @@ const char kClientLoginToOAuth2UrlSuffix[] = "o/oauth2/programmatic_auth"; const char kOAuth2AuthUrlSuffix[] = "o/oauth2/auth"; const char kOAuth2RevokeUrlSuffix[] = "o/oauth2/revoke"; const char kOAuth2TokenUrlSuffix[] = "o/oauth2/token"; -const char kClientOAuthUrlSuffix[] = "ClientOAuth"; // API calls from www.googleapis.com const char kOAuth2IssueTokenUrlSuffix[] = "oauth2/v2/IssueToken"; @@ -104,6 +106,9 @@ GaiaUrls::GaiaUrls() { oauth_wrap_bridge_url_ = gaia_url_.Resolve(kOAuthWrapBridgeUrlSuffix); oauth_revoke_token_url_ = gaia_url_.Resolve(kOAuthRevokeTokenUrlSuffix); oauth1_login_url_ = gaia_url_.Resolve(kOAuth1LoginUrlSuffix); + list_accounts_url_ = gaia_url_.Resolve(kListAccountsSuffix); + embedded_signin_url_ = gaia_url_.Resolve(kEmbeddedSigninSuffix); + add_account_url_ = gaia_url_.Resolve(kAddAccountSuffix); // URLs from accounts.google.com (LSO). get_oauth_token_url_ = lso_origin_url_.Resolve(kGetOAuthTokenUrlSuffix); @@ -199,6 +204,18 @@ const GURL& GaiaUrls::oauth1_login_url() const { return oauth1_login_url_; } +const GURL& GaiaUrls::list_accounts_url() const { + return list_accounts_url_; +} + +const GURL& GaiaUrls::embedded_signin_url() const { + return embedded_signin_url_; +} + +const GURL& GaiaUrls::add_account_url() const { + return add_account_url_; +} + const std::string& GaiaUrls::oauth1_login_scope() const { return oauth1_login_scope_; } diff --git a/chromium/google_apis/gaia/gaia_urls.h b/chromium/google_apis/gaia/gaia_urls.h index e06b95d11e1..2ec6499663e 100644 --- a/chromium/google_apis/gaia/gaia_urls.h +++ b/chromium/google_apis/gaia/gaia_urls.h @@ -32,6 +32,9 @@ class GaiaUrls { const GURL& oauth_user_info_url() const; const GURL& oauth_revoke_token_url() const; const GURL& oauth1_login_url() const; + const GURL& list_accounts_url() const; + const GURL& embedded_signin_url() const; + const GURL& add_account_url() const; const std::string& oauth1_login_scope() const; const std::string& oauth_wrap_bridge_user_info_scope() const; @@ -73,6 +76,9 @@ class GaiaUrls { GURL oauth_user_info_url_; GURL oauth_revoke_token_url_; GURL oauth1_login_url_; + GURL list_accounts_url_; + GURL embedded_signin_url_; + GURL add_account_url_; std::string oauth1_login_scope_; std::string oauth_wrap_bridge_user_info_scope_; diff --git a/chromium/google_apis/gaia/google_service_auth_error.cc b/chromium/google_apis/gaia/google_service_auth_error.cc index ab3d9c07de5..736bbd62785 100644 --- a/chromium/google_apis/gaia/google_service_auth_error.cc +++ b/chromium/google_apis/gaia/google_service_auth_error.cc @@ -147,7 +147,7 @@ const std::string& GoogleServiceAuthError::token() const { default: NOTREACHED(); } - return EmptyString(); + return base::EmptyString(); } const std::string& GoogleServiceAuthError::error_message() const { diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc index 44f2d4a7795..ab382056d4b 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc @@ -9,6 +9,8 @@ #include <vector> #include "base/json/json_reader.h" +#include "base/metrics/histogram.h" +#include "base/metrics/sparse_histogram.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/time/time.h" @@ -44,6 +46,38 @@ static const char kGetAccessTokenBodyWithScopeFormat[] = static const char kAccessTokenKey[] = "access_token"; static const char kExpiresInKey[] = "expires_in"; +static const char kErrorKey[] = "error"; + +// Enumerated constants for logging server responses on 400 errors, matching +// RFC 6749. +enum OAuth2ErrorCodesForHistogram { + OAUTH2_ACCESS_ERROR_INVALID_REQUEST = 0, + OAUTH2_ACCESS_ERROR_INVALID_CLIENT, + OAUTH2_ACCESS_ERROR_INVALID_GRANT, + OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT, + OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE, + OAUTH2_ACCESS_ERROR_INVALID_SCOPE, + OAUTH2_ACCESS_ERROR_UNKNOWN, + OAUTH2_ACCESS_ERROR_COUNT +}; + +OAuth2ErrorCodesForHistogram OAuth2ErrorToHistogramValue( + const std::string& error) { + if (error == "invalid_request") + return OAUTH2_ACCESS_ERROR_INVALID_REQUEST; + else if (error == "invalid_client") + return OAUTH2_ACCESS_ERROR_INVALID_CLIENT; + else if (error == "invalid_grant") + return OAUTH2_ACCESS_ERROR_INVALID_GRANT; + else if (error == "unauthorized_client") + return OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT; + else if (error == "unsupported_grant_type") + return OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE; + else if (error == "invalid_scope") + return OAUTH2_ACCESS_ERROR_INVALID_SCOPE; + + return OAUTH2_ACCESS_ERROR_UNKNOWN; +} static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { CHECK(!status.is_success()); @@ -124,31 +158,60 @@ void OAuth2AccessTokenFetcher::EndGetAccessToken( state_ = GET_ACCESS_TOKEN_DONE; URLRequestStatus status = source->GetStatus(); + int histogram_value = status.is_success() ? source->GetResponseCode() : + status.error(); + UMA_HISTOGRAM_SPARSE_SLOWLY("Gaia.ResponseCodesForOAuth2AccessToken", + histogram_value); if (!status.is_success()) { OnGetTokenFailure(CreateAuthError(status)); return; } - // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be - // '403 Rate Limit Exeeded.' - if (source->GetResponseCode() == net::HTTP_FORBIDDEN) { - OnGetTokenFailure(GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_UNAVAILABLE)); - return; - } + switch (source->GetResponseCode()) { + case net::HTTP_OK: + break; + case net::HTTP_FORBIDDEN: + case net::HTTP_INTERNAL_SERVER_ERROR: + // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be + // '403 Rate Limit Exeeded.' 500 is always treated as transient. + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + case net::HTTP_BAD_REQUEST: { + // HTTP_BAD_REQUEST (400) usually contains error as per + // http://tools.ietf.org/html/rfc6749#section-5.2. + std::string gaia_error; + if (!ParseGetAccessTokenFailureResponse(source, &gaia_error)) { + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_ERROR)); + return; + } - // The other errors are treated as permanent error. - if (source->GetResponseCode() != net::HTTP_OK) { - OnGetTokenFailure(GoogleServiceAuthError( - GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); - return; + OAuth2ErrorCodesForHistogram access_error(OAuth2ErrorToHistogramValue( + gaia_error)); + UMA_HISTOGRAM_ENUMERATION("Gaia.BadRequestTypeForOAuth2AccessToken", + access_error, OAUTH2_ACCESS_ERROR_COUNT); + + OnGetTokenFailure(access_error == OAUTH2_ACCESS_ERROR_INVALID_GRANT ? + GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) : + GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_ERROR)); + return; + } + default: + // The other errors are treated as permanent error. + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; } // The request was successfully fetched and it returned OK. // Parse out the access token and the expiration time. std::string access_token; int expires_in; - if (!ParseGetAccessTokenResponse(source, &access_token, &expires_in)) { + if (!ParseGetAccessTokenSuccessResponse( + source, &access_token, &expires_in)) { DLOG(WARNING) << "Response doesn't match expected format"; OnGetTokenFailure( GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); @@ -213,21 +276,43 @@ std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( } } -// static -bool OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( - const net::URLFetcher* source, - std::string* access_token, - int* expires_in) { +scoped_ptr<base::DictionaryValue> ParseGetAccessTokenResponse( + const net::URLFetcher* source) { CHECK(source); - CHECK(access_token); + std::string data; source->GetResponseAsString(&data); scoped_ptr<base::Value> value(base::JSONReader::Read(data)); if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + value.reset(); + + return scoped_ptr<base::DictionaryValue>( + static_cast<base::DictionaryValue*>(value.release())); +} + +// static +bool OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( + const net::URLFetcher* source, + std::string* access_token, + int* expires_in) { + CHECK(access_token); + scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse( + source); + if (value.get() == NULL) return false; - base::DictionaryValue* dict = - static_cast<base::DictionaryValue*>(value.get()); - return dict->GetString(kAccessTokenKey, access_token) && - dict->GetInteger(kExpiresInKey, expires_in); + return value->GetString(kAccessTokenKey, access_token) && + value->GetInteger(kExpiresInKey, expires_in); +} + +// static +bool OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( + const net::URLFetcher* source, + std::string* error) { + CHECK(error); + scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse( + source); + if (value.get() == NULL) + return false; + return value->GetString(kErrorKey, error); } diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h index 11ac6ea8b5c..90805c09963 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h @@ -90,9 +90,15 @@ class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate { const std::string& client_secret, const std::string& refresh_token, const std::vector<std::string>& scopes); - static bool ParseGetAccessTokenResponse(const net::URLFetcher* source, - std::string* access_token, - int* expires_in); + + static bool ParseGetAccessTokenSuccessResponse( + const net::URLFetcher* source, + std::string* access_token, + int* expires_in); + + static bool ParseGetAccessTokenFailureResponse( + const net::URLFetcher* source, + std::string* error); // State that is set during construction. OAuth2AccessTokenConsumer* const consumer_; diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc index 6b12da3fbd0..135e292d7c0 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc @@ -50,6 +50,11 @@ static const char kTokenResponseNoAccessToken[] = " \"token_type\": \"Bearer\"" "}"; +static const char kValidFailureTokenResponse[] = + "{" + " \"error\": \"invalid_grant\"" + "}"; + class MockUrlFetcherFactory : public ScopedURLFetcherFactory, public URLFetcherFactory { public: @@ -195,7 +200,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } @@ -205,7 +210,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } @@ -215,7 +220,7 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } @@ -225,9 +230,27 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( &url_fetcher, &at, &expires_in)); EXPECT_EQ("at1", at); EXPECT_EQ(3600, expires_in); } + { // Valid json: invalid error response. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + + std::string error; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( + &url_fetcher, &error)); + EXPECT_TRUE(error.empty()); + } + { // Valid json: error response. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kValidFailureTokenResponse); + + std::string error; + EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( + &url_fetcher, &error)); + EXPECT_EQ("invalid_grant", error); + } } diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc index b705ab872b0..a1e3ff3cfeb 100644 --- a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc @@ -30,29 +30,28 @@ using net::URLRequestStatus; namespace { -static const char kForceValueFalse[] = "false"; -static const char kForceValueTrue[] = "true"; -static const char kResponseTypeValueNone[] = "none"; -static const char kResponseTypeValueToken[] = "token"; +const char kForceValueFalse[] = "false"; +const char kForceValueTrue[] = "true"; +const char kResponseTypeValueNone[] = "none"; +const char kResponseTypeValueToken[] = "token"; -static const char kOAuth2IssueTokenBodyFormat[] = +const char kOAuth2IssueTokenBodyFormat[] = "force=%s" "&response_type=%s" "&scope=%s" "&client_id=%s" "&origin=%s"; -static const char kIssueAdviceKey[] = "issueAdvice"; -static const char kIssueAdviceValueAuto[] = "auto"; -static const char kIssueAdviceValueConsent[] = "consent"; -static const char kAccessTokenKey[] = "token"; -static const char kConsentKey[] = "consent"; -static const char kExpiresInKey[] = "expiresIn"; -static const char kScopesKey[] = "scopes"; -static const char kDescriptionKey[] = "description"; -static const char kDetailKey[] = "detail"; -static const char kDetailSeparators[] = "\n"; -static const char kError[] = "error"; -static const char kMessage[] = "message"; +const char kIssueAdviceKey[] = "issueAdvice"; +const char kIssueAdviceValueConsent[] = "consent"; +const char kAccessTokenKey[] = "token"; +const char kConsentKey[] = "consent"; +const char kExpiresInKey[] = "expiresIn"; +const char kScopesKey[] = "scopes"; +const char kDescriptionKey[] = "description"; +const char kDetailKey[] = "detail"; +const char kDetailSeparators[] = "\n"; +const char kError[] = "error"; +const char kMessage[] = "message"; static GoogleServiceAuthError CreateAuthError(const net::URLFetcher* source) { URLRequestStatus status = source->GetStatus(); @@ -259,7 +258,7 @@ bool OAuth2MintTokenFlow::ParseIssueAdviceResponse( for (size_t index = 0; index < scopes_list->GetSize(); ++index) { const base::DictionaryValue* scopes_entry = NULL; IssueAdviceInfoEntry entry; - string16 detail; + base::string16 detail; if (!scopes_list->GetDictionary(index, &scopes_entry) || !scopes_entry->GetString(kDescriptionKey, &entry.description) || !scopes_entry->GetString(kDetailKey, &detail)) { @@ -268,7 +267,8 @@ bool OAuth2MintTokenFlow::ParseIssueAdviceResponse( } TrimWhitespace(entry.description, TRIM_ALL, &entry.description); - static const string16 detail_separators = ASCIIToUTF16(kDetailSeparators); + static const base::string16 detail_separators = + ASCIIToUTF16(kDetailSeparators); Tokenize(detail, detail_separators, &entry.details); for (size_t i = 0; i < entry.details.size(); i++) TrimWhitespace(entry.details[i], TRIM_ALL, &entry.details[i]); diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.h b/chromium/google_apis/gaia/oauth2_mint_token_flow.h index 5bb751d6c71..06c823ce4bc 100644 --- a/chromium/google_apis/gaia/oauth2_mint_token_flow.h +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.h @@ -43,8 +43,8 @@ struct IssueAdviceInfoEntry { IssueAdviceInfoEntry(); ~IssueAdviceInfoEntry(); - string16 description; - std::vector<string16> details; + base::string16 description; + std::vector<base::string16> details; bool operator==(const IssueAdviceInfoEntry& rhs) const; }; diff --git a/chromium/google_apis/gaia/oauth2_token_service.cc b/chromium/google_apis/gaia/oauth2_token_service.cc index 3259e283b64..9684281097b 100644 --- a/chromium/google_apis/gaia/oauth2_token_service.cc +++ b/chromium/google_apis/gaia/oauth2_token_service.cc @@ -47,14 +47,20 @@ bool OAuth2TokenService::RequestParameters::operator<( } OAuth2TokenService::RequestImpl::RequestImpl( + const std::string& account_id, OAuth2TokenService::Consumer* consumer) - : consumer_(consumer) { + : account_id_(account_id), + consumer_(consumer) { } OAuth2TokenService::RequestImpl::~RequestImpl() { DCHECK(CalledOnValidThread()); } +std::string OAuth2TokenService::RequestImpl::GetAccountId() const { + return account_id_; +} + void OAuth2TokenService::RequestImpl::InformConsumer( const GoogleServiceAuthError& error, const std::string& access_token, @@ -445,7 +451,7 @@ OAuth2TokenService::StartRequestForClientWithContext( Consumer* consumer) { DCHECK(CalledOnValidThread()); - scoped_ptr<RequestImpl> request(new RequestImpl(consumer)); + scoped_ptr<RequestImpl> request = CreateRequest(account_id, consumer); if (!RefreshTokenIsAvailable(account_id)) { base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind( @@ -473,6 +479,12 @@ OAuth2TokenService::StartRequestForClientWithContext( return request.PassAs<Request>(); } +scoped_ptr<OAuth2TokenService::RequestImpl> OAuth2TokenService::CreateRequest( + const std::string& account_id, + Consumer* consumer) { + return scoped_ptr<RequestImpl>(new RequestImpl(account_id, consumer)); +} + void OAuth2TokenService::FetchOAuth2Token(RequestImpl* request, const std::string& account_id, net::URLRequestContextGetter* getter, diff --git a/chromium/google_apis/gaia/oauth2_token_service.h b/chromium/google_apis/gaia/oauth2_token_service.h index d092430f9a5..1745315abf4 100644 --- a/chromium/google_apis/gaia/oauth2_token_service.h +++ b/chromium/google_apis/gaia/oauth2_token_service.h @@ -56,6 +56,7 @@ class OAuth2TokenService : public base::NonThreadSafe { class Request { public: virtual ~Request(); + virtual std::string GetAccountId() const = 0; protected: Request(); }; @@ -111,11 +112,9 @@ class OAuth2TokenService : public base::NonThreadSafe { // |scopes| is the set of scopes to get an access token for, |consumer| is // the object that will be called back with results if the returned request // is not deleted. - // TODO(atwilson): Make this non-virtual when we change - // ProfileOAuth2TokenServiceRequestTest to use FakeProfileOAuth2TokenService. - virtual scoped_ptr<Request> StartRequest(const std::string& account_id, - const ScopeSet& scopes, - Consumer* consumer); + scoped_ptr<Request> StartRequest(const std::string& account_id, + const ScopeSet& scopes, + Consumer* consumer); // This method does the same as |StartRequest| except it uses |client_id| and // |client_secret| to identify OAuth client app instead of using @@ -179,9 +178,12 @@ class OAuth2TokenService : public base::NonThreadSafe { public Request { public: // |consumer| is required to outlive this. - explicit RequestImpl(Consumer* consumer); + explicit RequestImpl(const std::string& account_id, Consumer* consumer); virtual ~RequestImpl(); + // Overridden from Request: + virtual std::string GetAccountId() const OVERRIDE; + // Informs |consumer_| that this request is completed. void InformConsumer(const GoogleServiceAuthError& error, const std::string& access_token, @@ -189,6 +191,7 @@ class OAuth2TokenService : public base::NonThreadSafe { private: // |consumer_| to call back when this request completes. + const std::string account_id_; Consumer* const consumer_; }; @@ -225,9 +228,16 @@ class OAuth2TokenService : public base::NonThreadSafe { void CancelRequestsForAccount(const std::string& account_id); // Called by subclasses to notify observers. - void FireRefreshTokenAvailable(const std::string& account_id); - void FireRefreshTokenRevoked(const std::string& account_id); - void FireRefreshTokensLoaded(); + virtual void FireRefreshTokenAvailable(const std::string& account_id); + virtual void FireRefreshTokenRevoked(const std::string& account_id); + virtual void FireRefreshTokensLoaded(); + + // Creates a request implementation. Can be overriden by derived classes to + // provide additional control of token consumption. |consumer| will outlive + // the created request. + virtual scoped_ptr<RequestImpl> CreateRequest( + const std::string& account_id, + Consumer* consumer); // Fetches an OAuth token for the specified client/scopes. Virtual so it can // be overridden for tests and for platform-specific behavior on Android. diff --git a/chromium/google_apis/gaia/oauth_request_signer.cc b/chromium/google_apis/gaia/oauth_request_signer.cc index b16744cbccd..115c14d6f01 100644 --- a/chromium/google_apis/gaia/oauth_request_signer.cc +++ b/chromium/google_apis/gaia/oauth_request_signer.cc @@ -24,26 +24,24 @@ namespace { -static const int kHexBase = 16; -static char kHexDigits[] = "0123456789ABCDEF"; -static const size_t kHmacDigestLength = 20; -static const int kMaxNonceLength = 30; -static const int kMinNonceLength = 15; +const int kHexBase = 16; +char kHexDigits[] = "0123456789ABCDEF"; +const size_t kHmacDigestLength = 20; +const int kMaxNonceLength = 30; +const int kMinNonceLength = 15; -static const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key"; -static const char kOAuthConsumerSecretLabel[] = "oauth_consumer_secret"; -static const char kOAuthNonceCharacters[] = +const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key"; +const char kOAuthNonceCharacters[] = "abcdefghijklmnopqrstuvwyz" "ABCDEFGHIJKLMNOPQRSTUVWYZ" "0123456789_"; -static const char kOAuthNonceLabel[] = "oauth_nonce"; -static const char kOAuthSignatureLabel[] = "oauth_signature"; -static const char kOAuthSignatureMethodLabel[] = "oauth_signature_method"; -static const char kOAuthTimestampLabel[] = "oauth_timestamp"; -static const char kOAuthTokenLabel[] = "oauth_token"; -static const char kOAuthTokenSecretLabel[] = "oauth_token_secret"; -static const char kOAuthVersion[] = "1.0"; -static const char kOAuthVersionLabel[] = "oauth_version"; +const char kOAuthNonceLabel[] = "oauth_nonce"; +const char kOAuthSignatureLabel[] = "oauth_signature"; +const char kOAuthSignatureMethodLabel[] = "oauth_signature_method"; +const char kOAuthTimestampLabel[] = "oauth_timestamp"; +const char kOAuthTokenLabel[] = "oauth_token"; +const char kOAuthVersion[] = "1.0"; +const char kOAuthVersionLabel[] = "oauth_version"; enum ParseQueryState { START_STATE, @@ -206,10 +204,12 @@ bool SignHmacSha1(const std::string& text, DCHECK(hmac.DigestLength() == kHmacDigestLength); unsigned char digest[kHmacDigestLength]; bool result = hmac.Init(key) && - hmac.Sign(text, digest, kHmacDigestLength) && - base::Base64Encode(std::string(reinterpret_cast<const char*>(digest), - kHmacDigestLength), - signature_return); + hmac.Sign(text, digest, kHmacDigestLength); + if (result) { + base::Base64Encode( + std::string(reinterpret_cast<const char*>(digest), kHmacDigestLength), + signature_return); + } return result; } diff --git a/chromium/google_apis/gcm/DEPS b/chromium/google_apis/gcm/DEPS new file mode 100644 index 00000000000..08ac400e0d4 --- /dev/null +++ b/chromium/google_apis/gcm/DEPS @@ -0,0 +1,14 @@ +include_rules = [ + # Repeat these from the top-level DEPS file so one can just run + # + # checkdeps.py google_apis/gcm + # + # to test. + "+base", + "+testing", + + "+components/webdata/encryptor", + "+google", # For third_party/protobuf/src. + "+net", + "+third_party/leveldatabase", +] diff --git a/chromium/google_apis/gcm/OWNERS b/chromium/google_apis/gcm/OWNERS new file mode 100644 index 00000000000..493c918a07c --- /dev/null +++ b/chromium/google_apis/gcm/OWNERS @@ -0,0 +1,3 @@ +zea@chromium.org +dimich@chromium.org +tim@chromium.org diff --git a/chromium/google_apis/gcm/base/gcm_export.h b/chromium/google_apis/gcm/base/gcm_export.h new file mode 100644 index 00000000000..b66eb8e54cf --- /dev/null +++ b/chromium/google_apis/gcm/base/gcm_export.h @@ -0,0 +1,29 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_GCM_EXPORT_H_ +#define GOOGLE_APIS_GCM_GCM_EXPORT_H_ + +#if defined(COMPONENT_BUILD) +#if defined(WIN32) + +#if defined(GCM_IMPLEMENTATION) +#define GCM_EXPORT __declspec(dllexport) +#else +#define GCM_EXPORT __declspec(dllimport) +#endif // defined(GCM_IMPLEMENTATION) + +#else // defined(WIN32) +#if defined(GCM_IMPLEMENTATION) +#define GCM_EXPORT __attribute__((visibility("default"))) +#else +#define GCM_EXPORT +#endif // defined(GCM_IMPLEMENTATION) +#endif + +#else // defined(COMPONENT_BUILD) +#define GCM_EXPORT +#endif + +#endif // GOOGLE_APIS_GCM_GCM_EXPORT_H_ diff --git a/chromium/google_apis/gcm/base/mcs_message.cc b/chromium/google_apis/gcm/base/mcs_message.cc new file mode 100644 index 00000000000..f0c130baf89 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_message.cc @@ -0,0 +1,78 @@ +// Copyright 2013 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 "google_apis/gcm/base/mcs_message.h" + +#include "base/logging.h" +#include "google_apis/gcm/base/mcs_util.h" + +namespace gcm { + +MCSMessage::Core::Core() {} + +MCSMessage::Core::Core(uint8 tag, + const google::protobuf::MessageLite& protobuf) { + scoped_ptr<google::protobuf::MessageLite> owned_protobuf(protobuf.New()); + owned_protobuf->CheckTypeAndMergeFrom(protobuf); + protobuf_ = owned_protobuf.Pass(); +} + +MCSMessage::Core::Core( + uint8 tag, + scoped_ptr<const google::protobuf::MessageLite> protobuf) { + protobuf_ = protobuf.Pass(); +} + +MCSMessage::Core::~Core() {} + +const google::protobuf::MessageLite& MCSMessage::Core::Get() const { + return *protobuf_; +} + +MCSMessage::MCSMessage() : tag_(0), size_(0) {} + +MCSMessage::MCSMessage(const google::protobuf::MessageLite& protobuf) + : tag_(GetMCSProtoTag(protobuf)), + size_(protobuf.ByteSize()), + core_(new Core(tag_, protobuf)) { +} + +MCSMessage::MCSMessage(uint8 tag, + const google::protobuf::MessageLite& protobuf) + : tag_(tag), + size_(protobuf.ByteSize()), + core_(new Core(tag_, protobuf)) { + DCHECK_EQ(tag, GetMCSProtoTag(protobuf)); +} + +MCSMessage::MCSMessage(uint8 tag, + scoped_ptr<const google::protobuf::MessageLite> protobuf) + : tag_(tag), + size_(protobuf->ByteSize()), + core_(new Core(tag_, protobuf.Pass())) { + DCHECK_EQ(tag, GetMCSProtoTag(core_->Get())); +} + +MCSMessage::~MCSMessage() { +} + +bool MCSMessage::IsValid() const { + return core_.get() != NULL; +} + +std::string MCSMessage::SerializeAsString() const { + return core_->Get().SerializeAsString(); +} + +const google::protobuf::MessageLite& MCSMessage::GetProtobuf() const { + return core_->Get(); +} + +scoped_ptr<google::protobuf::MessageLite> MCSMessage::CloneProtobuf() const { + scoped_ptr<google::protobuf::MessageLite> clone(GetProtobuf().New()); + clone->CheckTypeAndMergeFrom(GetProtobuf()); + return clone.Pass(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/mcs_message.h b/chromium/google_apis/gcm/base/mcs_message.h new file mode 100644 index 00000000000..14d80ed3b97 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_message.h @@ -0,0 +1,85 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_ +#define GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace google { +namespace protobuf { +class MessageLite; +} // namespace protobuf +} // namespace google + +namespace gcm { + +// A wrapper for MCS protobuffers that encapsulates their tag, size and data +// in an immutable and thread-safe format. If a mutable version is desired, +// CloneProtobuf() should use used to create a new copy of the protobuf. +// +// Note: default copy and assign welcome. +class GCM_EXPORT MCSMessage { + public: + // Creates an invalid MCSMessage. + MCSMessage(); + // Infers tag from |message|. + explicit MCSMessage(const google::protobuf::MessageLite& protobuf); + // |tag| must match |protobuf|'s message type. + MCSMessage(uint8 tag, const google::protobuf::MessageLite& protobuf); + // |tag| must match |protobuf|'s message type. Takes ownership of |protobuf|. + MCSMessage(uint8 tag, + scoped_ptr<const google::protobuf::MessageLite> protobuf); + ~MCSMessage(); + + // Returns whether this message is valid or not (whether a protobuf was + // provided at construction time or not). + bool IsValid() const; + + // Getters for serialization. + uint8 tag() const { return tag_; } + int size() const {return size_; } + std::string SerializeAsString() const; + + // Getter for accessing immutable probotuf fields. + const google::protobuf::MessageLite& GetProtobuf() const; + + // Getter for creating a mutated version of the protobuf. + scoped_ptr<google::protobuf::MessageLite> CloneProtobuf() const; + + private: + class Core : public base::RefCountedThreadSafe<MCSMessage::Core> { + public: + Core(); + Core(uint8 tag, const google::protobuf::MessageLite& protobuf); + Core(uint8 tag, scoped_ptr<const google::protobuf::MessageLite> protobuf); + + const google::protobuf::MessageLite& Get() const; + + private: + friend class base::RefCountedThreadSafe<MCSMessage::Core>; + ~Core(); + + // The immutable protobuf. + scoped_ptr<const google::protobuf::MessageLite> protobuf_; + + DISALLOW_COPY_AND_ASSIGN(Core); + }; + + // These are cached separately to avoid having to recompute them. + const uint8 tag_; + const int size_; + + // The refcounted core, containing the protobuf memory. + scoped_refptr<const Core> core_; +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_BASE_MCS_MESSAGE_H_ diff --git a/chromium/google_apis/gcm/base/mcs_message_unittest.cc b/chromium/google_apis/gcm/base/mcs_message_unittest.cc new file mode 100644 index 00000000000..4d4ef598ad2 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_message_unittest.cc @@ -0,0 +1,92 @@ +// Copyright 2013 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 "google_apis/gcm/base/mcs_message.h" + +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +const uint64 kAndroidId = 12345; +const uint64 kSecret = 54321; + +class MCSMessageTest : public testing::Test { + public: + MCSMessageTest(); + virtual ~MCSMessageTest(); + private: + base::MessageLoop message_loop_; +}; + +MCSMessageTest::MCSMessageTest() { +} + +MCSMessageTest::~MCSMessageTest() { +} + +TEST_F(MCSMessageTest, Invalid) { + MCSMessage message; + EXPECT_FALSE(message.IsValid()); +} + +TEST_F(MCSMessageTest, InitInferTag) { + scoped_ptr<mcs_proto::LoginRequest> login_request( + BuildLoginRequest(kAndroidId, kSecret)); + scoped_ptr<google::protobuf::MessageLite> login_copy( + new mcs_proto::LoginRequest(*login_request)); + MCSMessage message(*login_copy); + login_copy.reset(); + ASSERT_TRUE(message.IsValid()); + EXPECT_EQ(kLoginRequestTag, message.tag()); + EXPECT_EQ(login_request->ByteSize(), message.size()); + EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString()); + EXPECT_EQ(login_request->SerializeAsString(), + message.GetProtobuf().SerializeAsString()); + login_copy = message.CloneProtobuf(); + EXPECT_EQ(login_request->SerializeAsString(), + login_copy->SerializeAsString()); +} + +TEST_F(MCSMessageTest, InitWithTag) { + scoped_ptr<mcs_proto::LoginRequest> login_request( + BuildLoginRequest(kAndroidId, kSecret)); + scoped_ptr<google::protobuf::MessageLite> login_copy( + new mcs_proto::LoginRequest(*login_request)); + MCSMessage message(kLoginRequestTag, *login_copy); + login_copy.reset(); + ASSERT_TRUE(message.IsValid()); + EXPECT_EQ(kLoginRequestTag, message.tag()); + EXPECT_EQ(login_request->ByteSize(), message.size()); + EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString()); + EXPECT_EQ(login_request->SerializeAsString(), + message.GetProtobuf().SerializeAsString()); + login_copy = message.CloneProtobuf(); + EXPECT_EQ(login_request->SerializeAsString(), + login_copy->SerializeAsString()); +} + +TEST_F(MCSMessageTest, InitPassOwnership) { + scoped_ptr<mcs_proto::LoginRequest> login_request( + BuildLoginRequest(kAndroidId, kSecret)); + scoped_ptr<google::protobuf::MessageLite> login_copy( + new mcs_proto::LoginRequest(*login_request)); + MCSMessage message(kLoginRequestTag, + login_copy.PassAs<const google::protobuf::MessageLite>()); + EXPECT_FALSE(login_copy.get()); + ASSERT_TRUE(message.IsValid()); + EXPECT_EQ(kLoginRequestTag, message.tag()); + EXPECT_EQ(login_request->ByteSize(), message.size()); + EXPECT_EQ(login_request->SerializeAsString(), message.SerializeAsString()); + EXPECT_EQ(login_request->SerializeAsString(), + message.GetProtobuf().SerializeAsString()); + login_copy = message.CloneProtobuf(); + EXPECT_EQ(login_request->SerializeAsString(), + login_copy->SerializeAsString()); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/mcs_util.cc b/chromium/google_apis/gcm/base/mcs_util.cc new file mode 100644 index 00000000000..736556079f8 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_util.cc @@ -0,0 +1,233 @@ +// Copyright 2013 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 "google_apis/gcm/base/mcs_util.h" + +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" + +namespace gcm { + +namespace { + +// Type names corresponding to MCSProtoTags. Useful for identifying what type +// of MCS protobuf is contained within a google::protobuf::MessageLite object. +// WARNING: must match the order in MCSProtoTag. +const char* kProtoNames[] = { + "mcs_proto.HeartbeatPing", + "mcs_proto.HeartbeatAck", + "mcs_proto.LoginRequest", + "mcs_proto.LoginResponse", + "mcs_proto.Close", + "mcs_proto.MessageStanza", + "mcs_proto.PresenceStanza", + "mcs_proto.IqStanza", + "mcs_proto.DataMessageStanza", + "mcs_proto.BatchPresenceStanza", + "mcs_proto.StreamErrorStanza", + "mcs_proto.HttpRequest", + "mcs_proto.HttpResponse", + "mcs_proto.BindAccountRequest", + "mcs_proto.BindAccountResponse", + "mcs_proto.TalkMetadata" +}; +COMPILE_ASSERT(arraysize(kProtoNames) == kNumProtoTypes, + ProtoNamesMustIncludeAllTags); + +// TODO(zea): replace these with proper values. +const char kLoginId[] = "login-1"; +const char kLoginDomain[] = "mcs.android.com"; +const char kLoginDeviceIdPrefix[] = "android-"; +const char kLoginSettingName[] = "new_vc"; +const char kLoginSettingValue[] = "1"; + +} // namespace + +scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id, + uint64 auth_token) { + // Create a hex encoded auth id for the device id field. + std::string auth_id_hex; + auth_id_hex = base::StringPrintf("%" PRIx64, auth_id); + + std::string auth_id_str = base::Uint64ToString(auth_id); + std::string auth_token_str = base::Uint64ToString(auth_token); + + scoped_ptr<mcs_proto::LoginRequest> login_request( + new mcs_proto::LoginRequest()); + + // TODO(zea): set better values. + login_request->set_account_id(1000000); + login_request->set_adaptive_heartbeat(false); + login_request->set_auth_service(mcs_proto::LoginRequest::ANDROID_ID); + login_request->set_auth_token(auth_token_str); + login_request->set_id(kLoginId); + login_request->set_domain(kLoginDomain); + login_request->set_device_id(kLoginDeviceIdPrefix + auth_id_hex); + login_request->set_network_type(1); + login_request->set_resource(auth_id_str); + login_request->set_user(auth_id_str); + login_request->set_use_rmq2(true); + + login_request->add_setting(); + login_request->mutable_setting(0)->set_name(kLoginSettingName); + login_request->mutable_setting(0)->set_value(kLoginSettingValue); + return login_request.Pass(); +} + +scoped_ptr<mcs_proto::IqStanza> BuildStreamAck() { + scoped_ptr<mcs_proto::IqStanza> stream_ack_iq(new mcs_proto::IqStanza()); + stream_ack_iq->set_type(mcs_proto::IqStanza::SET); + stream_ack_iq->set_id(""); + stream_ack_iq->mutable_extension()->set_id(kStreamAck); + stream_ack_iq->mutable_extension()->set_data(""); + return stream_ack_iq.Pass(); +} + +scoped_ptr<mcs_proto::IqStanza> BuildSelectiveAck( + const std::vector<std::string>& acked_ids) { + scoped_ptr<mcs_proto::IqStanza> selective_ack_iq(new mcs_proto::IqStanza()); + selective_ack_iq->set_type(mcs_proto::IqStanza::SET); + selective_ack_iq->set_id(""); + selective_ack_iq->mutable_extension()->set_id(kSelectiveAck); + mcs_proto::SelectiveAck selective_ack; + for (size_t i = 0; i < acked_ids.size(); ++i) + selective_ack.add_id(acked_ids[i]); + selective_ack_iq->mutable_extension()->set_data( + selective_ack.SerializeAsString()); + return selective_ack_iq.Pass(); +} + +// Utility method to build a google::protobuf::MessageLite object from a MCS +// tag. +scoped_ptr<google::protobuf::MessageLite> BuildProtobufFromTag(uint8 tag) { + switch(tag) { + case kHeartbeatPingTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::HeartbeatPing()); + case kHeartbeatAckTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::HeartbeatAck()); + case kLoginRequestTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::LoginRequest()); + case kLoginResponseTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::LoginResponse()); + case kCloseTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::Close()); + case kIqStanzaTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::IqStanza()); + case kDataMessageStanzaTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::DataMessageStanza()); + case kStreamErrorStanzaTag: + return scoped_ptr<google::protobuf::MessageLite>( + new mcs_proto::StreamErrorStanza()); + default: + return scoped_ptr<google::protobuf::MessageLite>(); + } +} + +// Utility method to extract a MCS tag from a google::protobuf::MessageLite +// object. +int GetMCSProtoTag(const google::protobuf::MessageLite& message) { + const std::string& type_name = message.GetTypeName(); + if (type_name == kProtoNames[kHeartbeatPingTag]) { + return kHeartbeatPingTag; + } else if (type_name == kProtoNames[kHeartbeatAckTag]) { + return kHeartbeatAckTag; + } else if (type_name == kProtoNames[kLoginRequestTag]) { + return kLoginRequestTag; + } else if (type_name == kProtoNames[kLoginResponseTag]) { + return kLoginResponseTag; + } else if (type_name == kProtoNames[kCloseTag]) { + return kCloseTag; + } else if (type_name == kProtoNames[kIqStanzaTag]) { + return kIqStanzaTag; + } else if (type_name == kProtoNames[kDataMessageStanzaTag]) { + return kDataMessageStanzaTag; + } else if (type_name == kProtoNames[kStreamErrorStanzaTag]) { + return kStreamErrorStanzaTag; + } + return -1; +} + +std::string GetPersistentId(const google::protobuf::MessageLite& protobuf) { + if (protobuf.GetTypeName() == kProtoNames[kIqStanzaTag]) { + return reinterpret_cast<const mcs_proto::IqStanza*>(&protobuf)-> + persistent_id(); + } else if (protobuf.GetTypeName() == kProtoNames[kDataMessageStanzaTag]) { + return reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)-> + persistent_id(); + } + // Not all message types have persistent ids. Just return empty string; + return ""; +} + +void SetPersistentId(const std::string& persistent_id, + google::protobuf::MessageLite* protobuf) { + if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) { + reinterpret_cast<mcs_proto::IqStanza*>(protobuf)-> + set_persistent_id(persistent_id); + return; + } else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) { + reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)-> + set_persistent_id(persistent_id); + return; + } + NOTREACHED(); +} + +uint32 GetLastStreamIdReceived(const google::protobuf::MessageLite& protobuf) { + if (protobuf.GetTypeName() == kProtoNames[kIqStanzaTag]) { + return reinterpret_cast<const mcs_proto::IqStanza*>(&protobuf)-> + last_stream_id_received(); + } else if (protobuf.GetTypeName() == kProtoNames[kDataMessageStanzaTag]) { + return reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)-> + last_stream_id_received(); + } else if (protobuf.GetTypeName() == kProtoNames[kHeartbeatPingTag]) { + return reinterpret_cast<const mcs_proto::HeartbeatPing*>(&protobuf)-> + last_stream_id_received(); + } else if (protobuf.GetTypeName() == kProtoNames[kHeartbeatAckTag]) { + return reinterpret_cast<const mcs_proto::HeartbeatAck*>(&protobuf)-> + last_stream_id_received(); + } else if (protobuf.GetTypeName() == kProtoNames[kLoginResponseTag]) { + return reinterpret_cast<const mcs_proto::LoginResponse*>(&protobuf)-> + last_stream_id_received(); + } + // Not all message types have last stream ids. Just return 0. + return 0; +} + +void SetLastStreamIdReceived(uint32 val, + google::protobuf::MessageLite* protobuf) { + if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) { + reinterpret_cast<mcs_proto::IqStanza*>(protobuf)-> + set_last_stream_id_received(val); + return; + } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatPingTag]) { + reinterpret_cast<mcs_proto::HeartbeatPing*>(protobuf)-> + set_last_stream_id_received(val); + return; + } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatAckTag]) { + reinterpret_cast<mcs_proto::HeartbeatAck*>(protobuf)-> + set_last_stream_id_received(val); + return; + } else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) { + reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)-> + set_last_stream_id_received(val); + return; + } else if (protobuf->GetTypeName() == kProtoNames[kLoginResponseTag]) { + reinterpret_cast<mcs_proto::LoginResponse*>(protobuf)-> + set_last_stream_id_received(val); + return; + } + NOTREACHED(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/mcs_util.h b/chromium/google_apis/gcm/base/mcs_util.h new file mode 100644 index 00000000000..7f92564ad27 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_util.h @@ -0,0 +1,81 @@ +// Copyright 2013 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. +// +// Utility methods for MCS interactions. + +#ifndef GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_ +#define GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/protocol/mcs.pb.h" + +namespace net { +class StreamSocket; +} + +namespace gcm { + +// MCS Message tags. +// WARNING: the order of these tags must remain the same, as the tag values +// must be consistent with those used on the server. +enum MCSProtoTag { + kHeartbeatPingTag = 0, + kHeartbeatAckTag, + kLoginRequestTag, + kLoginResponseTag, + kCloseTag, + kMessageStanzaTag, + kPresenceStanzaTag, + kIqStanzaTag, + kDataMessageStanzaTag, + kBatchPresenceStanzaTag, + kStreamErrorStanzaTag, + kHttpRequestTag, + kHttpResponseTag, + kBindAccountRequestTag, + kBindAccountResponseTag, + kTalkMetadataTag, + kNumProtoTypes, +}; + +enum MCSIqStanzaExtension { + kSelectiveAck = 12, + kStreamAck = 13, +}; + +// Builds a LoginRequest with the hardcoded local data. +GCM_EXPORT scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest( + uint64 auth_id, + uint64 auth_token); + +// Builds a StreamAck IqStanza message. +GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildStreamAck(); +GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildSelectiveAck( + const std::vector<std::string>& acked_ids); + +// Utility methods for building and identifying MCS protobufs. +GCM_EXPORT scoped_ptr<google::protobuf::MessageLite> + BuildProtobufFromTag(uint8 tag); +GCM_EXPORT int GetMCSProtoTag(const google::protobuf::MessageLite& message); + +// RMQ utility methods for extracting/setting common data from/to protobufs. +GCM_EXPORT std::string GetPersistentId( + const google::protobuf::MessageLite& message); +GCM_EXPORT void SetPersistentId( + const std::string& persistent_id, + google::protobuf::MessageLite* message); +GCM_EXPORT uint32 GetLastStreamIdReceived( + const google::protobuf::MessageLite& protobuf); +GCM_EXPORT void SetLastStreamIdReceived( + uint32 last_stream_id_received, + google::protobuf::MessageLite* protobuf); + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_ diff --git a/chromium/google_apis/gcm/base/mcs_util_unittest.cc b/chromium/google_apis/gcm/base/mcs_util_unittest.cc new file mode 100644 index 00000000000..d25914583a4 --- /dev/null +++ b/chromium/google_apis/gcm/base/mcs_util_unittest.cc @@ -0,0 +1,82 @@ +// Copyright 2013 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 "google_apis/gcm/base/mcs_util.h" + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { +namespace { + +const uint64 kAuthId = 4421448356646222460; +const uint64 kAuthToken = 12345; + +// Build a login request protobuf. +TEST(MCSUtilTest, BuildLoginRequest) { + scoped_ptr<mcs_proto::LoginRequest> login_request = + BuildLoginRequest(kAuthId, kAuthToken); + ASSERT_EQ("login-1", login_request->id()); + ASSERT_EQ(base::Uint64ToString(kAuthToken), login_request->auth_token()); + ASSERT_EQ(base::Uint64ToString(kAuthId), login_request->user()); + ASSERT_EQ("android-3d5c23dac2a1fa7c", login_request->device_id()); + // TODO(zea): test the other fields once they have valid values. +} + +// Test building a protobuf and extracting the tag from a protobuf. +TEST(MCSUtilTest, ProtobufToTag) { + for (size_t i = 0; i < kNumProtoTypes; ++i) { + scoped_ptr<google::protobuf::MessageLite> protobuf = + BuildProtobufFromTag(i); + if (!protobuf.get()) // Not all tags have protobuf definitions. + continue; + ASSERT_EQ((int)i, GetMCSProtoTag(*protobuf)) << "Type " << i; + } +} + +// Test getting and setting persistent ids. +TEST(MCSUtilTest, PersistentIds) { + COMPILE_ASSERT(kNumProtoTypes == 16U, UpdatePersistentIds); + const int kTagsWithPersistentIds[] = { + kIqStanzaTag, + kDataMessageStanzaTag + }; + for (size_t i = 0; i < arraysize(kTagsWithPersistentIds); ++i) { + int tag = kTagsWithPersistentIds[i]; + scoped_ptr<google::protobuf::MessageLite> protobuf = + BuildProtobufFromTag(tag); + ASSERT_TRUE(protobuf.get()); + SetPersistentId(base::IntToString(tag), protobuf.get()); + int get_val = 0; + base::StringToInt(GetPersistentId(*protobuf), &get_val); + ASSERT_EQ(tag, get_val); + } +} + +// Test getting and setting stream ids. +TEST(MCSUtilTest, StreamIds) { + COMPILE_ASSERT(kNumProtoTypes == 16U, UpdateStreamIds); + const int kTagsWithStreamIds[] = { + kIqStanzaTag, + kDataMessageStanzaTag, + kHeartbeatPingTag, + kHeartbeatAckTag, + kLoginResponseTag, + }; + for (size_t i = 0; i < arraysize(kTagsWithStreamIds); ++i) { + int tag = kTagsWithStreamIds[i]; + scoped_ptr<google::protobuf::MessageLite> protobuf = + BuildProtobufFromTag(tag); + ASSERT_TRUE(protobuf.get()); + SetLastStreamIdReceived(tag, protobuf.get()); + int get_id = GetLastStreamIdReceived(*protobuf); + ASSERT_EQ(tag, get_id); + } +} + +} // namespace +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/socket_stream.cc b/chromium/google_apis/gcm/base/socket_stream.cc new file mode 100644 index 00000000000..1a0b29d8d07 --- /dev/null +++ b/chromium/google_apis/gcm/base/socket_stream.cc @@ -0,0 +1,332 @@ +// Copyright 2013 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 "google_apis/gcm/base/socket_stream.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "net/base/io_buffer.h" +#include "net/socket/stream_socket.h" + +namespace gcm { + +namespace { + +// TODO(zea): consider having dynamically-sized buffers if this becomes too +// expensive. +const uint32 kDefaultBufferSize = 8*1024; + +} // namespace + +SocketInputStream::SocketInputStream(net::StreamSocket* socket) + : socket_(socket), + io_buffer_(new net::IOBuffer(kDefaultBufferSize)), + read_buffer_(new net::DrainableIOBuffer(io_buffer_.get(), + kDefaultBufferSize)), + next_pos_(0), + last_error_(net::OK), + weak_ptr_factory_(this) { + DCHECK(socket->IsConnected()); +} + +SocketInputStream::~SocketInputStream() { +} + +bool SocketInputStream::Next(const void** data, int* size) { + if (GetState() != EMPTY && GetState() != READY) { + NOTREACHED() << "Invalid input stream read attempt."; + return false; + } + + if (GetState() == EMPTY) { + DVLOG(1) << "No unread data remaining, ending read."; + return false; + } + + DCHECK_EQ(GetState(), READY) + << " Input stream must have pending data before reading."; + DCHECK_LT(next_pos_, read_buffer_->BytesConsumed()); + *data = io_buffer_->data() + next_pos_; + *size = UnreadByteCount(); + next_pos_ = read_buffer_->BytesConsumed(); + DVLOG(1) << "Consuming " << *size << " bytes in input buffer."; + return true; +} + +void SocketInputStream::BackUp(int count) { + DCHECK(GetState() == READY || GetState() == EMPTY); + DCHECK_GT(count, 0); + DCHECK_LE(count, next_pos_); + + next_pos_ -= count; + DVLOG(1) << "Backing up " << count << " bytes in input buffer. " + << "Current position now at " << next_pos_ + << " of " << read_buffer_->BytesConsumed(); +} + +bool SocketInputStream::Skip(int count) { + NOTIMPLEMENTED(); + return false; +} + +int64 SocketInputStream::ByteCount() const { + DCHECK_NE(GetState(), CLOSED); + DCHECK_NE(GetState(), READING); + return next_pos_; +} + +size_t SocketInputStream::UnreadByteCount() const { + DCHECK_NE(GetState(), CLOSED); + DCHECK_NE(GetState(), READING); + return read_buffer_->BytesConsumed() - next_pos_; +} + +net::Error SocketInputStream::Refresh(const base::Closure& callback, + int byte_limit) { + DCHECK_NE(GetState(), CLOSED); + DCHECK_NE(GetState(), READING); + DCHECK_GT(byte_limit, 0); + + if (byte_limit > read_buffer_->BytesRemaining()) { + NOTREACHED() << "Out of buffer space, closing input stream."; + CloseStream(net::ERR_UNEXPECTED, base::Closure()); + return net::OK; + } + + if (!socket_->IsConnected()) { + LOG(ERROR) << "Socket was disconnected, closing input stream"; + CloseStream(net::ERR_CONNECTION_CLOSED, base::Closure()); + return net::OK; + } + + DVLOG(1) << "Refreshing input stream, limit of " << byte_limit << " bytes."; + int result = socket_->Read( + read_buffer_, + byte_limit, + base::Bind(&SocketInputStream::RefreshCompletionCallback, + weak_ptr_factory_.GetWeakPtr(), + callback)); + DVLOG(1) << "Read returned " << result; + if (result == net::ERR_IO_PENDING) { + last_error_ = net::ERR_IO_PENDING; + return net::ERR_IO_PENDING; + } + + RefreshCompletionCallback(base::Closure(), result); + return net::OK; +} + +void SocketInputStream::RebuildBuffer() { + DVLOG(1) << "Rebuilding input stream, consumed " + << next_pos_ << " bytes."; + DCHECK_NE(GetState(), READING); + DCHECK_NE(GetState(), CLOSED); + + int unread_data_size = 0; + const void* unread_data_ptr = NULL; + Next(&unread_data_ptr, &unread_data_size); + ResetInternal(); + + if (unread_data_ptr != io_buffer_->data()) { + DVLOG(1) << "Have " << unread_data_size + << " unread bytes remaining, shifting."; + // Move any remaining unread data to the start of the buffer; + std::memmove(io_buffer_->data(), unread_data_ptr, unread_data_size); + } else { + DVLOG(1) << "Have " << unread_data_size << " unread bytes remaining."; + } + read_buffer_->DidConsume(unread_data_size); +} + +net::Error SocketInputStream::last_error() const { + return last_error_; +} + +SocketInputStream::State SocketInputStream::GetState() const { + if (last_error_ < net::ERR_IO_PENDING) + return CLOSED; + + if (last_error_ == net::ERR_IO_PENDING) + return READING; + + DCHECK_EQ(last_error_, net::OK); + if (read_buffer_->BytesConsumed() == next_pos_) + return EMPTY; + + return READY; +} + +void SocketInputStream::RefreshCompletionCallback( + const base::Closure& callback, int result) { + // If an error occurred before the completion callback could complete, ignore + // the result. + if (GetState() == CLOSED) + return; + + // Result == 0 implies EOF, which is treated as an error. + if (result == 0) + result = net::ERR_CONNECTION_CLOSED; + + DCHECK_NE(result, net::ERR_IO_PENDING); + + if (result < net::OK) { + DVLOG(1) << "Failed to refresh socket: " << result; + CloseStream(static_cast<net::Error>(result), callback); + return; + } + + DCHECK_GT(result, 0); + last_error_ = net::OK; + read_buffer_->DidConsume(result); + + DVLOG(1) << "Refresh complete with " << result << " new bytes. " + << "Current position " << next_pos_ + << " of " << read_buffer_->BytesConsumed() << "."; + + if (!callback.is_null()) + callback.Run(); +} + +void SocketInputStream::ResetInternal() { + read_buffer_->SetOffset(0); + next_pos_ = 0; + last_error_ = net::OK; + weak_ptr_factory_.InvalidateWeakPtrs(); // Invalidate any callbacks. +} + +void SocketInputStream::CloseStream(net::Error error, + const base::Closure& callback) { + DCHECK_LT(error, net::ERR_IO_PENDING); + ResetInternal(); + last_error_ = error; + LOG(ERROR) << "Closing stream with result " << error; + if (!callback.is_null()) + callback.Run(); +} + +SocketOutputStream::SocketOutputStream(net::StreamSocket* socket) + : socket_(socket), + io_buffer_(new net::IOBuffer(kDefaultBufferSize)), + write_buffer_(new net::DrainableIOBuffer(io_buffer_.get(), + kDefaultBufferSize)), + next_pos_(0), + last_error_(net::OK), + weak_ptr_factory_(this) { + DCHECK(socket->IsConnected()); +} + +SocketOutputStream::~SocketOutputStream() { +} + +bool SocketOutputStream::Next(void** data, int* size) { + DCHECK_NE(GetState(), CLOSED); + DCHECK_NE(GetState(), FLUSHING); + if (next_pos_ == write_buffer_->size()) + return false; + + *data = write_buffer_->data() + next_pos_; + *size = write_buffer_->size() - next_pos_; + next_pos_ = write_buffer_->size(); + return true; +} + +void SocketOutputStream::BackUp(int count) { + DCHECK_GE(count, 0); + if (count > next_pos_) + next_pos_ = 0; + next_pos_ -= count; + DVLOG(1) << "Backing up " << count << " bytes in output buffer. " + << next_pos_ << " bytes used."; +} + +int64 SocketOutputStream::ByteCount() const { + DCHECK_NE(GetState(), CLOSED); + DCHECK_NE(GetState(), FLUSHING); + return next_pos_; +} + +net::Error SocketOutputStream::Flush(const base::Closure& callback) { + DCHECK_EQ(GetState(), READY); + + if (!socket_->IsConnected()) { + LOG(ERROR) << "Socket was disconnected, closing output stream"; + last_error_ = net::ERR_CONNECTION_CLOSED; + return net::OK; + } + + DVLOG(1) << "Flushing " << next_pos_ << " bytes into socket."; + int result = socket_->Write( + write_buffer_, + next_pos_, + base::Bind(&SocketOutputStream::FlushCompletionCallback, + weak_ptr_factory_.GetWeakPtr(), + callback)); + DVLOG(1) << "Write returned " << result; + if (result == net::ERR_IO_PENDING) { + last_error_ = net::ERR_IO_PENDING; + return net::ERR_IO_PENDING; + } + + FlushCompletionCallback(base::Closure(), result); + return net::OK; +} + +SocketOutputStream::State SocketOutputStream::GetState() const{ + if (last_error_ < net::ERR_IO_PENDING) + return CLOSED; + + if (last_error_ == net::ERR_IO_PENDING) + return FLUSHING; + + DCHECK_EQ(last_error_, net::OK); + if (next_pos_ == 0) + return EMPTY; + + return READY; +} + +net::Error SocketOutputStream::last_error() const { + return last_error_; +} + +void SocketOutputStream::FlushCompletionCallback( + const base::Closure& callback, int result) { + // If an error occurred before the completion callback could complete, ignore + // the result. + if (GetState() == CLOSED) + return; + + // Result == 0 implies EOF, which is treated as an error. + if (result == 0) + result = net::ERR_CONNECTION_CLOSED; + + DCHECK_NE(result, net::ERR_IO_PENDING); + + if (result < net::OK) { + LOG(ERROR) << "Failed to flush socket."; + last_error_ = static_cast<net::Error>(result); + if (!callback.is_null()) + callback.Run(); + return; + } + + DCHECK_GT(result, net::OK); + last_error_ = net::OK; + + if (write_buffer_->BytesConsumed() + result < next_pos_) { + DVLOG(1) << "Partial flush complete. Retrying."; + // Only a partial write was completed. Flush again to finish the write. + write_buffer_->DidConsume(result); + Flush(callback); + return; + } + + DVLOG(1) << "Socket flush complete."; + write_buffer_->SetOffset(0); + next_pos_ = 0; + if (!callback.is_null()) + callback.Run(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/socket_stream.h b/chromium/google_apis/gcm/base/socket_stream.h new file mode 100644 index 00000000000..a45842016f6 --- /dev/null +++ b/chromium/google_apis/gcm/base/socket_stream.h @@ -0,0 +1,205 @@ +// Copyright 2013 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. +// +// Protobuf ZeroCopy[Input/Output]Stream implementations capable of using a +// net::StreamSocket. Built to work with Protobuf CodedStreams. + +#ifndef GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_ +#define GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_ + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "google/protobuf/io/zero_copy_stream.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "net/base/net_errors.h" + +namespace net { +class DrainableIOBuffer; +class IOBuffer; +class StreamSocket; +} // namespace net + +namespace gcm { + +// A helper class for interacting with a net::StreamSocket that is receiving +// protobuf encoded messages. A SocketInputStream does not take ownership of +// the socket itself, and it is expected that the life of the input stream +// should match the life of the socket itself (while the socket remains +// connected). If an error is encounters, the input stream will store the error +// in |last_error_|, and GetState() will be set to CLOSED. +// Typical usage: +// 1. Check the GetState() of the input stream before using it. If CLOSED, the +// input stream must be rebuilt (and the socket likely needs to be +// reconnected as an error was encountered). +// 2. If GetState() is EMPTY, call Refresh(..), passing the maximum byte size +// for a message, and wait until completion. It is invalid to attempt to +// Refresh an input stream or read data from the stream while a Refresh is +// pending. +// 3. Check GetState() again to ensure the Refresh was successful. +// 4. Use a CodedInputStream to read from the ZeroCopyInputStream interface of +// the SocketInputStream. Next(..) will return true until there is no data +// remaining. +// 5. Call RebuildBuffer when done reading, to shift any unread data to the +// start of the buffer. +// 6. Repeat as necessary. +class GCM_EXPORT SocketInputStream + : public google::protobuf::io::ZeroCopyInputStream { + public: + enum State { + // No valid data to read. This means the buffer is either empty or all data + // in the buffer has already been consumed. + EMPTY, + // Valid data to read. + READY, + // In the process of reading new data from the socket. + READING, + // An permanent error occurred and the stream is now closed. + CLOSED, + }; + + // |socket| should already be connected. + explicit SocketInputStream(net::StreamSocket* socket); + virtual ~SocketInputStream(); + + // ZeroCopyInputStream implementation. + virtual bool Next(const void** data, int* size) OVERRIDE; + virtual void BackUp(int count) OVERRIDE; + virtual bool Skip(int count) OVERRIDE; // Not implemented. + virtual int64 ByteCount() const OVERRIDE; + + // The remaining amount of valid data available to be read. + size_t UnreadByteCount() const; + + // Reads from the socket, appending a max of |byte_limit| bytes onto the read + // buffer. net::ERR_IO_PENDING is returned if the refresh can't complete + // synchronously, in which case the callback is invoked upon completion. If + // the refresh can complete synchronously, even in case of an error, returns + // net::OK without invoking callback. + // Note: GetState() (and possibly last_error()) should be checked upon + // completion to determine whether the Refresh encountered an error. + net::Error Refresh(const base::Closure& callback, int byte_limit); + + // Rebuilds the buffer state by copying over any unread data to the beginning + // of the buffer and resetting the buffer read/write positions. + // Note: it is not valid to call Rebuild() if GetState() == CLOSED. The stream + // must be recreated from scratch in such a scenario. + void RebuildBuffer(); + + // Returns the last fatal error encountered. Only valid if GetState() == + // CLOSED. + net::Error last_error() const; + + // Returns the current state. + State GetState() const; + + private: + // Clears the local state. + void ResetInternal(); + + // Callback for Socket::Read calls. + void RefreshCompletionCallback(const base::Closure& callback, int result); + + // Permanently closes the stream. + void CloseStream(net::Error error, const base::Closure& callback); + + // Internal net components. + net::StreamSocket* const socket_; + const scoped_refptr<net::IOBuffer> io_buffer_; + // IOBuffer implementation that wraps the data within |io_buffer_| that hasn't + // been written to yet by Socket::Read calls. + const scoped_refptr<net::DrainableIOBuffer> read_buffer_; + + // Starting position of the data within |io_buffer_| to consume on subsequent + // Next(..) call. 0 <= next_pos_ <= read_buffer_.BytesConsumed() + // Note: next_pos == read_buffer_.BytesConsumed() implies GetState() == EMPTY. + int next_pos_; + + // If < net::ERR_IO_PENDING, the last net error received. + // Note: last_error_ == net::ERR_IO_PENDING implies GetState() == READING. + net::Error last_error_; + + base::WeakPtrFactory<SocketInputStream> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(SocketInputStream); +}; + +// A helper class for writing to a SocketStream with protobuf encoded data. +// A SocketOutputStream does not take ownership of the socket itself, and it is +// expected that the life of the output stream should match the life of the +// socket itself (while the socket remains connected). +// Typical usage: +// 1. Check the GetState() of the output stream before using it. If CLOSED, the +// output stream must be rebuilt (and the socket likely needs to be +// reconnected, as an error was encountered). +// 2. If EMPTY, the output stream can be written via a CodedOutputStream using +// the ZeroCopyOutputStream interface. +// 3. Once done writing, GetState() should be READY, so call Flush(..) to write +// the buffer into the StreamSocket. Wait for the callback to be invoked +// (it's invalid to write to an output stream while it's flushing). +// 4. Check the GetState() again to ensure the Flush was successful. GetState() +// should be EMPTY again. +// 5. Repeat. +class GCM_EXPORT SocketOutputStream + : public google::protobuf::io::ZeroCopyOutputStream { + public: + enum State { + // No valid data yet. + EMPTY, + // Ready for flushing (some data is present). + READY, + // In the process of flushing into the socket. + FLUSHING, + // A permanent error occurred, and the stream is now closed. + CLOSED, + }; + + // |socket| should already be connected. + explicit SocketOutputStream(net::StreamSocket* socket); + virtual ~SocketOutputStream(); + + // ZeroCopyOutputStream implementation. + virtual bool Next(void** data, int* size) OVERRIDE; + virtual void BackUp(int count) OVERRIDE; + virtual int64 ByteCount() const OVERRIDE; + + // Writes the buffer into the Socket. + net::Error Flush(const base::Closure& callback); + + // Returns the last fatal error encountered. Only valid if GetState() == + // CLOSED. + net::Error last_error() const; + + // Returns the current state. + State GetState() const; + + private: + void FlushCompletionCallback(const base::Closure& callback, int result); + + // Internal net components. + net::StreamSocket* const socket_; + const scoped_refptr<net::IOBuffer> io_buffer_; + // IOBuffer implementation that wraps the data within |io_buffer_| that hasn't + // been written to the socket yet. + const scoped_refptr<net::DrainableIOBuffer> write_buffer_; + + // Starting position of the data within |io_buffer_| to consume on subsequent + // Next(..) call. 0 <= write_buffer_.BytesConsumed() <= next_pos_ + // Note: next_pos == 0 implies GetState() == EMPTY. + int next_pos_; + + // If < net::ERR_IO_PENDING, the last net error received. + // Note: last_error_ == net::ERR_IO_PENDING implies GetState() == FLUSHING. + net::Error last_error_; + + base::WeakPtrFactory<SocketOutputStream> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(SocketOutputStream); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_BASE_SOCKET_STREAM_H_ diff --git a/chromium/google_apis/gcm/base/socket_stream_unittest.cc b/chromium/google_apis/gcm/base/socket_stream_unittest.cc new file mode 100644 index 00000000000..d7ba6793bf9 --- /dev/null +++ b/chromium/google_apis/gcm/base/socket_stream_unittest.cc @@ -0,0 +1,406 @@ +// Copyright 2013 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 "google_apis/gcm/base/socket_stream.h" + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "base/strings/string_piece.h" +#include "net/socket/socket_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { +namespace { + +typedef std::vector<net::MockRead> ReadList; +typedef std::vector<net::MockWrite> WriteList; + +const char kReadData[] = "read_data"; +const uint64 kReadDataSize = arraysize(kReadData) - 1; +const char kReadData2[] = "read_alternate_data"; +const uint64 kReadData2Size = arraysize(kReadData2) - 1; +const char kWriteData[] = "write_data"; +const uint64 kWriteDataSize = arraysize(kWriteData) - 1; + +class GCMSocketStreamTest : public testing::Test { + public: + GCMSocketStreamTest(); + virtual ~GCMSocketStreamTest(); + + // Build a socket with the expected reads and writes. + void BuildSocket(const ReadList& read_list, const WriteList& write_list); + + // Pump the message loop until idle. + void PumpLoop(); + + // Simulates a google::protobuf::io::CodedInputStream read. + base::StringPiece DoInputStreamRead(uint64 bytes); + // Simulates a google::protobuf::io::CodedOutputStream write. + uint64 DoOutputStreamWrite(const base::StringPiece& write_src); + + // Synchronous Refresh wrapper. + void WaitForData(size_t msg_size); + + base::MessageLoop* message_loop() { return &message_loop_; }; + net::DelayedSocketData* data_provider() { return data_provider_.get(); } + SocketInputStream* input_stream() { return socket_input_stream_.get(); } + SocketOutputStream* output_stream() { return socket_output_stream_.get(); } + net::StreamSocket* socket() { return socket_.get(); } + + private: + void OpenConnection(); + void ResetInputStream(); + void ResetOutputStream(); + + void ConnectCallback(int result); + + // SocketStreams and their data providers. + ReadList mock_reads_; + WriteList mock_writes_; + scoped_ptr<net::DelayedSocketData> data_provider_; + scoped_ptr<SocketInputStream> socket_input_stream_; + scoped_ptr<SocketOutputStream> socket_output_stream_; + + // net:: components. + scoped_ptr<net::StreamSocket> socket_; + net::MockClientSocketFactory socket_factory_; + net::AddressList address_list_; + + base::MessageLoopForIO message_loop_; +}; + +GCMSocketStreamTest::GCMSocketStreamTest() { + net::IPAddressNumber ip_number; + net::ParseIPLiteralToNumber("127.0.0.1", &ip_number); + address_list_ = net::AddressList::CreateFromIPAddress(ip_number, 5228); +} + +GCMSocketStreamTest::~GCMSocketStreamTest() {} + +void GCMSocketStreamTest::BuildSocket(const ReadList& read_list, + const WriteList& write_list) { + mock_reads_ = read_list; + mock_writes_ = write_list; + data_provider_.reset( + new net::DelayedSocketData( + 0, + vector_as_array(&mock_reads_), mock_reads_.size(), + vector_as_array(&mock_writes_), mock_writes_.size())); + socket_factory_.AddSocketDataProvider(data_provider_.get()); + OpenConnection(); + ResetInputStream(); + ResetOutputStream(); +} + +void GCMSocketStreamTest::PumpLoop() { + base::RunLoop run_loop; + run_loop.RunUntilIdle(); +} + +base::StringPiece GCMSocketStreamTest::DoInputStreamRead(uint64 bytes) { + uint64 total_bytes_read = 0; + const void* initial_buffer = NULL; + const void* buffer = NULL; + int size = 0; + + do { + DCHECK(socket_input_stream_->GetState() == SocketInputStream::EMPTY || + socket_input_stream_->GetState() == SocketInputStream::READY); + if (!socket_input_stream_->Next(&buffer, &size)) + break; + total_bytes_read += size; + if (initial_buffer) { // Verify the buffer doesn't skip data. + EXPECT_EQ(static_cast<const uint8*>(initial_buffer) + total_bytes_read, + static_cast<const uint8*>(buffer) + size); + } else { + initial_buffer = buffer; + } + } while (total_bytes_read < bytes); + + if (total_bytes_read > bytes) { + socket_input_stream_->BackUp(total_bytes_read - bytes); + total_bytes_read = bytes; + } + + return base::StringPiece(static_cast<const char*>(initial_buffer), + total_bytes_read); +} + +uint64 GCMSocketStreamTest::DoOutputStreamWrite( + const base::StringPiece& write_src) { + DCHECK_EQ(socket_output_stream_->GetState(), SocketOutputStream::EMPTY); + uint64 total_bytes_written = 0; + void* buffer = NULL; + int size = 0; + size_t bytes = write_src.size(); + + do { + if (!socket_output_stream_->Next(&buffer, &size)) + break; + uint64 bytes_to_write = (static_cast<uint64>(size) < bytes ? size : bytes); + memcpy(buffer, + write_src.data() + total_bytes_written, + bytes_to_write); + if (bytes_to_write < static_cast<uint64>(size)) + socket_output_stream_->BackUp(size - bytes_to_write); + total_bytes_written += bytes_to_write; + } while (total_bytes_written < bytes); + + base::RunLoop run_loop; + if (socket_output_stream_->Flush(run_loop.QuitClosure()) == + net::ERR_IO_PENDING) { + run_loop.Run(); + } + + return total_bytes_written; +} + +void GCMSocketStreamTest::WaitForData(size_t msg_size) { + while (input_stream()->UnreadByteCount() < msg_size) { + base::RunLoop run_loop; + if (input_stream()->Refresh(run_loop.QuitClosure(), + msg_size - input_stream()->UnreadByteCount()) == + net::ERR_IO_PENDING) { + run_loop.Run(); + } + if (input_stream()->GetState() == SocketInputStream::CLOSED) + return; + } +} + +void GCMSocketStreamTest::OpenConnection() { + socket_ = socket_factory_.CreateTransportClientSocket( + address_list_, NULL, net::NetLog::Source()); + socket_->Connect( + base::Bind(&GCMSocketStreamTest::ConnectCallback, + base::Unretained(this))); + PumpLoop(); +} + +void GCMSocketStreamTest::ConnectCallback(int result) {} + +void GCMSocketStreamTest::ResetInputStream() { + DCHECK(socket_.get()); + socket_input_stream_.reset(new SocketInputStream(socket_.get())); +} + +void GCMSocketStreamTest::ResetOutputStream() { + DCHECK(socket_.get()); + socket_output_stream_.reset(new SocketOutputStream(socket_.get())); +} + +// A read where all data is already available. +TEST_F(GCMSocketStreamTest, ReadDataSync) { + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, + kReadData, + kReadDataSize)), + WriteList()); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); +} + +// A read that comes in two parts. +TEST_F(GCMSocketStreamTest, ReadPartialDataSync) { + size_t first_read_len = kReadDataSize / 2; + size_t second_read_len = kReadDataSize - first_read_len; + ReadList read_list; + read_list.push_back( + net::MockRead(net::SYNCHRONOUS, + kReadData, + first_read_len)); + read_list.push_back( + net::MockRead(net::SYNCHRONOUS, + &kReadData[first_read_len], + second_read_len)); + BuildSocket(read_list, WriteList()); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); +} + +// A read where no data is available at first (IO_PENDING will be returned). +TEST_F(GCMSocketStreamTest, ReadAsync) { + size_t first_read_len = kReadDataSize / 2; + size_t second_read_len = kReadDataSize - first_read_len; + ReadList read_list; + read_list.push_back( + net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING)); + read_list.push_back( + net::MockRead(net::ASYNC, kReadData, first_read_len)); + read_list.push_back( + net::MockRead(net::ASYNC, &kReadData[first_read_len], second_read_len)); + BuildSocket(read_list, WriteList()); + + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&net::DelayedSocketData::ForceNextRead, + base::Unretained(data_provider()))); + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); +} + +// Simulate two packets arriving at once. Read them in two separate calls. +TEST_F(GCMSocketStreamTest, TwoReadsAtOnce) { + std::string long_data = std::string(kReadData, kReadDataSize) + + std::string(kReadData2, kReadData2Size); + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, + long_data.c_str(), + long_data.size())), + WriteList()); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); + + WaitForData(kReadData2Size); + ASSERT_EQ(std::string(kReadData2, kReadData2Size), + DoInputStreamRead(kReadData2Size)); +} + +// Simulate two packets arriving at once. Read them in two calls separated +// by a Rebuild. +TEST_F(GCMSocketStreamTest, TwoReadsAtOnceWithRebuild) { + std::string long_data = std::string(kReadData, kReadDataSize) + + std::string(kReadData2, kReadData2Size); + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, + long_data.c_str(), + long_data.size())), + WriteList()); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); + + input_stream()->RebuildBuffer(); + WaitForData(kReadData2Size); + ASSERT_EQ(std::string(kReadData2, kReadData2Size), + DoInputStreamRead(kReadData2Size)); +} + +// Simulate a read that is aborted. +TEST_F(GCMSocketStreamTest, ReadError) { + int result = net::ERR_ABORTED; + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, result)), + WriteList()); + + WaitForData(kReadDataSize); + ASSERT_EQ(SocketInputStream::CLOSED, input_stream()->GetState()); + ASSERT_EQ(result, input_stream()->last_error()); +} + +// Simulate a read after the connection is closed. +TEST_F(GCMSocketStreamTest, ReadDisconnected) { + BuildSocket(ReadList(), WriteList()); + socket()->Disconnect(); + WaitForData(kReadDataSize); + ASSERT_EQ(SocketInputStream::CLOSED, input_stream()->GetState()); + ASSERT_EQ(net::ERR_CONNECTION_CLOSED, input_stream()->last_error()); +} + +// Write a full message in one go. +TEST_F(GCMSocketStreamTest, WriteFull) { + BuildSocket(ReadList(), + WriteList(1, net::MockWrite(net::SYNCHRONOUS, + kWriteData, + kWriteDataSize))); + ASSERT_EQ(kWriteDataSize, + DoOutputStreamWrite(base::StringPiece(kWriteData, + kWriteDataSize))); +} + +// Write a message in two go's. +TEST_F(GCMSocketStreamTest, WritePartial) { + WriteList write_list; + write_list.push_back(net::MockWrite(net::SYNCHRONOUS, + kWriteData, + kWriteDataSize / 2)); + write_list.push_back(net::MockWrite(net::SYNCHRONOUS, + kWriteData + kWriteDataSize / 2, + kWriteDataSize / 2)); + BuildSocket(ReadList(), write_list); + ASSERT_EQ(kWriteDataSize, + DoOutputStreamWrite(base::StringPiece(kWriteData, + kWriteDataSize))); +} + +// Write a message completely asynchronously (returns IO_PENDING before +// finishing the write in two go's). +TEST_F(GCMSocketStreamTest, WriteNone) { + WriteList write_list; + write_list.push_back(net::MockWrite(net::SYNCHRONOUS, + kWriteData, + kWriteDataSize / 2)); + write_list.push_back(net::MockWrite(net::SYNCHRONOUS, + kWriteData + kWriteDataSize / 2, + kWriteDataSize / 2)); + BuildSocket(ReadList(), write_list); + ASSERT_EQ(kWriteDataSize, + DoOutputStreamWrite(base::StringPiece(kWriteData, + kWriteDataSize))); +} + +// Write a message then read a message. +TEST_F(GCMSocketStreamTest, WriteThenRead) { + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, + kReadData, + kReadDataSize)), + WriteList(1, net::MockWrite(net::SYNCHRONOUS, + kWriteData, + kWriteDataSize))); + + ASSERT_EQ(kWriteDataSize, + DoOutputStreamWrite(base::StringPiece(kWriteData, + kWriteDataSize))); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); +} + +// Read a message then write a message. +TEST_F(GCMSocketStreamTest, ReadThenWrite) { + BuildSocket(ReadList(1, net::MockRead(net::SYNCHRONOUS, + kReadData, + kReadDataSize)), + WriteList(1, net::MockWrite(net::SYNCHRONOUS, + kWriteData, + kWriteDataSize))); + + WaitForData(kReadDataSize); + ASSERT_EQ(std::string(kReadData, kReadDataSize), + DoInputStreamRead(kReadDataSize)); + + ASSERT_EQ(kWriteDataSize, + DoOutputStreamWrite(base::StringPiece(kWriteData, + kWriteDataSize))); +} + +// Simulate a write that gets aborted. +TEST_F(GCMSocketStreamTest, WriteError) { + int result = net::ERR_ABORTED; + BuildSocket(ReadList(), + WriteList(1, net::MockWrite(net::SYNCHRONOUS, result))); + DoOutputStreamWrite(base::StringPiece(kWriteData, kWriteDataSize)); + ASSERT_EQ(SocketOutputStream::CLOSED, output_stream()->GetState()); + ASSERT_EQ(result, output_stream()->last_error()); +} + +// Simulate a write after the connection is closed. +TEST_F(GCMSocketStreamTest, WriteDisconnected) { + BuildSocket(ReadList(), WriteList()); + socket()->Disconnect(); + DoOutputStreamWrite(base::StringPiece(kWriteData, kWriteDataSize)); + ASSERT_EQ(SocketOutputStream::CLOSED, output_stream()->GetState()); + ASSERT_EQ(net::ERR_CONNECTION_CLOSED, output_stream()->last_error()); +} + +} // namespace +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory.cc b/chromium/google_apis/gcm/engine/connection_factory.cc new file mode 100644 index 00000000000..016e1e2b89c --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_factory.cc @@ -0,0 +1,12 @@ +// Copyright 2013 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 "google_apis/gcm/engine/connection_factory.h" + +namespace gcm { + +ConnectionFactory::ConnectionFactory() {} +ConnectionFactory::~ConnectionFactory() {} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory.h b/chromium/google_apis/gcm/engine/connection_factory.h new file mode 100644 index 00000000000..3cff48299b6 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_factory.h @@ -0,0 +1,64 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_ +#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_ + +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/engine/connection_handler.h" + +namespace mcs_proto { +class LoginRequest; +} + +namespace gcm { + +// Factory for creating a ConnectionHandler and maintaining its connection. +// The factory retains ownership of the ConnectionHandler and will enforce +// backoff policies when attempting connections. +class GCM_EXPORT ConnectionFactory { + public: + typedef base::Callback<void(mcs_proto::LoginRequest* login_request)> + BuildLoginRequestCallback; + + ConnectionFactory(); + virtual ~ConnectionFactory(); + + // Initialize the factory, creating a connection handler with a disconnected + // socket. Should only be called once. + // Upon connection: + // |read_callback| will be invoked with the contents of any received protobuf + // message. + // |write_callback| will be invoked anytime a message has been successfully + // sent. Note: this just means the data was sent to the wire, not that the + // other end received it. + virtual void Initialize( + const BuildLoginRequestCallback& request_builder, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) = 0; + + // Get the connection handler for this factory. Initialize(..) must have + // been called. + virtual ConnectionHandler* GetConnectionHandler() const = 0; + + // Opens a new connection and initiates login handshake. Upon completion of + // the handshake, |read_callback| will be invoked with a valid + // mcs_proto::LoginResponse. + // Note: Initialize must have already been invoked. + virtual void Connect() = 0; + + // Whether or not the MCS endpoint is currently reachable with an active + // connection. + virtual bool IsEndpointReachable() const = 0; + + // If in backoff, the time at which the next retry will be made. Otherwise, + // a null time, indicating either no attempt to connect has been made or no + // backoff is in progress. + virtual base::TimeTicks NextRetryAttempt() const = 0; +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_ diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.cc b/chromium/google_apis/gcm/engine/connection_factory_impl.cc new file mode 100644 index 00000000000..388b9dca5e3 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_factory_impl.cc @@ -0,0 +1,205 @@ +// Copyright (c) 2013 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 "google_apis/gcm/engine/connection_factory_impl.h" + +#include "base/message_loop/message_loop.h" +#include "google_apis/gcm/engine/connection_handler_impl.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/base/net_errors.h" +#include "net/http/http_network_session.h" +#include "net/http/http_request_headers.h" +#include "net/proxy/proxy_info.h" +#include "net/socket/client_socket_handle.h" +#include "net/socket/client_socket_pool_manager.h" +#include "net/ssl/ssl_config_service.h" + +namespace gcm { + +namespace { + +// The amount of time a Socket read should wait before timing out. +const int kReadTimeoutMs = 30000; // 30 seconds. + +// Backoff policy. +const net::BackoffEntry::Policy kConnectionBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + 0, + + // Initial delay for exponential back-off in ms. + 10000, // 10 seconds. + + // Factor by which the waiting time will be multiplied. + 2, + + // Fuzzing percentage. ex: 10% will spread requests randomly + // between 90%-100% of the calculated time. + 0.2, // 20%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 3600 * 4, // 4 hours. + + // Time to keep an entry from being discarded even when it + // has no significant state, -1 to never discard. + -1, + + // Don't use initial delay unless the last request was an error. + false, +}; + +} // namespace + +ConnectionFactoryImpl::ConnectionFactoryImpl( + const GURL& mcs_endpoint, + scoped_refptr<net::HttpNetworkSession> network_session, + net::NetLog* net_log) + : mcs_endpoint_(mcs_endpoint), + network_session_(network_session), + net_log_(net_log), + weak_ptr_factory_(this) { +} + +ConnectionFactoryImpl::~ConnectionFactoryImpl() { +} + +void ConnectionFactoryImpl::Initialize( + const BuildLoginRequestCallback& request_builder, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) { + DCHECK(!connection_handler_); + + backoff_entry_ = CreateBackoffEntry(&kConnectionBackoffPolicy); + request_builder_ = request_builder; + + net::NetworkChangeNotifier::AddIPAddressObserver(this); + net::NetworkChangeNotifier::AddConnectionTypeObserver(this); + connection_handler_.reset( + new ConnectionHandlerImpl( + base::TimeDelta::FromMilliseconds(kReadTimeoutMs), + read_callback, + write_callback, + base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback, + weak_ptr_factory_.GetWeakPtr()))); +} + +ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const { + return connection_handler_.get(); +} + +void ConnectionFactoryImpl::Connect() { + DCHECK(connection_handler_); + DCHECK(!IsEndpointReachable()); + + if (backoff_entry_->ShouldRejectRequest()) { + DVLOG(1) << "Delaying MCS endpoint connection for " + << backoff_entry_->GetTimeUntilRelease().InMilliseconds() + << " milliseconds."; + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&ConnectionFactoryImpl::Connect, + weak_ptr_factory_.GetWeakPtr()), + NextRetryAttempt() - base::TimeTicks::Now()); + return; + } + + DVLOG(1) << "Attempting connection to MCS endpoint."; + ConnectImpl(); +} + +bool ConnectionFactoryImpl::IsEndpointReachable() const { + return connection_handler_ && connection_handler_->CanSendMessage(); +} + +base::TimeTicks ConnectionFactoryImpl::NextRetryAttempt() const { + if (!backoff_entry_) + return base::TimeTicks(); + return backoff_entry_->GetReleaseTime(); +} + +void ConnectionFactoryImpl::OnConnectionTypeChanged( + net::NetworkChangeNotifier::ConnectionType type) { + if (type == net::NetworkChangeNotifier::CONNECTION_NONE) + return; + + // TODO(zea): implement different backoff/retry policies based on connection + // type. + DVLOG(1) << "Connection type changed to " << type << ", resetting backoff."; + backoff_entry_->Reset(); + // Connect(..) should be retrying with backoff already if a connection is + // necessary, so no need to call again. +} + +void ConnectionFactoryImpl::OnIPAddressChanged() { + DVLOG(1) << "IP Address changed, resetting backoff."; + backoff_entry_->Reset(); + // Connect(..) should be retrying with backoff already if a connection is + // necessary, so no need to call again. +} + +void ConnectionFactoryImpl::ConnectImpl() { + DCHECK(!IsEndpointReachable()); + + // TODO(zea): resolve proxies. + net::ProxyInfo proxy_info; + proxy_info.UseDirect(); + net::SSLConfig ssl_config; + network_session_->ssl_config_service()->GetSSLConfig(&ssl_config); + + int status = net::InitSocketHandleForTlsConnect( + net::HostPortPair::FromURL(mcs_endpoint_), + network_session_.get(), + proxy_info, + ssl_config, + ssl_config, + net::kPrivacyModeDisabled, + net::BoundNetLog::Make(net_log_, net::NetLog::SOURCE_SOCKET), + &socket_handle_, + base::Bind(&ConnectionFactoryImpl::OnConnectDone, + weak_ptr_factory_.GetWeakPtr())); + if (status != net::ERR_IO_PENDING) + OnConnectDone(status); +} + +void ConnectionFactoryImpl::InitHandler() { + // May be null in tests. + mcs_proto::LoginRequest login_request; + if (!request_builder_.is_null()) { + request_builder_.Run(&login_request); + DCHECK(login_request.IsInitialized()); + } + + connection_handler_->Init(login_request, socket_handle_.PassSocket()); +} + +scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry( + const net::BackoffEntry::Policy* const policy) { + return scoped_ptr<net::BackoffEntry>(new net::BackoffEntry(policy)); +} + +void ConnectionFactoryImpl::OnConnectDone(int result) { + if (result != net::OK) { + LOG(ERROR) << "Failed to connect to MCS endpoint with error " << result; + backoff_entry_->InformOfRequest(false); + Connect(); + return; + } + + DVLOG(1) << "MCS endpoint connection success."; + + // Reset the backoff. + backoff_entry_->Reset(); + + InitHandler(); +} + +void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) { + // TODO(zea): Consider how to handle errors that may require some sort of + // user intervention (login page, etc.). + LOG(ERROR) << "Connection reset with error " << result; + backoff_entry_->InformOfRequest(false); + Connect(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.h b/chromium/google_apis/gcm/engine/connection_factory_impl.h new file mode 100644 index 00000000000..d807270bfdc --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_factory_impl.h @@ -0,0 +1,101 @@ +// Copyright (c) 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_ +#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_ + +#include "google_apis/gcm/engine/connection_factory.h" + +#include "base/memory/weak_ptr.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/base/backoff_entry.h" +#include "net/base/network_change_notifier.h" +#include "net/socket/client_socket_handle.h" +#include "url/gurl.h" + +namespace net { +class HttpNetworkSession; +class NetLog; +} + +namespace gcm { + +class ConnectionHandlerImpl; + +class GCM_EXPORT ConnectionFactoryImpl : + public ConnectionFactory, + public net::NetworkChangeNotifier::ConnectionTypeObserver, + public net::NetworkChangeNotifier::IPAddressObserver { + public: + ConnectionFactoryImpl( + const GURL& mcs_endpoint, + scoped_refptr<net::HttpNetworkSession> network_session, + net::NetLog* net_log); + virtual ~ConnectionFactoryImpl(); + + // ConnectionFactory implementation. + virtual void Initialize( + const BuildLoginRequestCallback& request_builder, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) OVERRIDE; + virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE; + virtual void Connect() OVERRIDE; + virtual bool IsEndpointReachable() const OVERRIDE; + virtual base::TimeTicks NextRetryAttempt() const OVERRIDE; + + // NetworkChangeNotifier observer implementations. + virtual void OnConnectionTypeChanged( + net::NetworkChangeNotifier::ConnectionType type) OVERRIDE; + virtual void OnIPAddressChanged() OVERRIDE; + + protected: + // Implementation of Connect(..). If not in backoff, uses |login_request_| + // in attempting a connection/handshake. On connection/handshake failure, goes + // into backoff. + // Virtual for testing. + virtual void ConnectImpl(); + + // Helper method for initalizing the connection hander. + // Virtual for testing. + virtual void InitHandler(); + + // Helper method for creating a backoff entry. + // Virtual for testing. + virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry( + const net::BackoffEntry::Policy* const policy); + + // Callback for Socket connection completion. + void OnConnectDone(int result); + + private: + // ConnectionHandler callback for connection issues. + void ConnectionHandlerCallback(int result); + + // The MCS endpoint to make connections to. + const GURL mcs_endpoint_; + + // ---- net:: components for establishing connections. ---- + // Network session for creating new connections. + const scoped_refptr<net::HttpNetworkSession> network_session_; + // Net log to use in connection attempts. + net::NetLog* const net_log_; + // The handle to the socket for the current connection, if one exists. + net::ClientSocketHandle socket_handle_; + // Connection attempt backoff policy. + scoped_ptr<net::BackoffEntry> backoff_entry_; + + // The current connection handler, if one exists. + scoped_ptr<ConnectionHandlerImpl> connection_handler_; + + // Builder for generating new login requests. + BuildLoginRequestCallback request_builder_; + + base::WeakPtrFactory<ConnectionFactoryImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ConnectionFactoryImpl); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_IMPL_H_ diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc new file mode 100644 index 00000000000..1e0ccefef26 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc @@ -0,0 +1,303 @@ +// Copyright (c) 2013 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 "google_apis/gcm/engine/connection_factory_impl.h" + +#include <cmath> + +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/time/time.h" +#include "net/base/backoff_entry.h" +#include "net/http/http_network_session.h" +#include "testing/gtest/include/gtest/gtest.h" + +class Policy; + +namespace gcm { +namespace { + +const char kMCSEndpoint[] = "http://my.server"; + +const int kBackoffDelayMs = 1; +const int kBackoffMultiplier = 2; + +// A backoff policy with small enough delays that tests aren't burdened. +const net::BackoffEntry::Policy kTestBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + 0, + + // Initial delay for exponential back-off in ms. + kBackoffDelayMs, + + // Factor by which the waiting time will be multiplied. + kBackoffMultiplier, + + // Fuzzing percentage. ex: 10% will spread requests randomly + // between 90%-100% of the calculated time. + 0, + + // Maximum amount of time we are willing to delay our request in ms. + 10, + + // Time to keep an entry from being discarded even when it + // has no significant state, -1 to never discard. + -1, + + // Don't use initial delay unless the last request was an error. + false, +}; + +// Helper for calculating total expected exponential backoff delay given an +// arbitrary number of failed attempts. See BackoffEntry::CalculateReleaseTime. +double CalculateBackoff(int num_attempts) { + double delay = kBackoffDelayMs; + for (int i = 1; i < num_attempts; ++i) { + delay += kBackoffDelayMs * pow(static_cast<double>(kBackoffMultiplier), + i - 1); + } + DVLOG(1) << "Expected backoff " << delay << " milliseconds."; + return delay; +} + +// Helper methods that should never actually be called due to real connections +// being stubbed out. +void ReadContinuation( + scoped_ptr<google::protobuf::MessageLite> message) { + ADD_FAILURE(); +} + +void WriteContinuation() { + ADD_FAILURE(); +} + +// A connection factory that stubs out network requests and overrides the +// backoff policy. +class TestConnectionFactoryImpl : public ConnectionFactoryImpl { + public: + TestConnectionFactoryImpl(const base::Closure& finished_callback); + virtual ~TestConnectionFactoryImpl(); + + // Overridden stubs. + virtual void ConnectImpl() OVERRIDE; + virtual void InitHandler() OVERRIDE; + virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry( + const net::BackoffEntry::Policy* const policy) OVERRIDE; + + // Helpers for verifying connection attempts are made. Connection results + // must be consumed. + void SetConnectResult(int connect_result); + void SetMultipleConnectResults(int connect_result, int num_expected_attempts); + + private: + // The result to return on the next connect attempt. + int connect_result_; + // The number of expected connection attempts; + int num_expected_attempts_; + // Whether all expected connection attempts have been fulfilled since an + // expectation was last set. + bool connections_fulfilled_; + // Callback to invoke when all connection attempts have been made. + base::Closure finished_callback_; +}; + +TestConnectionFactoryImpl::TestConnectionFactoryImpl( + const base::Closure& finished_callback) + : ConnectionFactoryImpl(GURL(kMCSEndpoint), NULL, NULL), + connect_result_(net::ERR_UNEXPECTED), + num_expected_attempts_(0), + connections_fulfilled_(true), + finished_callback_(finished_callback) { +} + +TestConnectionFactoryImpl::~TestConnectionFactoryImpl() { + EXPECT_EQ(0, num_expected_attempts_); +} + +void TestConnectionFactoryImpl::ConnectImpl() { + ASSERT_GT(num_expected_attempts_, 0); + + OnConnectDone(connect_result_); + --num_expected_attempts_; + if (num_expected_attempts_ == 0) { + connect_result_ = net::ERR_UNEXPECTED; + connections_fulfilled_ = true; + finished_callback_.Run(); + } +} + +void TestConnectionFactoryImpl::InitHandler() { + EXPECT_NE(connect_result_, net::ERR_UNEXPECTED); +} + +scoped_ptr<net::BackoffEntry> TestConnectionFactoryImpl::CreateBackoffEntry( + const net::BackoffEntry::Policy* const policy) { + return scoped_ptr<net::BackoffEntry>( + new net::BackoffEntry(&kTestBackoffPolicy)); +} + +void TestConnectionFactoryImpl::SetConnectResult(int connect_result) { + DCHECK_NE(connect_result, net::ERR_UNEXPECTED); + ASSERT_EQ(0, num_expected_attempts_); + connections_fulfilled_ = false; + connect_result_ = connect_result; + num_expected_attempts_ = 1; +} + +void TestConnectionFactoryImpl::SetMultipleConnectResults( + int connect_result, + int num_expected_attempts) { + DCHECK_NE(connect_result, net::ERR_UNEXPECTED); + DCHECK_GT(num_expected_attempts, 0); + ASSERT_EQ(0, num_expected_attempts_); + connections_fulfilled_ = false; + connect_result_ = connect_result; + num_expected_attempts_ = num_expected_attempts; +} + +class ConnectionFactoryImplTest : public testing::Test { + public: + ConnectionFactoryImplTest(); + virtual ~ConnectionFactoryImplTest(); + + TestConnectionFactoryImpl* factory() { return &factory_; } + + void WaitForConnections(); + + private: + void ConnectionsComplete(); + + TestConnectionFactoryImpl factory_; + base::MessageLoop message_loop_; + scoped_ptr<base::RunLoop> run_loop_; +}; + +ConnectionFactoryImplTest::ConnectionFactoryImplTest() + : factory_(base::Bind(&ConnectionFactoryImplTest::ConnectionsComplete, + base::Unretained(this))), + run_loop_(new base::RunLoop()) {} +ConnectionFactoryImplTest::~ConnectionFactoryImplTest() {} + +void ConnectionFactoryImplTest::WaitForConnections() { + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + +void ConnectionFactoryImplTest::ConnectionsComplete() { + if (!run_loop_) + return; + run_loop_->Quit(); +} + +// Verify building a connection handler works. +TEST_F(ConnectionFactoryImplTest, Initialize) { + EXPECT_FALSE(factory()->IsEndpointReachable()); + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + base::Bind(&ReadContinuation), + base::Bind(&WriteContinuation)); + ConnectionHandler* handler = factory()->GetConnectionHandler(); + ASSERT_TRUE(handler); + EXPECT_FALSE(factory()->IsEndpointReachable()); +} + +// An initial successful connection should not result in backoff. +TEST_F(ConnectionFactoryImplTest, ConnectSuccess) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + factory()->SetConnectResult(net::OK); + factory()->Connect(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); +} + +// A connection failure should result in backoff. +TEST_F(ConnectionFactoryImplTest, ConnectFail) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + factory()->Connect(); + EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); +} + +// A connection success after a failure should reset backoff. +TEST_F(ConnectionFactoryImplTest, FailThenSucceed) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + base::TimeTicks connect_time = base::TimeTicks::Now(); + factory()->Connect(); + WaitForConnections(); + base::TimeTicks retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); + EXPECT_GE((retry_time - connect_time).InMilliseconds(), CalculateBackoff(1)); + factory()->SetConnectResult(net::OK); + WaitForConnections(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); +} + +// Multiple connection failures should retry with an exponentially increasing +// backoff, then reset on success. +TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + + const int kNumAttempts = 5; + factory()->SetMultipleConnectResults(net::ERR_CONNECTION_FAILED, + kNumAttempts); + + base::TimeTicks connect_time = base::TimeTicks::Now(); + factory()->Connect(); + WaitForConnections(); + base::TimeTicks retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); + EXPECT_GE((retry_time - connect_time).InMilliseconds(), + CalculateBackoff(kNumAttempts)); + + factory()->SetConnectResult(net::OK); + WaitForConnections(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); +} + +// IP events should reset backoff. +TEST_F(ConnectionFactoryImplTest, FailThenIPEvent) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + factory()->Connect(); + WaitForConnections(); + EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); + + factory()->OnIPAddressChanged(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); +} + +// Connection type events should reset backoff. +TEST_F(ConnectionFactoryImplTest, FailThenConnectionTypeEvent) { + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + factory()->Connect(); + WaitForConnections(); + EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); + + factory()->OnConnectionTypeChanged( + net::NetworkChangeNotifier::CONNECTION_WIFI); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); +} + +} // namespace +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_handler.cc b/chromium/google_apis/gcm/engine/connection_handler.cc new file mode 100644 index 00000000000..bc9b6585979 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_handler.cc @@ -0,0 +1,15 @@ +// Copyright 2013 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 "google_apis/gcm/engine/connection_handler.h" + +namespace gcm { + +ConnectionHandler::ConnectionHandler() { +} + +ConnectionHandler::~ConnectionHandler() { +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_handler.h b/chromium/google_apis/gcm/engine/connection_handler.h new file mode 100644 index 00000000000..5b9ea715c78 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_handler.h @@ -0,0 +1,63 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_ +#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_ + +#include "base/callback.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace net{ +class StreamSocket; +} // namespace net + +namespace google { +namespace protobuf { +class MessageLite; +} // namespace protobuf +} // namepsace google + +namespace mcs_proto { +class LoginRequest; +} + +namespace gcm { + +class SocketInputStream; +class SocketOutputStream; + +// Handles performing the protocol handshake and sending/receiving protobuf +// messages. Note that no retrying or queueing is enforced at this layer. +// Once a connection error is encountered, the ConnectionHandler will disconnect +// the socket and must be reinitialized with a new StreamSocket before +// messages can be sent/received again. +class GCM_EXPORT ConnectionHandler { + public: + typedef base::Callback<void(scoped_ptr<google::protobuf::MessageLite>)> + ProtoReceivedCallback; + typedef base::Closure ProtoSentCallback; + typedef base::Callback<void(int)> ConnectionChangedCallback; + + ConnectionHandler(); + virtual ~ConnectionHandler(); + + // Starts a new MCS connection handshake (using |login_request|) and, upon + // success, begins listening for incoming/outgoing messages. + // + // Note: It is correct and expected to call Init more than once, as connection + // issues are encountered and new connections must be made. + virtual void Init(const mcs_proto::LoginRequest& login_request, + scoped_ptr<net::StreamSocket> socket) = 0; + + // Checks that a handshake has been completed and a message is not already + // in flight. + virtual bool CanSendMessage() const = 0; + + // Send an MCS protobuf message. CanSendMessage() must be true. + virtual void SendMessage(const google::protobuf::MessageLite& message) = 0; +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_H_ diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.cc b/chromium/google_apis/gcm/engine/connection_handler_impl.cc new file mode 100644 index 00000000000..aff0dfd3651 --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_handler_impl.cc @@ -0,0 +1,404 @@ +// Copyright 2013 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 "google_apis/gcm/engine/connection_handler_impl.h" + +#include "base/message_loop/message_loop.h" +#include "google/protobuf/io/coded_stream.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/base/socket_stream.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/base/net_errors.h" +#include "net/socket/stream_socket.h" + +using namespace google::protobuf::io; + +namespace gcm { + +namespace { + +// # of bytes a MCS version packet consumes. +const int kVersionPacketLen = 1; +// # of bytes a tag packet consumes. +const int kTagPacketLen = 1; +// Max # of bytes a length packet consumes. +const int kSizePacketLenMin = 1; +const int kSizePacketLenMax = 2; + +// The current MCS protocol version. +// TODO(zea): bump to 41 once the server supports it. +const int kMCSVersion = 38; + +} // namespace + +ConnectionHandlerImpl::ConnectionHandlerImpl( + base::TimeDelta read_timeout, + const ProtoReceivedCallback& read_callback, + const ProtoSentCallback& write_callback, + const ConnectionChangedCallback& connection_callback) + : read_timeout_(read_timeout), + handshake_complete_(false), + message_tag_(0), + message_size_(0), + read_callback_(read_callback), + write_callback_(write_callback), + connection_callback_(connection_callback), + weak_ptr_factory_(this) { +} + +ConnectionHandlerImpl::~ConnectionHandlerImpl() { +} + +void ConnectionHandlerImpl::Init( + const mcs_proto::LoginRequest& login_request, + scoped_ptr<net::StreamSocket> socket) { + DCHECK(!read_callback_.is_null()); + DCHECK(!write_callback_.is_null()); + DCHECK(!connection_callback_.is_null()); + + // Invalidate any previously outstanding reads. + weak_ptr_factory_.InvalidateWeakPtrs(); + + handshake_complete_ = false; + message_tag_ = 0; + message_size_ = 0; + socket_ = socket.Pass(); + input_stream_.reset(new SocketInputStream(socket_.get())); + output_stream_.reset(new SocketOutputStream(socket_.get())); + + Login(login_request); +} + +bool ConnectionHandlerImpl::CanSendMessage() const { + return handshake_complete_ && output_stream_.get() && + output_stream_->GetState() == SocketOutputStream::EMPTY; +} + +void ConnectionHandlerImpl::SendMessage( + const google::protobuf::MessageLite& message) { + DCHECK_EQ(output_stream_->GetState(), SocketOutputStream::EMPTY); + DCHECK(handshake_complete_); + + { + CodedOutputStream coded_output_stream(output_stream_.get()); + DVLOG(1) << "Writing proto of size " << message.ByteSize(); + int tag = GetMCSProtoTag(message); + DCHECK_NE(tag, -1); + coded_output_stream.WriteRaw(&tag, 1); + coded_output_stream.WriteVarint32(message.ByteSize()); + message.SerializeToCodedStream(&coded_output_stream); + } + + if (output_stream_->Flush( + base::Bind(&ConnectionHandlerImpl::OnMessageSent, + weak_ptr_factory_.GetWeakPtr())) != net::ERR_IO_PENDING) { + OnMessageSent(); + } +} + +void ConnectionHandlerImpl::Login( + const google::protobuf::MessageLite& login_request) { + DCHECK_EQ(output_stream_->GetState(), SocketOutputStream::EMPTY); + + const char version_byte[1] = {kMCSVersion}; + const char login_request_tag[1] = {kLoginRequestTag}; + { + CodedOutputStream coded_output_stream(output_stream_.get()); + coded_output_stream.WriteRaw(version_byte, 1); + coded_output_stream.WriteRaw(login_request_tag, 1); + coded_output_stream.WriteVarint32(login_request.ByteSize()); + login_request.SerializeToCodedStream(&coded_output_stream); + } + + if (output_stream_->Flush( + base::Bind(&ConnectionHandlerImpl::OnMessageSent, + weak_ptr_factory_.GetWeakPtr())) != net::ERR_IO_PENDING) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ConnectionHandlerImpl::OnMessageSent, + weak_ptr_factory_.GetWeakPtr())); + } + + read_timeout_timer_.Start(FROM_HERE, + read_timeout_, + base::Bind(&ConnectionHandlerImpl::OnTimeout, + weak_ptr_factory_.GetWeakPtr())); + WaitForData(MCS_VERSION_TAG_AND_SIZE); +} + +void ConnectionHandlerImpl::OnMessageSent() { + if (!output_stream_.get()) { + // The connection has already been closed. Just return. + DCHECK(!input_stream_.get()); + DCHECK(!read_timeout_timer_.IsRunning()); + return; + } + + if (output_stream_->GetState() != SocketOutputStream::EMPTY) { + int last_error = output_stream_->last_error(); + CloseConnection(); + // If the socket stream had an error, plumb it up, else plumb up FAILED. + if (last_error == net::OK) + last_error = net::ERR_FAILED; + connection_callback_.Run(last_error); + return; + } + + write_callback_.Run(); +} + +void ConnectionHandlerImpl::GetNextMessage() { + DCHECK(SocketInputStream::EMPTY == input_stream_->GetState() || + SocketInputStream::READY == input_stream_->GetState()); + message_tag_ = 0; + message_size_ = 0; + + WaitForData(MCS_TAG_AND_SIZE); +} + +void ConnectionHandlerImpl::WaitForData(ProcessingState state) { + DVLOG(1) << "Waiting for MCS data: state == " << state; + + if (!input_stream_) { + // The connection has already been closed. Just return. + DCHECK(!output_stream_.get()); + DCHECK(!read_timeout_timer_.IsRunning()); + return; + } + + if (input_stream_->GetState() != SocketInputStream::EMPTY && + input_stream_->GetState() != SocketInputStream::READY) { + // An error occurred. + int last_error = output_stream_->last_error(); + CloseConnection(); + // If the socket stream had an error, plumb it up, else plumb up FAILED. + if (last_error == net::OK) + last_error = net::ERR_FAILED; + connection_callback_.Run(last_error); + return; + } + + // Used to determine whether a Socket::Read is necessary. + int min_bytes_needed = 0; + // Used to limit the size of the Socket::Read. + int max_bytes_needed = 0; + + switch(state) { + case MCS_VERSION_TAG_AND_SIZE: + min_bytes_needed = kVersionPacketLen + kTagPacketLen + kSizePacketLenMin; + max_bytes_needed = kVersionPacketLen + kTagPacketLen + kSizePacketLenMax; + break; + case MCS_TAG_AND_SIZE: + min_bytes_needed = kTagPacketLen + kSizePacketLenMin; + max_bytes_needed = kTagPacketLen + kSizePacketLenMax; + break; + case MCS_FULL_SIZE: + // If in this state, the minimum size packet length must already have been + // insufficient, so set both to the max length. + min_bytes_needed = kSizePacketLenMax; + max_bytes_needed = kSizePacketLenMax; + break; + case MCS_PROTO_BYTES: + read_timeout_timer_.Reset(); + // No variability in the message size, set both to the same. + min_bytes_needed = message_size_; + max_bytes_needed = message_size_; + break; + default: + NOTREACHED(); + } + DCHECK_GE(max_bytes_needed, min_bytes_needed); + + int byte_count = input_stream_->UnreadByteCount(); + if (min_bytes_needed - byte_count > 0 && + input_stream_->Refresh( + base::Bind(&ConnectionHandlerImpl::WaitForData, + weak_ptr_factory_.GetWeakPtr(), + state), + max_bytes_needed - byte_count) == net::ERR_IO_PENDING) { + return; + } + + // Check for refresh errors. + if (input_stream_->GetState() != SocketInputStream::READY) { + // An error occurred. + int last_error = output_stream_->last_error(); + CloseConnection(); + // If the socket stream had an error, plumb it up, else plumb up FAILED. + if (last_error == net::OK) + last_error = net::ERR_FAILED; + connection_callback_.Run(last_error); + return; + } + + // Received enough bytes, process them. + DVLOG(1) << "Processing MCS data: state == " << state; + switch(state) { + case MCS_VERSION_TAG_AND_SIZE: + OnGotVersion(); + break; + case MCS_TAG_AND_SIZE: + OnGotMessageTag(); + break; + case MCS_FULL_SIZE: + OnGotMessageSize(); + break; + case MCS_PROTO_BYTES: + OnGotMessageBytes(); + break; + default: + NOTREACHED(); + } +} + +void ConnectionHandlerImpl::OnGotVersion() { + uint8 version = 0; + { + CodedInputStream coded_input_stream(input_stream_.get()); + coded_input_stream.ReadRaw(&version, 1); + } + if (version < kMCSVersion) { + LOG(ERROR) << "Invalid GCM version response: " << static_cast<int>(version); + connection_callback_.Run(net::ERR_FAILED); + return; + } + + input_stream_->RebuildBuffer(); + + // Process the LoginResponse message tag. + OnGotMessageTag(); +} + +void ConnectionHandlerImpl::OnGotMessageTag() { + if (input_stream_->GetState() != SocketInputStream::READY) { + LOG(ERROR) << "Failed to receive protobuf tag."; + read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>()); + return; + } + + { + CodedInputStream coded_input_stream(input_stream_.get()); + coded_input_stream.ReadRaw(&message_tag_, 1); + } + + DVLOG(1) << "Received proto of type " + << static_cast<unsigned int>(message_tag_); + + if (!read_timeout_timer_.IsRunning()) { + read_timeout_timer_.Start(FROM_HERE, + read_timeout_, + base::Bind(&ConnectionHandlerImpl::OnTimeout, + weak_ptr_factory_.GetWeakPtr())); + } + OnGotMessageSize(); +} + +void ConnectionHandlerImpl::OnGotMessageSize() { + if (input_stream_->GetState() != SocketInputStream::READY) { + LOG(ERROR) << "Failed to receive message size."; + read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>()); + return; + } + + bool need_another_byte = false; + int prev_byte_count = input_stream_->ByteCount(); + { + CodedInputStream coded_input_stream(input_stream_.get()); + if (!coded_input_stream.ReadVarint32(&message_size_)) + need_another_byte = true; + } + + if (need_another_byte) { + DVLOG(1) << "Expecting another message size byte."; + if (prev_byte_count >= kSizePacketLenMax) { + // Already had enough bytes, something else went wrong. + LOG(ERROR) << "Failed to process message size."; + read_callback_.Run(scoped_ptr<google::protobuf::MessageLite>()); + return; + } + // Back up by the amount read (should always be 1 byte). + int bytes_read = prev_byte_count - input_stream_->ByteCount(); + DCHECK_EQ(bytes_read, 1); + input_stream_->BackUp(bytes_read); + WaitForData(MCS_FULL_SIZE); + return; + } + + DVLOG(1) << "Proto size: " << message_size_; + + if (message_size_ > 0) + WaitForData(MCS_PROTO_BYTES); + else + OnGotMessageBytes(); +} + +void ConnectionHandlerImpl::OnGotMessageBytes() { + read_timeout_timer_.Stop(); + scoped_ptr<google::protobuf::MessageLite> protobuf( + BuildProtobufFromTag(message_tag_)); + // Messages with no content are valid; just use the default protobuf for + // that tag. + if (protobuf.get() && message_size_ == 0) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ConnectionHandlerImpl::GetNextMessage, + weak_ptr_factory_.GetWeakPtr())); + read_callback_.Run(protobuf.Pass()); + return; + } + + if (!protobuf.get() || + input_stream_->GetState() != SocketInputStream::READY) { + LOG(ERROR) << "Failed to extract protobuf bytes of type " + << static_cast<unsigned int>(message_tag_); + protobuf.reset(); // Return a null pointer to denote an error. + read_callback_.Run(protobuf.Pass()); + return; + } + + { + CodedInputStream coded_input_stream(input_stream_.get()); + if (!protobuf->ParsePartialFromCodedStream(&coded_input_stream)) { + NOTREACHED() << "Unable to parse GCM message of type " + << static_cast<unsigned int>(message_tag_); + protobuf.reset(); // Return a null pointer to denote an error. + read_callback_.Run(protobuf.Pass()); + return; + } + } + + input_stream_->RebuildBuffer(); + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ConnectionHandlerImpl::GetNextMessage, + weak_ptr_factory_.GetWeakPtr())); + if (message_tag_ == kLoginResponseTag) { + if (handshake_complete_) { + LOG(ERROR) << "Unexpected login response."; + } else { + handshake_complete_ = true; + DVLOG(1) << "GCM Handshake complete."; + } + } + read_callback_.Run(protobuf.Pass()); +} + +void ConnectionHandlerImpl::OnTimeout() { + LOG(ERROR) << "Timed out waiting for GCM Protocol buffer."; + CloseConnection(); + connection_callback_.Run(net::ERR_TIMED_OUT); +} + +void ConnectionHandlerImpl::CloseConnection() { + DVLOG(1) << "Closing connection."; + read_callback_.Reset(); + write_callback_.Reset(); + read_timeout_timer_.Stop(); + socket_->Disconnect(); + input_stream_.reset(); + output_stream_.reset(); + weak_ptr_factory_.InvalidateWeakPtrs(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.h b/chromium/google_apis/gcm/engine/connection_handler_impl.h new file mode 100644 index 00000000000..110cdcdddda --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_handler_impl.h @@ -0,0 +1,122 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_ +#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_ + +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "google_apis/gcm/engine/connection_handler.h" + +namespace mcs_proto { +class LoginRequest; +} // namespace mcs_proto + +namespace gcm { + +class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler { + public: + // |read_callback| will be invoked with the contents of any received protobuf + // message. + // |write_callback| will be invoked anytime a message has been successfully + // sent. Note: this just means the data was sent to the wire, not that the + // other end received it. + // |connection_callback| will be invoked with any fatal read/write errors + // encountered. + ConnectionHandlerImpl( + base::TimeDelta read_timeout, + const ProtoReceivedCallback& read_callback, + const ProtoSentCallback& write_callback, + const ConnectionChangedCallback& connection_callback); + virtual ~ConnectionHandlerImpl(); + + // ConnectionHandler implementation. + virtual void Init(const mcs_proto::LoginRequest& login_request, + scoped_ptr<net::StreamSocket> socket) OVERRIDE; + virtual bool CanSendMessage() const OVERRIDE; + virtual void SendMessage(const google::protobuf::MessageLite& message) + OVERRIDE; + + private: + // State machine for handling incoming data. See WaitForData(..) for usage. + enum ProcessingState { + // Processing the version, tag, and size packets (assuming minimum length + // size packet). Only used during the login handshake. + MCS_VERSION_TAG_AND_SIZE = 0, + // Processing the tag and size packets (assuming minimum length size + // packet). Used for normal messages. + MCS_TAG_AND_SIZE, + // Processing a maximum length size packet (for messages with length > 128). + // Used when a normal size packet was not sufficient to read the message + // size. + MCS_FULL_SIZE, + // Processing the protocol buffer bytes (for those messages with non-zero + // sizes). + MCS_PROTO_BYTES + }; + + // Sends the protocol version and login request. First step in the MCS + // connection handshake. + void Login(const google::protobuf::MessageLite& login_request); + + // SendMessage continuation. Invoked when Socket::Write completes. + void OnMessageSent(); + + // Starts the message processing process, which is comprised of the tag, + // message size, and bytes packet types. + void GetNextMessage(); + + // Performs any necessary SocketInputStream refreshing until the data + // associated with |packet_type| is fully ready, then calls the appropriate + // OnGot* message to process the packet data. If the read times out, + // will close the stream and invoke the connection callback. + void WaitForData(ProcessingState state); + + // Incoming data helper methods. + void OnGotVersion(); + void OnGotMessageTag(); + void OnGotMessageSize(); + void OnGotMessageBytes(); + + // Timeout handler. + void OnTimeout(); + + // Closes the current connection. + void CloseConnection(); + + // Timeout policy: the timeout is only enforced while waiting on the + // handshake (version and/or LoginResponse) or once at least a tag packet has + // been received. It is reset every time new data is received, and is + // only stopped when a full message is processed. + // TODO(zea): consider enforcing a separate timeout when waiting for + // a message to send. + const base::TimeDelta read_timeout_; + base::OneShotTimer<ConnectionHandlerImpl> read_timeout_timer_; + + // This connection's socket and the input/output streams attached to it. + scoped_ptr<net::StreamSocket> socket_; + scoped_ptr<SocketInputStream> input_stream_; + scoped_ptr<SocketOutputStream> output_stream_; + + // Whether the MCS login handshake has successfully completed. See Init(..) + // description for more info on what the handshake involves. + bool handshake_complete_; + + // State for the message currently being processed, if there is one. + uint8 message_tag_; + uint32 message_size_; + + ProtoReceivedCallback read_callback_; + ProtoSentCallback write_callback_; + ConnectionChangedCallback connection_callback_; + + base::WeakPtrFactory<ConnectionHandlerImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ConnectionHandlerImpl); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_CONNECTION_HANDLER_IMPL_H_ diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc new file mode 100644 index 00000000000..0cdcdc621ff --- /dev/null +++ b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc @@ -0,0 +1,628 @@ +// Copyright 2013 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 "google_apis/gcm/engine/connection_handler_impl.h" + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/test/test_timeouts.h" +#include "google/protobuf/io/coded_stream.h" +#include "google/protobuf/io/zero_copy_stream_impl_lite.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/base/socket_stream.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/socket/socket_test_util.h" +#include "net/socket/stream_socket.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { +namespace { + +typedef scoped_ptr<google::protobuf::MessageLite> ScopedMessage; +typedef std::vector<net::MockRead> ReadList; +typedef std::vector<net::MockWrite> WriteList; + +const uint64 kAuthId = 54321; +const uint64 kAuthToken = 12345; +const char kMCSVersion = 38; // The protocol version. +const int kMCSPort = 5228; // The server port. +const char kDataMsgFrom[] = "data_from"; +const char kDataMsgCategory[] = "data_category"; +const char kDataMsgFrom2[] = "data_from2"; +const char kDataMsgCategory2[] = "data_category2"; +const char kDataMsgFromLong[] = + "this is a long from that will result in a message > 128 bytes"; +const char kDataMsgCategoryLong[] = + "this is a long category that will result in a message > 128 bytes"; +const char kDataMsgFromLong2[] = + "this is a second long from that will result in a message > 128 bytes"; +const char kDataMsgCategoryLong2[] = + "this is a second long category that will result in a message > 128 bytes"; + +// ---- Helpers for building messages. ---- + +// Encode a protobuf packet with protobuf type |tag| and serialized protobuf +// bytes |proto| into the MCS message form (tag + varint size + bytes). +std::string EncodePacket(uint8 tag, const std::string& proto) { + std::string result; + google::protobuf::io::StringOutputStream string_output_stream(&result); + google::protobuf::io::CodedOutputStream coded_output_stream( + &string_output_stream); + const unsigned char tag_byte[1] = {tag}; + coded_output_stream.WriteRaw(tag_byte, 1); + coded_output_stream.WriteVarint32(proto.size()); + coded_output_stream.WriteRaw(proto.c_str(), proto.size()); + return result; +} + +// Encode a handshake request into the MCS message form. +std::string EncodeHandshakeRequest() { + std::string result; + const char version_byte[1] = {kMCSVersion}; + result.append(version_byte, 1); + ScopedMessage login_request(BuildLoginRequest(kAuthId, kAuthToken)); + result.append(EncodePacket(kLoginRequestTag, + login_request->SerializeAsString())); + return result; +} + +// Build a serialized login response protobuf. +std::string BuildLoginResponse() { + std::string result; + mcs_proto::LoginResponse login_response; + login_response.set_id("id"); + result.append(login_response.SerializeAsString()); + return result; +} + +// Encoode a handshake response into the MCS message form. +std::string EncodeHandshakeResponse() { + std::string result; + const char version_byte[1] = {kMCSVersion}; + result.append(version_byte, 1); + result.append(EncodePacket(kLoginResponseTag, BuildLoginResponse())); + return result; +} + +// Build a serialized data message stanza protobuf. +std::string BuildDataMessage(const std::string& from, + const std::string& category) { + std::string result; + mcs_proto::DataMessageStanza data_message; + data_message.set_from(from); + data_message.set_category(category); + return data_message.SerializeAsString(); +} + +class GCMConnectionHandlerImplTest : public testing::Test { + public: + GCMConnectionHandlerImplTest(); + virtual ~GCMConnectionHandlerImplTest(); + + net::StreamSocket* BuildSocket(const ReadList& read_list, + const WriteList& write_list); + + // Pump |message_loop_|, resetting |run_loop_| after completion. + void PumpLoop(); + + ConnectionHandlerImpl* connection_handler() { + return connection_handler_.get(); + } + base::MessageLoop* message_loop() { return &message_loop_; }; + net::DelayedSocketData* data_provider() { return data_provider_.get(); } + int last_error() const { return last_error_; } + + // Initialize the connection handler, setting |dst_proto| as the destination + // for any received messages. + void Connect(ScopedMessage* dst_proto); + + // Runs the message loop until a message is received. + void WaitForMessage(); + + private: + void ReadContinuation(ScopedMessage* dst_proto, ScopedMessage new_proto); + void WriteContinuation(); + void ConnectionContinuation(int error); + + // SocketStreams and their data provider. + ReadList mock_reads_; + WriteList mock_writes_; + scoped_ptr<net::DelayedSocketData> data_provider_; + scoped_ptr<SocketInputStream> socket_input_stream_; + scoped_ptr<SocketOutputStream> socket_output_stream_; + + // The connection handler being tested. + scoped_ptr<ConnectionHandlerImpl> connection_handler_; + + // The last connection error received. + int last_error_; + + // net:: components. + scoped_ptr<net::StreamSocket> socket_; + net::MockClientSocketFactory socket_factory_; + net::AddressList address_list_; + + base::MessageLoopForIO message_loop_; + scoped_ptr<base::RunLoop> run_loop_; +}; + +GCMConnectionHandlerImplTest::GCMConnectionHandlerImplTest() + : last_error_(0) { + net::IPAddressNumber ip_number; + net::ParseIPLiteralToNumber("127.0.0.1", &ip_number); + address_list_ = net::AddressList::CreateFromIPAddress(ip_number, kMCSPort); +} + +GCMConnectionHandlerImplTest::~GCMConnectionHandlerImplTest() { +} + +net::StreamSocket* GCMConnectionHandlerImplTest::BuildSocket( + const ReadList& read_list, + const WriteList& write_list) { + mock_reads_ = read_list; + mock_writes_ = write_list; + data_provider_.reset( + new net::DelayedSocketData(0, + &(mock_reads_[0]), mock_reads_.size(), + &(mock_writes_[0]), mock_writes_.size())); + socket_factory_.AddSocketDataProvider(data_provider_.get()); + + socket_ = socket_factory_.CreateTransportClientSocket( + address_list_, NULL, net::NetLog::Source()); + socket_->Connect(net::CompletionCallback()); + + run_loop_.reset(new base::RunLoop()); + PumpLoop(); + + DCHECK(socket_->IsConnected()); + return socket_.get(); +} + +void GCMConnectionHandlerImplTest::PumpLoop() { + run_loop_->RunUntilIdle(); + run_loop_.reset(new base::RunLoop()); +} + +void GCMConnectionHandlerImplTest::Connect( + ScopedMessage* dst_proto) { + connection_handler_.reset(new ConnectionHandlerImpl( + TestTimeouts::tiny_timeout(), + base::Bind(&GCMConnectionHandlerImplTest::ReadContinuation, + base::Unretained(this), + dst_proto), + base::Bind(&GCMConnectionHandlerImplTest::WriteContinuation, + base::Unretained(this)), + base::Bind(&GCMConnectionHandlerImplTest::ConnectionContinuation, + base::Unretained(this)))); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + connection_handler_->Init(*BuildLoginRequest(kAuthId, kAuthToken), + socket_.Pass()); +} + +void GCMConnectionHandlerImplTest::ReadContinuation( + ScopedMessage* dst_proto, + ScopedMessage new_proto) { + *dst_proto = new_proto.Pass(); + run_loop_->Quit(); +} + +void GCMConnectionHandlerImplTest::WaitForMessage() { + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + +void GCMConnectionHandlerImplTest::WriteContinuation() { + run_loop_->Quit(); +} + +void GCMConnectionHandlerImplTest::ConnectionContinuation(int error) { + last_error_ = error; + run_loop_->Quit(); +} + +// Initialize the connection handler and ensure the handshake completes +// successfully. +TEST_F(GCMConnectionHandlerImplTest, Init) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + ReadList read_list(1, net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(BuildLoginResponse(), received_message->SerializeAsString()); + EXPECT_TRUE(connection_handler()->CanSendMessage()); +} + +// Simulate the handshake response returning an older version. Initialization +// should fail. +TEST_F(GCMConnectionHandlerImplTest, InitFailedVersionCheck) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + // Overwrite the version byte. + handshake_response[0] = 37; + ReadList read_list(1, net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. Should result in a connection error. + EXPECT_FALSE(received_message.get()); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + EXPECT_EQ(net::ERR_FAILED, last_error()); +} + +// Attempt to initialize, but receive no server response, resulting in a time +// out. +TEST_F(GCMConnectionHandlerImplTest, InitTimeout) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + ReadList read_list(1, net::MockRead(net::SYNCHRONOUS, + net::ERR_IO_PENDING)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. Should result in a connection error. + EXPECT_FALSE(received_message.get()); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + EXPECT_EQ(net::ERR_TIMED_OUT, last_error()); +} + +// Attempt to initialize, but receive an incomplete server response, resulting +// in a time out. +TEST_F(GCMConnectionHandlerImplTest, InitIncompleteTimeout) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size() / 2)); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + net::ERR_IO_PENDING)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. Should result in a connection error. + EXPECT_FALSE(received_message.get()); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + EXPECT_EQ(net::ERR_TIMED_OUT, last_error()); +} + +// Reinitialize the connection handler after failing to initialize. +TEST_F(GCMConnectionHandlerImplTest, ReInit) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + ReadList read_list(1, net::MockRead(net::SYNCHRONOUS, + net::ERR_IO_PENDING)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. Should result in a connection error. + EXPECT_FALSE(received_message.get()); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + EXPECT_EQ(net::ERR_TIMED_OUT, last_error()); + + // Build a new socket and reconnect, successfully this time. + std::string handshake_response = EncodeHandshakeResponse(); + read_list[0] = net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size()); + BuildSocket(read_list, write_list); + Connect(&received_message); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(BuildLoginResponse(), received_message->SerializeAsString()); + EXPECT_TRUE(connection_handler()->CanSendMessage()); +} + +// Verify that messages can be received after initialization. +TEST_F(GCMConnectionHandlerImplTest, RecvMsg) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = BuildDataMessage(kDataMsgFrom, + kDataMsgCategory); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + WaitForMessage(); // The data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); +} + +// Verify that if two messages arrive at once, they're treated appropriately. +TEST_F(GCMConnectionHandlerImplTest, Recv2Msgs) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = BuildDataMessage(kDataMsgFrom, + kDataMsgCategory); + std::string data_message_proto2 = BuildDataMessage(kDataMsgFrom2, + kDataMsgCategory2); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + data_message_pkt += EncodePacket(kDataMessageStanzaTag, data_message_proto2); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + WaitForMessage(); // The first data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); + received_message.reset(); + WaitForMessage(); // The second data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto2, received_message->SerializeAsString()); +} + +// Receive a long (>128 bytes) message. +TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = + BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + DCHECK_GT(data_message_pkt.size(), 128U); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + WaitForMessage(); // The data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); +} + +// Receive two long (>128 bytes) message. +TEST_F(GCMConnectionHandlerImplTest, Recv2LongMsgs) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = + BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong); + std::string data_message_proto2 = + BuildDataMessage(kDataMsgFromLong2, kDataMsgCategoryLong2); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + data_message_pkt += EncodePacket(kDataMessageStanzaTag, data_message_proto2); + DCHECK_GT(data_message_pkt.size(), 256U); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + WaitForMessage(); // The first data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); + received_message.reset(); + WaitForMessage(); // The second data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(data_message_proto2, received_message->SerializeAsString()); +} + +// Simulate a message where the end of the data does not arrive in time and the +// read times out. +TEST_F(GCMConnectionHandlerImplTest, ReadTimeout) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = BuildDataMessage(kDataMsgFrom, + kDataMsgCategory); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + int bytes_in_first_message = data_message_pkt.size() / 2; + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str(), + bytes_in_first_message)); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + net::ERR_IO_PENDING)); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str() + + bytes_in_first_message, + data_message_pkt.size() - + bytes_in_first_message)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + received_message.reset(); + WaitForMessage(); // Should time out. + EXPECT_FALSE(received_message.get()); + EXPECT_EQ(net::ERR_TIMED_OUT, last_error()); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + + // Finish the socket read. Should have no effect. + data_provider()->ForceNextRead(); +} + +// Receive a message with zero data bytes. +TEST_F(GCMConnectionHandlerImplTest, RecvMsgNoData) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_pkt = EncodePacket(kHeartbeatPingTag, ""); + ASSERT_EQ(data_message_pkt.size(), 2U); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + received_message.reset(); + WaitForMessage(); // The heartbeat ping. + EXPECT_TRUE(received_message.get()); + EXPECT_EQ(GetMCSProtoTag(*received_message), kHeartbeatPingTag); + EXPECT_EQ(net::OK, last_error()); + EXPECT_TRUE(connection_handler()->CanSendMessage()); +} + +// Send a message after performing the handshake. +TEST_F(GCMConnectionHandlerImplTest, SendMsg) { + mcs_proto::DataMessageStanza data_message; + data_message.set_from(kDataMsgFrom); + data_message.set_category(kDataMsgCategory); + std::string handshake_request = EncodeHandshakeRequest(); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message.SerializeAsString()); + WriteList write_list; + write_list.push_back(net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + write_list.push_back(net::MockWrite(net::ASYNC, + data_message_pkt.c_str(), + data_message_pkt.size())); + std::string handshake_response = EncodeHandshakeResponse(); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + EXPECT_TRUE(connection_handler()->CanSendMessage()); + connection_handler()->SendMessage(data_message); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + WaitForMessage(); // The message send. + EXPECT_TRUE(connection_handler()->CanSendMessage()); +} + +// Attempt to send a message after the socket is disconnected due to a timeout. +TEST_F(GCMConnectionHandlerImplTest, SendMsgSocketDisconnected) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list; + write_list.push_back(net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, net::ERR_IO_PENDING)); + net::StreamSocket* socket = BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + EXPECT_TRUE(connection_handler()->CanSendMessage()); + socket->Disconnect(); + mcs_proto::DataMessageStanza data_message; + data_message.set_from(kDataMsgFrom); + data_message.set_category(kDataMsgCategory); + connection_handler()->SendMessage(data_message); + EXPECT_FALSE(connection_handler()->CanSendMessage()); + WaitForMessage(); // The message send. Should result in an error + EXPECT_FALSE(connection_handler()->CanSendMessage()); + EXPECT_EQ(net::ERR_CONNECTION_CLOSED, last_error()); +} + +} // namespace +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.cc b/chromium/google_apis/gcm/engine/fake_connection_factory.cc new file mode 100644 index 00000000000..54b3423b2d5 --- /dev/null +++ b/chromium/google_apis/gcm/engine/fake_connection_factory.cc @@ -0,0 +1,46 @@ +// Copyright 2013 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 "google_apis/gcm/engine/fake_connection_factory.h" + +#include "google_apis/gcm/engine/fake_connection_handler.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/socket/stream_socket.h" + +namespace gcm { + +FakeConnectionFactory::FakeConnectionFactory() { +} + +FakeConnectionFactory::~FakeConnectionFactory() { +} + +void FakeConnectionFactory::Initialize( + const BuildLoginRequestCallback& request_builder, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) { + request_builder_ = request_builder; + connection_handler_.reset(new FakeConnectionHandler(read_callback, + write_callback)); +} + +ConnectionHandler* FakeConnectionFactory::GetConnectionHandler() const { + return connection_handler_.get(); +} + +void FakeConnectionFactory::Connect() { + mcs_proto::LoginRequest login_request; + request_builder_.Run(&login_request); + connection_handler_->Init(login_request, scoped_ptr<net::StreamSocket>()); +} + +bool FakeConnectionFactory::IsEndpointReachable() const { + return connection_handler_.get() && connection_handler_->CanSendMessage(); +} + +base::TimeTicks FakeConnectionFactory::NextRetryAttempt() const { + return base::TimeTicks(); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.h b/chromium/google_apis/gcm/engine/fake_connection_factory.h new file mode 100644 index 00000000000..60b10e130db --- /dev/null +++ b/chromium/google_apis/gcm/engine/fake_connection_factory.h @@ -0,0 +1,42 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_ +#define GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_ + +#include "base/memory/scoped_ptr.h" +#include "google_apis/gcm/engine/connection_factory.h" + +namespace gcm { + +class FakeConnectionHandler; + +// A connection factory that mocks out real connections, using a fake connection +// handler instead. +class FakeConnectionFactory : public ConnectionFactory { + public: + FakeConnectionFactory(); + virtual ~FakeConnectionFactory(); + + // ConnectionFactory implementation. + virtual void Initialize( + const BuildLoginRequestCallback& request_builder, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) OVERRIDE; + virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE; + virtual void Connect() OVERRIDE; + virtual bool IsEndpointReachable() const OVERRIDE; + virtual base::TimeTicks NextRetryAttempt() const OVERRIDE; + + private: + scoped_ptr<FakeConnectionHandler> connection_handler_; + + BuildLoginRequestCallback request_builder_; + + DISALLOW_COPY_AND_ASSIGN(FakeConnectionFactory); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_FACTORY_H_ diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.cc b/chromium/google_apis/gcm/engine/fake_connection_handler.cc new file mode 100644 index 00000000000..06639331ebe --- /dev/null +++ b/chromium/google_apis/gcm/engine/fake_connection_handler.cc @@ -0,0 +1,86 @@ +// Copyright 2013 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 "google_apis/gcm/engine/fake_connection_handler.h" + +#include "base/logging.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "net/socket/stream_socket.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +// Build a basic login response. +scoped_ptr<google::protobuf::MessageLite> BuildLoginResponse(bool fail_login) { + scoped_ptr<mcs_proto::LoginResponse> login_response( + new mcs_proto::LoginResponse()); + login_response->set_id("id"); + if (fail_login) + login_response->mutable_error()->set_code(1); + return login_response.PassAs<google::protobuf::MessageLite>(); +} + +} // namespace + +FakeConnectionHandler::FakeConnectionHandler( + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback) + : read_callback_(read_callback), + write_callback_(write_callback), + fail_login_(false), + fail_send_(false), + initialized_(false) { +} + +FakeConnectionHandler::~FakeConnectionHandler() { +} + +void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request, + scoped_ptr<net::StreamSocket> socket) { + EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(), + login_request.SerializeAsString()); + expected_outgoing_messages_.pop_front(); + DVLOG(1) << "Received init call."; + read_callback_.Run(BuildLoginResponse(fail_login_)); + initialized_ = !fail_login_; +} + +bool FakeConnectionHandler::CanSendMessage() const { + return initialized_; +} + +void FakeConnectionHandler::SendMessage( + const google::protobuf::MessageLite& message) { + if (expected_outgoing_messages_.empty()) + FAIL() << "Unexpected message sent."; + EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(), + message.SerializeAsString()); + expected_outgoing_messages_.pop_front(); + DVLOG(1) << "Received message, " + << (fail_send_ ? " failing send." : "calling back."); + if (!fail_send_) + write_callback_.Run(); + else + initialized_ = false; // Prevent future messages until reconnect. +} + +void FakeConnectionHandler::ExpectOutgoingMessage(const MCSMessage& message) { + expected_outgoing_messages_.push_back(message); +} + +void FakeConnectionHandler::ResetOutgoingMessageExpectations() { + expected_outgoing_messages_.clear(); +} + +bool FakeConnectionHandler::AllOutgoingMessagesReceived() const { + return expected_outgoing_messages_.empty(); +} + +void FakeConnectionHandler::ReceiveMessage(const MCSMessage& message) { + read_callback_.Run(message.CloneProtobuf()); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.h b/chromium/google_apis/gcm/engine/fake_connection_handler.h new file mode 100644 index 00000000000..5356b771086 --- /dev/null +++ b/chromium/google_apis/gcm/engine/fake_connection_handler.h @@ -0,0 +1,74 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_ +#define GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_ + +#include <list> + +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/engine/connection_handler.h" + +namespace gcm { + +// A fake implementation of a ConnectionHandler that can arbitrarily receive +// messages and verify expectations for outgoing messages. +class FakeConnectionHandler : public ConnectionHandler { + public: + FakeConnectionHandler( + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback); + virtual ~FakeConnectionHandler(); + + // ConnectionHandler implementation. + virtual void Init(const mcs_proto::LoginRequest& login_request, + scoped_ptr<net::StreamSocket> socket) OVERRIDE; + virtual bool CanSendMessage() const OVERRIDE; + virtual void SendMessage(const google::protobuf::MessageLite& message) + OVERRIDE; + + // EXPECT's receipt of |message| via SendMessage(..). + void ExpectOutgoingMessage(const MCSMessage& message); + + // Reset the expected outgoing messages. + void ResetOutgoingMessageExpectations(); + + // Whether all expected outgoing messages have been received; + bool AllOutgoingMessagesReceived() const; + + // Passes on |message| to |write_callback_|. + void ReceiveMessage(const MCSMessage& message); + + // Whether to return an error with the next login response. + void set_fail_login(bool fail_login) { + fail_login_ = fail_login; + } + + // Whether to invoke the write callback on the next send attempt or fake a + // connection error instead. + void set_fail_send(bool fail_send) { + fail_send_ = fail_send; + } + + private: + ConnectionHandler::ProtoReceivedCallback read_callback_; + ConnectionHandler::ProtoSentCallback write_callback_; + + std::list<MCSMessage> expected_outgoing_messages_; + + // Whether to fail the login or not. + bool fail_login_; + + // Whether to fail a SendMessage call or not. + bool fail_send_; + + // Whether a successful login has completed. + bool initialized_; + + DISALLOW_COPY_AND_ASSIGN(FakeConnectionHandler); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_FAKE_CONNECTION_HANDLER_H_ diff --git a/chromium/google_apis/gcm/engine/mcs_client.cc b/chromium/google_apis/gcm/engine/mcs_client.cc new file mode 100644 index 00000000000..f0af051379f --- /dev/null +++ b/chromium/google_apis/gcm/engine/mcs_client.cc @@ -0,0 +1,659 @@ +// Copyright 2013 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 "google_apis/gcm/engine/mcs_client.h" + +#include "base/basictypes.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_number_conversions.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/base/socket_stream.h" +#include "google_apis/gcm/engine/connection_factory.h" +#include "google_apis/gcm/engine/rmq_store.h" + +using namespace google::protobuf::io; + +namespace gcm { + +namespace { + +typedef scoped_ptr<google::protobuf::MessageLite> MCSProto; + +// TODO(zea): get these values from MCS settings. +const int64 kHeartbeatDefaultSeconds = 60 * 15; // 15 minutes. + +// The category of messages intended for the GCM client itself from MCS. +const char kMCSCategory[] = "com.google.android.gsf.gtalkservice"; + +// The from field for messages originating in the GCM client. +const char kGCMFromField[] = "gcm@android.com"; + +// MCS status message types. +const char kIdleNotification[] = "IdleNotification"; +// TODO(zea): consume the following message types: +// const char kAlwaysShowOnIdle[] = "ShowAwayOnIdle"; +// const char kPowerNotification[] = "PowerNotification"; +// const char kDataActiveNotification[] = "DataActiveNotification"; + +// The number of unacked messages to allow before sending a stream ack. +// Applies to both incoming and outgoing messages. +// TODO(zea): make this server configurable. +const int kUnackedMessageBeforeStreamAck = 10; + +// The global maximum number of pending messages to have in the send queue. +const size_t kMaxSendQueueSize = 10 * 1024; + +// The maximum message size that can be sent to the server. +const int kMaxMessageBytes = 4 * 1024; // 4KB, like the server. + +// Helper for converting a proto persistent id list to a vector of strings. +bool BuildPersistentIdListFromProto(const google::protobuf::string& bytes, + std::vector<std::string>* id_list) { + mcs_proto::SelectiveAck selective_ack; + if (!selective_ack.ParseFromString(bytes)) + return false; + std::vector<std::string> new_list; + for (int i = 0; i < selective_ack.id_size(); ++i) { + DCHECK(!selective_ack.id(i).empty()); + new_list.push_back(selective_ack.id(i)); + } + id_list->swap(new_list); + return true; +} + +} // namespace + +struct ReliablePacketInfo { + ReliablePacketInfo(); + ~ReliablePacketInfo(); + + // The stream id with which the message was sent. + uint32 stream_id; + + // If reliable delivery was requested, the persistent id of the message. + std::string persistent_id; + + // The type of message itself (for easier lookup). + uint8 tag; + + // The protobuf of the message itself. + MCSProto protobuf; +}; + +ReliablePacketInfo::ReliablePacketInfo() + : stream_id(0), tag(0) { +} +ReliablePacketInfo::~ReliablePacketInfo() {} + +MCSClient::MCSClient( + const base::FilePath& rmq_path, + ConnectionFactory* connection_factory, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) + : state_(UNINITIALIZED), + android_id_(0), + security_token_(0), + connection_factory_(connection_factory), + connection_handler_(NULL), + last_device_to_server_stream_id_received_(0), + last_server_to_device_stream_id_received_(0), + stream_id_out_(0), + stream_id_in_(0), + rmq_store_(rmq_path, blocking_task_runner), + heartbeat_interval_( + base::TimeDelta::FromSeconds(kHeartbeatDefaultSeconds)), + heartbeat_timer_(true, true), + blocking_task_runner_(blocking_task_runner), + weak_ptr_factory_(this) { +} + +MCSClient::~MCSClient() { +} + +void MCSClient::Initialize( + const InitializationCompleteCallback& initialization_callback, + const OnMessageReceivedCallback& message_received_callback, + const OnMessageSentCallback& message_sent_callback) { + DCHECK_EQ(state_, UNINITIALIZED); + initialization_callback_ = initialization_callback; + message_received_callback_ = message_received_callback; + message_sent_callback_ = message_sent_callback; + + state_ = LOADING; + rmq_store_.Load(base::Bind(&MCSClient::OnRMQLoadFinished, + weak_ptr_factory_.GetWeakPtr())); + + connection_factory_->Initialize( + base::Bind(&MCSClient::ResetStateAndBuildLoginRequest, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&MCSClient::HandlePacketFromWire, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&MCSClient::MaybeSendMessage, + weak_ptr_factory_.GetWeakPtr())); + connection_handler_ = connection_factory_->GetConnectionHandler(); +} + +void MCSClient::Login(uint64 android_id, uint64 security_token) { + DCHECK_EQ(state_, LOADED); + if (android_id != android_id_ && security_token != security_token_) { + DCHECK(android_id); + DCHECK(security_token); + DCHECK(restored_unackeds_server_ids_.empty()); + android_id_ = android_id; + security_token_ = security_token; + rmq_store_.SetDeviceCredentials(android_id_, + security_token_, + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + } + + state_ = CONNECTING; + connection_factory_->Connect(); +} + +void MCSClient::SendMessage(const MCSMessage& message, bool use_rmq) { + DCHECK_EQ(state_, CONNECTED); + if (to_send_.size() > kMaxSendQueueSize) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_sent_callback_, "Message queue full.")); + return; + } + if (message.size() > kMaxMessageBytes) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_sent_callback_, "Message too large.")); + return; + } + + ReliablePacketInfo* packet_info = new ReliablePacketInfo(); + packet_info->protobuf = message.CloneProtobuf(); + + if (use_rmq) { + PersistentId persistent_id = GetNextPersistentId(); + DVLOG(1) << "Setting persistent id to " << persistent_id; + packet_info->persistent_id = persistent_id; + SetPersistentId(persistent_id, + packet_info->protobuf.get()); + rmq_store_.AddOutgoingMessage(persistent_id, + MCSMessage(message.tag(), + *(packet_info->protobuf)), + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + } else { + // Check that there is an active connection to the endpoint. + if (!connection_handler_->CanSendMessage()) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_sent_callback_, "Unable to reach endpoint")); + return; + } + } + to_send_.push_back(make_linked_ptr(packet_info)); + MaybeSendMessage(); +} + +void MCSClient::Destroy() { + rmq_store_.Destroy(base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +void MCSClient::ResetStateAndBuildLoginRequest( + mcs_proto::LoginRequest* request) { + DCHECK(android_id_); + DCHECK(security_token_); + stream_id_in_ = 0; + stream_id_out_ = 1; + last_device_to_server_stream_id_received_ = 0; + last_server_to_device_stream_id_received_ = 0; + + // TODO(zea): expire all messages older than their TTL. + + // Add any pending acknowledgments to the list of ids. + for (StreamIdToPersistentIdMap::const_iterator iter = + unacked_server_ids_.begin(); + iter != unacked_server_ids_.end(); ++iter) { + restored_unackeds_server_ids_.push_back(iter->second); + } + unacked_server_ids_.clear(); + + // Any acknowledged server ids which have not been confirmed by the server + // are treated like unacknowledged ids. + for (std::map<StreamId, PersistentIdList>::const_iterator iter = + acked_server_ids_.begin(); + iter != acked_server_ids_.end(); ++iter) { + restored_unackeds_server_ids_.insert(restored_unackeds_server_ids_.end(), + iter->second.begin(), + iter->second.end()); + } + acked_server_ids_.clear(); + + // Then build the request, consuming all pending acknowledgments. + request->Swap(BuildLoginRequest(android_id_, security_token_).get()); + for (PersistentIdList::const_iterator iter = + restored_unackeds_server_ids_.begin(); + iter != restored_unackeds_server_ids_.end(); ++iter) { + request->add_received_persistent_id(*iter); + } + acked_server_ids_[stream_id_out_] = restored_unackeds_server_ids_; + restored_unackeds_server_ids_.clear(); + + // Push all unacknowledged messages to front of send queue. No need to save + // to RMQ, as all messages that reach this point should already have been + // saved as necessary. + while (!to_resend_.empty()) { + to_send_.push_front(to_resend_.back()); + to_resend_.pop_back(); + } + DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size() + << " incoming acks pending, and " << to_send_.size() + << " pending outgoing messages."; + + heartbeat_timer_.Stop(); + + state_ = CONNECTING; +} + +void MCSClient::SendHeartbeat() { + SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing()), + false); +} + +void MCSClient::OnRMQLoadFinished(const RMQStore::LoadResult& result) { + if (!result.success) { + state_ = UNINITIALIZED; + LOG(ERROR) << "Failed to load/create RMQ state. Not connecting."; + initialization_callback_.Run(false, 0, 0); + return; + } + state_ = LOADED; + stream_id_out_ = 1; // Login request is hardcoded to id 1. + + if (result.device_android_id == 0 || result.device_security_token == 0) { + DVLOG(1) << "No device credentials found, assuming new client."; + initialization_callback_.Run(true, 0, 0); + return; + } + + android_id_ = result.device_android_id; + security_token_ = result.device_security_token; + + DVLOG(1) << "RMQ Load finished with " << result.incoming_messages.size() + << " incoming acks pending and " << result.outgoing_messages.size() + << " outgoing messages pending."; + + restored_unackeds_server_ids_ = result.incoming_messages; + + // First go through and order the outgoing messages by recency. + std::map<uint64, google::protobuf::MessageLite*> ordered_messages; + for (std::map<PersistentId, google::protobuf::MessageLite*>::const_iterator + iter = result.outgoing_messages.begin(); + iter != result.outgoing_messages.end(); ++iter) { + uint64 timestamp = 0; + if (!base::StringToUint64(iter->first, ×tamp)) { + LOG(ERROR) << "Invalid restored message."; + return; + } + ordered_messages[timestamp] = iter->second; + } + + // Now go through and add the outgoing messages to the send queue in their + // appropriate order (oldest at front, most recent at back). + for (std::map<uint64, google::protobuf::MessageLite*>::const_iterator + iter = ordered_messages.begin(); + iter != ordered_messages.end(); ++iter) { + ReliablePacketInfo* packet_info = new ReliablePacketInfo(); + packet_info->protobuf.reset(iter->second); + packet_info->persistent_id = base::Uint64ToString(iter->first); + to_send_.push_back(make_linked_ptr(packet_info)); + } + + initialization_callback_.Run(true, android_id_, security_token_); +} + +void MCSClient::OnRMQUpdateFinished(bool success) { + LOG_IF(ERROR, !success) << "RMQ Update failed!"; + // TODO(zea): Rebuild the store from scratch in case of persistence failure? +} + +void MCSClient::MaybeSendMessage() { + if (to_send_.empty()) + return; + + if (!connection_handler_->CanSendMessage()) + return; + + // TODO(zea): drop messages older than their TTL. + + DVLOG(1) << "Pending output message found, sending."; + MCSPacketInternal packet = to_send_.front(); + to_send_.pop_front(); + if (!packet->persistent_id.empty()) + to_resend_.push_back(packet); + SendPacketToWire(packet.get()); +} + +void MCSClient::SendPacketToWire(ReliablePacketInfo* packet_info) { + // Reset the heartbeat interval. + heartbeat_timer_.Reset(); + packet_info->stream_id = ++stream_id_out_; + DVLOG(1) << "Sending packet of type " << packet_info->protobuf->GetTypeName(); + + // Set the proper last received stream id to acknowledge received server + // packets. + DVLOG(1) << "Setting last stream id received to " + << stream_id_in_; + SetLastStreamIdReceived(stream_id_in_, + packet_info->protobuf.get()); + if (stream_id_in_ != last_server_to_device_stream_id_received_) { + last_server_to_device_stream_id_received_ = stream_id_in_; + // Mark all acknowledged server messages as such. Note: they're not dropped, + // as it may be that they'll need to be re-acked if this message doesn't + // make it. + PersistentIdList persistent_id_list; + for (StreamIdToPersistentIdMap::const_iterator iter = + unacked_server_ids_.begin(); + iter != unacked_server_ids_.end(); ++iter) { + DCHECK_LE(iter->first, last_server_to_device_stream_id_received_); + persistent_id_list.push_back(iter->second); + } + unacked_server_ids_.clear(); + acked_server_ids_[stream_id_out_] = persistent_id_list; + } + + connection_handler_->SendMessage(*packet_info->protobuf); +} + +void MCSClient::HandleMCSDataMesssage( + scoped_ptr<google::protobuf::MessageLite> protobuf) { + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf.get()); + // TODO(zea): implement a proper status manager rather than hardcoding these + // values. + scoped_ptr<mcs_proto::DataMessageStanza> response( + new mcs_proto::DataMessageStanza()); + response->set_from(kGCMFromField); + bool send = false; + for (int i = 0; i < data_message->app_data_size(); ++i) { + const mcs_proto::AppData& app_data = data_message->app_data(i); + if (app_data.key() == kIdleNotification) { + // Tell the MCS server the client is not idle. + send = true; + mcs_proto::AppData data; + data.set_key(kIdleNotification); + data.set_value("false"); + response->add_app_data()->CopyFrom(data); + response->set_category(kMCSCategory); + } + } + + if (send) { + SendMessage( + MCSMessage(kDataMessageStanzaTag, + response.PassAs<const google::protobuf::MessageLite>()), + false); + } +} + +void MCSClient::HandlePacketFromWire( + scoped_ptr<google::protobuf::MessageLite> protobuf) { + if (!protobuf.get()) + return; + uint8 tag = GetMCSProtoTag(*protobuf); + PersistentId persistent_id = GetPersistentId(*protobuf); + StreamId last_stream_id_received = GetLastStreamIdReceived(*protobuf); + + if (last_stream_id_received != 0) { + last_device_to_server_stream_id_received_ = last_stream_id_received; + + // Process device to server messages that have now been acknowledged by the + // server. Because messages are stored in order, just pop off all that have + // a stream id lower than server's last received stream id. + HandleStreamAck(last_stream_id_received); + + // Process server_to_device_messages that the server now knows were + // acknowledged. Again, they're in order, so just keep going until the + // stream id is reached. + StreamIdList acked_stream_ids_to_remove; + for (std::map<StreamId, PersistentIdList>::iterator iter = + acked_server_ids_.begin(); + iter != acked_server_ids_.end() && + iter->first <= last_stream_id_received; ++iter) { + acked_stream_ids_to_remove.push_back(iter->first); + } + for (StreamIdList::iterator iter = acked_stream_ids_to_remove.begin(); + iter != acked_stream_ids_to_remove.end(); ++iter) { + acked_server_ids_.erase(*iter); + } + } + + ++stream_id_in_; + if (!persistent_id.empty()) { + unacked_server_ids_[stream_id_in_] = persistent_id; + rmq_store_.AddIncomingMessage(persistent_id, + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + } + + DVLOG(1) << "Received message of type " << protobuf->GetTypeName() + << " with persistent id " + << (persistent_id.empty() ? "NULL" : persistent_id) + << ", stream id " << stream_id_in_ << " and last stream id received " + << last_stream_id_received; + + if (unacked_server_ids_.size() > 0 && + unacked_server_ids_.size() % kUnackedMessageBeforeStreamAck == 0) { + SendMessage(MCSMessage(kIqStanzaTag, + BuildStreamAck(). + PassAs<const google::protobuf::MessageLite>()), + false); + } + + switch (tag) { + case kLoginResponseTag: { + mcs_proto::LoginResponse* login_response = + reinterpret_cast<mcs_proto::LoginResponse*>(protobuf.get()); + DVLOG(1) << "Received login response:"; + DVLOG(1) << " Id: " << login_response->id(); + DVLOG(1) << " Timestamp: " << login_response->server_timestamp(); + if (login_response->has_error()) { + state_ = UNINITIALIZED; + DVLOG(1) << " Error code: " << login_response->error().code(); + DVLOG(1) << " Error message: " << login_response->error().message(); + initialization_callback_.Run(false, 0, 0); + return; + } + + state_ = CONNECTED; + stream_id_in_ = 1; // To account for the login response. + DCHECK_EQ(1U, stream_id_out_); + + // Pass the login response on up. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_received_callback_, + MCSMessage(tag, + protobuf.PassAs< + const google::protobuf::MessageLite>()))); + + // If there are pending messages, attempt to send one. + if (!to_send_.empty()) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&MCSClient::MaybeSendMessage, + weak_ptr_factory_.GetWeakPtr())); + } + + heartbeat_timer_.Start(FROM_HERE, + heartbeat_interval_, + base::Bind(&MCSClient::SendHeartbeat, + weak_ptr_factory_.GetWeakPtr())); + return; + } + case kHeartbeatPingTag: + DCHECK_GE(stream_id_in_, 1U); + DVLOG(1) << "Received heartbeat ping, sending ack."; + SendMessage( + MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck()), false); + return; + case kHeartbeatAckTag: + DCHECK_GE(stream_id_in_, 1U); + DVLOG(1) << "Received heartbeat ack."; + // TODO(zea): add logic to reconnect if no ack received within a certain + // timeout (with backoff). + return; + case kCloseTag: + LOG(ERROR) << "Received close command, closing connection."; + state_ = UNINITIALIZED; + initialization_callback_.Run(false, 0, 0); + // TODO(zea): should this happen in non-error cases? Reconnect? + return; + case kIqStanzaTag: { + DCHECK_GE(stream_id_in_, 1U); + mcs_proto::IqStanza* iq_stanza = + reinterpret_cast<mcs_proto::IqStanza*>(protobuf.get()); + const mcs_proto::Extension& iq_extension = iq_stanza->extension(); + switch (iq_extension.id()) { + case kSelectiveAck: { + PersistentIdList acked_ids; + if (BuildPersistentIdListFromProto(iq_extension.data(), + &acked_ids)) { + HandleSelectiveAck(acked_ids); + } + return; + } + case kStreamAck: + // Do nothing. The last received stream id is always processed if it's + // present. + return; + default: + LOG(WARNING) << "Received invalid iq stanza extension " + << iq_extension.id(); + return; + } + } + case kDataMessageStanzaTag: { + DCHECK_GE(stream_id_in_, 1U); + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf.get()); + if (data_message->category() == kMCSCategory) { + HandleMCSDataMesssage(protobuf.Pass()); + return; + } + + DCHECK(protobuf.get()); + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_received_callback_, + MCSMessage(tag, + protobuf.PassAs< + const google::protobuf::MessageLite>()))); + return; + } + default: + LOG(ERROR) << "Received unexpected message of type " + << static_cast<int>(tag); + return; + } +} + +void MCSClient::HandleStreamAck(StreamId last_stream_id_received) { + PersistentIdList acked_outgoing_persistent_ids; + StreamIdList acked_outgoing_stream_ids; + while (!to_resend_.empty() && + to_resend_.front()->stream_id <= last_stream_id_received) { + const MCSPacketInternal& outgoing_packet = to_resend_.front(); + acked_outgoing_persistent_ids.push_back(outgoing_packet->persistent_id); + acked_outgoing_stream_ids.push_back(outgoing_packet->stream_id); + to_resend_.pop_front(); + } + + DVLOG(1) << "Server acked " << acked_outgoing_persistent_ids.size() + << " outgoing messages, " << to_resend_.size() + << " remaining unacked"; + rmq_store_.RemoveOutgoingMessages(acked_outgoing_persistent_ids, + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + + HandleServerConfirmedReceipt(last_stream_id_received); +} + +void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { + // First check the to_resend_ queue. Acknowledgments should always happen + // in the order they were sent, so if messages are present they should match + // the acknowledge list. + PersistentIdList::const_iterator iter = id_list.begin(); + for (; iter != id_list.end() && !to_resend_.empty(); ++iter) { + const MCSPacketInternal& outgoing_packet = to_resend_.front(); + DCHECK_EQ(outgoing_packet->persistent_id, *iter); + + // No need to re-acknowledge any server messages this message already + // acknowledged. + StreamId device_stream_id = outgoing_packet->stream_id; + HandleServerConfirmedReceipt(device_stream_id); + + to_resend_.pop_front(); + } + + // If the acknowledged ids aren't all there, they might be in the to_send_ + // queue (typically when a StreamAck confirms messages as part of a login + // response). + for (; iter != id_list.end() && !to_send_.empty(); ++iter) { + const MCSPacketInternal& outgoing_packet = to_send_.front(); + DCHECK_EQ(outgoing_packet->persistent_id, *iter); + + // No need to re-acknowledge any server messages this message already + // acknowledged. + StreamId device_stream_id = outgoing_packet->stream_id; + HandleServerConfirmedReceipt(device_stream_id); + + to_send_.pop_front(); + } + + DCHECK(iter == id_list.end()); + + DVLOG(1) << "Server acked " << id_list.size() + << " messages, " << to_resend_.size() << " remaining unacked."; + rmq_store_.RemoveOutgoingMessages(id_list, + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + + // Resend any remaining outgoing messages, as they were not received by the + // server. + DVLOG(1) << "Resending " << to_resend_.size() << " messages."; + while (!to_resend_.empty()) { + to_send_.push_front(to_resend_.back()); + to_resend_.pop_back(); + } +} + +void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) { + // TODO(zea): use a message id the sender understands. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(message_sent_callback_, + "Message " + base::UintToString(device_stream_id) + " sent.")); + + PersistentIdList acked_incoming_ids; + for (std::map<StreamId, PersistentIdList>::iterator iter = + acked_server_ids_.begin(); + iter != acked_server_ids_.end() && + iter->first <= device_stream_id;) { + acked_incoming_ids.insert(acked_incoming_ids.end(), + iter->second.begin(), + iter->second.end()); + acked_server_ids_.erase(iter++); + } + + DVLOG(1) << "Server confirmed receipt of " << acked_incoming_ids.size() + << " acknowledged server messages."; + rmq_store_.RemoveIncomingMessages(acked_incoming_ids, + base::Bind(&MCSClient::OnRMQUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +MCSClient::PersistentId MCSClient::GetNextPersistentId() { + return base::Uint64ToString(base::TimeTicks::Now().ToInternalValue()); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/mcs_client.h b/chromium/google_apis/gcm/engine/mcs_client.h new file mode 100644 index 00000000000..4de62cb127e --- /dev/null +++ b/chromium/google_apis/gcm/engine/mcs_client.h @@ -0,0 +1,231 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_ +#define GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_ + +#include <deque> +#include <map> +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/memory/linked_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/timer/timer.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/engine/connection_handler.h" +#include "google_apis/gcm/engine/rmq_store.h" + +namespace google { +namespace protobuf { +class MessageLite; +} // namespace protobuf +} // namespace google + +namespace mcs_proto { +class LoginRequest; +} + +namespace gcm { + +class ConnectionFactory; +struct ReliablePacketInfo; + +// An MCS client. This client is in charge of all communications with an +// MCS endpoint, and is capable of reliably sending/receiving GCM messages. +// NOTE: Not thread safe. This class should live on the same thread as that +// network requests are performed on. +class GCM_EXPORT MCSClient { + public: + enum State { + UNINITIALIZED, // Uninitialized. + LOADING, // Waiting for RMQ load to finish. + LOADED, // RMQ Load finished, waiting to connect. + CONNECTING, // Connection in progress. + CONNECTED, // Connected and running. + }; + + // Callback for informing MCSClient status. It is valid for this to be + // invoked more than once if a permanent error is encountered after a + // successful login was initiated. + typedef base::Callback< + void(bool success, + uint64 restored_android_id, + uint64 restored_security_token)> InitializationCompleteCallback; + // Callback when a message is received. + typedef base::Callback<void(const MCSMessage& message)> + OnMessageReceivedCallback; + // Callback when a message is sent (and receipt has been acknowledged by + // the MCS endpoint). + // TODO(zea): pass some sort of structure containing more details about + // send failures. + typedef base::Callback<void(const std::string& message_id)> + OnMessageSentCallback; + + MCSClient(const base::FilePath& rmq_path, + ConnectionFactory* connection_factory, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner); + virtual ~MCSClient(); + + // Initialize the client. Will load any previous id/token information as well + // as unacknowledged message information from the RMQ storage, if it exists, + // passing the id/token information back via |initialization_callback| along + // with a |success == true| result. If no RMQ information is present (and + // this is therefore a fresh client), a clean RMQ store will be created and + // values of 0 will be returned via |initialization_callback| with + // |success == true|. + /// If an error loading the RMQ store is encountered, + // |initialization_callback| will be invoked with |success == false|. + void Initialize(const InitializationCompleteCallback& initialization_callback, + const OnMessageReceivedCallback& message_received_callback, + const OnMessageSentCallback& message_sent_callback); + + // Logs the client into the server. Client must be initialized. + // |android_id| and |security_token| are optional if this is not a new + // client, else they must be non-zero. + // Successful login will result in |message_received_callback| being invoked + // with a valid LoginResponse. + // Login failure (typically invalid id/token) will shut down the client, and + // |initialization_callback| to be invoked with |success = false|. + void Login(uint64 android_id, uint64 security_token); + + // Sends a message, with or without reliable message queueing (RMQ) support. + // Will asynchronously invoke the OnMessageSent callback regardless. + // TODO(zea): support TTL. + void SendMessage(const MCSMessage& message, bool use_rmq); + + // Disconnects the client and permanently destroys the persistent RMQ store. + // WARNING: This is permanent, and the client must be recreated with new + // credentials afterwards. + void Destroy(); + + // Returns the current state of the client. + State state() const { return state_; } + + private: + typedef uint32 StreamId; + typedef std::string PersistentId; + typedef std::vector<StreamId> StreamIdList; + typedef std::vector<PersistentId> PersistentIdList; + typedef std::map<StreamId, PersistentId> StreamIdToPersistentIdMap; + typedef linked_ptr<ReliablePacketInfo> MCSPacketInternal; + + // Resets the internal state and builds a new login request, acknowledging + // any pending server-to-device messages and rebuilding the send queue + // from all unacknowledged device-to-server messages. + // Should only be called when the connection has been reset. + void ResetStateAndBuildLoginRequest(mcs_proto::LoginRequest* request); + + // Send a heartbeat to the MCS server. + void SendHeartbeat(); + + // RMQ Store callbacks. + void OnRMQLoadFinished(const RMQStore::LoadResult& result); + void OnRMQUpdateFinished(bool success); + + // Attempt to send a message. + void MaybeSendMessage(); + + // Helper for sending a protobuf along with any unacknowledged ids to the + // wire. + void SendPacketToWire(ReliablePacketInfo* packet_info); + + // Handle a data message sent to the MCS client system from the MCS server. + void HandleMCSDataMesssage( + scoped_ptr<google::protobuf::MessageLite> protobuf); + + // Handle a packet received over the wire. + void HandlePacketFromWire(scoped_ptr<google::protobuf::MessageLite> protobuf); + + // ReliableMessageQueue acknowledgment helpers. + // Handle a StreamAck sent by the server confirming receipt of all + // messages up to the message with stream id |last_stream_id_received|. + void HandleStreamAck(StreamId last_stream_id_received_); + // Handle a SelectiveAck sent by the server confirming all messages + // in |id_list|. + void HandleSelectiveAck(const PersistentIdList& id_list); + // Handle server confirmation of a device message, including device's + // acknowledgment of receipt of messages. + void HandleServerConfirmedReceipt(StreamId device_stream_id); + + // Generates a new persistent id for messages. + // Virtual for testing. + virtual PersistentId GetNextPersistentId(); + + // Client state. + State state_; + + // Callbacks for owner. + InitializationCompleteCallback initialization_callback_; + OnMessageReceivedCallback message_received_callback_; + OnMessageSentCallback message_sent_callback_; + + // The android id and security token in use by this device. + uint64 android_id_; + uint64 security_token_; + + // Factory for creating new connections and connection handlers. + ConnectionFactory* connection_factory_; + + // Connection handler to handle all over-the-wire protocol communication + // with the mobile connection server. + ConnectionHandler* connection_handler_; + + // ----- Reliablie Message Queue section ----- + // Note: all queues/maps are ordered from oldest (front/begin) message to + // most recent (back/end). + + // Send/acknowledge queues. + std::deque<MCSPacketInternal> to_send_; + std::deque<MCSPacketInternal> to_resend_; + + // Last device_to_server stream id acknowledged by the server. + StreamId last_device_to_server_stream_id_received_; + // Last server_to_device stream id acknowledged by this device. + StreamId last_server_to_device_stream_id_received_; + // The stream id for the last sent message. A new message should consume + // stream_id_out_ + 1. + StreamId stream_id_out_; + // The stream id of the last received message. The LoginResponse will always + // have a stream id of 1, and stream ids increment by 1 for each received + // message. + StreamId stream_id_in_; + + // The server messages that have not been acked by the device yet. Keyed by + // server stream id. + StreamIdToPersistentIdMap unacked_server_ids_; + + // Those server messages that have been acked. They must remain tracked + // until the ack message is itself confirmed. The list of all message ids + // acknowledged are keyed off the device stream id of the message that + // acknowledged them. + std::map<StreamId, PersistentIdList> acked_server_ids_; + + // Those server messages from a previous connection that were not fully + // acknowledged. They do not have associated stream ids, and will be + // acknowledged on the next login attempt. + PersistentIdList restored_unackeds_server_ids_; + + // The reliable message queue persistent store. + RMQStore rmq_store_; + + // ----- Heartbeats ----- + // The current heartbeat interval. + base::TimeDelta heartbeat_interval_; + // Timer for triggering heartbeats. + base::Timer heartbeat_timer_; + + // The task runner for blocking tasks (i.e. persisting RMQ state to disk). + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + + base::WeakPtrFactory<MCSClient> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(MCSClient); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_ diff --git a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc new file mode 100644 index 00000000000..6ef140586e9 --- /dev/null +++ b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc @@ -0,0 +1,540 @@ +// Copyright 2013 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 "google_apis/gcm/engine/mcs_client.h" + +#include "base/files/scoped_temp_dir.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "components/webdata/encryptor/encryptor.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/engine/fake_connection_factory.h" +#include "google_apis/gcm/engine/fake_connection_handler.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const uint64 kAndroidId = 54321; +const uint64 kSecurityToken = 12345; + +// Number of messages to send when testing batching. +// Note: must be even for tests that split batches in half. +const int kMessageBatchSize = 6; + +// The number of unacked messages the client will receive before sending a +// stream ack. +// TODO(zea): get this (and other constants) directly from the mcs client. +const int kAckLimitSize = 10; + +// Helper for building arbitrary data messages. +MCSMessage BuildDataMessage(const std::string& from, + const std::string& category, + int last_stream_id_received, + const std::string persistent_id) { + mcs_proto::DataMessageStanza data_message; + data_message.set_from(from); + data_message.set_category(category); + data_message.set_last_stream_id_received(last_stream_id_received); + if (!persistent_id.empty()) + data_message.set_persistent_id(persistent_id); + return MCSMessage(kDataMessageStanzaTag, data_message); +} + +// MCSClient with overriden exposed persistent id logic. +class TestMCSClient : public MCSClient { + public: + TestMCSClient(const base::FilePath& rmq_path, + ConnectionFactory* connection_factory, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) + : MCSClient(rmq_path, connection_factory, blocking_task_runner), + next_id_(0) { + } + + virtual std::string GetNextPersistentId() OVERRIDE { + return base::UintToString(++next_id_); + } + + private: + uint32 next_id_; +}; + +class MCSClientTest : public testing::Test { + public: + MCSClientTest(); + virtual ~MCSClientTest(); + + void BuildMCSClient(); + void InitializeClient(); + void LoginClient(const std::vector<std::string>& acknowledged_ids); + + TestMCSClient* mcs_client() const { return mcs_client_.get(); } + FakeConnectionFactory* connection_factory() { + return &connection_factory_; + } + bool init_success() const { return init_success_; } + uint64 restored_android_id() const { return restored_android_id_; } + uint64 restored_security_token() const { return restored_security_token_; } + MCSMessage* received_message() const { return received_message_.get(); } + std::string sent_message_id() const { return sent_message_id_;} + + FakeConnectionHandler* GetFakeHandler() const; + + void WaitForMCSEvent(); + void PumpLoop(); + + private: + void InitializationCallback(bool success, + uint64 restored_android_id, + uint64 restored_security_token); + void MessageReceivedCallback(const MCSMessage& message); + void MessageSentCallback(const std::string& message_id); + + base::ScopedTempDir temp_directory_; + base::MessageLoop message_loop_; + scoped_ptr<base::RunLoop> run_loop_; + + FakeConnectionFactory connection_factory_; + scoped_ptr<TestMCSClient> mcs_client_; + bool init_success_; + uint64 restored_android_id_; + uint64 restored_security_token_; + scoped_ptr<MCSMessage> received_message_; + std::string sent_message_id_; +}; + +MCSClientTest::MCSClientTest() + : run_loop_(new base::RunLoop()), + init_success_(false), + restored_android_id_(0), + restored_security_token_(0) { + EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); + run_loop_.reset(new base::RunLoop()); + + // On OSX, prevent the Keychain permissions popup during unit tests. +#if defined(OS_MACOSX) + Encryptor::UseMockKeychain(true); +#endif +} + +MCSClientTest::~MCSClientTest() {} + +void MCSClientTest::BuildMCSClient() { + mcs_client_.reset( + new TestMCSClient(temp_directory_.path(), + &connection_factory_, + message_loop_.message_loop_proxy())); +} + +void MCSClientTest::InitializeClient() { + mcs_client_->Initialize(base::Bind(&MCSClientTest::InitializationCallback, + base::Unretained(this)), + base::Bind(&MCSClientTest::MessageReceivedCallback, + base::Unretained(this)), + base::Bind(&MCSClientTest::MessageSentCallback, + base::Unretained(this))); + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + +void MCSClientTest::LoginClient( + const std::vector<std::string>& acknowledged_ids) { + scoped_ptr<mcs_proto::LoginRequest> login_request = + BuildLoginRequest(kAndroidId, kSecurityToken); + for (size_t i = 0; i < acknowledged_ids.size(); ++i) + login_request->add_received_persistent_id(acknowledged_ids[i]); + GetFakeHandler()->ExpectOutgoingMessage( + MCSMessage(kLoginRequestTag, + login_request.PassAs<const google::protobuf::MessageLite>())); + mcs_client_->Login(kAndroidId, kSecurityToken); + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + +FakeConnectionHandler* MCSClientTest::GetFakeHandler() const { + return reinterpret_cast<FakeConnectionHandler*>( + connection_factory_.GetConnectionHandler()); +} + +void MCSClientTest::WaitForMCSEvent() { + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + +void MCSClientTest::PumpLoop() { + run_loop_->RunUntilIdle(); + run_loop_.reset(new base::RunLoop()); +} + +void MCSClientTest::InitializationCallback(bool success, + uint64 restored_android_id, + uint64 restored_security_token) { + init_success_ = success; + restored_android_id_ = restored_android_id; + restored_security_token_ = restored_security_token; + DVLOG(1) << "Initialization callback invoked, killing loop."; + run_loop_->Quit(); +} + +void MCSClientTest::MessageReceivedCallback(const MCSMessage& message) { + received_message_.reset(new MCSMessage(message)); + DVLOG(1) << "Message received callback invoked, killing loop."; + run_loop_->Quit(); +} + +void MCSClientTest::MessageSentCallback(const std::string& message_id) { + DVLOG(1) << "Message sent callback invoked, killing loop."; + run_loop_->Quit(); +} + +// Initialize a new client. +TEST_F(MCSClientTest, InitializeNew) { + BuildMCSClient(); + InitializeClient(); + EXPECT_EQ(0U, restored_android_id()); + EXPECT_EQ(0U, restored_security_token()); + EXPECT_TRUE(init_success()); +} + +// Initialize a new client, shut it down, then restart the client. Should +// reload the existing device credentials. +TEST_F(MCSClientTest, InitializeExisting) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Rebuild the client, to reload from the RMQ. + BuildMCSClient(); + InitializeClient(); + EXPECT_EQ(kAndroidId, restored_android_id()); + EXPECT_EQ(kSecurityToken, restored_security_token()); + EXPECT_TRUE(init_success()); +} + +// Log in successfully to the MCS endpoint. +TEST_F(MCSClientTest, LoginSuccess) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + EXPECT_TRUE(connection_factory()->IsEndpointReachable()); + EXPECT_TRUE(init_success()); + ASSERT_TRUE(received_message()); + EXPECT_EQ(kLoginResponseTag, received_message()->tag()); +} + +// Encounter a server error during the login attempt. +TEST_F(MCSClientTest, FailLogin) { + BuildMCSClient(); + InitializeClient(); + GetFakeHandler()->set_fail_login(true); + LoginClient(std::vector<std::string>()); + EXPECT_FALSE(connection_factory()->IsEndpointReachable()); + EXPECT_FALSE(init_success()); + EXPECT_FALSE(received_message()); +} + +// Send a message without RMQ support. +TEST_F(MCSClientTest, SendMessageNoRMQ) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + MCSMessage message(BuildDataMessage("from", "category", 1, "")); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, false); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Send a message with RMQ support. +TEST_F(MCSClientTest, SendMessageRMQ) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Send a message with RMQ support while disconnected. On reconnect, the message +// should be resent. +TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + GetFakeHandler()->set_fail_send(true); + MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + + // The initial (failed) send. + GetFakeHandler()->ExpectOutgoingMessage(message); + // The login request. + GetFakeHandler()->ExpectOutgoingMessage( + MCSMessage(kLoginRequestTag, + BuildLoginRequest(kAndroidId, kSecurityToken). + PassAs<const google::protobuf::MessageLite>())); + // The second (re)send. + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + EXPECT_FALSE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + GetFakeHandler()->set_fail_send(false); + connection_factory()->Connect(); + WaitForMCSEvent(); // Wait for the login to finish. + PumpLoop(); // Wait for the send to happen. + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Send a message with RMQ support without receiving an acknowledgement. On +// restart the message should be resent. +TEST_F(MCSClientTest, SendMessageRMQOnRestart) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + GetFakeHandler()->set_fail_send(true); + MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + + // The initial (failed) send. + GetFakeHandler()->ExpectOutgoingMessage(message); + GetFakeHandler()->set_fail_send(false); + mcs_client()->SendMessage(message, true); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Rebuild the client, which should resend the old message. + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + GetFakeHandler()->ExpectOutgoingMessage(message); + PumpLoop(); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Send messages with RMQ support, followed by receiving a stream ack. On +// restart nothing should be recent. +TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Send some messages. + for (int i = 1; i <= kMessageBatchSize; ++i) { + MCSMessage message( + BuildDataMessage("from", "category", 1, base::IntToString(i))); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + } + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Receive the ack. + scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck(); + ack->set_last_stream_id_received(kMessageBatchSize + 1); + GetFakeHandler()->ReceiveMessage( + MCSMessage(kIqStanzaTag, + ack.PassAs<const google::protobuf::MessageLite>())); + WaitForMCSEvent(); + + // Reconnect and ensure no messages are resent. + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + PumpLoop(); +} + +// Send messages with RMQ support. On restart, receive a SelectiveAck with +// the login response. No messages should be resent. +TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Send some messages. + std::vector<std::string> id_list; + for (int i = 1; i <= kMessageBatchSize; ++i) { + id_list.push_back(base::IntToString(i)); + MCSMessage message( + BuildDataMessage("from", "category", 1, id_list.back())); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + } + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Rebuild the client, and receive an acknowledgment for the messages as + // part of the login response. + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(id_list)); + GetFakeHandler()->ReceiveMessage( + MCSMessage(kIqStanzaTag, + ack.PassAs<const google::protobuf::MessageLite>())); + WaitForMCSEvent(); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Send messages with RMQ support. On restart, receive a SelectiveAck with +// the login response that only acks some messages. The unacked messages should +// be resent. +TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Send some messages. + std::vector<std::string> id_list; + for (int i = 1; i <= kMessageBatchSize; ++i) { + id_list.push_back(base::IntToString(i)); + MCSMessage message( + BuildDataMessage("from", "category", 1, id_list.back())); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + } + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Rebuild the client, and receive an acknowledgment for the messages as + // part of the login response. + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + std::vector<std::string> acked_ids, remaining_ids; + acked_ids.insert(acked_ids.end(), + id_list.begin(), + id_list.begin() + kMessageBatchSize / 2); + remaining_ids.insert(remaining_ids.end(), + id_list.begin() + kMessageBatchSize / 2, + id_list.end()); + for (int i = 1; i <= kMessageBatchSize / 2; ++i) { + MCSMessage message( + BuildDataMessage("from", + "category", + 2, + remaining_ids[i - 1])); + GetFakeHandler()->ExpectOutgoingMessage(message); + } + scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(acked_ids)); + GetFakeHandler()->ReceiveMessage( + MCSMessage(kIqStanzaTag, + ack.PassAs<const google::protobuf::MessageLite>())); + WaitForMCSEvent(); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Receive some messages. On restart, the login request should contain the +// appropriate acknowledged ids. +TEST_F(MCSClientTest, AckOnLogin) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Receive some messages. + std::vector<std::string> id_list; + for (int i = 1; i <= kMessageBatchSize; ++i) { + id_list.push_back(base::IntToString(i)); + MCSMessage message( + BuildDataMessage("from", "category", i, id_list.back())); + GetFakeHandler()->ReceiveMessage(message); + WaitForMCSEvent(); + PumpLoop(); + } + + // Restart the client. + BuildMCSClient(); + InitializeClient(); + LoginClient(id_list); +} + +// Receive some messages. On the next send, the outgoing message should contain +// the appropriate last stream id received field to ack the received messages. +TEST_F(MCSClientTest, AckOnSend) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // Receive some messages. + std::vector<std::string> id_list; + for (int i = 1; i <= kMessageBatchSize; ++i) { + id_list.push_back(base::IntToString(i)); + MCSMessage message( + BuildDataMessage("from", "category", i, id_list.back())); + GetFakeHandler()->ReceiveMessage(message); + WaitForMCSEvent(); + PumpLoop(); + } + + // Trigger a message send, which should acknowledge via stream ack. + MCSMessage message( + BuildDataMessage("from", "category", kMessageBatchSize + 1, "1")); + GetFakeHandler()->ExpectOutgoingMessage(message); + mcs_client()->SendMessage(message, true); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +// Receive the ack limit in messages, which should trigger an automatic +// stream ack. Receive a heartbeat to confirm the ack. +TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + + // The stream ack. + scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck(); + ack->set_last_stream_id_received(kAckLimitSize + 1); + GetFakeHandler()->ExpectOutgoingMessage( + MCSMessage(kIqStanzaTag, + ack.PassAs<const google::protobuf::MessageLite>())); + + // Receive some messages. + std::vector<std::string> id_list; + for (int i = 1; i <= kAckLimitSize; ++i) { + id_list.push_back(base::IntToString(i)); + MCSMessage message( + BuildDataMessage("from", "category", i, id_list.back())); + GetFakeHandler()->ReceiveMessage(message); + WaitForMCSEvent(); + PumpLoop(); + } + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Receive a heartbeat confirming the ack (and receive the heartbeat ack). + scoped_ptr<mcs_proto::HeartbeatPing> heartbeat( + new mcs_proto::HeartbeatPing()); + heartbeat->set_last_stream_id_received(2); + + scoped_ptr<mcs_proto::HeartbeatAck> heartbeat_ack( + new mcs_proto::HeartbeatAck()); + heartbeat_ack->set_last_stream_id_received(kAckLimitSize + 2); + GetFakeHandler()->ExpectOutgoingMessage( + MCSMessage(kHeartbeatAckTag, + heartbeat_ack.PassAs<const google::protobuf::MessageLite>())); + + GetFakeHandler()->ReceiveMessage( + MCSMessage(kHeartbeatPingTag, + heartbeat.PassAs<const google::protobuf::MessageLite>())); + WaitForMCSEvent(); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); + + // Rebuild the client. Nothing should be sent on login. + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + EXPECT_TRUE(GetFakeHandler()-> + AllOutgoingMessagesReceived()); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/rmq_store.cc b/chromium/google_apis/gcm/engine/rmq_store.cc new file mode 100644 index 00000000000..eaa6dcc8059 --- /dev/null +++ b/chromium/google_apis/gcm/engine/rmq_store.cc @@ -0,0 +1,491 @@ +// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h" + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/sequenced_task_runner.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/tracked_objects.h" +#include "components/webdata/encryptor/encryptor.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "third_party/leveldatabase/src/include/leveldb/db.h" + +namespace gcm { + +namespace { + +// ---- LevelDB keys. ---- +// Key for this device's android id. +const char kDeviceAIDKey[] = "device_aid_key"; +// Key for this device's android security token. +const char kDeviceTokenKey[] = "device_token_key"; +// Lowest lexicographically ordered incoming message key. +// Used for prefixing messages. +const char kIncomingMsgKeyStart[] = "incoming1-"; +// Key guaranteed to be higher than all incoming message keys. +// Used for limiting iteration. +const char kIncomingMsgKeyEnd[] = "incoming2-"; +// Lowest lexicographically ordered outgoing message key. +// Used for prefixing outgoing messages. +const char kOutgoingMsgKeyStart[] = "outgoing1-"; +// Key guaranteed to be higher than all outgoing message keys. +// Used for limiting iteration. +const char kOutgoingMsgKeyEnd[] = "outgoing2-"; + +std::string MakeIncomingKey(const std::string& persistent_id) { + return kIncomingMsgKeyStart + persistent_id; +} + +std::string MakeOutgoingKey(const std::string& persistent_id) { + return kOutgoingMsgKeyStart + persistent_id; +} + +std::string ParseOutgoingKey(const std::string& key) { + return key.substr(arraysize(kOutgoingMsgKeyStart) - 1); +} + +leveldb::Slice MakeSlice(const base::StringPiece& s) { + return leveldb::Slice(s.begin(), s.size()); +} + +} // namespace + +class RMQStore::Backend : public base::RefCountedThreadSafe<RMQStore::Backend> { + public: + Backend(const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> foreground_runner); + + // Blocking implementations of RMQStore methods. + void Load(const LoadCallback& callback); + void Destroy(const UpdateCallback& callback); + void SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback); + void AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback); + void RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback); + void AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback); + void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback); + + private: + friend class base::RefCountedThreadSafe<Backend>; + ~Backend(); + + bool LoadDeviceCredentials(uint64* android_id, uint64* security_token); + bool LoadIncomingMessages(std::vector<std::string>* incoming_messages); + bool LoadOutgoingMessages( + std::map<std::string, google::protobuf::MessageLite*>* outgoing_messages); + + const base::FilePath path_; + scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_; + + scoped_ptr<leveldb::DB> db_; +}; + +RMQStore::Backend::Backend( + const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> foreground_task_runner) + : path_(path), + foreground_task_runner_(foreground_task_runner) { +} + +RMQStore::Backend::~Backend() { +} + +void RMQStore::Backend::Load(const LoadCallback& callback) { + LoadResult result; + + leveldb::Options options; + options.create_if_missing = true; + leveldb::DB* db; + leveldb::Status status = leveldb::DB::Open(options, + path_.AsUTF8Unsafe(), + &db); + if (!status.ok()) { + LOG(ERROR) << "Failed to open database " << path_.value() + << ": " << status.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, result)); + return; + } + db_.reset(db); + + if (!LoadDeviceCredentials(&result.device_android_id, + &result.device_security_token) || + !LoadIncomingMessages(&result.incoming_messages) || + !LoadOutgoingMessages(&result.outgoing_messages)) { + result.device_android_id = 0; + result.device_security_token = 0; + result.incoming_messages.clear(); + STLDeleteContainerPairSecondPointers(result.outgoing_messages.begin(), + result.outgoing_messages.end()); + result.outgoing_messages.clear(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, result)); + return; + } + + DVLOG(1) << "Succeeded in loading " << result.incoming_messages.size() + << " unacknowledged incoming messages and " + << result.outgoing_messages.size() + << " unacknowledged outgoing messages."; + result.success = true; + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, result)); + return; +} + +void RMQStore::Backend::Destroy(const UpdateCallback& callback) { + DVLOG(1) << "Destroying RMQ store."; + const leveldb::Status s = + leveldb::DestroyDB(path_.AsUTF8Unsafe(), + leveldb::Options()); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "Destroy failed."; + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); +} + +void RMQStore::Backend::SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) { + DVLOG(1) << "Saving device credentials with AID " << device_android_id; + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string encrypted_token; + Encryptor::EncryptString(base::Uint64ToString(device_security_token), + &encrypted_token); + leveldb::Status s = + db_->Put(write_options, + MakeSlice(kDeviceAIDKey), + MakeSlice(base::Uint64ToString(device_android_id))); + if (s.ok()) { + s = db_->Put(write_options, + MakeSlice(kDeviceTokenKey), + MakeSlice(encrypted_token)); + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); +} + +void RMQStore::Backend::AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + DVLOG(1) << "Saving incoming message with id " << persistent_id; + leveldb::WriteOptions write_options; + write_options.sync = true; + + const leveldb::Status s = + db_->Put(write_options, + MakeSlice(MakeIncomingKey(persistent_id)), + MakeSlice(persistent_id)); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); +} + +void RMQStore::Backend::RemoveIncomingMessages( + const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + leveldb::WriteOptions write_options; + write_options.sync = true; + + leveldb::Status s; + for (PersistentIdList::const_iterator iter = persistent_ids.begin(); + iter != persistent_ids.end(); ++iter){ + DVLOG(1) << "Removing incoming message with id " << *iter; + s = db_->Delete(write_options, + MakeSlice(MakeIncomingKey(*iter))); + if (!s.ok()) + break; + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); +} + +void RMQStore::Backend::AddOutgoingMessage( + const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) { + DVLOG(1) << "Saving outgoing message with id " << persistent_id; + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string data = static_cast<char>(message.tag()) + + message.SerializeAsString(); + const leveldb::Status s = + db_->Put(write_options, + MakeSlice(MakeOutgoingKey(persistent_id)), + MakeSlice(data)); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); + +} + +void RMQStore::Backend::RemoveOutgoingMessages( + const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + leveldb::WriteOptions write_options; + write_options.sync = true; + + leveldb::Status s; + for (PersistentIdList::const_iterator iter = persistent_ids.begin(); + iter != persistent_ids.end(); ++iter){ + DVLOG(1) << "Removing outgoing message with id " << *iter; + s = db_->Delete(write_options, + MakeSlice(MakeOutgoingKey(*iter))); + if (!s.ok()) + break; + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, false)); +} + +bool RMQStore::Backend::LoadDeviceCredentials(uint64* android_id, + uint64* security_token) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + std::string result; + leveldb::Status s = db_->Get(read_options, + MakeSlice(kDeviceAIDKey), + &result); + if (s.ok()) { + if (!base::StringToUint64(result, android_id)) { + LOG(ERROR) << "Failed to restore device id."; + return false; + } + result.clear(); + s = db_->Get(read_options, + MakeSlice(kDeviceTokenKey), + &result); + } + if (s.ok()) { + std::string decrypted_token; + Encryptor::DecryptString(result, &decrypted_token); + if (!base::StringToUint64(decrypted_token, security_token)) { + LOG(ERROR) << "Failed to restore security token."; + return false; + } + return true; + } + + if (s.IsNotFound()) { + DVLOG(1) << "No credentials found."; + return true; + } + + LOG(ERROR) << "Error reading credentials from store."; + return false; +} + +bool RMQStore::Backend::LoadIncomingMessages( + std::vector<std::string>* incoming_messages) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kIncomingMsgKeyStart)); + iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd; + iter->Next()) { + leveldb::Slice s = iter->value(); + if (s.empty()) { + LOG(ERROR) << "Error reading incoming message with key " + << iter->key().ToString(); + return false; + } + DVLOG(1) << "Found incoming message with id " << s.ToString(); + incoming_messages->push_back(s.ToString()); + } + + return true; +} + +bool RMQStore::Backend::LoadOutgoingMessages( + std::map<std::string, google::protobuf::MessageLite*>* + outgoing_messages) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart)); + iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd; + iter->Next()) { + leveldb::Slice s = iter->value(); + if (s.size() <= 1) { + LOG(ERROR) << "Error reading incoming message with key " << s.ToString(); + return false; + } + uint8 tag = iter->value().data()[0]; + std::string id = ParseOutgoingKey(iter->key().ToString()); + scoped_ptr<google::protobuf::MessageLite> message( + BuildProtobufFromTag(tag)); + if (!message.get() || + !message->ParseFromString(iter->value().ToString().substr(1))) { + LOG(ERROR) << "Failed to parse outgoing message with id " + << id << " and tag " << tag; + return false; + } + DVLOG(1) << "Found outgoing message with id " << id << " of type " + << base::IntToString(tag); + (*outgoing_messages)[id] = message.release(); + } + + return true; +} + +RMQStore::LoadResult::LoadResult() + : success(false), + device_android_id(0), + device_security_token(0) { +} +RMQStore::LoadResult::~LoadResult() {} + +RMQStore::RMQStore( + const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) + : backend_(new Backend(path, base::MessageLoopProxy::current())), + blocking_task_runner_(blocking_task_runner) { +} + +RMQStore::~RMQStore() { +} + +void RMQStore::Load(const LoadCallback& callback) { + blocking_task_runner_->PostTask(FROM_HERE, + base::Bind(&RMQStore::Backend::Load, + backend_, + callback)); +} + +void RMQStore::Destroy(const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::Destroy, + backend_, + callback)); +} + +void RMQStore::SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::SetDeviceCredentials, + backend_, + device_android_id, + device_security_token, + callback)); +} + +void RMQStore::AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::AddIncomingMessage, + backend_, + persistent_id, + callback)); +} + +void RMQStore::RemoveIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::RemoveIncomingMessages, + backend_, + PersistentIdList(1, persistent_id), + callback)); +} + +void RMQStore::RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::RemoveIncomingMessages, + backend_, + persistent_ids, + callback)); +} + +void RMQStore::AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::AddOutgoingMessage, + backend_, + persistent_id, + message, + callback)); +} + +void RMQStore::RemoveOutgoingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::RemoveOutgoingMessages, + backend_, + PersistentIdList(1, persistent_id), + callback)); +} + +void RMQStore::RemoveOutgoingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RMQStore::Backend::RemoveOutgoingMessages, + backend_, + persistent_ids, + callback)); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/rmq_store.h b/chromium/google_apis/gcm/engine/rmq_store.h new file mode 100644 index 00000000000..d3762a199c7 --- /dev/null +++ b/chromium/google_apis/gcm/engine/rmq_store.h @@ -0,0 +1,102 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ +#define GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/ref_counted.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace google { +namespace protobuf { +class MessageLite; +} // namespace protobuf +} // namespace google + +namespace gcm { + +class MCSMessage; + +// A Reliable Message Queue store. +// Will perform all blocking operations on the blocking task runner, and will +// post all callbacks to the thread on which the RMQStore is created. +class GCM_EXPORT RMQStore { + public: + // Container for Load(..) results. + struct GCM_EXPORT LoadResult { + LoadResult(); + ~LoadResult(); + + bool success; + uint64 device_android_id; + uint64 device_security_token; + std::vector<std::string> incoming_messages; + std::map<std::string, google::protobuf::MessageLite*> + outgoing_messages; + }; + + typedef std::vector<std::string> PersistentIdList; + // Note: callee receives ownership of |outgoing_messages|' values. + typedef base::Callback<void(const LoadResult& result)> LoadCallback; + typedef base::Callback<void(bool success)> UpdateCallback; + + RMQStore(const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner); + ~RMQStore(); + + // Load the directory and pass the initial state back to caller. + void Load(const LoadCallback& callback); + + // Clears the RMQ store of all data and destroys any LevelDB files associated + // with this store. + // WARNING: this will permanently destroy any pending outgoing messages + // and require the device to re-create credentials. + void Destroy(const UpdateCallback& callback); + + // Sets this device's messaging credentials. + void SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback); + + // Unacknowledged incoming message handling. + void AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback); + void RemoveIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback); + void RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback); + + // Unacknowledged outgoing messages handling. + // TODO(zea): implement per-app limits on the number of outgoing messages. + void AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback); + void RemoveOutgoingMessage(const std::string& persistent_id, + const UpdateCallback& callback); + void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback); + + private: + class Backend; + + scoped_refptr<Backend> backend_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + + DISALLOW_COPY_AND_ASSIGN(RMQStore); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ diff --git a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc new file mode 100644 index 00000000000..1fd55bcf14b --- /dev/null +++ b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc @@ -0,0 +1,303 @@ +// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "components/webdata/encryptor/encryptor.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +// Number of persistent ids to use in tests. +const int kNumPersistentIds = 10; + +const uint64 kDeviceId = 22; +const uint64 kDeviceToken = 55; + +class RMQStoreTest : public testing::Test { + public: + RMQStoreTest(); + virtual ~RMQStoreTest(); + + scoped_ptr<RMQStore> BuildRMQStore(); + + std::string GetNextPersistentId(); + + void PumpLoop(); + + void LoadCallback(RMQStore::LoadResult* result_dst, + const RMQStore::LoadResult& result); + void UpdateCallback(bool success); + + private: + base::MessageLoop message_loop_; + base::ScopedTempDir temp_directory_; + scoped_ptr<base::RunLoop> run_loop_; +}; + +RMQStoreTest::RMQStoreTest() { + EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); + run_loop_.reset(new base::RunLoop()); + + // On OSX, prevent the Keychain permissions popup during unit tests. + #if defined(OS_MACOSX) + Encryptor::UseMockKeychain(true); + #endif +} + +RMQStoreTest::~RMQStoreTest() { +} + +scoped_ptr<RMQStore> RMQStoreTest::BuildRMQStore() { + return scoped_ptr<RMQStore>(new RMQStore(temp_directory_.path(), + message_loop_.message_loop_proxy())); +} + +std::string RMQStoreTest::GetNextPersistentId() { + return base::Uint64ToString(base::Time::Now().ToInternalValue()); +} + +void RMQStoreTest::PumpLoop() { + message_loop_.RunUntilIdle(); +} + +void RMQStoreTest::LoadCallback(RMQStore::LoadResult* result_dst, + const RMQStore::LoadResult& result) { + ASSERT_TRUE(result.success); + *result_dst = result; + run_loop_->Quit(); + run_loop_.reset(new base::RunLoop()); +} + +void RMQStoreTest::UpdateCallback(bool success) { + ASSERT_TRUE(success); +} + +// Verify creating a new database and loading it. +TEST_F(RMQStoreTest, LoadNew) { + scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); + RMQStore::LoadResult load_result; + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_EQ(0U, load_result.device_android_id); + ASSERT_EQ(0U, load_result.device_security_token); + ASSERT_TRUE(load_result.incoming_messages.empty()); + ASSERT_TRUE(load_result.outgoing_messages.empty()); +} + +TEST_F(RMQStoreTest, DeviceCredentials) { + scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); + RMQStore::LoadResult load_result; + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + rmq_store->SetDeviceCredentials(kDeviceId, + kDeviceToken, + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + rmq_store = BuildRMQStore().Pass(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_EQ(kDeviceId, load_result.device_android_id); + ASSERT_EQ(kDeviceToken, load_result.device_security_token); +} + +// Verify saving some incoming messages, reopening the directory, and then +// removing those incoming messages. +TEST_F(RMQStoreTest, IncomingMessages) { + scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); + RMQStore::LoadResult load_result; + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + rmq_store->AddIncomingMessage(persistent_ids.back(), + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + } + + rmq_store = BuildRMQStore().Pass(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_EQ(persistent_ids, load_result.incoming_messages); + ASSERT_TRUE(load_result.outgoing_messages.empty()); + + rmq_store->RemoveIncomingMessages(persistent_ids, + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + rmq_store = BuildRMQStore().Pass(); + load_result.incoming_messages.clear(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result.incoming_messages.empty()); + ASSERT_TRUE(load_result.outgoing_messages.empty()); +} + +// Verify saving some outgoing messages, reopening the directory, and then +// removing those outgoing messages. +TEST_F(RMQStoreTest, OutgoingMessages) { + scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); + RMQStore::LoadResult load_result; + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + const int kNumPersistentIds = 10; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + mcs_proto::DataMessageStanza message; + message.set_from(persistent_ids.back()); + message.set_category(persistent_ids.back()); + rmq_store->AddOutgoingMessage(persistent_ids.back(), + MCSMessage(message), + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + } + + rmq_store = BuildRMQStore().Pass(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result.incoming_messages.empty()); + ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size()); + for (int i =0 ; i < kNumPersistentIds; ++i) { + std::string id = persistent_ids[i]; + ASSERT_TRUE(load_result.outgoing_messages[id]); + const mcs_proto::DataMessageStanza* message = + reinterpret_cast<mcs_proto::DataMessageStanza *>( + load_result.outgoing_messages[id]); + ASSERT_EQ(message->from(), id); + ASSERT_EQ(message->category(), id); + } + + rmq_store->RemoveOutgoingMessages(persistent_ids, + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + rmq_store = BuildRMQStore().Pass(); + load_result.outgoing_messages.clear(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result.incoming_messages.empty()); + ASSERT_TRUE(load_result.outgoing_messages.empty()); +} + +// Verify incoming and outgoing messages don't conflict. +TEST_F(RMQStoreTest, IncomingAndOutgoingMessages) { + scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); + RMQStore::LoadResult load_result; + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + const int kNumPersistentIds = 10; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + rmq_store->AddIncomingMessage(persistent_ids.back(), + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + mcs_proto::DataMessageStanza message; + message.set_from(persistent_ids.back()); + message.set_category(persistent_ids.back()); + rmq_store->AddOutgoingMessage(persistent_ids.back(), + MCSMessage(message), + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + } + + + rmq_store = BuildRMQStore().Pass(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_EQ(persistent_ids, load_result.incoming_messages); + ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size()); + for (int i =0 ; i < kNumPersistentIds; ++i) { + std::string id = persistent_ids[i]; + ASSERT_TRUE(load_result.outgoing_messages[id]); + const mcs_proto::DataMessageStanza* message = + reinterpret_cast<mcs_proto::DataMessageStanza *>( + load_result.outgoing_messages[id]); + ASSERT_EQ(message->from(), id); + ASSERT_EQ(message->category(), id); + } + + rmq_store->RemoveIncomingMessages(persistent_ids, + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + rmq_store->RemoveOutgoingMessages(persistent_ids, + base::Bind(&RMQStoreTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + rmq_store = BuildRMQStore().Pass(); + load_result.incoming_messages.clear(); + load_result.outgoing_messages.clear(); + rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result.incoming_messages.empty()); + ASSERT_TRUE(load_result.outgoing_messages.empty()); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/google_apis/gcm/gcm.gyp b/chromium/google_apis/gcm/gcm.gyp new file mode 100644 index 00000000000..f81c4ef4493 --- /dev/null +++ b/chromium/google_apis/gcm/gcm.gyp @@ -0,0 +1,126 @@ +# Copyright 2013 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. + +{ + 'variables': { + 'chromium_code': 1, + }, + + 'targets': [ + # The public GCM target. + { + 'target_name': 'gcm', + 'type': '<(component)', + 'variables': { + 'enable_wexit_time_destructors': 1, + 'proto_in_dir': './protocol', + 'proto_out_dir': 'google_apis/gcm/protocol', + 'cc_generator_options': 'dllexport_decl=GCM_EXPORT:', + 'cc_include': 'google_apis/gcm/base/gcm_export.h', + }, + 'include_dirs': [ + '../..', + ], + 'defines': [ + 'GCM_IMPLEMENTATION', + ], + 'export_dependent_settings': [ + '../../third_party/protobuf/protobuf.gyp:protobuf_lite' + ], + 'dependencies': [ + '../../base/base.gyp:base', + '../../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations', + '../../components/components.gyp:encryptor', + '../../net/net.gyp:net', + '../../third_party/leveldatabase/leveldatabase.gyp:leveldatabase', + '../../third_party/protobuf/protobuf.gyp:protobuf_lite', + '../../url/url.gyp:url_lib', + ], + 'sources': [ + 'base/mcs_message.h', + 'base/mcs_message.cc', + 'base/mcs_util.h', + 'base/mcs_util.cc', + 'base/socket_stream.h', + 'base/socket_stream.cc', + 'engine/connection_factory.h', + 'engine/connection_factory.cc', + 'engine/connection_factory_impl.h', + 'engine/connection_factory_impl.cc', + 'engine/connection_handler.h', + 'engine/connection_handler.cc', + 'engine/connection_handler_impl.h', + 'engine/connection_handler_impl.cc', + 'engine/mcs_client.h', + 'engine/mcs_client.cc', + 'engine/rmq_store.h', + 'engine/rmq_store.cc', + 'gcm_client.cc', + 'gcm_client.h', + 'gcm_client_impl.cc', + 'gcm_client_impl.h', + 'protocol/mcs.proto', + ], + 'includes': [ + '../../build/protoc.gypi' + ], + }, + + # A standalone MCS (mobile connection server) client. + { + 'target_name': 'mcs_probe', + 'type': 'executable', + 'variables': { 'enable_wexit_time_destructors': 1, }, + 'include_dirs': [ + '../..', + ], + 'dependencies': [ + '../../base/base.gyp:base', + '../../net/net.gyp:net', + '../../net/net.gyp:net_test_support', + '../../third_party/protobuf/protobuf.gyp:protobuf_lite', + 'gcm' + ], + 'sources': [ + 'tools/mcs_probe.cc', + ], + }, + + # The main GCM unit tests. + { + 'target_name': 'gcm_unit_tests', + 'type': '<(gtest_target_type)', + 'variables': { 'enable_wexit_time_destructors': 1, }, + 'include_dirs': [ + '../..', + ], + 'export_dependent_settings': [ + '../../third_party/protobuf/protobuf.gyp:protobuf_lite' + ], + 'dependencies': [ + '../../base/base.gyp:run_all_unittests', + '../../base/base.gyp:base', + '../../components/components.gyp:encryptor', + '../../net/net.gyp:net', + '../../net/net.gyp:net_test_support', + '../../testing/gtest.gyp:gtest', + '../../third_party/protobuf/protobuf.gyp:protobuf_lite', + 'gcm' + ], + 'sources': [ + 'base/mcs_message_unittest.cc', + 'base/mcs_util_unittest.cc', + 'base/socket_stream_unittest.cc', + 'engine/connection_factory_impl_unittest.cc', + 'engine/connection_handler_impl_unittest.cc', + 'engine/fake_connection_factory.h', + 'engine/fake_connection_factory.cc', + 'engine/fake_connection_handler.h', + 'engine/fake_connection_handler.cc', + 'engine/mcs_client_unittest.cc', + 'engine/rmq_store_unittest.cc', + ] + }, + ], +} diff --git a/chromium/google_apis/gcm/gcm_client.cc b/chromium/google_apis/gcm/gcm_client.cc new file mode 100644 index 00000000000..e437a305e47 --- /dev/null +++ b/chromium/google_apis/gcm/gcm_client.cc @@ -0,0 +1,45 @@ +// Copyright 2013 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 "google_apis/gcm/gcm_client.h" + +#include "base/lazy_instance.h" +#include "google_apis/gcm/gcm_client_impl.h" + +namespace gcm { + +namespace { + +static base::LazyInstance<GCMClientImpl>::Leaky g_gcm_client = + LAZY_INSTANCE_INITIALIZER; +static GCMClient* g_gcm_client_override = NULL; + +} // namespace + +GCMClient::OutgoingMessage::OutgoingMessage() + : time_to_live(0) { +} + +GCMClient::OutgoingMessage::~OutgoingMessage() { +} + +GCMClient::IncomingMessage::IncomingMessage() { +} + +GCMClient::IncomingMessage::~IncomingMessage() { +} + +// static +GCMClient* GCMClient::Get() { + if (g_gcm_client_override) + return g_gcm_client_override; + return g_gcm_client.Pointer(); +} + +// static +void GCMClient::SetForTesting(GCMClient* client) { + g_gcm_client_override = client; +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/gcm_client.h b/chromium/google_apis/gcm/gcm_client.h new file mode 100644 index 00000000000..66e17a04489 --- /dev/null +++ b/chromium/google_apis/gcm/gcm_client.h @@ -0,0 +1,193 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_GCM_CLIENT_H_ +#define GOOGLE_APIS_GCM_GCM_CLIENT_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace base { +class TaskRunner; +} + +namespace gcm { + +// Interface that encapsulates the network communications with the Google Cloud +// Messaging server. This interface is not supposed to be thread-safe. +class GCM_EXPORT GCMClient { + public: + enum Result { + // Successful operation. + SUCCESS, + // Invalid parameter. + INVALID_PARAMETER, + // Previous asynchronous operation is still pending to finish. Certain + // operation, like register, is only allowed one at a time. + ASYNC_OPERATION_PENDING, + // Network socket error. + NETWORK_ERROR, + // Problem at the server. + SERVER_ERROR, + // Exceeded the specified TTL during message sending. + TTL_EXCEEDED, + // Other errors. + UNKNOWN_ERROR + }; + + // Message data consisting of key-value pairs. + typedef std::map<std::string, std::string> MessageData; + + // Message to be delivered to the other party. + struct GCM_EXPORT OutgoingMessage { + OutgoingMessage(); + ~OutgoingMessage(); + + // Message ID. + std::string id; + // In seconds. + int time_to_live; + MessageData data; + }; + + // Message being received from the other party. + struct GCM_EXPORT IncomingMessage { + IncomingMessage(); + ~IncomingMessage(); + + MessageData data; + }; + + // The check-in info for the user. Returned by the server. + struct GCM_EXPORT CheckInInfo { + CheckInInfo() : android_id(0), secret(0) {} + bool IsValid() const { return android_id != 0 && secret != 0; } + void Reset() { + android_id = 0; + secret = 0; + } + + uint64 android_id; + uint64 secret; + }; + + // A delegate interface that allows the GCMClient instance to interact with + // its caller, i.e. notifying asynchronous event. + class Delegate { + public: + // Called when the user has been checked in successfully or an error occurs. + // |checkin_info|: valid if the checkin completed successfully. + // |result|: the type of the error if an error occured, success otherwise. + virtual void OnCheckInFinished(const CheckInInfo& checkin_info, + Result result) = 0; + + // Called when the registration completed successfully or an error occurs. + // |app_id|: application ID. + // |registration_id|: non-empty if the registration completed successfully. + // |result|: the type of the error if an error occured, success otherwise. + virtual void OnRegisterFinished(const std::string& app_id, + const std::string& registration_id, + Result result) = 0; + + // Called when the message is scheduled to send successfully or an error + // occurs. + // |app_id|: application ID. + // |message_id|: ID of the message being sent. + // |result|: the type of the error if an error occured, success otherwise. + virtual void OnSendFinished(const std::string& app_id, + const std::string& message_id, + Result result) = 0; + + // Called when a message has been received. + // |app_id|: application ID. + // |message|: message received. + virtual void OnMessageReceived(const std::string& app_id, + const IncomingMessage& message) = 0; + + // Called when some messages have been deleted from the server. + // |app_id|: application ID. + virtual void OnMessagesDeleted(const std::string& app_id) = 0; + + // Called when a message failed to send to the server. + // |app_id|: application ID. + // |message_id|: ID of the message being sent. + // |result|: the type of the error if an error occured, success otherwise. + virtual void OnMessageSendError(const std::string& app_id, + const std::string& message_id, + Result result) = 0; + + // Returns the checkin info associated with this user. The delegate class + // is expected to persist the checkin info that is provided by + // OnCheckInFinished. + virtual CheckInInfo GetCheckInInfo() const = 0; + + // Called when the loading from the persistent store is done. The loading + // is triggered asynchronously when GCMClient is created. + virtual void OnLoadingCompleted() = 0; + + // Returns a task runner for file operations that may block. This is used + // in writing to or reading from the persistent store. + virtual base::TaskRunner* GetFileTaskRunner() = 0; + }; + + // Returns the single instance. Multiple profiles share the same client + // that makes use of the same MCS connection. + static GCMClient* Get(); + + // Passes a mocked instance for testing purpose. + static void SetForTesting(GCMClient* client); + + // Checks in the user to use GCM. If the device has not been checked in, it + // will be done first. + // |username|: the username (email address) used to check in with the server. + // |delegate|: the delegate whose methods will be called asynchronously in + // response to events and messages. + virtual void CheckIn(const std::string& username, Delegate* delegate) = 0; + + // Registers the application for GCM. Delegate::OnRegisterFinished will be + // called asynchronously upon completion. + // |username|: the username (email address) passed in CheckIn. + // |app_id|: application ID. + // |cert|: SHA-1 of public key of the application, in base16 format. + // |sender_ids|: list of IDs of the servers that are allowed to send the + // messages to the application. These IDs are assigned by the + // Google API Console. + virtual void Register(const std::string& username, + const std::string& app_id, + const std::string& cert, + const std::vector<std::string>& sender_ids) = 0; + + // Unregisters the application from GCM when it is uninstalled. + // Delegate::OnUnregisterFinished will be called asynchronously upon + // completion. + // |username|: the username (email address) passed in CheckIn. + // |app_id|: application ID. + virtual void Unregister(const std::string& username, + const std::string& app_id) = 0; + + // Sends a message to a given receiver. Delegate::OnSendFinished will be + // called asynchronously upon completion. + // |username|: the username (email address) passed in CheckIn. + // |app_id|: application ID. + // |receiver_id|: registration ID of the receiver party. + // |message|: message to be sent. + virtual void Send(const std::string& username, + const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) = 0; + + // Returns true if the loading from the persistent store is still in progress. + virtual bool IsLoading() const = 0; + + protected: + virtual ~GCMClient() {} +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_GCM_CLIENT_H_ diff --git a/chromium/google_apis/gcm/gcm_client_impl.cc b/chromium/google_apis/gcm/gcm_client_impl.cc new file mode 100644 index 00000000000..76900cda747 --- /dev/null +++ b/chromium/google_apis/gcm/gcm_client_impl.cc @@ -0,0 +1,39 @@ +// Copyright 2013 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 "google_apis/gcm/gcm_client_impl.h" + +namespace gcm { + +GCMClientImpl::GCMClientImpl() { +} + +GCMClientImpl::~GCMClientImpl() { +} + +void GCMClientImpl::CheckIn(const std::string& username, + Delegate* delegate) { +} + +void GCMClientImpl::Register(const std::string& username, + const std::string& app_id, + const std::string& cert, + const std::vector<std::string>& sender_ids) { +} + +void GCMClientImpl::Unregister(const std::string& username, + const std::string& app_id) { +} + +void GCMClientImpl::Send(const std::string& username, + const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { +} + +bool GCMClientImpl::IsLoading() const { + return false; +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/gcm_client_impl.h b/chromium/google_apis/gcm/gcm_client_impl.h new file mode 100644 index 00000000000..46f910e546a --- /dev/null +++ b/chromium/google_apis/gcm/gcm_client_impl.h @@ -0,0 +1,39 @@ +// Copyright 2013 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. + +#ifndef GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ +#define GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ + +#include "base/compiler_specific.h" +#include "google_apis/gcm/gcm_client.h" + +namespace gcm { + +class GCMClientImpl : public GCMClient { + public: + GCMClientImpl(); + virtual ~GCMClientImpl(); + + // Overridden from GCMClient: + virtual void CheckIn(const std::string& username, + Delegate* delegate) OVERRIDE; + virtual void Register(const std::string& username, + const std::string& app_id, + const std::string& cert, + const std::vector<std::string>& sender_ids) OVERRIDE; + virtual void Unregister(const std::string& username, + const std::string& app_id) OVERRIDE; + virtual void Send(const std::string& username, + const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) OVERRIDE; + virtual bool IsLoading() const OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(GCMClientImpl); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ diff --git a/chromium/google_apis/gcm/protocol/mcs.proto b/chromium/google_apis/gcm/protocol/mcs.proto new file mode 100644 index 00000000000..2926d1037f3 --- /dev/null +++ b/chromium/google_apis/gcm/protocol/mcs.proto @@ -0,0 +1,269 @@ +// Copyright 2013 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. +// +// MCS protocol for communication between Chrome client and Mobile Connection +// Server . + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option retain_unknown_fields = true; + +package mcs_proto; + +/* + Common fields/comments: + + stream_id: no longer sent by server, each side keeps a counter + last_stream_id_received: sent only if a packet was received since last time + a last_stream was sent + status: new bitmask including the 'idle' as bit 0. + + */ + +/** + TAG: 0 + */ +message HeartbeatPing { + optional int32 stream_id = 1; + optional int32 last_stream_id_received = 2; + optional int64 status = 3; +} + +/** + TAG: 1 + */ +message HeartbeatAck { + optional int32 stream_id = 1; + optional int32 last_stream_id_received = 2; + optional int64 status = 3; +} + +message ErrorInfo { + required int32 code = 1; + optional string message = 2; + optional string type = 3; + optional Extension extension = 4; +} + +// MobileSettings class. +// "u:f", "u:b", "u:s" - multi user devices reporting foreground, background +// and stopped users. +// hbping: heatbeat ping interval +// rmq2v: include explicit stream IDs + +message Setting { + required string name = 1; + required string value = 2; +} + +message HeartbeatStat { + required string ip = 1; + required bool timeout = 2; + required int32 interval_ms = 3; +} + +message HeartbeatConfig { + optional bool upload_stat = 1; + optional string ip = 2; + optional int32 interval_ms = 3; +} + +/** + TAG: 2 + */ +message LoginRequest { + enum AuthService { + ANDROID_ID = 2; + } + required string id = 1; // Must be present ( proto required ), may be empty + // string. + // mcs.android.com. + required string domain = 2; + // Decimal android ID + required string user = 3; + + required string resource = 4; + + // Secret + required string auth_token = 5; + + // Format is: android-HEX_DEVICE_ID + // The user is the decimal value. + optional string device_id = 6; + + // RMQ1 - no longer used + optional int64 last_rmq_id = 7; + + repeated Setting setting = 8; + //optional int32 compress = 9; + repeated string received_persistent_id = 10; + + // Replaced by "rmq2v" setting + // optional bool include_stream_ids = 11; + + optional bool adaptive_heartbeat = 12; + optional HeartbeatStat heartbeat_stat = 13; + // Must be true. + optional bool use_rmq2 = 14; + optional int64 account_id = 15; + + // ANDROID_ID = 2 + optional AuthService auth_service = 16; + + optional int32 network_type = 17; + optional int64 status = 18; +} + +/** + * TAG: 3 + */ +message LoginResponse { + required string id = 1; + // Not used. + optional string jid = 2; + // Null if login was ok. + optional ErrorInfo error = 3; + repeated Setting setting = 4; + optional int32 stream_id = 5; + // Should be "1" + optional int32 last_stream_id_received = 6; + optional HeartbeatConfig heartbeat_config = 7; + // used by the client to synchronize with the server timestamp. + optional int64 server_timestamp = 8; +} + +message StreamErrorStanza { + required string type = 1; + optional string text = 2; +} + +/** + * TAG: 4 + */ +message Close { +} + +message Extension { + // 12: SelectiveAck + // 13: StreamAck + required int32 id = 1; + required bytes data = 2; +} + +/** + * TAG: 7 + * IqRequest must contain a single extension. IqResponse may contain 0 or 1 + * extensions. + */ +message IqStanza { + enum IqType { + GET = 0; + SET = 1; + RESULT = 2; + IQ_ERROR = 3; + } + + optional int64 rmq_id = 1; + required IqType type = 2; + required string id = 3; + optional string from = 4; + optional string to = 5; + optional ErrorInfo error = 6; + + // Only field used in the 38+ protocol (besides common last_stream_id_received, status, rmq_id) + optional Extension extension = 7; + + optional string persistent_id = 8; + optional int32 stream_id = 9; + optional int32 last_stream_id_received = 10; + optional int64 account_id = 11; + optional int64 status = 12; +} + +message AppData { + required string key = 1; + required string value = 2; +} + +/** + * TAG: 8 + */ +message DataMessageStanza { + // Not used. + // optional int64 rmq_id = 1; + + // This is the message ID, set by client, DMP.9 (message_id) + optional string id = 2; + + // Project ID of the sender, DMP.1 + required string from = 3; + + // Part of DMRequest - also the key in DataMessageProto. + optional string to = 4; + + // Package name. DMP.2 + required string category = 5; + + // The collapsed key, DMP.3 + optional string token = 6; + + // User data + GOOGLE. prefixed special entries, DMP.4 + repeated AppData app_data = 7; + + // Not used. + optional bool from_trusted_server = 8; + + // Part of the ACK protocol, returned in DataMessageResponse on server side. + // It's part of the key of DMP. + optional string persistent_id = 9; + + // In-stream ack. Increments on each message sent - a bit redundant + // Not used in DMP/DMR. + optional int32 stream_id = 10; + optional int32 last_stream_id_received = 11; + + // Not used. + // optional string permission = 12; + + // Sent by the device shortly after registration. + optional string reg_id = 13; + + // Not used. + // optional string pkg_signature = 14; + // Not used. + // optional string client_id = 15; + + // serial number of the target user, DMP.8 + // It is the 'serial number' according to user manager. + optional int64 device_user_id = 16; + + // Time to live, in seconds. + optional int32 ttl = 17; + // Timestamp ( according to client ) when message was sent by app, in seconds + optional int64 sent = 18; + + // How long has the message been queued before the flush, in seconds. + // This is needed to account for the time difference between server and + // client: server should adjust 'sent' based on his 'receive' time. + optional int32 queued = 19; + + optional int64 status = 20; +} + +/** + Included in IQ with ID 13, sent from client or server after 10 unconfirmed + messages. + */ +message StreamAck { + // No last_streamid_received required. This is included within an IqStanza, + // which includes the last_stream_id_received. +} + +/** + Included in IQ sent after LoginResponse from server with ID 12. +*/ +message SelectiveAck { + repeated string id = 1; +} diff --git a/chromium/google_apis/gcm/tools/mcs_probe.cc b/chromium/google_apis/gcm/tools/mcs_probe.cc new file mode 100644 index 00000000000..bc4ad7cad3a --- /dev/null +++ b/chromium/google_apis/gcm/tools/mcs_probe.cc @@ -0,0 +1,372 @@ +// Copyright 2013 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. +// +// A standalone tool for testing MCS connections and the MCS client on their +// own. + +#include <cstddef> +#include <cstdio> +#include <string> + +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/threading/thread.h" +#include "base/threading/worker_pool.h" +#include "base/values.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/engine/connection_factory_impl.h" +#include "google_apis/gcm/engine/mcs_client.h" +#include "net/base/host_mapping_rules.h" +#include "net/base/net_log_logger.h" +#include "net/cert/cert_verifier.h" +#include "net/dns/host_resolver.h" +#include "net/http/http_auth_handler_factory.h" +#include "net/http/http_network_session.h" +#include "net/http/http_server_properties_impl.h" +#include "net/http/transport_security_state.h" +#include "net/socket/client_socket_factory.h" +#include "net/socket/ssl_client_socket.h" +#include "net/ssl/default_server_bound_cert_store.h" +#include "net/ssl/server_bound_cert_service.h" +#include "net/url_request/url_request_test_util.h" + +#if defined(OS_MACOSX) +#include "base/mac/scoped_nsautorelease_pool.h" +#endif + +// This is a simple utility that initializes an mcs client and +// prints out any events. +namespace gcm { +namespace { + +// The default server to communicate with. +const char kMCSServerHost[] = "mtalk.google.com"; +const uint16 kMCSServerPort = 5228; + +// Command line switches. +const char kRMQFileName[] = "rmq_file"; +const char kAndroidIdSwitch[] = "android_id"; +const char kSecretSwitch[] = "secret"; +const char kLogFileSwitch[] = "log-file"; +const char kIgnoreCertSwitch[] = "ignore-certs"; +const char kServerHostSwitch[] = "host"; +const char kServerPortSwitch[] = "port"; + +void MessageReceivedCallback(const MCSMessage& message) { + LOG(INFO) << "Received message with id " + << GetPersistentId(message.GetProtobuf()) << " and tag " + << static_cast<int>(message.tag()); + + if (message.tag() == kDataMessageStanzaTag) { + const mcs_proto::DataMessageStanza& data_message = + reinterpret_cast<const mcs_proto::DataMessageStanza&>( + message.GetProtobuf()); + DVLOG(1) << " to: " << data_message.to(); + DVLOG(1) << " from: " << data_message.from(); + DVLOG(1) << " category: " << data_message.category(); + DVLOG(1) << " sent: " << data_message.sent(); + for (int i = 0; i < data_message.app_data_size(); ++i) { + DVLOG(1) << " App data " << i << " " + << data_message.app_data(i).key() << " : " + << data_message.app_data(i).value(); + } + } +} + +void MessageSentCallback(const std::string& local_id) { + LOG(INFO) << "Message sent. Status: " << local_id; +} + +// Needed to use a real host resolver. +class MyTestURLRequestContext : public net::TestURLRequestContext { + public: + MyTestURLRequestContext() : TestURLRequestContext(true) { + context_storage_.set_host_resolver( + net::HostResolver::CreateDefaultResolver(NULL)); + context_storage_.set_transport_security_state( + new net::TransportSecurityState()); + Init(); + } + + virtual ~MyTestURLRequestContext() {} +}; + +class MyTestURLRequestContextGetter : public net::TestURLRequestContextGetter { + public: + explicit MyTestURLRequestContextGetter( + const scoped_refptr<base::MessageLoopProxy>& io_message_loop_proxy) + : TestURLRequestContextGetter(io_message_loop_proxy) {} + + virtual net::TestURLRequestContext* GetURLRequestContext() OVERRIDE { + // Construct |context_| lazily so it gets constructed on the right + // thread (the IO thread). + if (!context_) + context_.reset(new MyTestURLRequestContext()); + return context_.get(); + } + + private: + virtual ~MyTestURLRequestContextGetter() {} + + scoped_ptr<MyTestURLRequestContext> context_; +}; + +// A net log that logs all events by default. +class MyTestNetLog : public net::NetLog { + public: + MyTestNetLog() { + SetBaseLogLevel(LOG_ALL); + } + virtual ~MyTestNetLog() {} +}; + +// A cert verifier that access all certificates. +class MyTestCertVerifier : public net::CertVerifier { + public: + MyTestCertVerifier() {} + virtual ~MyTestCertVerifier() {} + + virtual int Verify(net::X509Certificate* cert, + const std::string& hostname, + int flags, + net::CRLSet* crl_set, + net::CertVerifyResult* verify_result, + const net::CompletionCallback& callback, + RequestHandle* out_req, + const net::BoundNetLog& net_log) OVERRIDE { + return net::OK; + } + + virtual void CancelRequest(RequestHandle req) OVERRIDE { + // Do nothing. + } +}; + +class MCSProbe { + public: + MCSProbe( + const CommandLine& command_line, + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter); + ~MCSProbe(); + + void Start(); + + uint64 android_id() const { return android_id_; } + uint64 secret() const { return secret_; } + + private: + void InitializeNetworkState(); + void BuildNetworkSession(); + + void InitializationCallback(bool success, + uint64 restored_android_id, + uint64 restored_security_token); + + CommandLine command_line_; + + base::FilePath rmq_path_; + uint64 android_id_; + uint64 secret_; + std::string server_host_; + int server_port_; + + // Network state. + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + MyTestNetLog net_log_; + scoped_ptr<net::NetLogLogger> logger_; + scoped_ptr<base::Value> net_constants_; + scoped_ptr<net::HostResolver> host_resolver_; + scoped_ptr<net::CertVerifier> cert_verifier_; + scoped_ptr<net::ServerBoundCertService> system_server_bound_cert_service_; + scoped_ptr<net::TransportSecurityState> transport_security_state_; + scoped_ptr<net::URLSecurityManager> url_security_manager_; + scoped_ptr<net::HttpAuthHandlerFactory> http_auth_handler_factory_; + scoped_ptr<net::HttpServerPropertiesImpl> http_server_properties_; + scoped_ptr<net::HostMappingRules> host_mapping_rules_; + scoped_refptr<net::HttpNetworkSession> network_session_; + scoped_ptr<net::ProxyService> proxy_service_; + + scoped_ptr<MCSClient> mcs_client_; + + scoped_ptr<ConnectionFactoryImpl> connection_factory_; + + base::Thread file_thread_; + + scoped_ptr<base::RunLoop> run_loop_; +}; + +MCSProbe::MCSProbe( + const CommandLine& command_line, + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) + : command_line_(command_line), + rmq_path_(base::FilePath(FILE_PATH_LITERAL("gcm_rmq_store"))), + android_id_(0), + secret_(0), + server_port_(0), + url_request_context_getter_(url_request_context_getter), + file_thread_("FileThread") { + if (command_line.HasSwitch(kRMQFileName)) { + rmq_path_ = command_line.GetSwitchValuePath(kRMQFileName); + } + if (command_line.HasSwitch(kAndroidIdSwitch)) { + base::StringToUint64(command_line.GetSwitchValueASCII(kAndroidIdSwitch), + &android_id_); + } + if (command_line.HasSwitch(kSecretSwitch)) { + base::StringToUint64(command_line.GetSwitchValueASCII(kSecretSwitch), + &secret_); + } + server_host_ = kMCSServerHost; + if (command_line.HasSwitch(kServerHostSwitch)) { + server_host_ = command_line.GetSwitchValueASCII(kServerHostSwitch); + } + server_port_ = kMCSServerPort; + if (command_line.HasSwitch(kServerPortSwitch)) { + base::StringToInt(command_line.GetSwitchValueASCII(kServerPortSwitch), + &server_port_); + } +} + +MCSProbe::~MCSProbe() { + file_thread_.Stop(); +} + +void MCSProbe::Start() { + file_thread_.Start(); + InitializeNetworkState(); + BuildNetworkSession(); + connection_factory_.reset( + new ConnectionFactoryImpl(GURL("https://" + net::HostPortPair( + server_host_, server_port_).ToString()), + network_session_, + &net_log_)); + mcs_client_.reset(new MCSClient(rmq_path_, + connection_factory_.get(), + file_thread_.message_loop_proxy())); + run_loop_.reset(new base::RunLoop()); + mcs_client_->Initialize(base::Bind(&MCSProbe::InitializationCallback, + base::Unretained(this)), + base::Bind(&MessageReceivedCallback), + base::Bind(&MessageSentCallback)); + run_loop_->Run(); +} + +void MCSProbe::InitializeNetworkState() { + FILE* log_file = NULL; + if (command_line_.HasSwitch(kLogFileSwitch)) { + base::FilePath log_path = command_line_.GetSwitchValuePath(kLogFileSwitch); +#if defined(OS_WIN) + log_file = _wfopen(log_path.value().c_str(), L"w"); +#elif defined(OS_POSIX) + log_file = fopen(log_path.value().c_str(), "w"); +#endif + } + net_constants_.reset(net::NetLogLogger::GetConstants()); + if (log_file != NULL) { + logger_.reset(new net::NetLogLogger(log_file, *net_constants_)); + logger_->StartObserving(&net_log_); + } + + host_resolver_ = net::HostResolver::CreateDefaultResolver(&net_log_); + + if (command_line_.HasSwitch(kIgnoreCertSwitch)) { + cert_verifier_.reset(new MyTestCertVerifier()); + } else { + cert_verifier_.reset(net::CertVerifier::CreateDefault()); + } + system_server_bound_cert_service_.reset( + new net::ServerBoundCertService( + new net::DefaultServerBoundCertStore(NULL), + base::WorkerPool::GetTaskRunner(true))); + + transport_security_state_.reset(new net::TransportSecurityState()); + url_security_manager_.reset(net::URLSecurityManager::Create(NULL, NULL)); + http_auth_handler_factory_.reset( + net::HttpAuthHandlerRegistryFactory::Create( + std::vector<std::string>(1, "basic"), + url_security_manager_.get(), + host_resolver_.get(), + std::string(), + false, + false)); + http_server_properties_.reset(new net::HttpServerPropertiesImpl()); + host_mapping_rules_.reset(new net::HostMappingRules()); + proxy_service_.reset(net::ProxyService::CreateDirectWithNetLog(&net_log_)); +} + +void MCSProbe::BuildNetworkSession() { + net::HttpNetworkSession::Params session_params; + session_params.host_resolver = host_resolver_.get(); + session_params.cert_verifier = cert_verifier_.get(); + session_params.server_bound_cert_service = + system_server_bound_cert_service_.get(); + session_params.transport_security_state = transport_security_state_.get(); + session_params.ssl_config_service = new net::SSLConfigServiceDefaults(); + session_params.http_auth_handler_factory = http_auth_handler_factory_.get(); + session_params.http_server_properties = + http_server_properties_->GetWeakPtr(); + session_params.network_delegate = NULL; // TODO(zea): implement? + session_params.host_mapping_rules = host_mapping_rules_.get(); + session_params.ignore_certificate_errors = true; + session_params.http_pipelining_enabled = false; + session_params.testing_fixed_http_port = 0; + session_params.testing_fixed_https_port = 0; + session_params.net_log = &net_log_; + session_params.proxy_service = proxy_service_.get(); + + network_session_ = new net::HttpNetworkSession(session_params); +} + +void MCSProbe::InitializationCallback(bool success, + uint64 restored_android_id, + uint64 restored_security_token) { + LOG(INFO) << "Initialization " << (success ? "success!" : "failure!"); + if (restored_android_id && restored_security_token) { + android_id_ = restored_android_id; + secret_ = restored_security_token; + } + if (success) + mcs_client_->Login(android_id_, secret_); +} + +int MCSProbeMain(int argc, char* argv[]) { + base::AtExitManager exit_manager; + + CommandLine::Init(argc, argv); + logging::LoggingSettings settings; + settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG; + logging::InitLogging(settings); + + base::MessageLoopForIO message_loop; + + // For check-in and creating registration ids. + const scoped_refptr<MyTestURLRequestContextGetter> context_getter = + new MyTestURLRequestContextGetter( + base::MessageLoop::current()->message_loop_proxy()); + + const CommandLine& command_line = *CommandLine::ForCurrentProcess(); + + MCSProbe mcs_probe(command_line, context_getter); + mcs_probe.Start(); + + base::RunLoop run_loop; + run_loop.Run(); + + return 0; +} + +} // namespace +} // namespace gcm + +int main(int argc, char* argv[]) { + return gcm::MCSProbeMain(argc, argv); +} diff --git a/chromium/google_apis/google_api_keys.cc b/chromium/google_apis/google_api_keys.cc index 0baf00dc393..94f6812ab74 100644 --- a/chromium/google_apis/google_api_keys.cc +++ b/chromium/google_apis/google_api_keys.cc @@ -204,14 +204,14 @@ class APIKeyCache { std::string temp; if (environment->GetVar(environment_variable_name, &temp)) { key_value = temp; - LOG(INFO) << "Overriding API key " << environment_variable_name - << " with value " << key_value << " from environment variable."; + VLOG(1) << "Overriding API key " << environment_variable_name + << " with value " << key_value << " from environment variable."; } if (command_line_switch && command_line->HasSwitch(command_line_switch)) { key_value = command_line->GetSwitchValueASCII(command_line_switch); - LOG(INFO) << "Overriding API key " << environment_variable_name - << " with value " << key_value << " from command-line switch."; + VLOG(1) << "Overriding API key " << environment_variable_name + << " with value " << key_value << " from command-line switch."; } if (key_value == DUMMY_API_TOKEN) { @@ -222,8 +222,8 @@ class APIKeyCache { CHECK(false); #endif if (default_if_unset.size() > 0) { - LOG(INFO) << "Using default value \"" << default_if_unset - << "\" for API key " << environment_variable_name; + VLOG(1) << "Using default value \"" << default_if_unset + << "\" for API key " << environment_variable_name; key_value = default_if_unset; } } diff --git a/chromium/google_apis/google_apis.gyp b/chromium/google_apis/google_apis.gyp index 762c3695847..2d281f2b2f3 100644 --- a/chromium/google_apis/google_apis.gyp +++ b/chromium/google_apis/google_apis.gyp @@ -20,6 +20,7 @@ '../base/base.gyp:base', '../crypto/crypto.gyp:crypto', '../net/net.gyp:net', + '../third_party/libxml/libxml.gyp:libxml', ], 'conditions': [ ['google_api_key!=""', { @@ -58,6 +59,38 @@ 'cup/client_update_protocol.h', 'cup/client_update_protocol_nss.cc', 'cup/client_update_protocol_openssl.cc', + 'drive/auth_service.cc', + 'drive/auth_service.h', + 'drive/auth_service_interface.h', + 'drive/auth_service_observer.h', + 'drive/base_requests.cc', + 'drive/base_requests.h', + 'drive/drive_api_parser.cc', + 'drive/drive_api_parser.h', + 'drive/drive_api_requests.cc', + 'drive/drive_api_requests.h', + 'drive/drive_api_url_generator.cc', + 'drive/drive_api_url_generator.h', + 'drive/drive_common_callbacks.h', + 'drive/drive_entry_kinds.h', + 'drive/gdata_contacts_requests.cc', + 'drive/gdata_contacts_requests.h', + 'drive/gdata_errorcode.cc', + 'drive/gdata_errorcode.h', + 'drive/gdata_wapi_requests.cc', + 'drive/gdata_wapi_requests.h', + 'drive/gdata_wapi_parser.cc', + 'drive/gdata_wapi_parser.h', + 'drive/gdata_wapi_url_generator.cc', + 'drive/gdata_wapi_url_generator.h', + 'drive/request_sender.cc', + 'drive/request_sender.h', + 'drive/request_util.cc', + 'drive/request_util.h', + 'drive/task_util.cc', + 'drive/task_util.h', + 'drive/time_util.cc', + 'drive/time_util.h', 'gaia/gaia_auth_consumer.cc', 'gaia/gaia_auth_consumer.h', 'gaia/gaia_auth_fetcher.cc', |