// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/app_notify_channel_setup.h"

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/metrics/histogram.h"
#include "base/stringprintf.h"
#include "chrome/browser/net/gaia/token_service.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "chrome/common/net/gaia/gaia_urls.h"
#include "chrome/common/net/http_return.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/url_fetcher.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/url_request_status.h"

using base::StringPrintf;
using content::BrowserThread;
using content::URLFetcher;

namespace {

static const char kChannelSetupAuthError[] = "unauthorized";
static const char kChannelSetupInternalError[] = "internal_error";
static const char kChannelSetupCanceledByUser[] = "canceled_by_user";
static const char kAuthorizationHeaderFormat[] =
    "Authorization: Bearer %s";
static const char kOAuth2IssueTokenURL[] =
    "https://www.googleapis.com/oauth2/v2/IssueToken";
static const char kOAuth2IssueTokenBodyFormat[] =
    "force=true"
    "&response_type=token"
    "&scope=%s"
    "&client_id=%s"
    "&origin=%s";
static const char kOAuth2IssueTokenScope[] =
    "https://www.googleapis.com/auth/chromewebstore.notification";
static const char kCWSChannelServiceURL[] =
    "https://www.googleapis.com/chromewebstore/v1.1/channels/id";

static AppNotifyChannelSetup::InterceptorForTests* g_interceptor_for_tests =
    NULL;

}  // namespace.

// static
void AppNotifyChannelSetup::SetInterceptorForTests(
    AppNotifyChannelSetup::InterceptorForTests* interceptor) {
  // Only one interceptor at a time, please.
  CHECK(g_interceptor_for_tests == NULL);
  g_interceptor_for_tests = interceptor;
}

AppNotifyChannelSetup::AppNotifyChannelSetup(
    Profile* profile,
    const std::string& extension_id,
    const std::string& client_id,
    const GURL& requestor_url,
    int return_route_id,
    int callback_id,
    AppNotifyChannelUI* ui,
    base::WeakPtr<AppNotifyChannelSetup::Delegate> delegate)
    : profile_(profile),
      extension_id_(extension_id),
      client_id_(client_id),
      requestor_url_(requestor_url),
      return_route_id_(return_route_id),
      callback_id_(callback_id),
      delegate_(delegate),
      ui_(ui),
      state_(INITIAL),
      oauth2_access_token_failure_(false) {}

AppNotifyChannelSetup::~AppNotifyChannelSetup() {}

void AppNotifyChannelSetup::Start() {
  if (g_interceptor_for_tests) {
    std::string channel_id;
    std::string error;
    g_interceptor_for_tests->DoIntercept(this, &channel_id, &error);
    delegate_->AppNotifyChannelSetupComplete(channel_id, error, this);
    return;
  }
  AddRef();  // Balanced in ReportResult.
  BeginLogin();
}

void AppNotifyChannelSetup::OnGetTokenSuccess(
    const std::string& access_token) {
  oauth2_access_token_ = access_token;
  EndGetAccessToken(true);
}
void AppNotifyChannelSetup::OnGetTokenFailure(
    const GoogleServiceAuthError& error) {
  EndGetAccessToken(false);
}

void AppNotifyChannelSetup::OnSyncSetupResult(bool enabled) {
  EndLogin(enabled);
}

void AppNotifyChannelSetup::OnURLFetchComplete(const URLFetcher* source) {
  CHECK(source);
  switch (state_) {
    case RECORD_GRANT_STARTED:
      EndRecordGrant(source);
      break;
    case CHANNEL_ID_SETUP_STARTED:
      EndGetChannelId(source);
      break;
    default:
      CHECK(false) << "Wrong state: " << state_;
      break;
  }
}

// The contents of |body| should be URL-encoded as appropriate.
URLFetcher* AppNotifyChannelSetup::CreateURLFetcher(
    const GURL& url, const std::string& body, const std::string& auth_token) {
  CHECK(url.is_valid());
  URLFetcher::RequestType type =
      body.empty() ? URLFetcher::GET : URLFetcher::POST;
  URLFetcher* fetcher = URLFetcher::Create(0, url, type, this);
  fetcher->SetRequestContext(profile_->GetRequestContext());
  // Always set flags to neither send nor save cookies.
  fetcher->SetLoadFlags(
      net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES);
  fetcher->SetExtraRequestHeaders(MakeAuthorizationHeader(auth_token));
  if (!body.empty()) {
    fetcher->SetUploadData("application/x-www-form-urlencoded", body);
  }
  return fetcher;
}

bool AppNotifyChannelSetup::ShouldPromptForLogin() const {
  std::string username = profile_->GetPrefs()->GetString(
      prefs::kGoogleServicesUsername);
  // Prompt for login if either:
  // 1. the user has not logged in at all or
  // 2. if the user is logged in but there is no OAuth2 login token.
  //    The latter happens for users who are already logged in before the
  //    code to generate OAuth2 login token is released.
  // 3. If the OAuth2 login token does not work anymore.
  //    This can happen if the user explicitly revoked access to Google Chrome
  //    from Google Accounts page.
  return username.empty() ||
         !profile_->GetTokenService()->HasOAuthLoginToken() ||
         oauth2_access_token_failure_;
}

namespace {

enum LoginNeededHistogram {
  LOGIN_NEEDED,
  LOGIN_NOT_NEEDED,
  LOGIN_NEEDED_BOUNDARY
};

enum LoginSuccessHistogram {
  LOGIN_SUCCESS,
  LOGIN_FAILURE,
  LOGIN_SUCCESS_BOUNDARY
};

}  // namespace

void AppNotifyChannelSetup::BeginLogin() {
  CHECK_EQ(INITIAL, state_);
  state_ = LOGIN_STARTED;
  bool login_needed = ShouldPromptForLogin();
  UMA_HISTOGRAM_ENUMERATION("AppNotify.ChannelSetupBegin",
                            login_needed ? LOGIN_NEEDED : LOGIN_NOT_NEEDED,
                            LOGIN_NEEDED_BOUNDARY);
  if (login_needed) {
    ui_->PromptSyncSetup(this);
    // We'll get called back in OnSyncSetupResult
  } else {
    EndLogin(true);
  }
}

void AppNotifyChannelSetup::EndLogin(bool success) {
  CHECK_EQ(LOGIN_STARTED, state_);
  UMA_HISTOGRAM_ENUMERATION("AppNotify.ChannelSetupLoginResult",
                            success ? LOGIN_SUCCESS : LOGIN_FAILURE,
                            LOGIN_SUCCESS_BOUNDARY);
  if (success) {
    state_ = LOGIN_DONE;
    BeginGetAccessToken();
  } else {
    state_ = ERROR_STATE;
    ReportResult("", USER_CANCELLED);
  }
}

void AppNotifyChannelSetup::BeginGetAccessToken() {
  CHECK_EQ(LOGIN_DONE, state_);
  state_ = FETCH_ACCESS_TOKEN_STARTED;

  oauth2_fetcher_.reset(new OAuth2AccessTokenFetcher(
      this, profile_->GetRequestContext()));
  std::vector<std::string> scopes;
  scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope());
  scopes.push_back(kOAuth2IssueTokenScope);
  oauth2_fetcher_->Start(
      GaiaUrls::GetInstance()->oauth2_chrome_client_id(),
      GaiaUrls::GetInstance()->oauth2_chrome_client_secret(),
      profile_->GetTokenService()->GetOAuth2LoginRefreshToken(),
      scopes);
}

void AppNotifyChannelSetup::EndGetAccessToken(bool success) {
  CHECK_EQ(FETCH_ACCESS_TOKEN_STARTED, state_);
  if (success) {
    state_ = FETCH_ACCESS_TOKEN_DONE;
    BeginRecordGrant();
  } else if (!oauth2_access_token_failure_) {
    oauth2_access_token_failure_ = true;
    // If access token generation fails, then it means somehow the
    // OAuth2 login scoped token became invalid. One way this can happen
    // is if a user explicitly revoked access to Google Chrome from
    // Google Accounts page. In such a case, we should try to show the
    // login setup again to the user, but only if we have not already
    // done so once (to avoid infinite loop).
    state_ = INITIAL;
    BeginLogin();
  } else {
    state_ = ERROR_STATE;
    ReportResult("", INTERNAL_ERROR);
  }
}

void AppNotifyChannelSetup::BeginRecordGrant() {
  CHECK_EQ(FETCH_ACCESS_TOKEN_DONE, state_);
  state_ = RECORD_GRANT_STARTED;

  GURL url = GetOAuth2IssueTokenURL();
  std::string body = MakeOAuth2IssueTokenBody(client_id_, extension_id_);

  url_fetcher_.reset(CreateURLFetcher(url, body, oauth2_access_token_));
  url_fetcher_->Start();
}

void AppNotifyChannelSetup::EndRecordGrant(const URLFetcher* source) {
  CHECK_EQ(RECORD_GRANT_STARTED, state_);

  net::URLRequestStatus status = source->GetStatus();

  if (status.status() == net::URLRequestStatus::SUCCESS) {
    if (source->GetResponseCode() == RC_REQUEST_OK) {
      state_ = RECORD_GRANT_DONE;
      BeginGetChannelId();
    } else {
      // Successfully done with HTTP request, but got an explicit error.
      state_ = ERROR_STATE;
      ReportResult("", AUTH_ERROR);
    }
  } else {
    // Could not do HTTP request.
    state_ = ERROR_STATE;
    ReportResult("", INTERNAL_ERROR);
  }
}

void AppNotifyChannelSetup::BeginGetChannelId() {
  CHECK_EQ(RECORD_GRANT_DONE, state_);
  state_ = CHANNEL_ID_SETUP_STARTED;

  GURL url = GetCWSChannelServiceURL();

  url_fetcher_.reset(CreateURLFetcher(url, "", oauth2_access_token_));
  url_fetcher_->Start();
}

void AppNotifyChannelSetup::EndGetChannelId(const URLFetcher* source) {
  CHECK_EQ(CHANNEL_ID_SETUP_STARTED, state_);
  net::URLRequestStatus status = source->GetStatus();

  if (status.status() == net::URLRequestStatus::SUCCESS) {
    if (source->GetResponseCode() == RC_REQUEST_OK) {
      std::string data;
      source->GetResponseAsString(&data);
      std::string channel_id;
      bool result = ParseCWSChannelServiceResponse(data, &channel_id);
      if (result) {
        state_ = CHANNEL_ID_SETUP_DONE;
        ReportResult(channel_id, NONE);
      } else {
        state_ = ERROR_STATE;
        ReportResult("", INTERNAL_ERROR);
      }
    } else {
      // Successfully done with HTTP request, but got an explicit error.
      state_ = ERROR_STATE;
      ReportResult("", AUTH_ERROR);
    }
  } else {
    // Could not do HTTP request.
    state_ = ERROR_STATE;
    ReportResult("", INTERNAL_ERROR);
  }
}

void AppNotifyChannelSetup::ReportResult(
    const std::string& channel_id,
    SetupError error) {
  CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  CHECK(state_ == CHANNEL_ID_SETUP_DONE || state_ == ERROR_STATE);

  UMA_HISTOGRAM_ENUMERATION("AppNotification.ChannelSetupFinalResult",
                            error, SETUP_ERROR_BOUNDARY);
  if (delegate_.get()) {
    delegate_->AppNotifyChannelSetupComplete(
        channel_id, GetErrorString(error), this);
  }
  Release(); // Matches AddRef in Start.
}

// static
std::string AppNotifyChannelSetup::GetErrorString(SetupError error) {
  switch (error) {
    case NONE:           return "";
    case AUTH_ERROR:     return kChannelSetupAuthError;
    case INTERNAL_ERROR: return kChannelSetupInternalError;
    case USER_CANCELLED: return kChannelSetupCanceledByUser;
    case SETUP_ERROR_BOUNDARY: {
      CHECK(false);
      break;
    }
  }
  CHECK(false) << "Unhandled enum value";
  return std::string();
}


// static
GURL AppNotifyChannelSetup::GetCWSChannelServiceURL() {
  CommandLine* command_line = CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kAppNotifyChannelServerURL)) {
    std::string switch_value = command_line->GetSwitchValueASCII(
        switches::kAppNotifyChannelServerURL);
    GURL result(switch_value);
    if (result.is_valid()) {
      return result;
    } else {
      LOG(ERROR) << "Invalid value for " <<
          switches::kAppNotifyChannelServerURL;
    }
  }
  return GURL(kCWSChannelServiceURL);
}

// static
GURL AppNotifyChannelSetup::GetOAuth2IssueTokenURL() {
  return GURL(kOAuth2IssueTokenURL);
}

// static
std::string AppNotifyChannelSetup::MakeOAuth2IssueTokenBody(
    const std::string& oauth_client_id,
    const std::string& extension_id) {
  return StringPrintf(kOAuth2IssueTokenBodyFormat,
      kOAuth2IssueTokenScope,
      net::EscapeUrlEncodedData(oauth_client_id, true).c_str(),
      net::EscapeUrlEncodedData(extension_id, true).c_str());
}

// static
std::string AppNotifyChannelSetup::MakeAuthorizationHeader(
    const std::string& auth_token) {
  return StringPrintf(kAuthorizationHeaderFormat, auth_token.c_str());
}

// static
bool AppNotifyChannelSetup::ParseCWSChannelServiceResponse(
    const std::string& data, std::string* result) {
  base::JSONReader reader;
  scoped_ptr<base::Value> value(reader.Read(data, false));
  if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY)
    return false;

  Value* channel_id_value;
  DictionaryValue* dict = static_cast<DictionaryValue*>(value.get());
  if (!dict->Get("id", &channel_id_value))
    return false;
  if (channel_id_value->GetType() != base::Value::TYPE_STRING)
    return false;

  StringValue* channel_id = static_cast<StringValue*>(channel_id_value);
  channel_id->GetAsString(result);
  return true;
}