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

Side by Side Diff: chrome/browser/download/download_history.cc

Issue 10665049: Make DownloadHistory observe manager, items (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: . Created 8 years, 3 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 | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 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 // DownloadHistory manages persisting DownloadItems to the history service by
6 // observing a single DownloadManager and all its DownloadItems.
7 // DownloadHistory decides whether and when to add items to, remove items from,
8 // and update items in the database. DownloadHistory uses DownloadHistoryData to
9 // store per-DownloadItem data such as its db_handle, whether the item is being
10 // added and waiting for its db_handle, and the last DownloadPersistentStoreInfo
11 // that was passed to the database. When the DownloadManager and its delegate
12 // (ChromeDownloadManagerDelegate) are initialized, DownloadHistory is created
13 // and queries the HistoryService. When the HistoryService calls back from
14 // QueryDownloads() to QueryCallback(), DownloadHistory uses
15 // DownloadManager::CreateDownloadItem() to inform DownloadManager of these
16 // persisted DownloadItems. CreateDownloadItem() internally calls
17 // OnDownloadCreated(), which normally adds items to the database, so
18 // QueryCallback() uses |loading_| to disable adding items to the database. If
19 // a download is removed via OnDownloadRemoved() while the item is still being
20 // added to the database, DownloadHistory uses |removed_while_adding_| to
21 // remember to remove the item when its ItemAdded() callback is called. All
22 // callbacks are bound with a weak pointer to DownloadHistory to prevent
23 // use-after-free bugs.
24 // ChromeDownloadManagerDelegate owns DownloadHistory, and deletes it in
25 // Shutdown(), which is called by DownloadManagerImpl::Shutdown() after all
26 // DownloadItems are destroyed.
27
5 #include "chrome/browser/download/download_history.h" 28 #include "chrome/browser/download/download_history.h"
6 29
7 #include "base/logging.h" 30 #include "base/metrics/histogram.h"
31 #include "chrome/browser/cancelable_request.h"
8 #include "chrome/browser/download/download_crx_util.h" 32 #include "chrome/browser/download/download_crx_util.h"
9 #include "chrome/browser/history/history_marshaling.h" 33 #include "chrome/browser/history/download_persistent_store_info.h"
10 #include "chrome/browser/history/history_service_factory.h" 34 #include "content/public/browser/browser_thread.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "content/public/browser/download_item.h" 35 #include "content/public/browser/download_item.h"
13 #include "content/public/browser/download_persistent_store_info.h" 36 #include "content/public/browser/download_manager.h"
14 37
38 using content::BrowserThread;
15 using content::DownloadItem; 39 using content::DownloadItem;
16 using content::DownloadPersistentStoreInfo; 40 using content::DownloadManager;
17 41
18 DownloadHistory::DownloadHistory(Profile* profile) 42 namespace {
19 : profile_(profile), 43
20 next_fake_db_handle_(DownloadItem::kUninitializedHandle - 1) { 44 // The value of |db_handle| indicating that the associated DownloadItem is not
21 DCHECK(profile); 45 // yet persisted.
22 } 46 static const int64 kUninitializedHandle = -1;
23 47
24 DownloadHistory::~DownloadHistory() {} 48 // Per-DownloadItem data. This information does not belong inside DownloadItem,
25 49 // and keeping maps in DownloadHistory from DownloadItem to this information is
26 void DownloadHistory::GetNextId( 50 // error-prone and complicated. Unfortunately, DownloadHistory::removing_ and
27 const HistoryService::DownloadNextIdCallback& callback) { 51 // removed_while_adding_ cannot be moved into this class partly because
28 HistoryService* hs = HistoryServiceFactory::GetForProfile( 52 // DownloadHistoryData is destroyed when DownloadItems are destroyed, and we
29 profile_, Profile::EXPLICIT_ACCESS); 53 // have no control over when DownloadItems are destroyed.
30 if (!hs) 54 class DownloadHistoryData : public base::SupportsUserData::Data {
31 return; 55 public:
32 56 static DownloadHistoryData* Get(DownloadItem* item) {
33 hs->GetNextDownloadId(&history_consumer_, callback); 57 base::SupportsUserData::Data* data = item->GetUserData(kKey);
34 } 58 return (data == NULL) ? NULL :
35 59 static_cast<DownloadHistoryData*>(data);
36 void DownloadHistory::Load( 60 }
37 const HistoryService::DownloadQueryCallback& callback) { 61
38 HistoryService* hs = HistoryServiceFactory::GetForProfile( 62 explicit DownloadHistoryData(DownloadItem* item)
39 profile_, Profile::EXPLICIT_ACCESS); 63 : is_adding_(false),
40 if (!hs) 64 db_handle_(kUninitializedHandle),
41 return; 65 info_(NULL) {
42 66 item->SetUserData(kKey, this);
43 hs->QueryDownloads(&history_consumer_, callback); 67 }
44 68
45 // This is the initial load, so do a cleanup of corrupt in-progress entries. 69 virtual ~DownloadHistoryData() {
46 hs->CleanUpInProgressEntries(); 70 }
71
72 // Whether this item is currently being added to the database.
73 bool is_adding() const { return is_adding_; }
74 void set_is_adding(bool a) { is_adding_ = a; }
75
76 // Whether this item is already persisted in the database.
77 bool is_persisted() const { return db_handle_ > kUninitializedHandle; }
78
79 int64 db_handle() const { return db_handle_; }
80 void set_db_handle(int64 h) { db_handle_ = h; }
81
82 // This allows OnDownloadUpdated() to see what changed in a DownloadItem if
83 // anything, in order to prevent writing to the database unnecessarily. It is
84 // nullified when the item is no longer in progress in order to save memory.
85 DownloadPersistentStoreInfo* info() { return info_.get(); }
86 void set_info(const DownloadPersistentStoreInfo& i) {
87 info_.reset(new DownloadPersistentStoreInfo(i));
88 }
89 void clear_info() {
90 info_.reset();
91 }
92
93 private:
94 static const char kKey[];
95
96 bool is_adding_;
97 int64 db_handle_;
98 scoped_ptr<DownloadPersistentStoreInfo> info_;
99
100 DISALLOW_COPY_AND_ASSIGN(DownloadHistoryData);
101 };
102
103 const char DownloadHistoryData::kKey[] =
104 "DownloadItem DownloadHistoryData";
105
106 DownloadPersistentStoreInfo GetPersistentStoreInfo(DownloadItem* item) {
107 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
108 return DownloadPersistentStoreInfo(
109 item->GetFullPath(),
110 item->GetURL(),
111 item->GetReferrerUrl(),
112 item->GetStartTime(),
113 item->GetEndTime(),
114 item->GetReceivedBytes(),
115 item->GetTotalBytes(),
116 item->GetState(),
117 ((dhd != NULL) ? dhd->db_handle() : kUninitializedHandle),
118 item->GetOpened());
119 }
120
121 typedef std::vector<DownloadPersistentStoreInfo> InfoVector;
122
123 // CancelableRequestConsumer would normally ensure that callbacks from the
124 // HistoryService are run on the same thread that issued the request, but
125 // DownloadHistory has opted for less magic, so it uses Ensure*OnUI to
126 // explicitly bounce to the UI thread. These methods are static to avoid
127 // creating WeakPtrs on the wrong thread.
128 void EnsureQueryOnUI(
129 const base::Callback<void(scoped_ptr<InfoVector>)>& callback,
130 scoped_ptr<InfoVector> infos) {
131 if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
132 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
133 callback, base::Passed(infos.Pass())));
134 return;
135 }
136 callback.Run(infos.Pass());
137 }
138
139 void EnsureItemAddedOnUI(const base::Callback<void(int64)>& callback,
140 int64 handle) {
141 if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
142 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
143 callback, handle));
144 return;
145 }
146 callback.Run(handle);
147 }
148
149 void EnsureVisitedOnUI(
150 const base::Callback<void(bool, int, base::Time)>& callback,
151 bool success, int count, base::Time first_visit) {
152 if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
153 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
154 callback, success, count, first_visit));
155 return;
156 }
157 callback.Run(success, count, first_visit);
158 }
159
160 } // anonymous namespace
161
162 DownloadHistory::DownloadHistory(
163 DownloadManager* manager,
164 HistoryService* history)
165 : manager_(manager),
166 history_(history),
167 loading_(false),
168 history_size_(0),
169 ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) {
170 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
171 DCHECK(manager_);
172 manager_->AddObserver(this);
173 history_->QueryDownloads(base::Bind(&EnsureQueryOnUI, base::Bind(
174 &DownloadHistory::QueryCallback, weak_ptr_factory_.GetWeakPtr())));
175 }
176
177 DownloadHistory::~DownloadHistory() {
178 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
179 for (ItemSet::const_iterator iter = observing_items_.begin();
180 iter != observing_items_.end(); ++iter) {
181 (*iter)->RemoveObserver(this);
182 }
183 observing_items_.clear();
184 if (manager_)
185 manager_->RemoveObserver(this);
186 }
187
188 void DownloadHistory::QueryCallback(scoped_ptr<InfoVector> infos) {
189 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
190 // ManagerGoingDown() may have happened before the history loaded.
191 if (!manager_)
192 return;
193 // OnDownloadCreated() is called inside DownloadManager::CreateDownloadItem(),
194 // so temporarily disable adding new unpersisted items to the history. All
195 // methods run on the UI thread and CreateDownloadItem() is synchronous, so it
196 // is impossible for an OnDownloadCreated() to come in for another
197 // DownloadItem while QueryCallback() is processing the database.
198 loading_ = true;
199 for (InfoVector::const_iterator it = infos->begin();
200 it != infos->end(); ++it) {
201 DownloadItem* download_item = manager_->CreateDownloadItem(
202 it->path,
203 it->url,
204 it->referrer_url,
205 it->start_time,
206 it->end_time,
207 it->received_bytes,
208 it->total_bytes,
209 it->state,
210 it->opened);
211 DownloadHistoryData* dhd = DownloadHistoryData::Get(download_item);
212 DCHECK(it->db_handle > kUninitializedHandle);
213 dhd->set_db_handle(it->db_handle);
214 ++history_size_;
215 }
216 manager_->CheckForHistoryFilesRemoval();
217 loading_ = false;
218 }
219
220 void DownloadHistory::OnDownloadCreated(
221 DownloadManager* manager, DownloadItem* item) {
222 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
223
224 // Observe even temporary downloads in case they are marked not temporary.
225 item->AddObserver(this);
226 observing_items_.insert(item);
227 // All downloads should pass through OnDownloadCreated exactly once.
228 CHECK(!DownloadHistoryData::Get(item));
229 DownloadHistoryData* dhd = new DownloadHistoryData(item);
230 if (item->GetState() == DownloadItem::IN_PROGRESS) {
231 dhd->set_info(GetPersistentStoreInfo(item));
232 }
233 MaybeAddToHistory(item);
234 }
235
236 void DownloadHistory::MaybeAddToHistory(DownloadItem* item) {
237 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
238 int32 download_id = item->GetId();
239 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
240 bool removing = (removing_.find(dhd->db_handle()) != removing_.end());
241 // TODO(benjhayden): Remove IsTemporary().
242 if (loading_ ||
243 download_crx_util::IsExtensionDownload(*item) ||
244 dhd->is_persisted() ||
245 item->IsTemporary() ||
246 removing ||
247 dhd->is_adding())
248 return;
249 dhd->set_is_adding(true);
250 if (dhd->info() == NULL) {
251 // Keep the info here regardless of whether the item is in progress so
252 // that, when ItemAdded() calls OnDownloadUpdated(), it can choose more
253 // intelligently whether to Update the db and/or discard the info.
254 dhd->set_info(GetPersistentStoreInfo(item));
255 }
256 history_->CreateDownload(*dhd->info(), base::Bind(
257 &EnsureItemAddedOnUI, base::Bind(
258 &DownloadHistory::ItemAdded, weak_ptr_factory_.GetWeakPtr(),
259 download_id)));
260 }
261
262 void DownloadHistory::ItemAdded(int32 download_id, int64 db_handle) {
263 if (!manager_)
264 return;
265
266 if (removed_while_adding_.find(download_id) !=
267 removed_while_adding_.end()) {
268 removed_while_adding_.erase(download_id);
269 if (removing_.empty()) {
270 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
271 base::Bind(&DownloadHistory::RemoveDownloadsBatch,
272 weak_ptr_factory_.GetWeakPtr()));
273 }
274 removing_.insert(db_handle);
275 return;
276 }
277
278 DownloadItem* item = manager_->GetDownload(download_id);
279 if (!item) {
280 // This item will have called OnDownloadDestroyed(). If the item should
281 // have been removed from history, then t would have also called
282 // OnDownloadRemoved(), which would have put |download_id| in
283 // removed_while_adding_, handled above.
284 return;
285 }
286
287 UMA_HISTOGRAM_CUSTOM_COUNTS("Download.HistorySize2",
288 history_size_,
289 0/*min*/,
290 (1 << 23)/*max*/,
291 (1 << 7)/*num_buckets*/);
292 ++history_size_;
293
294 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
295 dhd->set_is_adding(false);
296 DCHECK(db_handle > kUninitializedHandle);
297 dhd->set_db_handle(db_handle);
298
299 // In case the item changed or became temporary while it was being added.
300 // Don't just update all of the item's observers because we're the only
301 // observer that can also see db_handle, which is the only thing that
302 // ItemAdded changed.
303 OnDownloadUpdated(item);
304 }
305
306 void DownloadHistory::OnDownloadUpdated(DownloadItem* item) {
307 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
308
309 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
310 if (!dhd->is_persisted()) {
311 MaybeAddToHistory(item);
312 return;
313 }
314 if (item->IsTemporary()) {
315 OnDownloadRemoved(item);
316 return;
317 }
318
319 // TODO(asanka): Persist GetTargetFilePath() as well.
320 DownloadPersistentStoreInfo current_info(GetPersistentStoreInfo(item));
321 DownloadPersistentStoreInfo* previous_info = dhd->info();
322 bool do_update = (
323 (previous_info == NULL) ||
324 (previous_info->path != current_info.path) ||
325 (previous_info->end_time != current_info.end_time) ||
326 (previous_info->received_bytes != current_info.received_bytes) ||
327 (previous_info->total_bytes != current_info.total_bytes) ||
328 (previous_info->state != current_info.state) ||
329 (previous_info->opened != current_info.opened));
330 UMA_HISTOGRAM_ENUMERATION("Download.HistoryPropagatedUpdate", do_update, 2);
331 if (do_update) {
332 history_->UpdateDownload(current_info);
333 }
334 if (item->GetState() == DownloadItem::IN_PROGRESS) {
335 dhd->set_info(current_info);
336 } else {
337 dhd->clear_info();
338 }
339 }
340
341 // Downloads may be opened after they are completed.
342 void DownloadHistory::OnDownloadOpened(DownloadItem* item) {
343 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
344 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
345 if (!dhd->is_persisted()) {
346 MaybeAddToHistory(item);
347 return;
348 }
349 if (item->IsTemporary()) {
350 OnDownloadRemoved(item);
351 return;
352 }
353
354 DownloadPersistentStoreInfo current_info(GetPersistentStoreInfo(item));
355 history_->UpdateDownload(current_info);
356 if (item->GetState() == DownloadItem::IN_PROGRESS) {
357 dhd->set_info(current_info);
358 }
359 }
360
361 void DownloadHistory::OnDownloadRemoved(DownloadItem* item) {
362 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
363
364 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
365 if (!dhd->is_persisted()) {
366 if (dhd->is_adding()) {
367 removed_while_adding_.insert(item->GetId());
368 }
369 return;
370 }
371
372 // For database efficiency, batch removals together if they happen all at
373 // once.
374 if (removing_.empty()) {
375 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
376 base::Bind(&DownloadHistory::RemoveDownloadsBatch,
377 weak_ptr_factory_.GetWeakPtr()));
378 }
379 removing_.insert(dhd->db_handle());
380 dhd->set_db_handle(kUninitializedHandle);
381 --history_size_;
382 }
383
384 void DownloadHistory::RemoveDownloadsBatch() {
385 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
386 HandleSet remove_handles;
387 removing_.swap(remove_handles);
388 history_->RemoveDownloads(remove_handles);
389 }
390
391 void DownloadHistory::ManagerGoingDown(DownloadManager* manager) {
392 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
393 DCHECK_EQ(manager_, manager);
394 manager_->RemoveObserver(this);
395 manager_ = NULL;
396 }
397
398 void DownloadHistory::OnDownloadDestroyed(DownloadItem* item) {
399 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
400 item->RemoveObserver(this);
401 observing_items_.erase(item);
47 } 402 }
48 403
49 void DownloadHistory::CheckVisitedReferrerBefore( 404 void DownloadHistory::CheckVisitedReferrerBefore(
50 int32 download_id, 405 int32 download_id,
51 const GURL& referrer_url, 406 const GURL& referrer_url,
52 const VisitedBeforeDoneCallback& callback) { 407 const VisitedBeforeDoneCallback& callback) {
53 if (referrer_url.is_valid()) { 408 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
54 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists( 409 if (!referrer_url.is_valid()) {
55 profile_, Profile::EXPLICIT_ACCESS); 410 callback.Run(false);
56 if (hs) { 411 return;
57 HistoryService::Handle handle = 412 }
58 hs->GetVisibleVisitCountToHost(referrer_url, &history_consumer_, 413 history_->GetVisibleVisitCountToHostSimple(
59 base::Bind(&DownloadHistory::OnGotVisitCountToHost, 414 referrer_url,
60 base::Unretained(this))); 415 base::Bind(&EnsureVisitedOnUI, base::Bind(
61 visited_before_requests_[handle] = callback; 416 &DownloadHistory::OnGotVisitCountToHost,
62 return; 417 weak_ptr_factory_.GetWeakPtr(), callback)));
63 } 418 }
64 } 419
65 callback.Run(false); 420 void DownloadHistory::OnGotVisitCountToHost(
66 } 421 const VisitedBeforeDoneCallback& callback,
67 422 bool found_visits,
68 void DownloadHistory::AddEntry( 423 int count,
69 DownloadItem* download_item, 424 base::Time first_visit) {
70 const HistoryService::DownloadCreateCallback& callback) {
71 DCHECK(download_item);
72 // Do not store the download in the history database for a few special cases:
73 // - incognito mode (that is the point of this mode)
74 // - extensions (users don't think of extension installation as 'downloading')
75 // - temporary download, like in drag-and-drop
76 // - history service is not available (e.g. in tests)
77 // We have to make sure that these handles don't collide with normal db
78 // handles, so we use a negative value. Eventually, they could overlap, but
79 // you'd have to do enough downloading that your ISP would likely stab you in
80 // the neck first. YMMV.
81 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
82 profile_, Profile::EXPLICIT_ACCESS);
83 if (download_crx_util::IsExtensionDownload(*download_item) ||
84 download_item->IsTemporary() || !hs) {
85 callback.Run(download_item->GetId(), GetNextFakeDbHandle());
86 return;
87 }
88
89 int32 id = download_item->GetId();
90 DownloadPersistentStoreInfo history_info =
91 download_item->GetPersistentStoreInfo();
92 hs->CreateDownload(id, history_info, &history_consumer_, callback);
93 }
94
95 void DownloadHistory::UpdateEntry(DownloadItem* download_item) {
96 // Don't store info in the database if the download was initiated while in
97 // incognito mode or if it hasn't been initialized in our database table.
98 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
99 return;
100
101 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
102 profile_, Profile::EXPLICIT_ACCESS);
103 if (!hs)
104 return;
105 hs->UpdateDownload(download_item->GetPersistentStoreInfo());
106 }
107
108 void DownloadHistory::UpdateDownloadPath(DownloadItem* download_item,
109 const FilePath& new_path) {
110 // No update necessary if the download was initiated while in incognito mode.
111 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
112 return;
113
114 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
115 profile_, Profile::EXPLICIT_ACCESS);
116 if (hs)
117 hs->UpdateDownloadPath(new_path, download_item->GetDbHandle());
118 }
119
120 void DownloadHistory::RemoveEntry(DownloadItem* download_item) {
121 // No update necessary if the download was initiated while in incognito mode.
122 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
123 return;
124
125 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
126 profile_, Profile::EXPLICIT_ACCESS);
127 if (hs)
128 hs->RemoveDownload(download_item->GetDbHandle());
129 }
130
131 void DownloadHistory::RemoveEntriesBetween(const base::Time remove_begin,
132 const base::Time remove_end) {
133 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
134 profile_, Profile::EXPLICIT_ACCESS);
135 if (hs)
136 hs->RemoveDownloadsBetween(remove_begin, remove_end);
137 }
138
139 int64 DownloadHistory::GetNextFakeDbHandle() {
140 return next_fake_db_handle_--;
141 }
142
143 void DownloadHistory::OnGotVisitCountToHost(HistoryService::Handle handle,
144 bool found_visits,
145 int count,
146 base::Time first_visit) {
147 VisitedBeforeRequestsMap::iterator request =
148 visited_before_requests_.find(handle);
149 DCHECK(request != visited_before_requests_.end());
150 VisitedBeforeDoneCallback callback = request->second;
151 visited_before_requests_.erase(request);
152 callback.Run(found_visits && count && 425 callback.Run(found_visits && count &&
153 (first_visit.LocalMidnight() < base::Time::Now().LocalMidnight())); 426 (first_visit.LocalMidnight() < base::Time::Now().LocalMidnight()));
154 } 427 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698