OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "base/files/file_path.h" | 5 #include "base/files/file_path.h" |
6 #include "base/scoped_observer.h" | 6 #include "base/scoped_observer.h" |
7 #include "base/strings/string_number_conversions.h" | 7 #include "base/strings/string_number_conversions.h" |
8 #include "base/strings/string_util.h" | |
8 #include "base/strings/stringprintf.h" | 9 #include "base/strings/stringprintf.h" |
9 #include "chrome/browser/extensions/activity_log/activity_actions.h" | 10 #include "chrome/browser/extensions/activity_log/activity_actions.h" |
10 #include "chrome/browser/extensions/activity_log/activity_log.h" | 11 #include "chrome/browser/extensions/activity_log/activity_log.h" |
11 #include "chrome/browser/extensions/activity_log/ad_network_database.h" | 12 #include "chrome/browser/extensions/activity_log/ad_network_database.h" |
12 #include "chrome/browser/extensions/extension_browsertest.h" | 13 #include "chrome/browser/extensions/extension_browsertest.h" |
13 #include "chrome/browser/extensions/extension_test_message_listener.h" | 14 #include "chrome/browser/extensions/extension_test_message_listener.h" |
14 #include "chrome/test/base/ui_test_utils.h" | 15 #include "chrome/test/base/ui_test_utils.h" |
15 #include "extensions/common/extension.h" | 16 #include "extensions/common/extension.h" |
16 #include "net/test/embedded_test_server/embedded_test_server.h" | 17 #include "net/test/embedded_test_server/embedded_test_server.h" |
17 #include "net/test/embedded_test_server/http_response.h" | 18 #include "net/test/embedded_test_server/http_response.h" |
18 #include "url/gurl.h" | 19 #include "url/gurl.h" |
19 | 20 |
20 namespace net { | 21 namespace net { |
21 namespace test_server { | 22 namespace test_server { |
22 struct HttpRequest; | 23 struct HttpRequest; |
23 } | 24 } |
24 } | 25 } |
25 | 26 |
26 namespace extensions { | 27 namespace extensions { |
27 | 28 |
28 namespace { | 29 namespace { |
29 | 30 |
30 // The "ad network" that we are using. Any src or href equal to this should be | 31 // The "ad network" that we are using. Any src or href equal to this should be |
31 // considered an ad network. | 32 // considered an ad network. |
32 const char kAdNetwork[] = "http://www.known-ads.adnetwork"; | 33 const char kAdNetwork1[] = "http://www.known-ads.adnetwork"; |
34 const char kAdNetwork2[] = "http://www.also-known-ads.adnetwork"; | |
33 | 35 |
34 // The current stage of the test. | 36 // The current stage of the test. |
35 enum Stage { | 37 enum Stage { |
36 BEFORE_RESET, // We are about to reset the page. | 38 BEFORE_RESET, // We are about to reset the page. |
37 RESETTING, // We are resetting the page. | 39 RESETTING, // We are resetting the page. |
38 TESTING // The reset is complete, and we are testing. | 40 TESTING // The reset is complete, and we are testing. |
39 }; | 41 }; |
40 | 42 |
41 // The string sent by the test to indicate that the page reset will begin. | 43 // The string sent by the test to indicate that the page reset will begin. |
42 const char kResetBeginString[] = "Page Reset Begin"; | 44 const char kResetBeginString[] = "Page Reset Begin"; |
43 // The string sent by the test to indicate that page reset is complete. | 45 // The string sent by the test to indicate that page reset is complete. |
44 const char kResetEndString[] = "Page Reset End"; | 46 const char kResetEndString[] = "Page Reset End"; |
45 // The string sent by the test to indicate a JS error was caught in the test. | 47 // The string sent by the test to indicate a JS error was caught in the test. |
46 const char kJavascriptErrorString[] = "Testing Error"; | 48 const char kJavascriptErrorString[] = "Testing Error"; |
47 // The string sent by the test to indicate that we have concluded the full test. | 49 // The string sent by the test to indicate that we have concluded the full test. |
48 const char kTestCompleteString[] = "Test Complete"; | 50 const char kTestCompleteString[] = "Test Complete"; |
49 | 51 |
52 std::string InjectionTypeToString(Action::InjectionType type) { | |
53 switch (type) { | |
54 case Action::NO_AD_INJECTION: | |
55 return "No Ad Injection"; | |
56 case Action::INJECTION_NEW_AD: | |
57 return "Injection New Ad"; | |
58 case Action::INJECTION_REMOVED_AD: | |
59 return "Injection Removed Ad"; | |
60 case Action::INJECTION_REPLACED_AD: | |
61 return "Injection Replaced Ad"; | |
62 case Action::INJECTION_LIKELY_REPLACED_AD: | |
63 return "Injection Likely Replaced Ad"; | |
64 case Action::NUM_INJECTION_TYPES: | |
65 return "Num Injection Types"; | |
66 } | |
67 return std::string(); | |
68 } | |
69 | |
50 // An implementation of ActivityLog::Observer that, for every action, sends it | 70 // An implementation of ActivityLog::Observer that, for every action, sends it |
51 // through Action::DidInjectAd(). This will keep track of the observed | 71 // through Action::DidInjectAd(). This will keep track of the observed |
52 // injections, and can be enabled or disabled as needed (for instance, this | 72 // injections, and can be enabled or disabled as needed (for instance, this |
53 // should be disabled while we are resetting the page). | 73 // should be disabled while we are resetting the page). |
54 class ActivityLogObserver : public ActivityLog::Observer { | 74 class ActivityLogObserver : public ActivityLog::Observer { |
55 public: | 75 public: |
56 explicit ActivityLogObserver(content::BrowserContext* context); | 76 explicit ActivityLogObserver(content::BrowserContext* context); |
57 virtual ~ActivityLogObserver(); | 77 virtual ~ActivityLogObserver(); |
58 | 78 |
59 void set_enabled(bool enabled) { enabled_ = enabled; } | 79 // Disable the observer (e.g., to reset the page). |
60 size_t injection_count() const { return injection_count_; } | 80 void disable() { enabled_ = false; } |
81 | |
82 // Enable the observer, resetting the state. | |
83 void enable() { | |
84 injection_type_ = Action::NO_AD_INJECTION; | |
85 found_multiple_injections_ = false; | |
86 enabled_ = true; | |
87 } | |
88 | |
89 Action::InjectionType injection_type() const { return injection_type_; } | |
90 | |
91 bool found_multiple_injections() const { return found_multiple_injections_; } | |
61 | 92 |
62 private: | 93 private: |
63 virtual void OnExtensionActivity(scoped_refptr<Action> action) OVERRIDE; | 94 virtual void OnExtensionActivity(scoped_refptr<Action> action) OVERRIDE; |
64 | 95 |
65 ScopedObserver<ActivityLog, ActivityLog::Observer> scoped_observer_; | 96 ScopedObserver<ActivityLog, ActivityLog::Observer> scoped_observer_; |
97 | |
98 // The associated BrowserContext. | |
66 content::BrowserContext* context_; | 99 content::BrowserContext* context_; |
67 size_t injection_count_; | 100 |
101 // The type of the last injection. | |
102 Action::InjectionType injection_type_; | |
103 | |
104 // Whether or not we found multiple injection types (which shouldn't happen). | |
105 bool found_multiple_injections_; | |
106 | |
107 // Whether or not the observer is enabled. | |
68 bool enabled_; | 108 bool enabled_; |
69 }; | 109 }; |
70 | 110 |
71 ActivityLogObserver::ActivityLogObserver(content::BrowserContext* context) | 111 ActivityLogObserver::ActivityLogObserver(content::BrowserContext* context) |
72 : scoped_observer_(this), | 112 : scoped_observer_(this), |
73 context_(context), | 113 context_(context), |
74 injection_count_(0u), | 114 injection_type_(Action::NO_AD_INJECTION), |
115 found_multiple_injections_(false), | |
75 enabled_(false) { | 116 enabled_(false) { |
76 ActivityLog::GetInstance(context_)->AddObserver(this); | 117 ActivityLog::GetInstance(context_)->AddObserver(this); |
77 } | 118 } |
78 | 119 |
79 ActivityLogObserver::~ActivityLogObserver() {} | 120 ActivityLogObserver::~ActivityLogObserver() {} |
80 | 121 |
81 void ActivityLogObserver::OnExtensionActivity(scoped_refptr<Action> action) { | 122 void ActivityLogObserver::OnExtensionActivity(scoped_refptr<Action> action) { |
82 if (enabled_ && action->DidInjectAd(NULL /* no rappor service */) != | 123 if (enabled_) { |
felt
2014/05/08 18:43:55
nit:
if (!enabled_) return;
Devlin
2014/05/08 23:13:33
Done.
| |
83 Action::NO_AD_INJECTION) { | 124 Action::InjectionType type = |
84 ++injection_count_; | 125 action->DidInjectAd(NULL /* no rappor service */); |
126 if (type != Action::NO_AD_INJECTION) { | |
127 if (injection_type_ != Action::NO_AD_INJECTION) | |
128 found_multiple_injections_ = true; | |
129 injection_type_ = type; | |
130 } | |
85 } | 131 } |
86 } | 132 } |
87 | 133 |
88 // A mock for the AdNetworkDatabase. This simply says that the URL | 134 // A mock for the AdNetworkDatabase. This simply says that the URL |
89 // http://www.known-ads.adnetwork is an ad network, and nothing else is. | 135 // http://www.known-ads.adnetwork is an ad network, and nothing else is. |
90 class TestAdNetworkDatabase : public AdNetworkDatabase { | 136 class TestAdNetworkDatabase : public AdNetworkDatabase { |
91 public: | 137 public: |
92 TestAdNetworkDatabase(); | 138 TestAdNetworkDatabase(); |
93 virtual ~TestAdNetworkDatabase(); | 139 virtual ~TestAdNetworkDatabase(); |
94 | 140 |
95 private: | 141 private: |
96 virtual bool IsAdNetwork(const GURL& url) const OVERRIDE; | 142 virtual bool IsAdNetwork(const GURL& url) const OVERRIDE; |
97 | 143 |
98 GURL ad_network_url_; | 144 GURL ad_network_url1_; |
145 GURL ad_network_url2_; | |
99 }; | 146 }; |
100 | 147 |
101 TestAdNetworkDatabase::TestAdNetworkDatabase() : ad_network_url_(kAdNetwork) {} | 148 TestAdNetworkDatabase::TestAdNetworkDatabase() : ad_network_url1_(kAdNetwork1), |
149 ad_network_url2_(kAdNetwork2) { | |
150 } | |
151 | |
102 TestAdNetworkDatabase::~TestAdNetworkDatabase() {} | 152 TestAdNetworkDatabase::~TestAdNetworkDatabase() {} |
103 | 153 |
104 bool TestAdNetworkDatabase::IsAdNetwork(const GURL& url) const { | 154 bool TestAdNetworkDatabase::IsAdNetwork(const GURL& url) const { |
105 return url == ad_network_url_; | 155 return url == ad_network_url1_ || url == ad_network_url2_; |
106 } | 156 } |
107 | 157 |
108 scoped_ptr<net::test_server::HttpResponse> HandleRequest( | 158 scoped_ptr<net::test_server::HttpResponse> HandleRequest( |
109 const net::test_server::HttpRequest& request) { | 159 const net::test_server::HttpRequest& request) { |
110 scoped_ptr<net::test_server::BasicHttpResponse> response( | 160 scoped_ptr<net::test_server::BasicHttpResponse> response( |
111 new net::test_server::BasicHttpResponse()); | 161 new net::test_server::BasicHttpResponse()); |
112 response->set_code(net::HTTP_OK); | 162 response->set_code(net::HTTP_OK); |
113 return response.PassAs<net::test_server::HttpResponse>(); | 163 return response.PassAs<net::test_server::HttpResponse>(); |
114 } | 164 } |
115 | 165 |
(...skipping 12 matching lines...) Expand all Loading... | |
128 | 178 |
129 // Handle the "Reset End" stage of the test. | 179 // Handle the "Reset End" stage of the test. |
130 testing::AssertionResult HandleResetEndStage(); | 180 testing::AssertionResult HandleResetEndStage(); |
131 | 181 |
132 // Handle the "Testing" stage of the test. | 182 // Handle the "Testing" stage of the test. |
133 testing::AssertionResult HandleTestingStage(const std::string& message); | 183 testing::AssertionResult HandleTestingStage(const std::string& message); |
134 | 184 |
135 // Handle a JS error encountered in a test. | 185 // Handle a JS error encountered in a test. |
136 testing::AssertionResult HandleJSError(const std::string& message); | 186 testing::AssertionResult HandleJSError(const std::string& message); |
137 | 187 |
188 const std::string& last_test() const { return last_test_; } | |
felt
2014/05/08 18:43:55
where is this called? I can only find direct acces
Devlin
2014/05/08 23:13:33
Whoops! Originally I used it in the test body, bu
| |
189 | |
138 const base::FilePath& test_data_dir() { return test_data_dir_; } | 190 const base::FilePath& test_data_dir() { return test_data_dir_; } |
139 | 191 |
140 ExtensionTestMessageListener* listener() { return listener_.get(); } | 192 ExtensionTestMessageListener* listener() { return listener_.get(); } |
141 | 193 |
142 ActivityLogObserver* observer() { return observer_.get(); } | 194 ActivityLogObserver* observer() { return observer_.get(); } |
143 | 195 |
144 void set_expected_injections(size_t expected_injections) { | |
145 expected_injections_ = expected_injections; | |
146 } | |
147 | |
148 private: | 196 private: |
149 // The name of the last completed test; used in case of unexpected failure for | 197 // The name of the last completed test; used in case of unexpected failure for |
150 // debugging. | 198 // debugging. |
151 std::string last_test_; | 199 std::string last_test_; |
152 | 200 |
153 // The number of expected injections. | |
154 size_t expected_injections_; | |
155 | |
156 // A listener for any messages from our ad-injecting extension. | 201 // A listener for any messages from our ad-injecting extension. |
157 scoped_ptr<ExtensionTestMessageListener> listener_; | 202 scoped_ptr<ExtensionTestMessageListener> listener_; |
158 | 203 |
159 // An observer to be alerted when we detect ad injection. | 204 // An observer to be alerted when we detect ad injection. |
160 scoped_ptr<ActivityLogObserver> observer_; | 205 scoped_ptr<ActivityLogObserver> observer_; |
161 | 206 |
162 // The current stage of the test. | 207 // The current stage of the test. |
163 Stage stage_; | 208 Stage stage_; |
164 }; | 209 }; |
165 | 210 |
166 AdInjectionBrowserTest::AdInjectionBrowserTest() | 211 AdInjectionBrowserTest::AdInjectionBrowserTest() : stage_(BEFORE_RESET) { |
167 : expected_injections_(0u), stage_(BEFORE_RESET) {} | 212 } |
168 | 213 |
169 AdInjectionBrowserTest::~AdInjectionBrowserTest() {} | 214 AdInjectionBrowserTest::~AdInjectionBrowserTest() { |
215 } | |
170 | 216 |
171 void AdInjectionBrowserTest::SetUpOnMainThread() { | 217 void AdInjectionBrowserTest::SetUpOnMainThread() { |
172 ExtensionBrowserTest::SetUpOnMainThread(); | 218 ExtensionBrowserTest::SetUpOnMainThread(); |
173 | 219 |
174 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); | 220 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); |
175 embedded_test_server()->RegisterRequestHandler(base::Bind(&HandleRequest)); | 221 embedded_test_server()->RegisterRequestHandler(base::Bind(&HandleRequest)); |
176 | 222 |
177 test_data_dir_ = | 223 test_data_dir_ = |
178 test_data_dir_.AppendASCII("activity_log").AppendASCII("ad_injection"); | 224 test_data_dir_.AppendASCII("activity_log").AppendASCII("ad_injection"); |
179 observer_.reset(new ActivityLogObserver(profile())); | 225 observer_.reset(new ActivityLogObserver(profile())); |
(...skipping 21 matching lines...) Expand all Loading... | |
201 } | 247 } |
202 | 248 |
203 testing::AssertionResult AdInjectionBrowserTest::HandleResetBeginStage() { | 249 testing::AssertionResult AdInjectionBrowserTest::HandleResetBeginStage() { |
204 if (stage_ != BEFORE_RESET) { | 250 if (stage_ != BEFORE_RESET) { |
205 return testing::AssertionFailure() | 251 return testing::AssertionFailure() |
206 << "In incorrect stage. Last Test: " << last_test_; | 252 << "In incorrect stage. Last Test: " << last_test_; |
207 } | 253 } |
208 | 254 |
209 // Stop looking for ad injection, since some of the reset could be considered | 255 // Stop looking for ad injection, since some of the reset could be considered |
210 // ad injection. | 256 // ad injection. |
211 observer()->set_enabled(false); | 257 observer()->disable(); |
212 stage_ = RESETTING; | 258 stage_ = RESETTING; |
213 return testing::AssertionSuccess(); | 259 return testing::AssertionSuccess(); |
214 } | 260 } |
215 | 261 |
216 testing::AssertionResult AdInjectionBrowserTest::HandleResetEndStage() { | 262 testing::AssertionResult AdInjectionBrowserTest::HandleResetEndStage() { |
217 if (stage_ != RESETTING) { | 263 if (stage_ != RESETTING) { |
218 return testing::AssertionFailure() | 264 return testing::AssertionFailure() |
219 << "In incorrect stage. Last test: " << last_test_; | 265 << "In incorrect stage. Last test: " << last_test_; |
220 } | 266 } |
221 | 267 |
222 // Look for ad injection again, now that the reset is over. | 268 // Look for ad injection again, now that the reset is over. |
223 observer()->set_enabled(true); | 269 observer()->enable(); |
224 stage_ = TESTING; | 270 stage_ = TESTING; |
225 return testing::AssertionSuccess(); | 271 return testing::AssertionSuccess(); |
226 } | 272 } |
227 | 273 |
228 testing::AssertionResult AdInjectionBrowserTest::HandleTestingStage( | 274 testing::AssertionResult AdInjectionBrowserTest::HandleTestingStage( |
229 const std::string& message) { | 275 const std::string& message) { |
230 if (stage_ != TESTING) { | 276 if (stage_ != TESTING) { |
231 return testing::AssertionFailure() | 277 return testing::AssertionFailure() |
232 << "In incorrect stage. Last test: " << last_test_; | 278 << "In incorrect stage. Last test: " << last_test_; |
233 } | 279 } |
234 | 280 |
235 // The format for a testing message is: | 281 // The format for a testing message is: |
236 // "<test_name>:<expected_change>" | 282 // "<test_name>:<expected_change>" |
237 // where <test_name> is the name of the test and <expected_change> is | 283 // where <test_name> is the name of the test and <expected_change> is |
238 // either -1 for no ad injection (to test against false positives) or the | 284 // either -1 for no ad injection (to test against false positives) or the |
239 // number corresponding to ad_detection::InjectionType. | 285 // number corresponding to ad_detection::InjectionType. |
240 size_t sep = message.find(':'); | 286 size_t sep = message.find(':'); |
241 int expected_change = -1; | 287 int expected_change = -1; |
242 if (sep == std::string::npos || | 288 if (sep == std::string::npos || |
243 !base::StringToInt(message.substr(sep + 1), &expected_change) || | 289 !base::StringToInt(message.substr(sep + 1), &expected_change) || |
244 (expected_change < Action::NO_AD_INJECTION || | 290 (expected_change < Action::NO_AD_INJECTION || |
245 expected_change >= Action::NUM_INJECTION_TYPES)) { | 291 expected_change >= Action::NUM_INJECTION_TYPES)) { |
246 return testing::AssertionFailure() | 292 return testing::AssertionFailure() |
247 << "Invalid message received for testing stage: " << message; | 293 << "Invalid message received for testing stage: " << message; |
248 } | 294 } |
249 | 295 |
250 last_test_ = message.substr(0, sep); | 296 last_test_ = message.substr(0, sep); |
251 | 297 |
252 // TODO(rdevlin.cronin): Currently, we lump all kinds of ad injection into | 298 Action::InjectionType expected_injection = |
253 // one counter, because we can't differentiate (or catch all of them). Change | 299 static_cast<Action::InjectionType>(expected_change); |
254 // this when we can. | |
255 // Increment the expected change, and compare. | |
256 if (expected_change != Action::NO_AD_INJECTION) | |
257 ++expected_injections_; | |
258 std::string error; | 300 std::string error; |
259 if (expected_injections_ != observer()->injection_count()) { | 301 if (observer()->found_multiple_injections()) { |
302 error = "Found multiple injection types. " | |
303 "Only one injection is expected per test."; | |
304 } else if (expected_injection != observer()->injection_type()) { | |
260 // We need these static casts, because size_t is different on different | 305 // We need these static casts, because size_t is different on different |
261 // architectures, and printf becomes unhappy. | 306 // architectures, and printf becomes unhappy. |
262 error = | 307 error = base::StringPrintf( |
263 base::StringPrintf("Injection Count Mismatch: Expected %u, Actual %u", | 308 "Incorrect Injection Found: Expected: %s, Actual: %s", |
264 static_cast<unsigned int>(expected_injections_), | 309 InjectionTypeToString(expected_injection).c_str(), |
265 static_cast<unsigned int>( | 310 InjectionTypeToString(observer()->injection_type()).c_str()); |
266 observer()->injection_count())); | |
267 } | 311 } |
268 | 312 |
269 stage_ = BEFORE_RESET; | 313 stage_ = BEFORE_RESET; |
270 | 314 |
271 if (!error.empty()) | 315 if (!error.empty()) { |
272 return testing::AssertionFailure() << error; | 316 return testing::AssertionFailure() |
317 << "Error in Test '" << last_test_ << "': " << error; | |
318 } | |
273 | 319 |
274 return testing::AssertionSuccess(); | 320 return testing::AssertionSuccess(); |
275 } | 321 } |
276 | 322 |
277 testing::AssertionResult AdInjectionBrowserTest::HandleJSError( | 323 testing::AssertionResult AdInjectionBrowserTest::HandleJSError( |
278 const std::string& message) { | 324 const std::string& message) { |
279 // The format for a testing message is: | 325 // The format for a testing message is: |
280 // "Testing Error:<test_name>:<error>" | 326 // "Testing Error:<test_name>:<error>" |
281 // where <test_name> is the name of the test and <error> is the error which | 327 // where <test_name> is the name of the test and <error> is the error which |
282 // was encountered. | 328 // was encountered. |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
321 ASSERT_TRUE(HandleResetEndStage()); | 367 ASSERT_TRUE(HandleResetEndStage()); |
322 } else if (!message.compare( | 368 } else if (!message.compare( |
323 0, strlen(kJavascriptErrorString), kJavascriptErrorString)) { | 369 0, strlen(kJavascriptErrorString), kJavascriptErrorString)) { |
324 EXPECT_TRUE(HandleJSError(message)); | 370 EXPECT_TRUE(HandleJSError(message)); |
325 } else if (message == kTestCompleteString) { | 371 } else if (message == kTestCompleteString) { |
326 break; // We're done! | 372 break; // We're done! |
327 } else { // We're in some kind of test. | 373 } else { // We're in some kind of test. |
328 EXPECT_TRUE(HandleTestingStage(message)); | 374 EXPECT_TRUE(HandleTestingStage(message)); |
329 } | 375 } |
330 | 376 |
331 // We set the expected injections to be whatever they actually are so that | |
332 // we only fail one test, instead of all subsequent tests. | |
333 set_expected_injections(observer()->injection_count()); | |
334 | |
335 // In all cases (except for "Test Complete", in which case we already | 377 // In all cases (except for "Test Complete", in which case we already |
336 // break'ed), we reply with a continue message. | 378 // break'ed), we reply with a continue message. |
337 listener()->Reply("Continue"); | 379 listener()->Reply("Continue"); |
338 listener()->Reset(); | 380 listener()->Reset(); |
339 } | 381 } |
340 } | 382 } |
341 | 383 |
342 // TODO(rdevlin.cronin): We test a good amount of ways of injecting ads with | 384 // TODO(rdevlin.cronin): We test a good amount of ways of injecting ads with |
343 // the above test, but more is better in testing. | 385 // the above test, but more is better in testing. |
344 // See crbug.com/357204. | 386 // See crbug.com/357204. |
345 | 387 |
346 } // namespace extensions | 388 } // namespace extensions |
OLD | NEW |