1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "fuchsia/engine/browser/navigation_controller_impl.h"
6 
7 #include "base/strings/strcat.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "content/public/browser/navigation_entry.h"
10 #include "content/public/browser/navigation_handle.h"
11 #include "content/public/browser/web_contents.h"
12 #include "content/public/common/was_activated_option.mojom.h"
13 #include "fuchsia/base/string_util.h"
14 #include "net/http/http_util.h"
15 #include "ui/base/page_transition_types.h"
16 
17 namespace {
18 
UpdateNavigationStateFromNavigationEntry(content::NavigationEntry * entry,content::WebContents * web_contents,fuchsia::web::NavigationState * navigation_state)19 void UpdateNavigationStateFromNavigationEntry(
20     content::NavigationEntry* entry,
21     content::WebContents* web_contents,
22     fuchsia::web::NavigationState* navigation_state) {
23   DCHECK(entry);
24   DCHECK(web_contents);
25   DCHECK(navigation_state);
26 
27   navigation_state->set_title(base::UTF16ToUTF8(entry->GetTitleForDisplay()));
28   navigation_state->set_url(entry->GetURL().spec());
29 
30   switch (entry->GetPageType()) {
31     case content::PageType::PAGE_TYPE_NORMAL:
32     case content::PageType::PAGE_TYPE_INTERSTITIAL:
33       navigation_state->set_page_type(fuchsia::web::PageType::NORMAL);
34       break;
35     case content::PageType::PAGE_TYPE_ERROR:
36       navigation_state->set_page_type(fuchsia::web::PageType::ERROR);
37       break;
38   }
39 
40   navigation_state->set_can_go_back(web_contents->GetController().CanGoBack());
41   navigation_state->set_can_go_forward(
42       web_contents->GetController().CanGoForward());
43 }
44 
45 }  // namespace
46 
NavigationControllerImpl(content::WebContents * web_contents)47 NavigationControllerImpl::NavigationControllerImpl(
48     content::WebContents* web_contents)
49     : web_contents_(web_contents), weak_factory_(this) {
50   Observe(web_contents_);
51 }
52 
53 NavigationControllerImpl::~NavigationControllerImpl() = default;
54 
AddBinding(fidl::InterfaceRequest<fuchsia::web::NavigationController> controller)55 void NavigationControllerImpl::AddBinding(
56     fidl::InterfaceRequest<fuchsia::web::NavigationController> controller) {
57   controller_bindings_.AddBinding(this, std::move(controller));
58 }
59 
SetEventListener(fidl::InterfaceHandle<fuchsia::web::NavigationEventListener> listener)60 void NavigationControllerImpl::SetEventListener(
61     fidl::InterfaceHandle<fuchsia::web::NavigationEventListener> listener) {
62   // Reset the event buffer state.
63   waiting_for_navigation_event_ack_ = false;
64   previous_navigation_state_ = {};
65   pending_navigation_event_ = {};
66 
67   // Simply unbind if no new listener was set.
68   if (!listener) {
69     navigation_listener_.Unbind();
70     return;
71   }
72 
73   navigation_listener_.Bind(std::move(listener));
74   navigation_listener_.set_error_handler(
75       [this](zx_status_t status) { SetEventListener(nullptr); });
76 
77   // Immediately send the current navigation state, even if it is empty.
78   if (web_contents_->GetController().GetVisibleEntry() == nullptr) {
79     waiting_for_navigation_event_ack_ = true;
80     navigation_listener_->OnNavigationStateChanged(
81         fuchsia::web::NavigationState(), [this]() {
82           waiting_for_navigation_event_ack_ = false;
83           MaybeSendNavigationEvent();
84         });
85   } else {
86     OnNavigationEntryChanged();
87   }
88 }
89 
OnNavigationEntryChanged()90 void NavigationControllerImpl::OnNavigationEntryChanged() {
91   fuchsia::web::NavigationState new_state;
92   new_state.set_is_main_document_loaded(is_main_document_loaded_);
93   UpdateNavigationStateFromNavigationEntry(
94       web_contents_->GetController().GetVisibleEntry(), web_contents_,
95       &new_state);
96 
97   DiffNavigationEntries(previous_navigation_state_, new_state,
98                         &pending_navigation_event_);
99   previous_navigation_state_ = std::move(new_state);
100 
101   base::ThreadTaskRunnerHandle::Get()->PostTask(
102       FROM_HERE,
103       base::BindOnce(&NavigationControllerImpl::MaybeSendNavigationEvent,
104                      weak_factory_.GetWeakPtr()));
105 }
106 
MaybeSendNavigationEvent()107 void NavigationControllerImpl::MaybeSendNavigationEvent() {
108   if (!navigation_listener_)
109     return;
110 
111   if (pending_navigation_event_.IsEmpty() ||
112       waiting_for_navigation_event_ack_) {
113     return;
114   }
115 
116   waiting_for_navigation_event_ack_ = true;
117 
118   // Send the event to the observer and, upon acknowledgement, revisit this
119   // function to send another update.
120   navigation_listener_->OnNavigationStateChanged(
121       std::move(pending_navigation_event_), [this]() {
122         waiting_for_navigation_event_ack_ = false;
123         MaybeSendNavigationEvent();
124       });
125 
126   pending_navigation_event_ = {};
127 }
128 
LoadUrl(std::string url,fuchsia::web::LoadUrlParams params,LoadUrlCallback callback)129 void NavigationControllerImpl::LoadUrl(std::string url,
130                                        fuchsia::web::LoadUrlParams params,
131                                        LoadUrlCallback callback) {
132   fuchsia::web::NavigationController_LoadUrl_Result result;
133   GURL validated_url(url);
134   if (!validated_url.is_valid()) {
135     result.set_err(fuchsia::web::NavigationControllerError::INVALID_URL);
136     callback(std::move(result));
137     return;
138   }
139 
140   content::NavigationController::LoadURLParams params_converted(validated_url);
141   if (params.has_headers()) {
142     std::vector<std::string> extra_headers;
143     extra_headers.reserve(params.headers().size());
144     for (const auto& header : params.headers()) {
145       base::StringPiece header_name = cr_fuchsia::BytesAsString(header.name);
146       base::StringPiece header_value = cr_fuchsia::BytesAsString(header.value);
147       if (!net::HttpUtil::IsValidHeaderName(header_name) ||
148           !net::HttpUtil::IsValidHeaderValue(header_value)) {
149         result.set_err(fuchsia::web::NavigationControllerError::INVALID_HEADER);
150         callback(std::move(result));
151         return;
152       }
153 
154       extra_headers.emplace_back(
155           base::StrCat({header_name, ": ", header_value}));
156     }
157     params_converted.extra_headers = base::JoinString(extra_headers, "\n");
158   }
159 
160   if (validated_url.scheme() == url::kDataScheme)
161     params_converted.load_type = content::NavigationController::LOAD_TYPE_DATA;
162 
163   params_converted.transition_type = ui::PageTransitionFromInt(
164       ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
165   if (params.has_was_user_activated() && params.was_user_activated()) {
166     params_converted.was_activated = content::mojom::WasActivatedOption::kYes;
167   } else {
168     params_converted.was_activated = content::mojom::WasActivatedOption::kNo;
169   }
170 
171   web_contents_->GetController().LoadURLWithParams(params_converted);
172   result.set_response(fuchsia::web::NavigationController_LoadUrl_Response());
173   callback(std::move(result));
174 }
175 
GoBack()176 void NavigationControllerImpl::GoBack() {
177   if (web_contents_->GetController().CanGoBack())
178     web_contents_->GetController().GoBack();
179 }
180 
GoForward()181 void NavigationControllerImpl::GoForward() {
182   if (web_contents_->GetController().CanGoForward())
183     web_contents_->GetController().GoForward();
184 }
185 
Stop()186 void NavigationControllerImpl::Stop() {
187   web_contents_->Stop();
188 }
189 
Reload(fuchsia::web::ReloadType type)190 void NavigationControllerImpl::Reload(fuchsia::web::ReloadType type) {
191   content::ReloadType internal_reload_type;
192   switch (type) {
193     case fuchsia::web::ReloadType::PARTIAL_CACHE:
194       internal_reload_type = content::ReloadType::NORMAL;
195       break;
196     case fuchsia::web::ReloadType::NO_CACHE:
197       internal_reload_type = content::ReloadType::BYPASSING_CACHE;
198       break;
199   }
200   web_contents_->GetController().Reload(internal_reload_type, false);
201 }
202 
GetVisibleEntry(fuchsia::web::NavigationController::GetVisibleEntryCallback callback)203 void NavigationControllerImpl::GetVisibleEntry(
204     fuchsia::web::NavigationController::GetVisibleEntryCallback callback) {
205   content::NavigationEntry* entry =
206       web_contents_->GetController().GetVisibleEntry();
207   if (!entry) {
208     callback({});
209     return;
210   }
211 
212   fuchsia::web::NavigationState state;
213   state.set_is_main_document_loaded(is_main_document_loaded_);
214   UpdateNavigationStateFromNavigationEntry(entry, web_contents_, &state);
215   callback(std::move(state));
216 }
217 
TitleWasSet(content::NavigationEntry * entry)218 void NavigationControllerImpl::TitleWasSet(content::NavigationEntry* entry) {
219   // The title was changed after the document was loaded.
220   OnNavigationEntryChanged();
221 }
222 
DocumentAvailableInMainFrame()223 void NavigationControllerImpl::DocumentAvailableInMainFrame() {
224   // The main document is loaded, but not necessarily all the subresources. Some
225   // fields like "title" will change here.
226 
227   OnNavigationEntryChanged();
228 }
229 
DidFinishLoad(content::RenderFrameHost * render_frame_host,const GURL & validated_url)230 void NavigationControllerImpl::DidFinishLoad(
231     content::RenderFrameHost* render_frame_host,
232     const GURL& validated_url) {
233   // The document and its statically-declared subresources are loaded.
234   is_main_document_loaded_ = true;
235   OnNavigationEntryChanged();
236 }
237 
DidStartNavigation(content::NavigationHandle * navigation_handle)238 void NavigationControllerImpl::DidStartNavigation(
239     content::NavigationHandle* navigation_handle) {
240   if (!navigation_handle->IsInMainFrame() ||
241       navigation_handle->IsSameDocument()) {
242     return;
243   }
244 
245   is_main_document_loaded_ = false;
246   OnNavigationEntryChanged();
247 }
248 
DiffNavigationEntries(const fuchsia::web::NavigationState & old_entry,const fuchsia::web::NavigationState & new_entry,fuchsia::web::NavigationState * difference)249 void DiffNavigationEntries(const fuchsia::web::NavigationState& old_entry,
250                            const fuchsia::web::NavigationState& new_entry,
251                            fuchsia::web::NavigationState* difference) {
252   DCHECK(difference);
253 
254   DCHECK(new_entry.has_title());
255   if (!old_entry.has_title() || (new_entry.title() != old_entry.title())) {
256     difference->set_title(new_entry.title());
257   }
258 
259   DCHECK(new_entry.has_url());
260   if (!old_entry.has_url() || (new_entry.url() != old_entry.url())) {
261     difference->set_url(new_entry.url());
262   }
263 
264   DCHECK(new_entry.has_page_type());
265   if (!old_entry.has_page_type() ||
266       (new_entry.page_type() != old_entry.page_type())) {
267     difference->set_page_type(new_entry.page_type());
268   }
269 
270   DCHECK(new_entry.has_can_go_back());
271   if (!old_entry.has_can_go_back() ||
272       old_entry.can_go_back() != new_entry.can_go_back()) {
273     difference->set_can_go_back(new_entry.can_go_back());
274   }
275 
276   DCHECK(new_entry.has_can_go_forward());
277   if (!old_entry.has_can_go_forward() ||
278       old_entry.can_go_forward() != new_entry.can_go_forward()) {
279     difference->set_can_go_forward(new_entry.can_go_forward());
280   }
281 
282   DCHECK(new_entry.has_is_main_document_loaded());
283   if (!old_entry.has_is_main_document_loaded() ||
284       old_entry.is_main_document_loaded() !=
285           new_entry.is_main_document_loaded()) {
286     difference->set_is_main_document_loaded(
287         new_entry.is_main_document_loaded());
288   }
289 }
290