// Copyright 2019 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 "weblayer/browser/navigation_controller_impl.h" #include #include "base/auto_reset.h" #include "base/containers/contains.h" #include "base/memory/raw_ptr.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/navigation_throttle.h" #include "content/public/browser/web_contents.h" #include "third_party/blink/public/mojom/navigation/was_activated_option.mojom-shared.h" #include "ui/base/page_transition_types.h" #include "weblayer/browser/navigation_entry_data.h" #include "weblayer/browser/navigation_ui_data_impl.h" #include "weblayer/browser/page_impl.h" #include "weblayer/browser/tab_impl.h" #include "weblayer/public/navigation_observer.h" #if BUILDFLAG(IS_ANDROID) #include "base/android/jni_string.h" #include "base/trace_event/trace_event.h" #include "components/embedder_support/android/util/web_resource_response.h" #include "weblayer/browser/java/jni/NavigationControllerImpl_jni.h" #endif #if BUILDFLAG(IS_ANDROID) using base::android::AttachCurrentThread; using base::android::JavaParamRef; using base::android::ScopedJavaLocalRef; #endif namespace weblayer { class NavigationControllerImpl::DelayDeletionHelper { public: explicit DelayDeletionHelper(NavigationControllerImpl* controller) : controller_(controller->weak_ptr_factory_.GetWeakPtr()) { // This should never be called reentrantly. DCHECK(!controller->should_delay_web_contents_deletion_); controller->should_delay_web_contents_deletion_ = true; } DelayDeletionHelper(const DelayDeletionHelper&) = delete; DelayDeletionHelper& operator=(const DelayDeletionHelper&) = delete; ~DelayDeletionHelper() { if (controller_) controller_->should_delay_web_contents_deletion_ = false; } bool WasControllerDeleted() { return controller_.get() == nullptr; } private: base::WeakPtr controller_; }; // NavigationThrottle implementation responsible for delaying certain // operations and performing them when safe. This is necessary as content // does allow certain operations to be called at certain times. For example, // content does not allow calling WebContents::Stop() from // WebContentsObserver::DidStartNavigation() (to do so crashes). To work around // this NavigationControllerImpl detects these scenarios and delays processing // until safe. // // Most of the support for these scenarios is handled by a custom // NavigationThrottle. To make things interesting, the NavigationThrottle is // created after some of the scenarios this code wants to handle. As such, // NavigationImpl does some amount of caching until the NavigationThrottle is // created. class NavigationControllerImpl::NavigationThrottleImpl : public content::NavigationThrottle { public: NavigationThrottleImpl(NavigationControllerImpl* controller, content::NavigationHandle* handle) : NavigationThrottle(handle), controller_(controller) {} NavigationThrottleImpl(const NavigationThrottleImpl&) = delete; NavigationThrottleImpl& operator=(const NavigationThrottleImpl&) = delete; ~NavigationThrottleImpl() override = default; void ScheduleCancel() { should_cancel_ = true; } // content::NavigationThrottle: ThrottleCheckResult WillStartRequest() override { return should_cancel_ ? CANCEL : PROCEED; } ThrottleCheckResult WillRedirectRequest() override { controller_->WillRedirectRequest(this, navigation_handle()); return should_cancel_ ? CANCEL : PROCEED; } const char* GetNameForLogging() override { return "WebLayerNavigationControllerThrottle"; } private: raw_ptr controller_; bool should_cancel_ = false; }; NavigationControllerImpl::NavigationControllerImpl(TabImpl* tab) : WebContentsObserver(tab->web_contents()) {} NavigationControllerImpl::~NavigationControllerImpl() = default; std::unique_ptr NavigationControllerImpl::CreateNavigationThrottle( content::NavigationHandle* handle) { if (!handle->IsInMainFrame()) return nullptr; auto throttle = std::make_unique(this, handle); DCHECK(navigation_map_.find(handle) != navigation_map_.end()); auto* navigation = navigation_map_[handle].get(); if (navigation->should_stop_when_throttle_created()) throttle->ScheduleCancel(); return throttle; } NavigationImpl* NavigationControllerImpl::GetNavigationImplFromHandle( content::NavigationHandle* handle) { auto iter = navigation_map_.find(handle); return iter == navigation_map_.end() ? nullptr : iter->second.get(); } NavigationImpl* NavigationControllerImpl::GetNavigationImplFromId( int64_t navigation_id) { for (const auto& iter : navigation_map_) { if (iter.first->GetNavigationId() == navigation_id) return iter.second.get(); } return nullptr; } void NavigationControllerImpl::OnFirstContentfulPaint( const base::TimeTicks& navigation_start, const base::TimeDelta& first_contentful_paint) { #if BUILDFLAG(IS_ANDROID) TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_onFirstContentfulPaint2"); int64_t first_contentful_paint_ms = first_contentful_paint.InMilliseconds(); Java_NavigationControllerImpl_onFirstContentfulPaint2( AttachCurrentThread(), java_controller_, navigation_start.ToUptimeMillis(), first_contentful_paint_ms); #endif for (auto& observer : observers_) observer.OnFirstContentfulPaint(navigation_start, first_contentful_paint); } void NavigationControllerImpl::OnLargestContentfulPaint( const base::TimeTicks& navigation_start, const base::TimeDelta& largest_contentful_paint) { #if BUILDFLAG(IS_ANDROID) TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_onLargestContentfulPaint2"); int64_t largest_contentful_paint_ms = largest_contentful_paint.InMilliseconds(); Java_NavigationControllerImpl_onLargestContentfulPaint( AttachCurrentThread(), java_controller_, navigation_start.ToUptimeMillis(), largest_contentful_paint_ms); #endif for (auto& observer : observers_) observer.OnLargestContentfulPaint(navigation_start, largest_contentful_paint); } void NavigationControllerImpl::OnPageDestroyed(Page* page) { for (auto& observer : observers_) observer.OnPageDestroyed(page); } void NavigationControllerImpl::OnPageLanguageDetermined( Page* page, const std::string& language) { #if BUILDFLAG(IS_ANDROID) JNIEnv* env = AttachCurrentThread(); Java_NavigationControllerImpl_onPageLanguageDetermined( env, java_controller_, static_cast(page)->java_page(), base::android::ConvertUTF8ToJavaString(env, language)); #endif for (auto& observer : observers_) observer.OnPageLanguageDetermined(page, language); } #if BUILDFLAG(IS_ANDROID) void NavigationControllerImpl::SetNavigationControllerImpl( JNIEnv* env, const JavaParamRef& java_controller) { java_controller_ = java_controller; } void NavigationControllerImpl::Navigate( JNIEnv* env, const JavaParamRef& url, jboolean should_replace_current_entry, jboolean disable_intent_processing, jboolean allow_intent_launches_in_background, jboolean disable_network_error_auto_reload, jboolean enable_auto_play, const base::android::JavaParamRef& response) { auto params = std::make_unique( GURL(base::android::ConvertJavaStringToUTF8(env, url))); params->should_replace_current_entry = should_replace_current_entry; // On android, the transition type largely dictates whether intent processing // happens. PAGE_TRANSITION_TYPED does not process intents, where as // PAGE_TRANSITION_LINK will (with the caveat that even links may not trigger // intent processing under some circumstances). params->transition_type = disable_intent_processing ? ui::PAGE_TRANSITION_TYPED : ui::PAGE_TRANSITION_LINK; auto data = std::make_unique(); if (disable_network_error_auto_reload) data->set_disable_network_error_auto_reload(true); data->set_allow_intent_launches_in_background( allow_intent_launches_in_background); if (!response.is_null()) { data->SetResponse( std::make_unique(response)); } params->navigation_ui_data = std::move(data); if (enable_auto_play) params->was_activated = blink::mojom::WasActivatedOption::kYes; DoNavigate(std::move(params)); } ScopedJavaLocalRef NavigationControllerImpl::GetNavigationEntryDisplayUri(JNIEnv* env, int index) { return ScopedJavaLocalRef(base::android::ConvertUTF8ToJavaString( env, GetNavigationEntryDisplayURL(index).spec())); } ScopedJavaLocalRef NavigationControllerImpl::GetNavigationEntryTitle( JNIEnv* env, int index) { return ScopedJavaLocalRef(base::android::ConvertUTF8ToJavaString( env, GetNavigationEntryTitle(index))); } bool NavigationControllerImpl::IsNavigationEntrySkippable(JNIEnv* env, int index) { return IsNavigationEntrySkippable(index); } base::android::ScopedJavaGlobalRef NavigationControllerImpl::GetNavigationImplFromId(JNIEnv* env, int64_t id) { auto* navigation_impl = GetNavigationImplFromId(id); return navigation_impl ? navigation_impl->java_navigation() : nullptr; } #endif void NavigationControllerImpl::WillRedirectRequest( NavigationThrottleImpl* throttle, content::NavigationHandle* navigation_handle) { DCHECK(navigation_handle->IsInMainFrame()); DCHECK(navigation_map_.find(navigation_handle) != navigation_map_.end()); auto* navigation = navigation_map_[navigation_handle].get(); navigation->set_safe_to_set_request_headers(true); DCHECK(!active_throttle_); base::AutoReset auto_reset(&active_throttle_, throttle); #if BUILDFLAG(IS_ANDROID) if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_navigationRedirected"); Java_NavigationControllerImpl_navigationRedirected( AttachCurrentThread(), java_controller_, navigation->java_navigation()); } #endif for (auto& observer : observers_) observer.NavigationRedirected(navigation); navigation->set_safe_to_set_request_headers(false); } void NavigationControllerImpl::AddObserver(NavigationObserver* observer) { observers_.AddObserver(observer); } void NavigationControllerImpl::RemoveObserver(NavigationObserver* observer) { observers_.RemoveObserver(observer); } void NavigationControllerImpl::Navigate(const GURL& url) { DoNavigate( std::make_unique(url)); } void NavigationControllerImpl::Navigate( const GURL& url, const NavigationController::NavigateParams& params) { auto load_params = std::make_unique(url); load_params->should_replace_current_entry = params.should_replace_current_entry; if (params.enable_auto_play) load_params->was_activated = blink::mojom::WasActivatedOption::kYes; DoNavigate(std::move(load_params)); } void NavigationControllerImpl::GoBack() { web_contents()->GetController().GoBack(); } void NavigationControllerImpl::GoForward() { web_contents()->GetController().GoForward(); } bool NavigationControllerImpl::CanGoBack() { return web_contents()->GetController().CanGoBack(); } bool NavigationControllerImpl::CanGoForward() { return web_contents()->GetController().CanGoForward(); } void NavigationControllerImpl::GoToIndex(int index) { web_contents()->GetController().GoToIndex(index); } void NavigationControllerImpl::Reload() { web_contents()->GetController().Reload(content::ReloadType::NORMAL, true); } void NavigationControllerImpl::Stop() { CancelDelayedLoad(); NavigationImpl* navigation = nullptr; if (navigation_starting_) { navigation_starting_->set_should_stop_when_throttle_created(); navigation = navigation_starting_; } else if (active_throttle_) { active_throttle_->ScheduleCancel(); DCHECK(navigation_map_.find(active_throttle_->navigation_handle()) != navigation_map_.end()); navigation = navigation_map_[active_throttle_->navigation_handle()].get(); } else { web_contents()->Stop(); } if (navigation) navigation->set_was_stopped(); } int NavigationControllerImpl::GetNavigationListSize() { content::NavigationEntry* current_entry = web_contents()->GetController().GetLastCommittedEntry(); if (current_entry && current_entry->IsInitialEntry()) { // If we're currently on the initial NavigationEntry, no navigation has // committed, so the initial NavigationEntry should not be part of the // "Navigation List", and we should return 0 as the navigation list size. // This also preserves the old behavior where we used to not have the // initial NavigationEntry. return 0; } return web_contents()->GetController().GetEntryCount(); } int NavigationControllerImpl::GetNavigationListCurrentIndex() { content::NavigationEntry* current_entry = web_contents()->GetController().GetLastCommittedEntry(); if (current_entry && current_entry->IsInitialEntry()) { // If we're currently on the initial NavigationEntry, no navigation has // committed, so the initial NavigationEntry should not be part of the // "Navigation List", and we should return -1 as the current index. This // also preserves the old behavior where we used to not have the initial // NavigationEntry. return -1; } return web_contents()->GetController().GetCurrentEntryIndex(); } GURL NavigationControllerImpl::GetNavigationEntryDisplayURL(int index) { auto* entry = web_contents()->GetController().GetEntryAtIndex(index); // This function should never be called when GetNavigationListSize() is 0 // because `index` should be between 0 and GetNavigationListSize() - 1, which // also means `entry` must not be the initial NavigationEntry. DCHECK_NE(0, GetNavigationListSize()); DCHECK(!entry->IsInitialEntry()); return entry->GetVirtualURL(); } std::string NavigationControllerImpl::GetNavigationEntryTitle(int index) { auto* entry = web_contents()->GetController().GetEntryAtIndex(index); // This function should never be called when GetNavigationListSize() is 0 // because `index` should be between 0 and GetNavigationListSize() - 1, which // also means `entry` must not be the initial NavigationEntry. DCHECK_NE(0, GetNavigationListSize()); DCHECK(!entry->IsInitialEntry()); return base::UTF16ToUTF8(entry->GetTitle()); } bool NavigationControllerImpl::IsNavigationEntrySkippable(int index) { return web_contents()->GetController().IsEntryMarkedToBeSkipped(index); } void NavigationControllerImpl::DidStartNavigation( content::NavigationHandle* navigation_handle) { // TODO(https://crbug.com/1218946): With MPArch there may be multiple main // frames. This caller was converted automatically to the primary main frame // to preserve its semantics. Follow up to confirm correctness. if (!navigation_handle->IsInPrimaryMainFrame()) return; // This function should not be called reentrantly. DCHECK(!navigation_starting_); DCHECK(!base::Contains(navigation_map_, navigation_handle)); navigation_map_[navigation_handle] = std::make_unique(navigation_handle); auto* navigation = navigation_map_[navigation_handle].get(); base::AutoReset auto_reset(&navigation_starting_, navigation); navigation->set_safe_to_set_request_headers(true); navigation->set_safe_to_disable_network_error_auto_reload(true); navigation->set_safe_to_disable_intent_processing(true); #if BUILDFLAG(IS_ANDROID) // Desktop mode and per-navigation UA use the same mechanism and so don't // interact well. It's not possible to support both at the same time since // if there's a per-navigation UA active and desktop mode is turned on, or // was on previously, the WebContent's state would have to change before // navigation even though that would be wrong for the previous navigation if // the new navigation didn't commit. if (!TabImpl::FromWebContents(web_contents())->desktop_user_agent_enabled()) #endif navigation->set_safe_to_set_user_agent(true); #if BUILDFLAG(IS_ANDROID) NavigationUIDataImpl* navigation_ui_data = static_cast( navigation_handle->GetNavigationUIData()); if (navigation_ui_data) { auto response = navigation_ui_data->TakeResponse(); if (response) navigation->SetResponse(std::move(response)); } if (java_controller_) { JNIEnv* env = AttachCurrentThread(); { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_createNavigation"); ScopedJavaLocalRef java_navigation = Java_NavigationControllerImpl_createNavigation( env, java_controller_, reinterpret_cast(navigation)); navigation->SetJavaNavigation( base::android::ScopedJavaGlobalRef(java_navigation)); } TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_navigationStarted"); Java_NavigationControllerImpl_navigationStarted( env, java_controller_, navigation->java_navigation()); } #endif for (auto& observer : observers_) observer.NavigationStarted(navigation); navigation->set_safe_to_set_user_agent(false); navigation->set_safe_to_set_request_headers(false); navigation->set_safe_to_disable_network_error_auto_reload(false); navigation->set_safe_to_disable_intent_processing(false); } void NavigationControllerImpl::DidRedirectNavigation( content::NavigationHandle* navigation_handle) { // NOTE: this implementation should remain empty. Real implementation is in // WillRedirectNavigation(). See description of NavigationThrottleImpl for // more information. } void NavigationControllerImpl::ReadyToCommitNavigation( content::NavigationHandle* navigation_handle) { #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1218946): With MPArch there may be multiple main // frames. This caller was converted automatically to the primary main frame // to preserve its semantics. Follow up to confirm correctness. if (!navigation_handle->IsInPrimaryMainFrame()) return; DCHECK(navigation_map_.find(navigation_handle) != navigation_map_.end()); auto* navigation = navigation_map_[navigation_handle].get(); if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_readyToCommitNavigation"); Java_NavigationControllerImpl_readyToCommitNavigation( AttachCurrentThread(), java_controller_, navigation->java_navigation()); } #endif } void NavigationControllerImpl::DidFinishNavigation( content::NavigationHandle* navigation_handle) { // TODO(https://crbug.com/1218946): With MPArch there may be multiple main // frames. This caller was converted automatically to the primary main frame // to preserve its semantics. Follow up to confirm correctness. if (!navigation_handle->IsInPrimaryMainFrame()) return; DelayDeletionHelper deletion_helper(this); DCHECK(navigation_map_.find(navigation_handle) != navigation_map_.end()); auto* navigation = navigation_map_[navigation_handle].get(); navigation->set_finished(); if (navigation_handle->HasCommitted()) { // Set state on NavigationEntry user data if a per-navigation user agent was // specified. This can't be done earlier because a NavigationEntry might not // have existed at the time that SetUserAgentString was called. if (navigation->set_user_agent_string_called()) { auto* entry = web_contents()->GetController().GetLastCommittedEntry(); if (entry) { auto* entry_data = NavigationEntryData::Get(entry); if (entry_data) entry_data->set_per_navigation_user_agent_override(true); } } auto* rfh = navigation_handle->GetRenderFrameHost(); PageImpl::GetOrCreateForPage(rfh->GetPage()); navigation->set_safe_to_get_page(); #if BUILDFLAG(IS_ANDROID) // Ensure that the Java-side Page object for this navigation is // populated from and linked to the native Page object. Without this // call, the Java-side navigation object won't be created and linked to // the native object until/unless the client calls Navigation#getPage(), // which is problematic when implementation-side callers need to bridge // the C++ Page object into Java (e.g., to fire // NavigationCallback#onPageLanguageDetermined()). Java_NavigationControllerImpl_getOrCreatePageForNavigation( AttachCurrentThread(), java_controller_, navigation->java_navigation()); #endif } // In some corner cases (e.g., a tab closing with an ongoing navigation) // navigations finish without committing but without any other error state. // Such navigations are regarded as failed by WebLayer. if (navigation_handle->HasCommitted() && navigation_handle->GetNetErrorCode() == net::OK && !navigation_handle->IsErrorPage()) { #if BUILDFLAG(IS_ANDROID) if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_navigationCompleted"); Java_NavigationControllerImpl_navigationCompleted( AttachCurrentThread(), java_controller_, navigation->java_navigation()); if (deletion_helper.WasControllerDeleted()) return; } #endif for (auto& observer : observers_) { observer.NavigationCompleted(navigation); if (deletion_helper.WasControllerDeleted()) return; } } else { #if BUILDFLAG(IS_ANDROID) if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_navigationFailed"); Java_NavigationControllerImpl_navigationFailed( AttachCurrentThread(), java_controller_, navigation->java_navigation()); if (deletion_helper.WasControllerDeleted()) return; } #endif for (auto& observer : observers_) { observer.NavigationFailed(navigation); if (deletion_helper.WasControllerDeleted()) return; } } // Note InsertVisualStateCallback currently does not take into account // any delays from surface sync, ie a frame submitted by renderer may not // be displayed immediately. Such situations should be rare however, so // this should be good enough for the purposes needed. web_contents()->GetPrimaryMainFrame()->InsertVisualStateCallback( base::BindOnce(&NavigationControllerImpl::OldPageNoLongerRendered, weak_ptr_factory_.GetWeakPtr(), navigation_handle->GetURL())); navigation_map_.erase(navigation_map_.find(navigation_handle)); } void NavigationControllerImpl::DidStartLoading() { NotifyLoadStateChanged(); } void NavigationControllerImpl::DidStopLoading() { NotifyLoadStateChanged(); } void NavigationControllerImpl::LoadProgressChanged(double progress) { #if BUILDFLAG(IS_ANDROID) if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_loadProgressChanged"); Java_NavigationControllerImpl_loadProgressChanged( AttachCurrentThread(), java_controller_, progress); } #endif for (auto& observer : observers_) observer.LoadProgressChanged(progress); } void NavigationControllerImpl::DidFirstVisuallyNonEmptyPaint() { #if BUILDFLAG(IS_ANDROID) TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_onFirstContentfulPaint"); Java_NavigationControllerImpl_onFirstContentfulPaint(AttachCurrentThread(), java_controller_); #endif for (auto& observer : observers_) observer.OnFirstContentfulPaint(); } void NavigationControllerImpl::OldPageNoLongerRendered(const GURL& url, bool success) { #if BUILDFLAG(IS_ANDROID) TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_onOldPageNoLongerRendered"); JNIEnv* env = AttachCurrentThread(); Java_NavigationControllerImpl_onOldPageNoLongerRendered( env, java_controller_, base::android::ConvertUTF8ToJavaString(env, url.spec())); #endif for (auto& observer : observers_) observer.OnOldPageNoLongerRendered(url); } void NavigationControllerImpl::NotifyLoadStateChanged() { #if BUILDFLAG(IS_ANDROID) if (java_controller_) { TRACE_EVENT0("weblayer", "Java_NavigationControllerImpl_loadStateChanged"); Java_NavigationControllerImpl_loadStateChanged( AttachCurrentThread(), java_controller_, web_contents()->IsLoading(), web_contents()->ShouldShowLoadingUI()); } #endif for (auto& observer : observers_) { observer.LoadStateChanged(web_contents()->IsLoading(), web_contents()->ShouldShowLoadingUI()); } } void NavigationControllerImpl::DoNavigate( std::unique_ptr params) { CancelDelayedLoad(); // Navigations should use the default user-agent (which may be overridden if // desktop mode is turned on). If the embedder wants a custom user-agent, the // embedder will call Navigation::SetUserAgentString() in DidStartNavigation. #if BUILDFLAG(IS_ANDROID) // We need to set UA_OVERRIDE_FALSE if per navigation UA is set. However at // this point we don't know if the embedder will call that later. Since we // ensure that the two can't be set at the same time, it's sufficient to // not enable it if desktop mode is turned on. if (!TabImpl::FromWebContents(web_contents())->desktop_user_agent_enabled()) #endif params->override_user_agent = content::NavigationController::UA_OVERRIDE_FALSE; if (navigation_starting_ || active_throttle_) { // DoNavigate() is being called reentrantly. Delay processing until it's // safe. Stop(); ScheduleDelayedLoad(std::move(params)); return; } params->has_user_gesture = true; web_contents()->GetController().LoadURLWithParams(*params); // So that if the user had entered the UI in a bar it stops flashing the // caret. web_contents()->Focus(); } void NavigationControllerImpl::ScheduleDelayedLoad( std::unique_ptr params) { delayed_load_params_ = std::move(params); base::SequencedTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(&NavigationControllerImpl::ProcessDelayedLoad, weak_ptr_factory_.GetWeakPtr())); } void NavigationControllerImpl::CancelDelayedLoad() { delayed_load_params_.reset(); } void NavigationControllerImpl::ProcessDelayedLoad() { if (delayed_load_params_) DoNavigate(std::move(delayed_load_params_)); } #if BUILDFLAG(IS_ANDROID) static jlong JNI_NavigationControllerImpl_GetNavigationController(JNIEnv* env, jlong tab) { return reinterpret_cast( reinterpret_cast(tab)->GetNavigationController()); } #endif } // namespace weblayer