Harkiran Bolaria | 8dec6f9 | 2021-12-07 14:57:12 | [diff] [blame] | 1 | // Copyright (c) 2021 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 "content/browser/renderer_host/browsing_context_state.h" |
| 6 | |
Harkiran Bolaria | 2912a6b3 | 2022-02-22 16:43:45 | [diff] [blame] | 7 | #include "content/browser/renderer_host/frame_tree_node.h" |
Harkiran Bolaria | 3f83fba7 | 2022-03-10 17:48:40 | [diff] [blame] | 8 | #include "content/browser/renderer_host/render_frame_host_impl.h" |
Harkiran Bolaria | 2912a6b3 | 2022-02-22 16:43:45 | [diff] [blame] | 9 | #include "content/browser/renderer_host/render_view_host_impl.h" |
| 10 | #include "content/common/content_navigation_policy.h" |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 11 | #include "services/network/public/cpp/web_sandbox_flags.h" |
| 12 | #include "services/network/public/mojom/web_sandbox_flags.mojom.h" |
| 13 | |
Harkiran Bolaria | 8dec6f9 | 2021-12-07 14:57:12 | [diff] [blame] | 14 | namespace features { |
| 15 | const base::Feature kNewBrowsingContextStateOnBrowsingContextGroupSwap{ |
| 16 | "NewBrowsingContextStateOnBrowsingContextGroupSwap", |
| 17 | base::FEATURE_DISABLED_BY_DEFAULT}; |
| 18 | |
| 19 | BrowsingContextStateImplementationType GetBrowsingContextMode() { |
| 20 | if (base::FeatureList::IsEnabled( |
| 21 | kNewBrowsingContextStateOnBrowsingContextGroupSwap)) { |
| 22 | return BrowsingContextStateImplementationType:: |
| 23 | kSwapForCrossBrowsingInstanceNavigations; |
| 24 | } |
| 25 | |
| 26 | return BrowsingContextStateImplementationType:: |
| 27 | kLegacyOneToOneWithFrameTreeNode; |
| 28 | } |
| 29 | } // namespace features |
| 30 | |
| 31 | namespace content { |
| 32 | |
Harkiran Bolaria | 4eacb3a | 2021-12-13 20:03:47 | [diff] [blame] | 33 | BrowsingContextState::BrowsingContextState( |
Harkiran Bolaria | 880a763 | 2022-02-28 16:02:50 | [diff] [blame] | 34 | blink::mojom::FrameReplicationStatePtr replication_state, |
Harkiran Bolaria | 0b3bdef0 | 2022-03-10 13:04:40 | [diff] [blame] | 35 | raw_ptr<RenderFrameHostImpl> parent, |
| 36 | absl::optional<BrowsingInstanceId> browsing_instance_id) |
| 37 | : replication_state_(std::move(replication_state)), |
| 38 | parent_(parent), |
| 39 | browsing_instance_id_(browsing_instance_id) {} |
Harkiran Bolaria | 8dec6f9 | 2021-12-07 14:57:12 | [diff] [blame] | 40 | |
| 41 | BrowsingContextState::~BrowsingContextState() = default; |
Harkiran Bolaria | 4eacb3a | 2021-12-13 20:03:47 | [diff] [blame] | 42 | |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 43 | RenderFrameProxyHost* BrowsingContextState::GetRenderFrameProxyHost( |
| 44 | SiteInstanceGroup* site_instance_group) const { |
Harkiran Bolaria | 5c5a9739 | 2022-03-10 14:18:50 | [diff] [blame] | 45 | if (features::GetBrowsingContextMode() == |
| 46 | features::BrowsingContextStateImplementationType:: |
| 47 | kSwapForCrossBrowsingInstanceNavigations) { |
| 48 | // CHECK to verify that the proxy is being accessed from the correct |
| 49 | // BrowsingContextState. As both BrowsingContextState (in non-legacy mode) |
| 50 | // and RenderFrameProxyHost (via SiteInstance) are tied to a given |
| 51 | // BrowsingInstance, the browsing instance id of the BrowsingContextState |
| 52 | // (in the non-legacy mode) and of the SiteInstanceGroup should match. |
| 53 | // If they do not, the code calling this method has likely chosen the |
| 54 | // wrong BrowsingContextGroup (e.g. one from the current RenderFrameHost |
| 55 | // rather than from speculative or vice versa) – as this can lead to |
| 56 | // various unpredictable bugs in proxy management logic, we want to |
| 57 | // crash the browser here when this condition fails. |
| 58 | // |
| 59 | // Note that the outer delegate and opener proxies are an exception and the |
| 60 | // only cases of a proxy associated with a SiteInstanceGroup from another |
| 61 | // BrowsingInstance. Meanwhile, for openers the opener and openee have to be |
| 62 | // in the same BrowsingInstance as well. |
| 63 | // TODO(crbug.com/1270671): Add exception here for outer delegate proxies. |
| 64 | CHECK_EQ(browsing_instance_id_.value(), |
| 65 | site_instance_group->browsing_instance_id()); |
| 66 | } |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 67 | auto it = proxy_hosts_.find(site_instance_group->GetId()); |
| 68 | if (it != proxy_hosts_.end()) |
| 69 | return it->second.get(); |
| 70 | return nullptr; |
| 71 | } |
| 72 | |
Harkiran Bolaria | 5c5a9739 | 2022-03-10 14:18:50 | [diff] [blame] | 73 | void BrowsingContextState::DeleteRenderFrameProxyHost( |
| 74 | SiteInstanceGroup* site_instance_group) { |
| 75 | if (features::GetBrowsingContextMode() == |
| 76 | features::BrowsingContextStateImplementationType:: |
| 77 | kSwapForCrossBrowsingInstanceNavigations) { |
| 78 | // See comments in GetRenderFrameProxyHost for why this check is needed. |
| 79 | CHECK_EQ(browsing_instance_id_.value(), |
| 80 | site_instance_group->browsing_instance_id()); |
| 81 | } |
| 82 | site_instance_group->RemoveObserver(this); |
| 83 | proxy_hosts_.erase(site_instance_group->GetId()); |
| 84 | } |
| 85 | |
| 86 | RenderFrameProxyHost* BrowsingContextState::CreateRenderFrameProxyHost( |
| 87 | SiteInstance* site_instance, |
| 88 | const scoped_refptr<RenderViewHostImpl>& rvh, |
| 89 | FrameTreeNode* frame_tree_node) { |
| 90 | if (features::GetBrowsingContextMode() == |
| 91 | features::BrowsingContextStateImplementationType:: |
| 92 | kLegacyOneToOneWithFrameTreeNode) { |
| 93 | DCHECK_EQ(this, |
| 94 | frame_tree_node->current_frame_host()->browsing_context_state()); |
| 95 | } |
| 96 | |
| 97 | if (features::GetBrowsingContextMode() == |
| 98 | features::BrowsingContextStateImplementationType:: |
| 99 | kSwapForCrossBrowsingInstanceNavigations) { |
| 100 | // See comments in GetRenderFrameProxyHost for why this check is needed. |
| 101 | CHECK_EQ(browsing_instance_id_.value(), |
| 102 | site_instance->GetBrowsingInstanceId()); |
| 103 | } |
| 104 | |
| 105 | auto site_instance_group_id = |
| 106 | static_cast<SiteInstanceImpl*>(site_instance)->group()->GetId(); |
| 107 | CHECK(proxy_hosts_.find(site_instance_group_id) == proxy_hosts_.end()) |
| 108 | << "A proxy already existed for this SiteInstanceGroup."; |
| 109 | RenderFrameProxyHost* proxy_host = |
| 110 | new RenderFrameProxyHost(site_instance, std::move(rvh), frame_tree_node); |
| 111 | proxy_hosts_[site_instance_group_id] = base::WrapUnique(proxy_host); |
| 112 | static_cast<SiteInstanceImpl*>(site_instance)->group()->AddObserver(this); |
| 113 | |
| 114 | TRACE_EVENT_INSTANT( |
| 115 | "navigation", "BrowsingContextState::CreateRenderFrameProxyHost", |
| 116 | perfetto::protos::pbzero::ChromeTrackEvent::kRenderFrameProxyHost, |
| 117 | *proxy_host); |
| 118 | return proxy_host; |
| 119 | } |
| 120 | |
Harkiran Bolaria | d22a1dca | 2022-02-22 17:01:12 | [diff] [blame] | 121 | size_t BrowsingContextState::GetProxyCount() { |
| 122 | return proxy_hosts_.size(); |
| 123 | } |
| 124 | |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 125 | bool BrowsingContextState::UpdateFramePolicyHeaders( |
| 126 | network::mojom::WebSandboxFlags sandbox_flags, |
| 127 | const blink::ParsedPermissionsPolicy& parsed_header) { |
| 128 | bool changed = false; |
| 129 | if (replication_state_->permissions_policy_header != parsed_header) { |
| 130 | replication_state_->permissions_policy_header = parsed_header; |
| 131 | changed = true; |
| 132 | } |
| 133 | // TODO(iclelland): Kill the renderer if sandbox flags is not a subset of the |
| 134 | // currently effective sandbox flags from the frame. https://crbug.com/740556 |
| 135 | network::mojom::WebSandboxFlags updated_flags = |
| 136 | sandbox_flags | replication_state_->frame_policy.sandbox_flags; |
| 137 | if (replication_state_->active_sandbox_flags != updated_flags) { |
| 138 | replication_state_->active_sandbox_flags = updated_flags; |
| 139 | changed = true; |
| 140 | } |
| 141 | // Notify any proxies if the policies have been changed. |
| 142 | if (changed) { |
| 143 | for (const auto& pair : proxy_hosts_) { |
| 144 | pair.second->GetAssociatedRemoteFrame()->DidSetFramePolicyHeaders( |
| 145 | replication_state_->active_sandbox_flags, |
| 146 | replication_state_->permissions_policy_header); |
| 147 | } |
| 148 | } |
| 149 | return changed; |
| 150 | } |
| 151 | |
| 152 | bool BrowsingContextState::CommitFramePolicy( |
| 153 | const blink::FramePolicy& new_frame_policy) { |
| 154 | // Documents create iframes, iframes host new documents. Both are associated |
| 155 | // with sandbox flags. They are required to be stricter or equal to their |
| 156 | // owner when they change, as we go down. |
| 157 | // TODO(https://crbug.com/1262061). Enforce the invariant mentioned above, |
| 158 | // once the interactions with fenced frame has been tested and clarified. |
| 159 | |
| 160 | bool did_change_flags = new_frame_policy.sandbox_flags != |
| 161 | replication_state_->frame_policy.sandbox_flags; |
| 162 | bool did_change_container_policy = |
| 163 | new_frame_policy.container_policy != |
| 164 | replication_state_->frame_policy.container_policy; |
| 165 | bool did_change_required_document_policy = |
| 166 | new_frame_policy.required_document_policy != |
| 167 | replication_state_->frame_policy.required_document_policy; |
| 168 | DCHECK_EQ(new_frame_policy.is_fenced, |
| 169 | replication_state_->frame_policy.is_fenced); |
| 170 | |
Harkiran Bolaria | 4eacb3a | 2021-12-13 20:03:47 | [diff] [blame] | 171 | if (did_change_flags) { |
| 172 | replication_state_->frame_policy.sandbox_flags = |
| 173 | new_frame_policy.sandbox_flags; |
| 174 | } |
| 175 | if (did_change_container_policy) { |
| 176 | replication_state_->frame_policy.container_policy = |
| 177 | new_frame_policy.container_policy; |
| 178 | } |
| 179 | if (did_change_required_document_policy) { |
| 180 | replication_state_->frame_policy.required_document_policy = |
| 181 | new_frame_policy.required_document_policy; |
| 182 | } |
Harkiran Bolaria | e352143 | 2021-12-14 11:27:43 | [diff] [blame] | 183 | |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 184 | UpdateFramePolicyHeaders(new_frame_policy.sandbox_flags, |
| 185 | replication_state_->permissions_policy_header); |
| 186 | return did_change_flags || did_change_container_policy || |
| 187 | did_change_required_document_policy; |
Harkiran Bolaria | 7fdb4c64 | 2021-12-20 12:47:00 | [diff] [blame] | 188 | } |
| 189 | |
Harkiran Bolaria | 880a763 | 2022-02-28 16:02:50 | [diff] [blame] | 190 | void BrowsingContextState::SetFrameName(const std::string& name, |
| 191 | const std::string& unique_name) { |
| 192 | if (name == replication_state_->name) { |
| 193 | // |unique_name| shouldn't change unless |name| changes. |
| 194 | DCHECK_EQ(unique_name, replication_state_->unique_name); |
| 195 | return; |
| 196 | } |
| 197 | |
| 198 | if (parent_) { |
| 199 | // Non-main frames should have a non-empty unique name. |
| 200 | DCHECK(!unique_name.empty()); |
| 201 | } else { |
| 202 | // Unique name of main frames should always stay empty. |
| 203 | DCHECK(unique_name.empty()); |
| 204 | } |
| 205 | |
| 206 | // Note the unique name should only be able to change before the first real |
| 207 | // load is committed, but that's not strongly enforced here. |
| 208 | for (const auto& pair : proxy_hosts_) { |
| 209 | pair.second->GetAssociatedRemoteFrame()->SetReplicatedName(name, |
| 210 | unique_name); |
| 211 | } |
| 212 | replication_state_->unique_name = unique_name; |
| 213 | replication_state_->name = name; |
| 214 | } |
| 215 | |
Harkiran Bolaria | e352143 | 2021-12-14 11:27:43 | [diff] [blame] | 216 | void BrowsingContextState::SetCurrentOrigin( |
| 217 | const url::Origin& origin, |
| 218 | bool is_potentially_trustworthy_unique_origin) { |
| 219 | if (origin.IsSameOriginWith(replication_state_->origin) && |
| 220 | replication_state_->has_potentially_trustworthy_unique_origin == |
| 221 | is_potentially_trustworthy_unique_origin) { |
| 222 | return; |
| 223 | } |
| 224 | |
| 225 | for (const auto& pair : proxy_hosts_) { |
| 226 | pair.second->GetAssociatedRemoteFrame()->SetReplicatedOrigin( |
| 227 | origin, is_potentially_trustworthy_unique_origin); |
| 228 | } |
| 229 | |
| 230 | replication_state_->origin = origin; |
| 231 | replication_state_->has_potentially_trustworthy_unique_origin = |
| 232 | is_potentially_trustworthy_unique_origin; |
| 233 | } |
| 234 | |
| 235 | void BrowsingContextState::SetInsecureRequestPolicy( |
| 236 | blink::mojom::InsecureRequestPolicy policy) { |
| 237 | if (policy == replication_state_->insecure_request_policy) |
| 238 | return; |
| 239 | for (const auto& pair : proxy_hosts_) { |
| 240 | pair.second->GetAssociatedRemoteFrame()->EnforceInsecureRequestPolicy( |
| 241 | policy); |
| 242 | } |
| 243 | replication_state_->insecure_request_policy = policy; |
| 244 | } |
| 245 | |
| 246 | void BrowsingContextState::SetInsecureNavigationsSet( |
| 247 | const std::vector<uint32_t>& insecure_navigations_set) { |
| 248 | DCHECK(std::is_sorted(insecure_navigations_set.begin(), |
| 249 | insecure_navigations_set.end())); |
| 250 | if (insecure_navigations_set == replication_state_->insecure_navigations_set) |
| 251 | return; |
| 252 | for (const auto& pair : proxy_hosts_) { |
| 253 | pair.second->GetAssociatedRemoteFrame()->EnforceInsecureNavigationsSet( |
| 254 | insecure_navigations_set); |
| 255 | } |
| 256 | replication_state_->insecure_navigations_set = insecure_navigations_set; |
| 257 | } |
| 258 | |
| 259 | void BrowsingContextState::OnSetHadStickyUserActivationBeforeNavigation( |
| 260 | bool value) { |
| 261 | for (const auto& pair : proxy_hosts_) { |
| 262 | pair.second->GetAssociatedRemoteFrame() |
| 263 | ->SetHadStickyUserActivationBeforeNavigation(value); |
| 264 | } |
| 265 | replication_state_->has_received_user_gesture_before_nav = value; |
| 266 | } |
| 267 | |
| 268 | void BrowsingContextState::SetIsAdSubframe(bool is_ad_subframe) { |
| 269 | if (is_ad_subframe == replication_state_->is_ad_subframe) |
| 270 | return; |
| 271 | |
| 272 | replication_state_->is_ad_subframe = is_ad_subframe; |
| 273 | for (const auto& pair : proxy_hosts_) { |
| 274 | pair.second->GetAssociatedRemoteFrame()->SetReplicatedIsAdSubframe( |
| 275 | is_ad_subframe); |
| 276 | } |
| 277 | } |
| 278 | |
Harkiran Bolaria | e182a594 | 2021-12-20 17:23:31 | [diff] [blame] | 279 | void BrowsingContextState::ActiveFrameCountIsZero( |
Sharon Yang | a2fe85e | 2022-02-09 21:38:29 | [diff] [blame] | 280 | SiteInstanceGroup* site_instance_group) { |
| 281 | // |site_instance_group| no longer contains any active RenderFrameHosts, so we |
| 282 | // don't need to maintain a proxy there anymore. |
| 283 | RenderFrameProxyHost* proxy = GetRenderFrameProxyHost(site_instance_group); |
Harkiran Bolaria | e182a594 | 2021-12-20 17:23:31 | [diff] [blame] | 284 | CHECK(proxy); |
| 285 | |
Sharon Yang | a2fe85e | 2022-02-09 21:38:29 | [diff] [blame] | 286 | DeleteRenderFrameProxyHost(site_instance_group); |
Harkiran Bolaria | e182a594 | 2021-12-20 17:23:31 | [diff] [blame] | 287 | } |
| 288 | |
| 289 | void BrowsingContextState::RenderProcessGone( |
Sharon Yang | a2fe85e | 2022-02-09 21:38:29 | [diff] [blame] | 290 | SiteInstanceGroup* site_instance_group, |
Harkiran Bolaria | e182a594 | 2021-12-20 17:23:31 | [diff] [blame] | 291 | const ChildProcessTerminationInfo& info) { |
Sharon Yang | a2fe85e | 2022-02-09 21:38:29 | [diff] [blame] | 292 | GetRenderFrameProxyHost(site_instance_group) |
| 293 | ->SetRenderFrameProxyCreated(false); |
Harkiran Bolaria | e182a594 | 2021-12-20 17:23:31 | [diff] [blame] | 294 | } |
| 295 | |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 296 | void BrowsingContextState::SendFramePolicyUpdatesToProxies( |
Sharon Yang | 571baee | 2022-03-18 19:01:54 | [diff] [blame] | 297 | SiteInstanceGroup* parent_group, |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 298 | const blink::FramePolicy& frame_policy) { |
| 299 | // Notify all of the frame's proxies about updated policies, excluding |
| 300 | // the parent process since it already knows the latest state. |
| 301 | for (const auto& pair : proxy_hosts_) { |
Sharon Yang | 571baee | 2022-03-18 19:01:54 | [diff] [blame] | 302 | if (pair.second->site_instance_group() != parent_group) { |
Harkiran Bolaria | 5ce2763 | 2022-01-20 15:05:05 | [diff] [blame] | 303 | pair.second->GetAssociatedRemoteFrame()->DidUpdateFramePolicy( |
| 304 | frame_policy); |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | |
Harkiran Bolaria | 3f83fba7 | 2022-03-10 17:48:40 | [diff] [blame] | 309 | void BrowsingContextState::OnDidStartLoading() { |
| 310 | for (const auto& pair : proxy_hosts_) |
| 311 | pair.second->GetAssociatedRemoteFrame()->DidStartLoading(); |
| 312 | } |
| 313 | |
| 314 | void BrowsingContextState::OnDidStopLoading() { |
| 315 | for (const auto& pair : proxy_hosts_) |
| 316 | pair.second->GetAssociatedRemoteFrame()->DidStopLoading(); |
| 317 | } |
| 318 | |
Harkiran Bolaria | 0b3bdef0 | 2022-03-10 13:04:40 | [diff] [blame] | 319 | void BrowsingContextState::ResetProxyHosts() { |
| 320 | for (const auto& pair : proxy_hosts_) { |
| 321 | pair.second->site_instance_group()->RemoveObserver(this); |
| 322 | } |
| 323 | proxy_hosts_.clear(); |
| 324 | } |
| 325 | |
Sharon Yang | 571baee | 2022-03-18 19:01:54 | [diff] [blame] | 326 | void BrowsingContextState::UpdateOpener( |
| 327 | SiteInstanceGroup* source_site_instance_group) { |
Harkiran Bolaria | 3f83fba7 | 2022-03-10 17:48:40 | [diff] [blame] | 328 | for (const auto& pair : proxy_hosts_) { |
Sharon Yang | 571baee | 2022-03-18 19:01:54 | [diff] [blame] | 329 | if (pair.second->site_instance_group() == source_site_instance_group) |
Harkiran Bolaria | 3f83fba7 | 2022-03-10 17:48:40 | [diff] [blame] | 330 | continue; |
| 331 | pair.second->UpdateOpener(); |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | void BrowsingContextState::OnDidUpdateFrameOwnerProperties( |
| 336 | const blink::mojom::FrameOwnerProperties& properties) { |
| 337 | // Notify this frame's proxies if they live in a different process from its |
| 338 | // parent. This is only currently needed for the allowFullscreen property, |
| 339 | // since that can be queried on RemoteFrame ancestors. |
| 340 | // |
| 341 | // TODO(alexmos): It would be sufficient to only send this update to proxies |
| 342 | // in the current FrameTree. |
| 343 | for (const auto& pair : proxy_hosts_) { |
| 344 | if (pair.second->site_instance_group() != |
| 345 | parent_->GetSiteInstance()->group()) { |
| 346 | auto properties_for_remote_frame = properties.Clone(); |
| 347 | RenderFrameProxyHost* proxy = pair.second.get(); |
| 348 | proxy->GetAssociatedRemoteFrame()->SetFrameOwnerProperties( |
| 349 | std::move(properties_for_remote_frame)); |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | void BrowsingContextState::ExecuteRemoteFramesBroadcastMethod( |
| 355 | base::RepeatingCallback<void(RenderFrameProxyHost*)> callback, |
| 356 | SiteInstance* instance_to_skip, |
| 357 | RenderFrameProxyHost* outer_delegate_proxy) { |
| 358 | for (const auto& pair : proxy_hosts_) { |
| 359 | if (outer_delegate_proxy == pair.second.get()) |
| 360 | continue; |
| 361 | if (pair.second->GetSiteInstance() == instance_to_skip) |
| 362 | continue; |
| 363 | if (!pair.second->is_render_frame_proxy_live()) |
| 364 | continue; |
| 365 | callback.Run(pair.second.get()); |
| 366 | } |
| 367 | } |
| 368 | |
Harkiran Bolaria | 3bf5457f | 2022-03-10 20:04:37 | [diff] [blame] | 369 | void BrowsingContextState::WriteIntoTrace( |
Alexander Timin | 074cd18 | 2022-03-23 18:11:22 | [diff] [blame^] | 370 | perfetto::TracedProto<TraceProto> proto) const { |
Harkiran Bolaria | 3bf5457f | 2022-03-10 20:04:37 | [diff] [blame] | 371 | if (browsing_instance_id_.has_value()) |
| 372 | proto->set_browsing_instance_id(browsing_instance_id_.value().value()); |
Alexander Timin | 074cd18 | 2022-03-23 18:11:22 | [diff] [blame^] | 373 | |
| 374 | perfetto::TracedDictionary dict = std::move(proto).AddDebugAnnotations(); |
| 375 | dict.Add("this", static_cast<const void*>(this)); |
Harkiran Bolaria | 3bf5457f | 2022-03-10 20:04:37 | [diff] [blame] | 376 | } |
| 377 | |
Alexander Timin | 07cad076 | 2022-03-15 00:33:17 | [diff] [blame] | 378 | } // namespace content |