blob: eb653ece372a3d7469b0794abdcb150b3a577635 [file] [log] [blame]
Avi Drissman4e1b7bc32022-09-15 14:03:501// Copyright 2017 The Chromium Authors
Conley Owens47f4fbf12017-08-02 01:56:522// 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/geolocation/geolocation_service_impl.h"
6
Peter Boströmdd7e40ec2021-04-05 20:40:107#include <memory>
8
Avi Drissmanadac21992023-01-11 23:46:399#include "base/functional/bind.h"
10#include "base/functional/callback_helpers.h"
Gabriel Charette078e3662017-08-28 22:59:0411#include "base/run_loop.h"
Sean Mahere672a662023-01-09 21:42:2812#include "base/task/single_thread_task_runner.h"
Matt Reynolds5e784e92023-04-21 01:40:2713#include "base/test/test_future.h"
Andrey Lushnikovebff0442018-07-12 20:02:5814#include "content/browser/permissions/permission_controller_impl.h"
Ken Rockotce010f02019-12-12 23:32:3215#include "content/public/browser/device_service.h"
Andrey Lushnikovebff0442018-07-12 20:02:5816#include "content/public/browser/permission_controller.h"
Andy Paicu0a6d4b502023-08-29 15:13:0917#include "content/public/browser/permission_request_description.h"
Florian Jackyaaf42832025-08-19 04:03:2618#include "content/public/browser/permission_result.h"
Douglas Creager5964cda2018-03-01 16:40:2919#include "content/public/test/mock_permission_manager.h"
Conley Owens47f4fbf12017-08-02 01:56:5220#include "content/public/test/navigation_simulator.h"
Andrey Lushnikovebff0442018-07-12 20:02:5821#include "content/public/test/test_browser_context.h"
Conley Owens47f4fbf12017-08-02 01:56:5222#include "content/test/test_render_frame_host.h"
Miyoung Shin9d893fd2019-09-11 14:58:5423#include "mojo/public/cpp/bindings/remote.h"
Ke Hef4173392018-06-29 07:44:1524#include "services/device/public/cpp/test/scoped_geolocation_overrider.h"
Ke He31d0bb02018-02-24 07:16:2425#include "services/device/public/mojom/geolocation.mojom.h"
Ke He81d15332018-03-01 04:02:2826#include "services/device/public/mojom/geolocation_context.mojom.h"
Ke He31d0bb02018-02-24 07:16:2427#include "services/device/public/mojom/geoposition.mojom.h"
Sandor Majorca47512a2025-02-11 16:58:0428#include "services/network/public/cpp/permissions_policy/origin_with_possible_wildcards.h"
Sandor Major878f8352025-02-18 20:16:0229#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
Sandor «Alex» Majorc41217f2025-02-14 23:33:1330#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
Conley Owens47f4fbf12017-08-02 01:56:5231#include "testing/gtest/include/gtest/gtest.h"
Andy Paicua6d6d852022-04-28 18:08:3632#include "third_party/blink/public/common/permissions/permission_utils.h"
Conley Owens47f4fbf12017-08-02 01:56:5233
Conley Owens47f4fbf12017-08-02 01:56:5234namespace content {
35namespace {
36
Matt Reynolds5e784e92023-04-21 01:40:2737using ::base::test::TestFuture;
38using ::blink::mojom::GeolocationService;
39using ::blink::mojom::PermissionStatus;
40using ::device::mojom::Geolocation;
41using ::device::mojom::GeopositionPtr;
42using ::device::mojom::GeopositionResultPtr;
43
Florian Jackyaaf42832025-08-19 04:03:2644using PermissionCallback =
45 base::OnceCallback<void(const std::vector<PermissionResult>&)>;
danakj47c8fb52019-05-02 16:34:3646
Conley Owens47f4fbf12017-08-02 01:56:5247double kMockLatitude = 1.0;
48double kMockLongitude = 10.0;
Michael Thiessen3067e4e2020-01-17 19:13:2449
Conley Owens47f4fbf12017-08-02 01:56:5250class TestPermissionManager : public MockPermissionManager {
51 public:
Balazs Engedye30e9612021-04-02 10:37:2952 TestPermissionManager() = default;
Conley Owens47f4fbf12017-08-02 01:56:5253 ~TestPermissionManager() override = default;
54
Illia Klimov27239edc2022-05-11 17:14:5955 void RequestPermissionsFromCurrentDocument(
Andy Paicu0a6d4b502023-08-29 15:13:0956 RenderFrameHost* render_frame_host,
57 const PermissionRequestDescription& request_description,
Florian Jackyaaf42832025-08-19 04:03:2658 base::OnceCallback<void(const std::vector<content::PermissionResult>&)>
59 callback) override {
Andy Paicu0a6d4b502023-08-29 15:13:0960 ASSERT_EQ(request_description.permissions.size(), 1u);
Florian Jacky65b7d102025-04-07 10:02:5261 EXPECT_EQ(blink::PermissionDescriptorToPermissionType(
62 request_description.permissions[0]),
Andy Paicu0a6d4b502023-08-29 15:13:0963 blink::PermissionType::GEOLOCATION);
64 EXPECT_TRUE(request_description.user_gesture);
danakj47c8fb52019-05-02 16:34:3665 request_callback_.Run(std::move(callback));
Conley Owens47f4fbf12017-08-02 01:56:5266 }
67
Conley Owens47f4fbf12017-08-02 01:56:5268 void SetRequestCallback(
danakj47c8fb52019-05-02 16:34:3669 base::RepeatingCallback<void(PermissionCallback)> request_callback) {
70 request_callback_ = std::move(request_callback);
Conley Owens47f4fbf12017-08-02 01:56:5271 }
72
Conley Owens47f4fbf12017-08-02 01:56:5273 private:
danakj47c8fb52019-05-02 16:34:3674 base::RepeatingCallback<void(PermissionCallback)> request_callback_;
Conley Owens47f4fbf12017-08-02 01:56:5275};
76
77class GeolocationServiceTest : public RenderViewHostImplTestHarness {
Peter Boström9b036532021-10-28 23:37:2878 public:
79 GeolocationServiceTest(const GeolocationServiceTest&) = delete;
80 GeolocationServiceTest& operator=(const GeolocationServiceTest&) = delete;
81
Conley Owens47f4fbf12017-08-02 01:56:5282 protected:
83 GeolocationServiceTest() {}
84
85 ~GeolocationServiceTest() override {}
86
87 void SetUp() override {
88 RenderViewHostImplTestHarness::SetUp();
Peter Kasting8bdc82812020-02-17 18:25:1689 NavigateAndCommit(GURL("https://www.google.com/maps"));
Takumi Fujimoto66a5e3002023-09-14 22:58:3190 static_cast<TestBrowserContext*>(GetBrowserContext())
91 ->SetPermissionControllerDelegate(
92 std::make_unique<TestPermissionManager>());
Ke He81d15332018-03-01 04:02:2893
Ke He81d15332018-03-01 04:02:2894 geolocation_overrider_ =
95 std::make_unique<device::ScopedGeolocationOverrider>(kMockLatitude,
96 kMockLongitude);
Ken Rockotce010f02019-12-12 23:32:3297 GetDeviceService().BindGeolocationContext(
98 context_.BindNewPipeAndPassReceiver());
Ke He81d15332018-03-01 04:02:2899 }
100
101 void TearDown() override {
Yifan Luo8e5d3d52024-10-22 19:18:16102 service_.reset();
Miyoung Shin0f4480d2019-09-12 01:15:26103 context_.reset();
Ke He81d15332018-03-01 04:02:28104 geolocation_overrider_.reset();
Ke He81d15332018-03-01 04:02:28105 RenderViewHostImplTestHarness::TearDown();
Conley Owens47f4fbf12017-08-02 01:56:52106 }
107
Charlie Hue20fe2f2021-03-07 03:39:59108 void CreateEmbeddedFrameAndGeolocationService(
109 bool allow_via_permissions_policy) {
Peter Kasting8bdc82812020-02-17 18:25:16110 const GURL kEmbeddedUrl("https://embeddables.com/someframe");
Sandor Major878f8352025-02-18 20:16:02111 network::ParsedPermissionsPolicy frame_policy = {};
Charlie Hue20fe2f2021-03-07 03:39:59112 if (allow_via_permissions_policy) {
Ian Clelland156a5202020-09-30 17:48:21113 frame_policy.push_back(
Sandor «Alex» Majore9545a72025-01-31 20:40:46114 {network::mojom::PermissionsPolicyFeature::kGeolocation,
Sandor Majorca47512a2025-02-11 16:58:04115 std::vector{*network::OriginWithPossibleWildcards::FromOrigin(
Ari Chivukula30c4e912023-05-25 12:35:50116 url::Origin::Create(kEmbeddedUrl))},
Arthur Sonzognic686e8f2024-01-11 08:36:37117 /*self_if_matches=*/std::nullopt,
Ari Chivukula04f6ff7e2023-03-22 18:02:00118 /*matches_all_origins=*/false,
119 /*matches_opaque_src=*/false});
Raymes Khoury43613582017-09-28 09:22:36120 }
Conley Owens47f4fbf12017-08-02 01:56:52121 RenderFrameHost* embedded_rfh =
Ian Clelland156a5202020-09-30 17:48:21122 RenderFrameHostTester::For(main_rfh())
123 ->AppendChildWithPolicy("", frame_policy);
Conley Owens47f4fbf12017-08-02 01:56:52124 RenderFrameHostTester::For(embedded_rfh)->InitializeRenderFrameIfNeeded();
125 auto navigation_simulator = NavigationSimulator::CreateRendererInitiated(
Peter Kasting8bdc82812020-02-17 18:25:16126 kEmbeddedUrl, embedded_rfh);
Conley Owens47f4fbf12017-08-02 01:56:52127 navigation_simulator->Commit();
128 embedded_rfh = navigation_simulator->GetFinalRenderFrameHost();
Peter Boströmdd7e40ec2021-04-05 20:40:10129 service_ =
130 std::make_unique<GeolocationServiceImpl>(context_.get(), embedded_rfh);
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19131 service_->Bind(service_remote_.BindNewPipeAndPassReceiver());
Conley Owens47f4fbf12017-08-02 01:56:52132 }
133
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19134 mojo::Remote<blink::mojom::GeolocationService>& service_remote() {
135 return service_remote_;
136 }
Conley Owens47f4fbf12017-08-02 01:56:52137
Conley Owens47f4fbf12017-08-02 01:56:52138 TestPermissionManager* permission_manager() {
Andrey Lushnikovebff0442018-07-12 20:02:58139 return static_cast<TestPermissionManager*>(
Takumi Fujimoto66a5e3002023-09-14 22:58:31140 GetBrowserContext()->GetPermissionControllerDelegate());
Conley Owens47f4fbf12017-08-02 01:56:52141 }
142
143 private:
Ke He81d15332018-03-01 04:02:28144 std::unique_ptr<device::ScopedGeolocationOverrider> geolocation_overrider_;
Conley Owens9613ff9a2017-11-27 21:06:55145 std::unique_ptr<GeolocationServiceImpl> service_;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19146 mojo::Remote<blink::mojom::GeolocationService> service_remote_;
Miyoung Shin0f4480d2019-09-12 01:15:26147 mojo::Remote<device::mojom::GeolocationContext> context_;
Conley Owens47f4fbf12017-08-02 01:56:52148};
149
150} // namespace
151
152TEST_F(GeolocationServiceTest, PermissionGrantedPolicyViolation) {
Aaron Colwellda68cea2019-10-10 02:55:26153 // The embedded frame is not allowed.
Charlie Hue20fe2f2021-03-07 03:39:59154 CreateEmbeddedFrameAndGeolocationService(
155 /*allow_via_permissions_policy=*/false);
Conley Owens47f4fbf12017-08-02 01:56:52156
157 permission_manager()->SetRequestCallback(
danakj47c8fb52019-05-02 16:34:36158 base::BindRepeating([](PermissionCallback callback) {
Conley Owens47f4fbf12017-08-02 01:56:52159 ADD_FAILURE() << "Permissions checked unexpectedly.";
160 }));
Miyoung Shin9d893fd2019-09-11 14:58:54161 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19162 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54163 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00164 base::BindOnce([](blink::mojom::PermissionStatus status) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29165 EXPECT_EQ(blink::mojom::PermissionStatus::DENIED, status);
166 }));
Alvin Ji9e137ea2025-02-20 05:03:02167 TestFuture<void> disconnect_future;
168 geolocation.set_disconnect_handler(disconnect_future.GetCallback());
Conley Owens47f4fbf12017-08-02 01:56:52169
Matt Reynolds5e784e92023-04-21 01:40:27170 geolocation->QueryNextPosition(
171 base::BindOnce([](GeopositionResultPtr result) {
172 ADD_FAILURE() << "Position updated unexpectedly";
Conley Owens47f4fbf12017-08-02 01:56:52173 }));
Alvin Ji9e137ea2025-02-20 05:03:02174 EXPECT_TRUE(disconnect_future.Wait());
Conley Owens47f4fbf12017-08-02 01:56:52175}
176
177TEST_F(GeolocationServiceTest, PermissionGrantedSync) {
Charlie Hue20fe2f2021-03-07 03:39:59178 CreateEmbeddedFrameAndGeolocationService(
179 /*allow_via_permissions_policy=*/true);
Alvin Ji9e137ea2025-02-20 05:03:02180 TestFuture<PermissionCallback> permission_request_future;
Conley Owens47f4fbf12017-08-02 01:56:52181 permission_manager()->SetRequestCallback(
danakj47c8fb52019-05-02 16:34:36182 base::BindRepeating([](PermissionCallback callback) {
Florian Jackyaaf42832025-08-19 04:03:26183 std::move(callback).Run(std::vector{content::PermissionResult(
184 PermissionStatus::GRANTED, PermissionStatusSource::UNSPECIFIED)});
Conley Owens47f4fbf12017-08-02 01:56:52185 }));
Miyoung Shin9d893fd2019-09-11 14:58:54186 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19187 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54188 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00189 base::BindOnce([](blink::mojom::PermissionStatus status) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29190 EXPECT_EQ(blink::mojom::PermissionStatus::GRANTED, status);
191 }));
Conley Owens47f4fbf12017-08-02 01:56:52192
Miyoung Shin9d893fd2019-09-11 14:58:54193 geolocation.set_disconnect_handler(base::BindOnce(
Conley Owens47f4fbf12017-08-02 01:56:52194 [] { ADD_FAILURE() << "Connection error handler called unexpectedly"; }));
195
Matt Reynolds5e784e92023-04-21 01:40:27196 TestFuture<GeopositionResultPtr> result_future;
197 geolocation->QueryNextPosition(result_future.GetCallback());
198 ASSERT_TRUE(result_future.Get()->is_position());
199 const auto& position = *result_future.Get()->get_position();
200 EXPECT_DOUBLE_EQ(kMockLatitude, position.latitude);
201 EXPECT_DOUBLE_EQ(kMockLongitude, position.longitude);
Conley Owens47f4fbf12017-08-02 01:56:52202}
203
204TEST_F(GeolocationServiceTest, PermissionDeniedSync) {
Charlie Hue20fe2f2021-03-07 03:39:59205 CreateEmbeddedFrameAndGeolocationService(
206 /*allow_via_permissions_policy=*/true);
Conley Owens47f4fbf12017-08-02 01:56:52207 permission_manager()->SetRequestCallback(
danakj47c8fb52019-05-02 16:34:36208 base::BindRepeating([](PermissionCallback callback) {
Florian Jackyaaf42832025-08-19 04:03:26209 std::move(callback).Run(std::vector{content::PermissionResult(
210 PermissionStatus::DENIED, PermissionStatusSource::UNSPECIFIED)});
Conley Owens47f4fbf12017-08-02 01:56:52211 }));
Miyoung Shin9d893fd2019-09-11 14:58:54212 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19213 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54214 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00215 base::BindOnce([](blink::mojom::PermissionStatus status) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29216 EXPECT_EQ(blink::mojom::PermissionStatus::DENIED, status);
217 }));
Conley Owens47f4fbf12017-08-02 01:56:52218
Alvin Ji9e137ea2025-02-20 05:03:02219 TestFuture<void> disconnect_future;
220 geolocation.set_disconnect_handler(disconnect_future.GetCallback());
Conley Owens47f4fbf12017-08-02 01:56:52221
Matt Reynolds5e784e92023-04-21 01:40:27222 geolocation->QueryNextPosition(
223 base::BindOnce([](GeopositionResultPtr result) {
224 ADD_FAILURE() << "Position updated unexpectedly";
225 }));
Alvin Ji9e137ea2025-02-20 05:03:02226 EXPECT_TRUE(disconnect_future.Wait());
Conley Owens47f4fbf12017-08-02 01:56:52227}
228
229TEST_F(GeolocationServiceTest, PermissionGrantedAsync) {
Charlie Hue20fe2f2021-03-07 03:39:59230 CreateEmbeddedFrameAndGeolocationService(
231 /*allow_via_permissions_policy=*/true);
Conley Owens47f4fbf12017-08-02 01:56:52232 permission_manager()->SetRequestCallback(
danakj47c8fb52019-05-02 16:34:36233 base::BindRepeating([](PermissionCallback permission_callback) {
Sean Maher5b9af51f2022-11-21 15:32:47234 base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
Florian Jackyaaf42832025-08-19 04:03:26235 FROM_HERE,
236 base::BindOnce(std::move(permission_callback),
237 std::vector{content::PermissionResult(
238 PermissionStatus::GRANTED,
239 PermissionStatusSource::UNSPECIFIED)}));
Conley Owens47f4fbf12017-08-02 01:56:52240 }));
Miyoung Shin9d893fd2019-09-11 14:58:54241 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19242 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54243 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00244 base::BindOnce([](blink::mojom::PermissionStatus status) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29245 EXPECT_EQ(blink::mojom::PermissionStatus::GRANTED, status);
246 }));
Conley Owens47f4fbf12017-08-02 01:56:52247
Miyoung Shin9d893fd2019-09-11 14:58:54248 geolocation.set_disconnect_handler(base::BindOnce(
Conley Owens47f4fbf12017-08-02 01:56:52249 [] { ADD_FAILURE() << "Connection error handler called unexpectedly"; }));
250
Matt Reynolds5e784e92023-04-21 01:40:27251 TestFuture<GeopositionResultPtr> result_future;
252 geolocation->QueryNextPosition(result_future.GetCallback());
253 ASSERT_TRUE(result_future.Get()->is_position());
254 const auto& position = *result_future.Get()->get_position();
255 EXPECT_DOUBLE_EQ(kMockLatitude, position.latitude);
256 EXPECT_DOUBLE_EQ(kMockLongitude, position.longitude);
Conley Owens47f4fbf12017-08-02 01:56:52257}
258
259TEST_F(GeolocationServiceTest, PermissionDeniedAsync) {
Charlie Hue20fe2f2021-03-07 03:39:59260 CreateEmbeddedFrameAndGeolocationService(
261 /*allow_via_permissions_policy=*/true);
Conley Owens47f4fbf12017-08-02 01:56:52262 permission_manager()->SetRequestCallback(
danakj47c8fb52019-05-02 16:34:36263 base::BindRepeating([](PermissionCallback permission_callback) {
Sean Maher5b9af51f2022-11-21 15:32:47264 base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
Florian Jackyaaf42832025-08-19 04:03:26265 FROM_HERE,
266 base::BindOnce(std::move(permission_callback),
267 std::vector{content::PermissionResult(
268 PermissionStatus::DENIED,
269 PermissionStatusSource::UNSPECIFIED)}));
Conley Owens47f4fbf12017-08-02 01:56:52270 }));
Miyoung Shin9d893fd2019-09-11 14:58:54271 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19272 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54273 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00274 base::BindOnce([](blink::mojom::PermissionStatus status) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29275 EXPECT_EQ(blink::mojom::PermissionStatus::DENIED, status);
276 }));
Conley Owens47f4fbf12017-08-02 01:56:52277
Alvin Ji9e137ea2025-02-20 05:03:02278 TestFuture<void> disconnect_future;
279 geolocation.set_disconnect_handler(disconnect_future.GetCallback());
Conley Owens47f4fbf12017-08-02 01:56:52280
Matt Reynolds5e784e92023-04-21 01:40:27281 geolocation->QueryNextPosition(
282 base::BindOnce([](GeopositionResultPtr result) {
283 ADD_FAILURE() << "Position updated unexpectedly";
284 }));
Alvin Ji9e137ea2025-02-20 05:03:02285 EXPECT_TRUE(disconnect_future.Wait());
Conley Owens47f4fbf12017-08-02 01:56:52286}
287
288TEST_F(GeolocationServiceTest, ServiceClosedBeforePermissionResponse) {
Charlie Hue20fe2f2021-03-07 03:39:59289 CreateEmbeddedFrameAndGeolocationService(
290 /*allow_via_permissions_policy=*/true);
Miyoung Shin9d893fd2019-09-11 14:58:54291 mojo::Remote<Geolocation> geolocation;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19292 service_remote()->CreateGeolocation(
Miyoung Shin9d893fd2019-09-11 14:58:54293 geolocation.BindNewPipeAndPassReceiver(), true,
Jan Wilken Dörrie8c74db022020-04-20 09:05:00294 base::BindOnce([](blink::mojom::PermissionStatus) {
Matt Reynoldsf10fd2f2019-04-01 19:39:29295 ADD_FAILURE() << "PositionStatus received unexpectedly.";
296 }));
Conley Owens47f4fbf12017-08-02 01:56:52297 // Don't immediately respond to the request.
Peter Kasting341e1fb2018-02-24 00:03:01298 permission_manager()->SetRequestCallback(base::DoNothing());
Conley Owens47f4fbf12017-08-02 01:56:52299
300 base::RunLoop loop;
Mario Sanchez Pradafa6dda8c2019-11-25 18:20:19301 service_remote().reset();
Conley Owens47f4fbf12017-08-02 01:56:52302
Matt Reynolds5e784e92023-04-21 01:40:27303 geolocation->QueryNextPosition(
304 base::BindOnce([](GeopositionResultPtr result) {
305 ADD_FAILURE() << "Position updated unexpectedly";
306 }));
Raymes Khoury5bf2b082018-01-28 23:32:16307 loop.RunUntilIdle();
Conley Owens47f4fbf12017-08-02 01:56:52308}
309
310} // namespace content