| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/timezone/timezone_controller.h" |
| |
| #include "base/feature_list.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/renderer/core/dom/events/event.h" |
| #include "third_party/blink/renderer/core/frame/frame.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/workers/worker_backing_thread.h" |
| #include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h" |
| #include "third_party/blink/renderer/core/workers/worker_thread.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/main_thread.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/main_thread_scheduler.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "third_party/blink/renderer/platform/wtf/text/unicode_string.h" |
| #include "third_party/blink/renderer/platform/wtf/wtf.h" |
| #include "third_party/icu/source/i18n/unicode/timezone.h" |
| #include "v8/include/v8.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // When enabled, the host timezone id is evaluated only when needed. |
| // TODO(crbug.com/40287434): Cleanup the feature after running the experiment, |
| // no later than January 2025. |
| BASE_FEATURE(kLazyBlinkTimezoneInit, base::FEATURE_DISABLED_BY_DEFAULT); |
| |
| // Notify V8 that the date/time configuration of the system might have changed. |
| void NotifyTimezoneChangeToV8(v8::Isolate* isolate) { |
| DCHECK(isolate); |
| isolate->DateTimeConfigurationChangeNotification(); |
| } |
| |
| void NotifyTimezoneChangeOnWorkerThread(WorkerThread* worker_thread) { |
| DCHECK(worker_thread->IsCurrentThread()); |
| NotifyTimezoneChangeToV8(worker_thread->GlobalScope()->GetIsolate()); |
| if (RuntimeEnabledFeatures::TimeZoneChangeEventEnabled() && |
| worker_thread->GlobalScope()->IsWorkerGlobalScope()) { |
| worker_thread->GlobalScope()->DispatchEvent( |
| *Event::Create(event_type_names::kTimezonechange)); |
| } |
| } |
| |
| String GetTimezoneId(const icu::TimeZone& timezone) { |
| icu::UnicodeString unicode_timezone_id; |
| timezone.getID(unicode_timezone_id); |
| return String(unicode::ToSpan(unicode_timezone_id)); |
| } |
| |
| String GetCurrentTimezoneId() { |
| std::unique_ptr<icu::TimeZone> timezone(icu::TimeZone::createDefault()); |
| CHECK(timezone); |
| return GetTimezoneId(*timezone.get()); |
| } |
| |
| void DispatchTimeZoneChangeEventToFrames() { |
| if (!RuntimeEnabledFeatures::TimeZoneChangeEventEnabled()) |
| return; |
| |
| for (const Page* page : Page::OrdinaryPages()) { |
| for (Frame* frame = page->MainFrame(); frame; |
| frame = frame->Tree().TraverseNext()) { |
| if (auto* main_local_frame = DynamicTo<LocalFrame>(frame)) { |
| main_local_frame->DomWindow()->EnqueueWindowEvent( |
| *Event::Create(event_type_names::kTimezonechange), |
| TaskType::kMiscPlatformAPI); |
| } |
| } |
| } |
| } |
| |
| void NotifyTimezoneChangeOnMainThread() { |
| Thread::MainThread() |
| ->Scheduler() |
| ->ToMainThreadScheduler() |
| ->ForEachMainThreadIsolate(&NotifyTimezoneChangeToV8); |
| DispatchTimeZoneChangeEventToFrames(); |
| } |
| |
| } // namespace |
| |
| TimeZoneController::TimeZoneController() { |
| DCHECK(IsMainThread()); |
| if (!base::FeatureList::IsEnabled(kLazyBlinkTimezoneInit)) { |
| host_timezone_id_ = GetCurrentTimezoneId(); |
| } |
| } |
| |
| TimeZoneController::~TimeZoneController() = default; |
| |
| // static |
| void TimeZoneController::Init() { |
| // monitor must not use HeapMojoRemote. TimeZoneController is not managed by |
| // Oilpan. monitor is only used to bind receiver_ here and never used |
| // again. |
| mojo::Remote<device::mojom::blink::TimeZoneMonitor> monitor; |
| Platform::Current()->GetBrowserInterfaceBroker()->GetInterface( |
| monitor.BindNewPipeAndPassReceiver()); |
| monitor->AddClient(instance().receiver_.BindNewPipeAndPassRemote()); |
| } |
| |
| // static |
| TimeZoneController& TimeZoneController::instance() { |
| DEFINE_THREAD_SAFE_STATIC_LOCAL(TimeZoneController, instance, ()); |
| return instance; |
| } |
| |
| bool CanonicalEquals(const String& time_zone_a, const String& time_zone_b) { |
| if (time_zone_a == time_zone_b) { |
| return true; |
| } |
| icu::UnicodeString canonical_a, canonical_b; |
| UErrorCode status = U_ZERO_ERROR; |
| UBool dummy; |
| icu::TimeZone::getCanonicalID( |
| icu::UnicodeString(time_zone_a.Ascii().data(), -1, US_INV), canonical_a, |
| dummy, status); |
| icu::TimeZone::getCanonicalID( |
| icu::UnicodeString(time_zone_b.Ascii().data(), -1, US_INV), canonical_b, |
| dummy, status); |
| if (U_FAILURE(status)) { |
| return false; |
| } |
| return canonical_a == canonical_b; |
| } |
| |
| // static |
| TimeZoneController::TimeZoneOverrideResult |
| TimeZoneController::SetTimeZoneOverride(const String& timezone_id) { |
| DCHECK(!timezone_id.empty()); |
| |
| base::AutoLock locker(instance().lock_); |
| |
| if (timezone_id == instance().TimeZoneIdOverride()) { |
| // Do nothing. |
| return {TimeZoneOverrideStatus::kSuccess, nullptr}; |
| } |
| |
| if (HasTimeZoneOverride()) { |
| VLOG(1) << "Cannot override existing timezone override."; |
| return {TimeZoneOverrideStatus::kAlreadyInEffect, nullptr}; |
| } |
| |
| // Only notify if the override and the host are different. |
| if (!CanonicalEquals(timezone_id, instance().GetHostTimezoneId())) { |
| if (!SetIcuTimeZoneAndNotifyV8(timezone_id)) { |
| VLOG(1) << "Invalid override timezone id: " << timezone_id; |
| return {TimeZoneOverrideStatus::kInvalidTimezone, nullptr}; |
| } |
| } |
| instance().override_timezone_id_ = timezone_id; |
| |
| return {TimeZoneOverrideStatus::kSuccess, |
| std::unique_ptr<TimeZoneOverride>(new TimeZoneOverride())}; |
| } |
| |
| // static |
| bool TimeZoneController::HasTimeZoneOverride() { |
| instance().lock_.AssertAcquired(); |
| return !instance().override_timezone_id_.empty(); |
| } |
| |
| // static |
| const String& TimeZoneController::TimeZoneIdOverride() { |
| instance().lock_.AssertAcquired(); |
| return instance().override_timezone_id_; |
| } |
| |
| // static |
| void TimeZoneController::ClearTimeZoneOverride() { |
| base::AutoLock locker(instance().lock_); |
| |
| DCHECK(HasTimeZoneOverride()); |
| |
| if (!CanonicalEquals(instance().GetHostTimezoneId(), |
| instance().override_timezone_id_)) { |
| // Restore remembered timezone request. |
| // Only do so if the host timezone is now different. |
| SetIcuTimeZoneAndNotifyV8(instance().GetHostTimezoneId()); |
| } |
| instance().override_timezone_id_ = String(); |
| } |
| |
| // static |
| void TimeZoneController::ChangeTimeZoneOverride(const String& timezone_id) { |
| DCHECK(!timezone_id.empty()); |
| |
| base::AutoLock locker(instance().lock_); |
| |
| if (!HasTimeZoneOverride()) { |
| VLOG(1) << "Cannot change if there are no existing timezone override."; |
| return; |
| } |
| |
| if (CanonicalEquals(instance().override_timezone_id_, timezone_id)) { |
| return; |
| } |
| |
| if (!SetIcuTimeZoneAndNotifyV8(timezone_id)) { |
| VLOG(1) << "Invalid override timezone id: " << timezone_id; |
| return; |
| } |
| instance().override_timezone_id_ = timezone_id; |
| } |
| |
| bool TimeZoneController::SetIcuTimeZoneAndNotifyV8(const String& timezone_id) { |
| DCHECK(!timezone_id.empty()); |
| std::unique_ptr<icu::TimeZone> timezone(icu::TimeZone::createTimeZone( |
| icu::UnicodeString(timezone_id.Ascii().data(), -1, US_INV))); |
| CHECK(timezone); |
| |
| if (*timezone == icu::TimeZone::getUnknown()) { |
| return false; |
| } |
| |
| icu::TimeZone::adoptDefault(timezone.release()); |
| |
| if (IsMainThread()) { |
| NotifyTimezoneChangeOnMainThread(); |
| } else { |
| Thread::MainThread() |
| ->GetTaskRunner(MainThreadTaskRunnerRestricted()) |
| ->PostTask(FROM_HERE, ConvertToBaseOnceCallback(CrossThreadBindOnce( |
| &NotifyTimezoneChangeOnMainThread))); |
| } |
| WorkerThread::CallOnAllWorkerThreads(&NotifyTimezoneChangeOnWorkerThread, |
| TaskType::kInternalDefault); |
| return true; |
| } |
| |
| void TimeZoneController::OnTimeZoneChange(const String& timezone_id) { |
| DCHECK(IsMainThread()); |
| |
| base::AutoLock locker(instance().lock_); |
| |
| // Remember requested timezone id so we can set it when timezone |
| // override is removed. |
| instance().host_timezone_id_ = timezone_id; |
| |
| if (!HasTimeZoneOverride()) |
| SetIcuTimeZoneAndNotifyV8(timezone_id); |
| } |
| |
| const String& TimeZoneController::GetHostTimezoneId() { |
| lock_.AssertAcquired(); |
| if (host_timezone_id_.IsNull()) { |
| CHECK(base::FeatureList::IsEnabled(kLazyBlinkTimezoneInit)); |
| host_timezone_id_ = GetCurrentTimezoneId(); |
| } |
| return host_timezone_id_; |
| } |
| |
| // static |
| void TimeZoneController::ChangeTimeZoneForTesting(const String& timezone) { |
| instance().OnTimeZoneChange(timezone); |
| } |
| } // namespace blink |