Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(145)

Side by Side Diff: chrome/browser/webshare/share_service_impl_unittest.cc

Issue 2688413006: Fixed crash if tab closes while WebShare picker dialog is open. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/webshare/share_service_impl.cc ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 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 <memory> 5 #include <memory>
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/callback.h" 8 #include "base/callback.h"
9 #include "base/run_loop.h" 9 #include "base/run_loop.h"
10 #include "base/strings/utf_string_conversions.h" 10 #include "base/strings/utf_string_conversions.h"
(...skipping 27 matching lines...) Expand all
38 public: 38 public:
39 explicit ShareServiceTestImpl(blink::mojom::ShareServiceRequest request) 39 explicit ShareServiceTestImpl(blink::mojom::ShareServiceRequest request)
40 : binding_(this) { 40 : binding_(this) {
41 binding_.Bind(std::move(request)); 41 binding_.Bind(std::move(request));
42 42
43 pref_service_.reset(new TestingPrefServiceSimple()); 43 pref_service_.reset(new TestingPrefServiceSimple());
44 pref_service_->registry()->RegisterDictionaryPref( 44 pref_service_->registry()->RegisterDictionaryPref(
45 prefs::kWebShareVisitedTargets); 45 prefs::kWebShareVisitedTargets);
46 } 46 }
47 47
48 void set_picker_result(base::Optional<std::string> result) {
49 picker_result_ = result;
50 }
51
52 void AddShareTargetToPrefs(const std::string& manifest_url, 48 void AddShareTargetToPrefs(const std::string& manifest_url,
53 const std::string& name, 49 const std::string& name,
54 const std::string& url_template) { 50 const std::string& url_template) {
55 constexpr char kUrlTemplateKey[] = "url_template"; 51 constexpr char kUrlTemplateKey[] = "url_template";
56 constexpr char kNameKey[] = "name"; 52 constexpr char kNameKey[] = "name";
57 53
58 DictionaryPrefUpdate update(GetPrefService(), 54 DictionaryPrefUpdate update(GetPrefService(),
59 prefs::kWebShareVisitedTargets); 55 prefs::kWebShareVisitedTargets);
60 base::DictionaryValue* share_target_dict = update.Get(); 56 base::DictionaryValue* share_target_dict = update.Get();
61 57
62 std::unique_ptr<base::DictionaryValue> origin_dict( 58 std::unique_ptr<base::DictionaryValue> origin_dict(
63 new base::DictionaryValue); 59 new base::DictionaryValue);
64 60
65 origin_dict->SetStringWithoutPathExpansion(kUrlTemplateKey, url_template); 61 origin_dict->SetStringWithoutPathExpansion(kUrlTemplateKey, url_template);
66 origin_dict->SetStringWithoutPathExpansion(kNameKey, name); 62 origin_dict->SetStringWithoutPathExpansion(kNameKey, name);
67 63
68 share_target_dict->SetWithoutPathExpansion(manifest_url, 64 share_target_dict->SetWithoutPathExpansion(manifest_url,
69 std::move(origin_dict)); 65 std::move(origin_dict));
70 } 66 }
71 67
72 void SetEngagementForTarget(const std::string& manifest_url, 68 void SetEngagementForTarget(const std::string& manifest_url,
73 blink::mojom::EngagementLevel level) { 69 blink::mojom::EngagementLevel level) {
74 engagement_map_[manifest_url] = level; 70 engagement_map_[manifest_url] = level;
75 } 71 }
76 72
73 void set_runloop(base::RunLoop* runloop) {
74 quit_runloop_ = runloop->QuitClosure();
75 }
76
77 const std::string& GetLastUsedTargetURL() { return last_used_target_url_; } 77 const std::string& GetLastUsedTargetURL() { return last_used_target_url_; }
78 78
79 const std::vector<std::pair<base::string16, GURL>>& GetTargetsInPicker() { 79 const std::vector<std::pair<base::string16, GURL>>& GetTargetsInPicker() {
80 return targets_in_picker_; 80 return targets_in_picker_;
81 } 81 }
82 82
83 const base::Callback<void(base::Optional<std::string>)>& picker_callback() {
84 return picker_callback_;
85 }
86
83 private: 87 private:
84 void ShowPickerDialog( 88 void ShowPickerDialog(
85 const std::vector<std::pair<base::string16, GURL>>& targets, 89 const std::vector<std::pair<base::string16, GURL>>& targets,
86 const base::Callback<void(base::Optional<std::string>)>& callback) 90 const base::Callback<void(base::Optional<std::string>)>& callback)
87 override { 91 override {
92 // Store the arguments passed to the picker dialog.
88 targets_in_picker_ = targets; 93 targets_in_picker_ = targets;
89 callback.Run(picker_result_); 94 picker_callback_ = callback;
95
96 // Quit the test's run loop. It is the test's responsibility to call the
97 // callback, to simulate the user's choice.
98 quit_runloop_.Run();
90 } 99 }
91 100
92 void OpenTargetURL(const GURL& target_url) override { 101 void OpenTargetURL(const GURL& target_url) override {
93 last_used_target_url_ = target_url.spec(); 102 last_used_target_url_ = target_url.spec();
94 } 103 }
95 104
96 PrefService* GetPrefService() override { return pref_service_.get(); } 105 PrefService* GetPrefService() override { return pref_service_.get(); }
97 106
98 blink::mojom::EngagementLevel GetEngagementLevel(const GURL& url) override { 107 blink::mojom::EngagementLevel GetEngagementLevel(const GURL& url) override {
99 return engagement_map_[url.spec()]; 108 return engagement_map_[url.spec()];
100 } 109 }
101 110
102 mojo::Binding<blink::mojom::ShareService> binding_; 111 mojo::Binding<blink::mojom::ShareService> binding_;
112 std::unique_ptr<TestingPrefServiceSimple> pref_service_;
103 113
104 base::Optional<std::string> picker_result_; 114 std::map<std::string, blink::mojom::EngagementLevel> engagement_map_;
115 // Closure to quit the test's run loop.
116 base::Closure quit_runloop_;
117
118 // The last URL passed to OpenTargetURL.
105 std::string last_used_target_url_; 119 std::string last_used_target_url_;
106 std::unique_ptr<TestingPrefServiceSimple> pref_service_; 120 // The targets passed to ShowPickerDialog.
107 std::map<std::string, blink::mojom::EngagementLevel> engagement_map_;
108 std::vector<std::pair<base::string16, GURL>> targets_in_picker_; 121 std::vector<std::pair<base::string16, GURL>> targets_in_picker_;
122 // The callback passed to ShowPickerDialog (which is supposed to be called
123 // with the user's chosen result, or nullopt if cancelled).
124 base::Callback<void(base::Optional<std::string>)> picker_callback_;
109 }; 125 };
110 126
111 class ShareServiceImplUnittest : public ChromeRenderViewHostTestHarness { 127 class ShareServiceImplUnittest : public ChromeRenderViewHostTestHarness {
112 public: 128 public:
113 ShareServiceImplUnittest() = default; 129 ShareServiceImplUnittest() = default;
114 ~ShareServiceImplUnittest() override = default; 130 ~ShareServiceImplUnittest() override = default;
115 131
116 void SetUp() override { 132 void SetUp() override {
117 ChromeRenderViewHostTestHarness::SetUp(); 133 ChromeRenderViewHostTestHarness::SetUp();
118 134
119 share_service_helper_ = base::MakeUnique<ShareServiceTestImpl>( 135 share_service_helper_ = base::MakeUnique<ShareServiceTestImpl>(
120 mojo::MakeRequest(&share_service_)); 136 mojo::MakeRequest(&share_service_));
121 137
122 share_service_helper_->SetEngagementForTarget( 138 share_service_helper_->SetEngagementForTarget(
123 kManifestUrlHigh, blink::mojom::EngagementLevel::HIGH); 139 kManifestUrlHigh, blink::mojom::EngagementLevel::HIGH);
124 share_service_helper_->SetEngagementForTarget( 140 share_service_helper_->SetEngagementForTarget(
125 kManifestUrlMin, blink::mojom::EngagementLevel::MINIMAL); 141 kManifestUrlMin, blink::mojom::EngagementLevel::MINIMAL);
126 share_service_helper_->SetEngagementForTarget( 142 share_service_helper_->SetEngagementForTarget(
127 kManifestUrlLow, blink::mojom::EngagementLevel::LOW); 143 kManifestUrlLow, blink::mojom::EngagementLevel::LOW);
128 } 144 }
129 145
130 void TearDown() override { ChromeRenderViewHostTestHarness::TearDown(); } 146 void TearDown() override { ChromeRenderViewHostTestHarness::TearDown(); }
131 147
148 blink::mojom::ShareService* share_service() const {
149 return share_service_.get();
150 }
151
152 ShareServiceTestImpl* share_service_helper() const {
153 return share_service_helper_.get();
154 }
155
156 void DeleteShareService() { share_service_helper_.reset(); }
157
132 void DidShare(const std::vector<std::pair<base::string16, GURL>>& 158 void DidShare(const std::vector<std::pair<base::string16, GURL>>&
133 expected_targets_in_picker, 159 expected_targets_in_picker,
134 const std::string& expected_target_url, 160 const std::string& expected_target_url,
135 const base::Optional<std::string>& expected_error, 161 const base::Optional<std::string>& expected_error,
136 const base::Optional<std::string>& error) { 162 const base::Optional<std::string>& error) {
137 std::vector<std::pair<base::string16, GURL>> targets_in_picker = 163 std::vector<std::pair<base::string16, GURL>> targets_in_picker =
Sam McNally 2017/02/15 06:27:55 Move these out.
Matt Giuca 2017/02/15 08:13:26 Done. Oh hey, this can be a regular function now (
138 share_service_helper_->GetTargetsInPicker(); 164 share_service_helper_->GetTargetsInPicker();
139 EXPECT_EQ(expected_targets_in_picker, targets_in_picker); 165 EXPECT_EQ(expected_targets_in_picker, targets_in_picker);
140 166
141 std::string target_url = share_service_helper_->GetLastUsedTargetURL(); 167 std::string target_url = share_service_helper_->GetLastUsedTargetURL();
142 EXPECT_EQ(expected_target_url, target_url); 168 EXPECT_EQ(expected_target_url, target_url);
143 169
144 EXPECT_EQ(expected_error, error); 170 EXPECT_EQ(expected_error, error);
145
146 if (!on_callback_.is_null())
147 on_callback_.Run();
148 } 171 }
149 172
173 private:
150 blink::mojom::ShareServicePtr share_service_; 174 blink::mojom::ShareServicePtr share_service_;
151 std::unique_ptr<ShareServiceTestImpl> share_service_helper_; 175 std::unique_ptr<ShareServiceTestImpl> share_service_helper_;
152 base::Closure on_callback_;
153 }; 176 };
154 177
155 } // namespace 178 } // namespace
156 179
157 // Basic test to check the Share method calls the callback with the expected 180 // Basic test to check the Share method calls the callback with the expected
158 // parameters. 181 // parameters.
159 TEST_F(ShareServiceImplUnittest, ShareCallbackParams) { 182 TEST_F(ShareServiceImplUnittest, ShareCallbackParams) {
160 share_service_helper_->set_picker_result( 183 share_service_helper()->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
161 base::Optional<std::string>(kManifestUrlLow)); 184 kUrlTemplate);
162 185 share_service_helper()->AddShareTargetToPrefs(kManifestUrlHigh, kTargetName,
163 share_service_helper_->AddShareTargetToPrefs(kManifestUrlLow, kTargetName, 186 kUrlTemplate);
164 kUrlTemplate);
165 share_service_helper_->AddShareTargetToPrefs(kManifestUrlHigh, kTargetName,
166 kUrlTemplate);
167 187
168 std::string expected_url = 188 std::string expected_url =
169 "https://www.example-low.com/target/" 189 "https://www.example-low.com/target/"
170 "share?title=My%20title&text=My%20text&url=https%3A%2F%2Fwww." 190 "share?title=My%20title&text=My%20text&url=https%3A%2F%2Fwww."
171 "google.com%2F"; 191 "google.com%2F";
172 192
173 std::vector<std::pair<base::string16, GURL>> expected_targets{ 193 std::vector<std::pair<base::string16, GURL>> expected_targets{
174 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlHigh)), 194 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlHigh)),
175 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))}; 195 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))};
176 base::Callback<void(const base::Optional<std::string>&)> callback = 196 base::Callback<void(const base::Optional<std::string>&)> callback =
177 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this), 197 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this),
178 expected_targets, expected_url, base::Optional<std::string>()); 198 expected_targets, expected_url, base::Optional<std::string>());
179 199
180 base::RunLoop run_loop; 200 base::RunLoop run_loop;
181 on_callback_ = run_loop.QuitClosure(); 201 share_service_helper()->set_runloop(&run_loop);
182 202
183 const GURL url(kUrlSpec); 203 const GURL url(kUrlSpec);
184 share_service_->Share(kTitle, kText, url, callback); 204 share_service()->Share(kTitle, kText, url, callback);
185 205
186 run_loop.Run(); 206 run_loop.Run();
207
208 // Pick example-low.com.
209 share_service_helper()->picker_callback().Run(
210 base::Optional<std::string>(kManifestUrlLow));
187 } 211 }
188 212
189 // Tests the result of cancelling the share in the picker UI, that doesn't have 213 // Tests the result of cancelling the share in the picker UI, that doesn't have
190 // any targets. 214 // any targets.
191 TEST_F(ShareServiceImplUnittest, ShareCancelNoTargets) { 215 TEST_F(ShareServiceImplUnittest, ShareCancelNoTargets) {
192 // picker_result_ is set to nullopt by default, so this imitates the user
193 // cancelling a share.
194 // Expect an error message in response. 216 // Expect an error message in response.
195 base::Callback<void(const base::Optional<std::string>&)> callback = 217 base::Callback<void(const base::Optional<std::string>&)> callback =
196 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this), 218 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this),
197 std::vector<std::pair<base::string16, GURL>>(), std::string(), 219 std::vector<std::pair<base::string16, GURL>>(), std::string(),
198 base::Optional<std::string>("Share was cancelled")); 220 base::Optional<std::string>("Share was cancelled"));
199 221
200 base::RunLoop run_loop; 222 base::RunLoop run_loop;
201 on_callback_ = run_loop.QuitClosure(); 223 share_service_helper()->set_runloop(&run_loop);
202 224
203 const GURL url(kUrlSpec); 225 const GURL url(kUrlSpec);
204 share_service_->Share(kTitle, kText, url, callback); 226 share_service()->Share(kTitle, kText, url, callback);
205 227
206 run_loop.Run(); 228 run_loop.Run();
229
230 // Cancel the dialog.
231 share_service_helper()->picker_callback().Run(base::nullopt);
207 } 232 }
208 233
209 // Tests the result of cancelling the share in the picker UI, that has targets. 234 // Tests the result of cancelling the share in the picker UI, that has targets.
210 TEST_F(ShareServiceImplUnittest, ShareCancelWithTargets) { 235 TEST_F(ShareServiceImplUnittest, ShareCancelWithTargets) {
211 // picker_result_ is set to nullopt by default, so this imitates the user 236 share_service_helper()->AddShareTargetToPrefs(kManifestUrlHigh, kTargetName,
212 // cancelling a share. 237 kUrlTemplate);
213 share_service_helper_->AddShareTargetToPrefs(kManifestUrlHigh, kTargetName, 238 share_service_helper()->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
214 kUrlTemplate); 239 kUrlTemplate);
215 share_service_helper_->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
216 kUrlTemplate);
217 240
218 std::vector<std::pair<base::string16, GURL>> expected_targets{ 241 std::vector<std::pair<base::string16, GURL>> expected_targets{
219 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlHigh)), 242 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlHigh)),
220 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))}; 243 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))};
221 // Expect an error message in response. 244 // Expect an error message in response.
222 base::Callback<void(const base::Optional<std::string>&)> callback = 245 base::Callback<void(const base::Optional<std::string>&)> callback =
223 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this), 246 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this),
224 expected_targets, std::string(), 247 expected_targets, std::string(),
225 base::Optional<std::string>("Share was cancelled")); 248 base::Optional<std::string>("Share was cancelled"));
226 249
227 base::RunLoop run_loop; 250 base::RunLoop run_loop;
228 on_callback_ = run_loop.QuitClosure(); 251 share_service_helper()->set_runloop(&run_loop);
229 252
230 const GURL url(kUrlSpec); 253 const GURL url(kUrlSpec);
231 share_service_->Share(kTitle, kText, url, callback); 254 share_service()->Share(kTitle, kText, url, callback);
232 255
233 run_loop.Run(); 256 run_loop.Run();
257
258 // Cancel the dialog.
259 share_service_helper()->picker_callback().Run(base::nullopt);
234 } 260 }
235 261
236 // Test to check that only targets with enough engagement were in picker. 262 // Test to check that only targets with enough engagement were in picker.
237 TEST_F(ShareServiceImplUnittest, ShareWithSomeInsufficientlyEngagedTargets) { 263 TEST_F(ShareServiceImplUnittest, ShareWithSomeInsufficientlyEngagedTargets) {
238 std::string expected_url = 264 std::string expected_url =
239 "https://www.example-low.com/target/" 265 "https://www.example-low.com/target/"
240 "share?title=My%20title&text=My%20text&url=https%3A%2F%2Fwww." 266 "share?title=My%20title&text=My%20text&url=https%3A%2F%2Fwww."
241 "google.com%2F"; 267 "google.com%2F";
242 268
243 share_service_helper_->set_picker_result( 269 share_service_helper()->AddShareTargetToPrefs(kManifestUrlMin, kTargetName,
244 base::Optional<std::string>(kManifestUrlLow)); 270 kUrlTemplate);
245 271 share_service_helper()->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
246 share_service_helper_->AddShareTargetToPrefs(kManifestUrlMin, kTargetName, 272 kUrlTemplate);
247 kUrlTemplate);
248 share_service_helper_->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
249 kUrlTemplate);
250 273
251 std::vector<std::pair<base::string16, GURL>> expected_targets{ 274 std::vector<std::pair<base::string16, GURL>> expected_targets{
252 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))}; 275 make_pair(base::UTF8ToUTF16(kTargetName), GURL(kManifestUrlLow))};
253 base::Callback<void(const base::Optional<std::string>&)> callback = 276 base::Callback<void(const base::Optional<std::string>&)> callback =
254 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this), 277 base::Bind(&ShareServiceImplUnittest::DidShare, base::Unretained(this),
255 expected_targets, expected_url, base::Optional<std::string>()); 278 expected_targets, expected_url, base::Optional<std::string>());
256 279
257 base::RunLoop run_loop; 280 base::RunLoop run_loop;
258 on_callback_ = run_loop.QuitClosure(); 281 share_service_helper()->set_runloop(&run_loop);
259 282
260 const GURL url(kUrlSpec); 283 const GURL url(kUrlSpec);
261 share_service_->Share(kTitle, kText, url, callback); 284 share_service()->Share(kTitle, kText, url, callback);
262 285
263 run_loop.Run(); 286 run_loop.Run();
287
288 // Pick example-low.com.
289 share_service_helper()->picker_callback().Run(
290 base::Optional<std::string>(kManifestUrlLow));
291 }
292
293 // Test that deleting the share service while the picker is open does not crash
294 // (https://crbug.com/690775).
295 TEST_F(ShareServiceImplUnittest, ShareServiceDeletion) {
296 share_service_helper()->AddShareTargetToPrefs(kManifestUrlLow, kTargetName,
297 kUrlTemplate);
298
299 base::RunLoop run_loop;
300 share_service_helper()->set_runloop(&run_loop);
301
302 const GURL url(kUrlSpec);
303 // Expect the callback to never be called (since the share service is
304 // destroyed before the picker is closed).
305 // TODO(mgiuca): This probably should still complete the share, if not
306 // cancelled, even if the underlying tab is closed.
307 base::Callback<void(const base::Optional<std::string>&)> callback =
308 base::Bind(
309 [](const base::Optional<std::string>& error) { NOTREACHED(); });
Sam McNally 2017/02/15 06:27:55 FAIL()
Matt Giuca 2017/02/15 08:13:26 Done.
310 share_service()->Share(kTitle, kText, url, callback);
311
312 run_loop.Run();
313
314 const base::Callback<void(base::Optional<std::string>)> picker_callback =
315 share_service_helper()->picker_callback();
316
317 DeleteShareService();
318
319 // Pick example-low.com.
320 picker_callback.Run(base::Optional<std::string>(kManifestUrlLow));
264 } 321 }
265 322
266 // Replace various numbers of placeholders in various orders. Placeholders are 323 // Replace various numbers of placeholders in various orders. Placeholders are
267 // adjacent to eachother; there are no padding characters. 324 // adjacent to eachother; there are no padding characters.
268 TEST_F(ShareServiceImplUnittest, ReplacePlaceholders) { 325 TEST_F(ShareServiceImplUnittest, ReplacePlaceholders) {
269 const GURL url(kUrlSpec); 326 const GURL url(kUrlSpec);
270 std::string url_template_filled; 327 std::string url_template_filled;
271 bool succeeded; 328 bool succeeded;
272 329
273 // No placeholders 330 // No placeholders
(...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after
459 EXPECT_TRUE(succeeded); 516 EXPECT_TRUE(succeeded);
460 EXPECT_EQ("%C3%A9", url_template_filled); 517 EXPECT_EQ("%C3%A9", url_template_filled);
461 518
462 // U+1F4A9 519 // U+1F4A9
463 url_template = "{title}"; 520 url_template = "{title}";
464 succeeded = ShareServiceImpl::ReplacePlaceholders( 521 succeeded = ShareServiceImpl::ReplacePlaceholders(
465 url_template, "\xf0\x9f\x92\xa9", kText, url, &url_template_filled); 522 url_template, "\xf0\x9f\x92\xa9", kText, url, &url_template_filled);
466 EXPECT_TRUE(succeeded); 523 EXPECT_TRUE(succeeded);
467 EXPECT_EQ("%F0%9F%92%A9", url_template_filled); 524 EXPECT_EQ("%F0%9F%92%A9", url_template_filled);
468 } 525 }
OLDNEW
« no previous file with comments | « chrome/browser/webshare/share_service_impl.cc ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698