// Copyright 2015 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 "content/browser/service_worker/service_worker_client_utils.h"

#include <algorithm>
#include <tuple>

#include "base/location.h"
#include "base/macros.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_provider_host.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/service_worker/service_worker_types.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/payment_app_provider.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/child_process_host.h"
#include "services/network/public/mojom/request_context_frame_type.mojom.h"
#include "third_party/blink/public/mojom/page/page_visibility_state.mojom.h"
#include "ui/base/mojo/window_open_disposition.mojom.h"
#include "url/gurl.h"

namespace content {
namespace service_worker_client_utils {

namespace {

using OpenURLCallback = base::OnceCallback<void(int, int)>;

// The OpenURLObserver class is a WebContentsObserver that will wait for a
// WebContents to be initialized, run the |callback| passed to its constructor
// then self destroy.
// The callback will receive the process and frame ids. If something went wrong
// those will be (kInvalidUniqueID, MSG_ROUTING_NONE).
// The callback will be called in the IO thread.
class OpenURLObserver : public WebContentsObserver {
 public:
  OpenURLObserver(WebContents* web_contents,
                  int frame_tree_node_id,
                  OpenURLCallback callback)
      : WebContentsObserver(web_contents),
        frame_tree_node_id_(frame_tree_node_id),
        callback_(std::move(callback)) {}

  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    DCHECK(web_contents());
    if (!navigation_handle->HasCommitted()) {
      // Return error.
      RunCallback(ChildProcessHost::kInvalidUniqueID, MSG_ROUTING_NONE);
      return;
    }

    if (navigation_handle->GetFrameTreeNodeId() != frame_tree_node_id_) {
      // Return error.
      RunCallback(ChildProcessHost::kInvalidUniqueID, MSG_ROUTING_NONE);
      return;
    }

    RenderFrameHost* render_frame_host =
        navigation_handle->GetRenderFrameHost();
    RunCallback(render_frame_host->GetProcess()->GetID(),
                render_frame_host->GetRoutingID());
  }

  void RenderProcessGone(base::TerminationStatus status) override {
    RunCallback(ChildProcessHost::kInvalidUniqueID, MSG_ROUTING_NONE);
  }

  void WebContentsDestroyed() override {
    RunCallback(ChildProcessHost::kInvalidUniqueID, MSG_ROUTING_NONE);
  }

 private:
  void RunCallback(int render_process_id, int render_frame_id) {
    // After running the callback, |this| will stop observing, thus
    // web_contents() should return nullptr and |RunCallback| should no longer
    // be called. Then, |this| will self destroy.
    DCHECK(web_contents());
    DCHECK(callback_);

    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
                            base::BindOnce(std::move(callback_),
                                           render_process_id, render_frame_id));
    Observe(nullptr);
    base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
  }

  int frame_tree_node_id_;
  OpenURLCallback callback_;

  DISALLOW_COPY_AND_ASSIGN(OpenURLObserver);
};

blink::mojom::ServiceWorkerClientInfoPtr GetWindowClientInfoOnUI(
    int render_process_id,
    int render_frame_id,
    base::TimeTicks create_time,
    const std::string& client_uuid) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  RenderFrameHostImpl* render_frame_host =
      RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
  if (!render_frame_host)
    return nullptr;

  // TODO(mlamouri,michaeln): it is possible to end up collecting information
  // for a frame that is actually being navigated and isn't exactly what we are
  // expecting.
  return blink::mojom::ServiceWorkerClientInfo::New(
      render_frame_host->GetLastCommittedURL(), client_uuid,
      blink::mojom::ServiceWorkerClientType::kWindow,
      render_frame_host->GetVisibilityState(), render_frame_host->IsFocused(),
      render_frame_host->GetParent()
          ? network::mojom::RequestContextFrameType::kNested
          : network::mojom::RequestContextFrameType::kTopLevel,
      render_frame_host->frame_tree_node()->last_focus_time(), create_time);
}

blink::mojom::ServiceWorkerClientInfoPtr FocusOnUI(
    int render_process_id,
    int render_frame_id,
    base::TimeTicks create_time,
    const std::string& client_uuid) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  RenderFrameHostImpl* render_frame_host =
      RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
  WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
      WebContents::FromRenderFrameHost(render_frame_host));

  if (!render_frame_host || !web_contents)
    return nullptr;

  FrameTreeNode* frame_tree_node = render_frame_host->frame_tree_node();

  // Focus the frame in the frame tree node, in case it has changed.
  frame_tree_node->frame_tree()->SetFocusedFrame(
      frame_tree_node, render_frame_host->GetSiteInstance());

  // Focus the frame's view to make sure the frame is now considered as focused.
  render_frame_host->GetView()->Focus();

  // Move the web contents to the foreground.
  web_contents->Activate();

  return GetWindowClientInfoOnUI(render_process_id, render_frame_id,
                                 create_time, client_uuid);
}

// This is only called for main frame navigations in OpenWindowOnUI().
void DidOpenURLOnUI(WindowType type,
                    OpenURLCallback callback,
                    WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!web_contents) {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::BindOnce(std::move(callback), ChildProcessHost::kInvalidUniqueID,
                       MSG_ROUTING_NONE));
    return;
  }

  // ContentBrowserClient::OpenURL calls ui::BaseWindow::Show which
  // makes the destination window the main+key window, but won't make Chrome
  // the active application (https://crbug.com/470830). Since OpenWindow is
  // always called from a user gesture (e.g. notification click), we should
  // explicitly activate the window, which brings Chrome to the front.
  static_cast<WebContentsImpl*>(web_contents)->Activate();

  RenderFrameHostImpl* rfhi =
      static_cast<RenderFrameHostImpl*>(web_contents->GetMainFrame());
  new OpenURLObserver(web_contents,
                      rfhi->frame_tree_node()->frame_tree_node_id(),
                      std::move(callback));

  if (type == WindowType::PAYMENT_HANDLER_WINDOW) {
    // Set the opened web_contents to payment app provider to manage its life
    // cycle.
    PaymentAppProvider::GetInstance()->SetOpenedWindow(web_contents);
  }
}

void OpenWindowOnUI(
    const GURL& url,
    const GURL& script_url,
    int worker_process_id,
    const scoped_refptr<ServiceWorkerContextWrapper>& context_wrapper,
    WindowType type,
    OpenURLCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  BrowserContext* browser_context =
      context_wrapper->storage_partition()
          ? context_wrapper->storage_partition()->browser_context()
          : nullptr;
  // We are shutting down.
  if (!browser_context)
    return;

  RenderProcessHost* render_process_host =
      RenderProcessHost::FromID(worker_process_id);
  if (render_process_host->IsForGuestsOnly()) {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::BindOnce(std::move(callback), ChildProcessHost::kInvalidUniqueID,
                       MSG_ROUTING_NONE));
    return;
  }

  OpenURLParams params(
      url,
      Referrer::SanitizeForRequest(
          url, Referrer(script_url, blink::kWebReferrerPolicyDefault)),
      type == WindowType::PAYMENT_HANDLER_WINDOW
          ? WindowOpenDisposition::NEW_POPUP
          : WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui::PAGE_TRANSITION_AUTO_TOPLEVEL, true /* is_renderer_initiated */);
  params.open_app_window_if_possible = type == WindowType::NEW_TAB_WINDOW;

  GetContentClient()->browser()->OpenURL(
      browser_context, params,
      base::AdaptCallbackForRepeating(
          base::BindOnce(&DidOpenURLOnUI, type, std::move(callback))));
}

void NavigateClientOnUI(const GURL& url,
                        const GURL& script_url,
                        int process_id,
                        int frame_id,
                        OpenURLCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RenderFrameHostImpl* rfhi = RenderFrameHostImpl::FromID(process_id, frame_id);
  WebContents* web_contents = WebContents::FromRenderFrameHost(rfhi);

  if (!rfhi || !web_contents) {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::BindOnce(std::move(callback), ChildProcessHost::kInvalidUniqueID,
                       MSG_ROUTING_NONE));
    return;
  }

  ui::PageTransition transition = rfhi->GetParent()
                                      ? ui::PAGE_TRANSITION_AUTO_SUBFRAME
                                      : ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
  int frame_tree_node_id = rfhi->frame_tree_node()->frame_tree_node_id();

  OpenURLParams params(
      url,
      Referrer::SanitizeForRequest(
          url, Referrer(script_url, blink::kWebReferrerPolicyDefault)),
      frame_tree_node_id, WindowOpenDisposition::CURRENT_TAB, transition,
      true /* is_renderer_initiated */);
  web_contents->OpenURL(params);
  new OpenURLObserver(web_contents, frame_tree_node_id, std::move(callback));
}

void AddWindowClient(
    ServiceWorkerProviderHost* host,
    std::vector<std::tuple<int, int, base::TimeTicks, std::string>>*
        client_info) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (host->client_type() != blink::mojom::ServiceWorkerClientType::kWindow)
    return;
  client_info->push_back(std::make_tuple(host->process_id(), host->frame_id(),
                                         host->create_time(),
                                         host->client_uuid()));
}

void AddNonWindowClient(
    const ServiceWorkerProviderHost* host,
    blink::mojom::ServiceWorkerClientQueryOptionsPtr options,
    ServiceWorkerClientPtrs* out_clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  blink::mojom::ServiceWorkerClientType host_client_type = host->client_type();
  if (host_client_type == blink::mojom::ServiceWorkerClientType::kWindow)
    return;
  if (options->client_type != blink::mojom::ServiceWorkerClientType::kAll &&
      options->client_type != host_client_type)
    return;

  auto client_info = blink::mojom::ServiceWorkerClientInfo::New(
      host->document_url(), host->client_uuid(), host_client_type,
      blink::mojom::PageVisibilityState::kHidden,
      false,  // is_focused
      network::mojom::RequestContextFrameType::kNone, base::TimeTicks(),
      host->create_time());
  out_clients->push_back(std::move(client_info));
}

void OnGetWindowClientsOnUI(
    // The tuple contains process_id, frame_id, create_time, client_uuid.
    const std::vector<std::tuple<int, int, base::TimeTicks, std::string>>&
        clients_info,
    const GURL& script_url,
    ClientsCallback callback,
    std::unique_ptr<ServiceWorkerClientPtrs> out_clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  for (const auto& it : clients_info) {
    blink::mojom::ServiceWorkerClientInfoPtr info = GetWindowClientInfoOnUI(
        std::get<0>(it), std::get<1>(it), std::get<2>(it), std::get<3>(it));

    // If the request to the provider_host returned a null
    // ServiceWorkerClientInfo, that means that it wasn't possible to associate
    // it with a valid RenderFrameHost. It might be because the frame was killed
    // or navigated in between.
    if (!info)
      continue;
    DCHECK(!info->client_uuid.empty());

    // We can get info for a frame that was navigating end ended up with a
    // different URL than expected. In such case, we should make sure to not
    // expose cross-origin WindowClient.
    if (info->url.GetOrigin() != script_url.GetOrigin())
      continue;

    out_clients->push_back(std::move(info));
  }

  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::BindOnce(std::move(callback), std::move(out_clients)));
}

struct ServiceWorkerClientInfoSort {
  bool operator()(const blink::mojom::ServiceWorkerClientInfoPtr& a,
                  const blink::mojom::ServiceWorkerClientInfoPtr& b) const {
    // Clients for windows should be appeared earlier.
    if (a->client_type == blink::mojom::ServiceWorkerClientType::kWindow &&
        b->client_type != blink::mojom::ServiceWorkerClientType::kWindow) {
      return true;
    }
    if (a->client_type != blink::mojom::ServiceWorkerClientType::kWindow &&
        b->client_type == blink::mojom::ServiceWorkerClientType::kWindow) {
      return false;
    }

    // Clients focused recently should be appeared earlier.
    if (a->last_focus_time != b->last_focus_time)
      return a->last_focus_time > b->last_focus_time;

    // Clients created before should be appeared earlier.
    return a->creation_time < b->creation_time;
  }
};

void DidGetClients(ClientsCallback callback,
                   std::unique_ptr<ServiceWorkerClientPtrs> clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  std::sort(clients->begin(), clients->end(), ServiceWorkerClientInfoSort());

  std::move(callback).Run(std::move(clients));
}

void GetNonWindowClients(
    const base::WeakPtr<ServiceWorkerVersion>& controller,
    blink::mojom::ServiceWorkerClientQueryOptionsPtr options,
    ClientsCallback callback,
    std::unique_ptr<ServiceWorkerClientPtrs> clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (!options->include_uncontrolled) {
    for (auto& controllee : controller->controllee_map())
      AddNonWindowClient(controllee.second, std::move(options), clients.get());
  } else if (controller->context()) {
    GURL origin = controller->script_url().GetOrigin();
    for (auto it = controller->context()->GetClientProviderHostIterator(
             origin, false /* include_reserved_clients */);
         !it->IsAtEnd(); it->Advance()) {
      AddNonWindowClient(it->GetProviderHost(), std::move(options),
                         clients.get());
    }
  }
  DidGetClients(std::move(callback), std::move(clients));
}

void DidGetWindowClients(
    const base::WeakPtr<ServiceWorkerVersion>& controller,
    blink::mojom::ServiceWorkerClientQueryOptionsPtr options,
    ClientsCallback callback,
    std::unique_ptr<ServiceWorkerClientPtrs> clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (options->client_type == blink::mojom::ServiceWorkerClientType::kAll) {
    GetNonWindowClients(controller, std::move(options), std::move(callback),
                        std::move(clients));
    return;
  }
  DidGetClients(std::move(callback), std::move(clients));
}

void GetWindowClients(const base::WeakPtr<ServiceWorkerVersion>& controller,
                      blink::mojom::ServiceWorkerClientQueryOptionsPtr options,
                      ClientsCallback callback,
                      std::unique_ptr<ServiceWorkerClientPtrs> clients) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DCHECK(options->client_type ==
             blink::mojom::ServiceWorkerClientType::kWindow ||
         options->client_type == blink::mojom::ServiceWorkerClientType::kAll);

  std::vector<std::tuple<int, int, base::TimeTicks, std::string>> clients_info;
  if (!options->include_uncontrolled) {
    for (auto& controllee : controller->controllee_map())
      AddWindowClient(controllee.second, &clients_info);
  } else if (controller->context()) {
    GURL origin = controller->script_url().GetOrigin();
    for (auto it = controller->context()->GetClientProviderHostIterator(
             origin, false /* include_reserved_clients */);
         !it->IsAtEnd(); it->Advance()) {
      AddWindowClient(it->GetProviderHost(), &clients_info);
    }
  }

  if (clients_info.empty()) {
    DidGetWindowClients(controller, std::move(options), std::move(callback),
                        std::move(clients));
    return;
  }

  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(&OnGetWindowClientsOnUI, clients_info,
                     controller->script_url(),
                     base::BindOnce(&DidGetWindowClients, controller,
                                    std::move(options), std::move(callback)),
                     std::move(clients)));
}

}  // namespace

void FocusWindowClient(ServiceWorkerProviderHost* provider_host,
                       ClientCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DCHECK_EQ(blink::mojom::ServiceWorkerClientType::kWindow,
            provider_host->client_type());
  BrowserThread::PostTaskAndReplyWithResult(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(&FocusOnUI, provider_host->process_id(),
                     provider_host->frame_id(), provider_host->create_time(),
                     provider_host->client_uuid()),
      std::move(callback));
}

void OpenWindow(const GURL& url,
                const GURL& script_url,
                int worker_process_id,
                const base::WeakPtr<ServiceWorkerContextCore>& context,
                WindowType type,
                NavigationCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(
          &OpenWindowOnUI, url, script_url, worker_process_id,
          base::WrapRefCounted(context->wrapper()), type,
          base::BindOnce(&DidNavigate, context, script_url.GetOrigin(),
                         std::move(callback))));
}

void NavigateClient(const GURL& url,
                    const GURL& script_url,
                    int process_id,
                    int frame_id,
                    const base::WeakPtr<ServiceWorkerContextCore>& context,
                    NavigationCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(
          &NavigateClientOnUI, url, script_url, process_id, frame_id,
          base::BindOnce(&DidNavigate, context, script_url.GetOrigin(),
                         std::move(callback))));
}

void GetClient(const ServiceWorkerProviderHost* provider_host,
               ClientCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  blink::mojom::ServiceWorkerClientType client_type =
      provider_host->client_type();
  DCHECK(client_type == blink::mojom::ServiceWorkerClientType::kWindow ||
         client_type == blink::mojom::ServiceWorkerClientType::kSharedWorker)
      << client_type;

  if (client_type == blink::mojom::ServiceWorkerClientType::kWindow) {
    BrowserThread::PostTaskAndReplyWithResult(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&GetWindowClientInfoOnUI, provider_host->process_id(),
                       provider_host->route_id(), provider_host->create_time(),
                       provider_host->client_uuid()),
        std::move(callback));
    return;
  }

  auto client_info = blink::mojom::ServiceWorkerClientInfo::New(
      provider_host->document_url(), provider_host->client_uuid(),
      provider_host->client_type(), blink::mojom::PageVisibilityState::kHidden,
      false,  // is_focused
      network::mojom::RequestContextFrameType::kNone, base::TimeTicks(),
      provider_host->create_time());
  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::BindOnce(std::move(callback), std::move(client_info)));
}

void GetClients(const base::WeakPtr<ServiceWorkerVersion>& controller,
                blink::mojom::ServiceWorkerClientQueryOptionsPtr options,
                ClientsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  auto clients = std::make_unique<ServiceWorkerClientPtrs>();
  if (!controller->HasControllee() && !options->include_uncontrolled) {
    DidGetClients(std::move(callback), std::move(clients));
    return;
  }

  // For Window clients we want to query the info on the UI thread first.
  if (options->client_type == blink::mojom::ServiceWorkerClientType::kWindow ||
      options->client_type == blink::mojom::ServiceWorkerClientType::kAll) {
    GetWindowClients(controller, std::move(options), std::move(callback),
                     std::move(clients));
    return;
  }

  GetNonWindowClients(controller, std::move(options), std::move(callback),
                      std::move(clients));
}

void DidNavigate(const base::WeakPtr<ServiceWorkerContextCore>& context,
                 const GURL& origin,
                 NavigationCallback callback,
                 int render_process_id,
                 int render_frame_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (!context) {
    std::move(callback).Run(blink::ServiceWorkerStatusCode::kErrorAbort,
                            nullptr /* client_info */);
    return;
  }

  if (render_process_id == ChildProcessHost::kInvalidUniqueID &&
      render_frame_id == MSG_ROUTING_NONE) {
    std::move(callback).Run(blink::ServiceWorkerStatusCode::kErrorFailed,
                            nullptr /* client_info */);
    return;
  }

  for (std::unique_ptr<ServiceWorkerContextCore::ProviderHostIterator> it =
           context->GetClientProviderHostIterator(
               origin, false /* include_reserved_clients */);
       !it->IsAtEnd(); it->Advance()) {
    ServiceWorkerProviderHost* provider_host = it->GetProviderHost();
    if (provider_host->process_id() != render_process_id ||
        provider_host->frame_id() != render_frame_id) {
      continue;
    }
    BrowserThread::PostTaskAndReplyWithResult(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&GetWindowClientInfoOnUI, provider_host->process_id(),
                       provider_host->route_id(), provider_host->create_time(),
                       provider_host->client_uuid()),
        base::BindOnce(std::move(callback),
                       blink::ServiceWorkerStatusCode::kOk));
    return;
  }

  // If here, it means that no provider_host was found, in which case, the
  // renderer should still be informed that the window was opened.
  std::move(callback).Run(blink::ServiceWorkerStatusCode::kOk,
                          nullptr /* client_info */);
}

}  // namespace service_worker_client_utils
}  // namespace content
