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
3 // found in the LICENSE file.
4
5 #include "content/browser/appcache/appcache_service_impl.h"
6
7 #include <functional>
8
9 #include "base/bind.h"
10 #include "base/bind_helpers.h"
11 #include "base/logging.h"
12 #include "base/message_loop/message_loop.h"
13 #include "base/single_thread_task_runner.h"
14 #include "base/stl_util.h"
15 #include "content/browser/appcache/appcache.h"
16 #include "content/browser/appcache/appcache_backend_impl.h"
17 #include "content/browser/appcache/appcache_entry.h"
18 #include "content/browser/appcache/appcache_executable_handler.h"
19 #include "content/browser/appcache/appcache_histograms.h"
20 #include "content/browser/appcache/appcache_policy.h"
21 #include "content/browser/appcache/appcache_quota_client.h"
22 #include "content/browser/appcache/appcache_response.h"
23 #include "content/browser/appcache/appcache_service_impl.h"
24 #include "content/browser/appcache/appcache_storage_impl.h"
25 #include "net/base/completion_callback.h"
26 #include "net/base/io_buffer.h"
27 #include "storage/browser/quota/special_storage_policy.h"
28
29 namespace content {
30
31 namespace {
32
DeferredCallback(const net::CompletionCallback & callback,int rv)33 void DeferredCallback(const net::CompletionCallback& callback, int rv) {
34 callback.Run(rv);
35 }
36
37 } // namespace
38
AppCacheInfoCollection()39 AppCacheInfoCollection::AppCacheInfoCollection() {}
40
~AppCacheInfoCollection()41 AppCacheInfoCollection::~AppCacheInfoCollection() {}
42
43 // AsyncHelper -------
44
45 class AppCacheServiceImpl::AsyncHelper
46 : public AppCacheStorage::Delegate {
47 public:
AsyncHelper(AppCacheServiceImpl * service,const net::CompletionCallback & callback)48 AsyncHelper(AppCacheServiceImpl* service,
49 const net::CompletionCallback& callback)
50 : service_(service), callback_(callback) {
51 service_->pending_helpers_.insert(this);
52 }
53
~AsyncHelper()54 virtual ~AsyncHelper() {
55 if (service_)
56 service_->pending_helpers_.erase(this);
57 }
58
59 virtual void Start() = 0;
60 virtual void Cancel();
61
62 protected:
CallCallback(int rv)63 void CallCallback(int rv) {
64 if (!callback_.is_null()) {
65 // Defer to guarantee async completion.
66 base::MessageLoop::current()->PostTask(
67 FROM_HERE, base::Bind(&DeferredCallback, callback_, rv));
68 }
69 callback_.Reset();
70 }
71
72 AppCacheServiceImpl* service_;
73 net::CompletionCallback callback_;
74 };
75
Cancel()76 void AppCacheServiceImpl::AsyncHelper::Cancel() {
77 if (!callback_.is_null()) {
78 callback_.Run(net::ERR_ABORTED);
79 callback_.Reset();
80 }
81 service_->storage()->CancelDelegateCallbacks(this);
82 service_ = NULL;
83 }
84
85 // CanHandleOfflineHelper -------
86
87 class AppCacheServiceImpl::CanHandleOfflineHelper : AsyncHelper {
88 public:
CanHandleOfflineHelper(AppCacheServiceImpl * service,const GURL & url,const GURL & first_party,const net::CompletionCallback & callback)89 CanHandleOfflineHelper(
90 AppCacheServiceImpl* service, const GURL& url,
91 const GURL& first_party, const net::CompletionCallback& callback)
92 : AsyncHelper(service, callback),
93 url_(url),
94 first_party_(first_party) {
95 }
96
Start()97 virtual void Start() OVERRIDE {
98 AppCachePolicy* policy = service_->appcache_policy();
99 if (policy && !policy->CanLoadAppCache(url_, first_party_)) {
100 CallCallback(net::ERR_FAILED);
101 delete this;
102 return;
103 }
104
105 service_->storage()->FindResponseForMainRequest(url_, GURL(), this);
106 }
107
108 private:
109 // AppCacheStorage::Delegate implementation.
110 virtual void OnMainResponseFound(
111 const GURL& url, const AppCacheEntry& entry,
112 const GURL& fallback_url, const AppCacheEntry& fallback_entry,
113 int64 cache_id, int64 group_id, const GURL& mainfest_url) OVERRIDE;
114
115 GURL url_;
116 GURL first_party_;
117
118 DISALLOW_COPY_AND_ASSIGN(CanHandleOfflineHelper);
119 };
120
OnMainResponseFound(const GURL & url,const AppCacheEntry & entry,const GURL & fallback_url,const AppCacheEntry & fallback_entry,int64 cache_id,int64 group_id,const GURL & manifest_url)121 void AppCacheServiceImpl::CanHandleOfflineHelper::OnMainResponseFound(
122 const GURL& url, const AppCacheEntry& entry,
123 const GURL& fallback_url, const AppCacheEntry& fallback_entry,
124 int64 cache_id, int64 group_id, const GURL& manifest_url) {
125 bool can = (entry.has_response_id() || fallback_entry.has_response_id());
126 CallCallback(can ? net::OK : net::ERR_FAILED);
127 delete this;
128 }
129
130 // DeleteHelper -------
131
132 class AppCacheServiceImpl::DeleteHelper : public AsyncHelper {
133 public:
DeleteHelper(AppCacheServiceImpl * service,const GURL & manifest_url,const net::CompletionCallback & callback)134 DeleteHelper(
135 AppCacheServiceImpl* service, const GURL& manifest_url,
136 const net::CompletionCallback& callback)
137 : AsyncHelper(service, callback), manifest_url_(manifest_url) {
138 }
139
Start()140 virtual void Start() OVERRIDE {
141 service_->storage()->LoadOrCreateGroup(manifest_url_, this);
142 }
143
144 private:
145 // AppCacheStorage::Delegate implementation.
146 virtual void OnGroupLoaded(
147 AppCacheGroup* group, const GURL& manifest_url) OVERRIDE;
148 virtual void OnGroupMadeObsolete(AppCacheGroup* group,
149 bool success,
150 int response_code) OVERRIDE;
151
152 GURL manifest_url_;
153 DISALLOW_COPY_AND_ASSIGN(DeleteHelper);
154 };
155
OnGroupLoaded(AppCacheGroup * group,const GURL & manifest_url)156 void AppCacheServiceImpl::DeleteHelper::OnGroupLoaded(
157 AppCacheGroup* group, const GURL& manifest_url) {
158 if (group) {
159 group->set_being_deleted(true);
160 group->CancelUpdate();
161 service_->storage()->MakeGroupObsolete(group, this, 0);
162 } else {
163 CallCallback(net::ERR_FAILED);
164 delete this;
165 }
166 }
167
OnGroupMadeObsolete(AppCacheGroup * group,bool success,int response_code)168 void AppCacheServiceImpl::DeleteHelper::OnGroupMadeObsolete(
169 AppCacheGroup* group,
170 bool success,
171 int response_code) {
172 CallCallback(success ? net::OK : net::ERR_FAILED);
173 delete this;
174 }
175
176 // DeleteOriginHelper -------
177
178 class AppCacheServiceImpl::DeleteOriginHelper : public AsyncHelper {
179 public:
DeleteOriginHelper(AppCacheServiceImpl * service,const GURL & origin,const net::CompletionCallback & callback)180 DeleteOriginHelper(
181 AppCacheServiceImpl* service, const GURL& origin,
182 const net::CompletionCallback& callback)
183 : AsyncHelper(service, callback), origin_(origin),
184 num_caches_to_delete_(0), successes_(0), failures_(0) {
185 }
186
Start()187 virtual void Start() OVERRIDE {
188 // We start by listing all caches, continues in OnAllInfo().
189 service_->storage()->GetAllInfo(this);
190 }
191
192 private:
193 // AppCacheStorage::Delegate implementation.
194 virtual void OnAllInfo(AppCacheInfoCollection* collection) OVERRIDE;
195 virtual void OnGroupLoaded(
196 AppCacheGroup* group, const GURL& manifest_url) OVERRIDE;
197 virtual void OnGroupMadeObsolete(AppCacheGroup* group,
198 bool success,
199 int response_code) OVERRIDE;
200
201 void CacheCompleted(bool success);
202
203 GURL origin_;
204 int num_caches_to_delete_;
205 int successes_;
206 int failures_;
207
208 DISALLOW_COPY_AND_ASSIGN(DeleteOriginHelper);
209 };
210
OnAllInfo(AppCacheInfoCollection * collection)211 void AppCacheServiceImpl::DeleteOriginHelper::OnAllInfo(
212 AppCacheInfoCollection* collection) {
213 if (!collection) {
214 // Failed to get a listing.
215 CallCallback(net::ERR_FAILED);
216 delete this;
217 return;
218 }
219
220 std::map<GURL, AppCacheInfoVector>::iterator found =
221 collection->infos_by_origin.find(origin_);
222 if (found == collection->infos_by_origin.end() || found->second.empty()) {
223 // No caches for this origin.
224 CallCallback(net::OK);
225 delete this;
226 return;
227 }
228
229 // We have some caches to delete.
230 const AppCacheInfoVector& caches_to_delete = found->second;
231 successes_ = 0;
232 failures_ = 0;
233 num_caches_to_delete_ = static_cast<int>(caches_to_delete.size());
234 for (AppCacheInfoVector::const_iterator iter = caches_to_delete.begin();
235 iter != caches_to_delete.end(); ++iter) {
236 service_->storage()->LoadOrCreateGroup(iter->manifest_url, this);
237 }
238 }
239
OnGroupLoaded(AppCacheGroup * group,const GURL & manifest_url)240 void AppCacheServiceImpl::DeleteOriginHelper::OnGroupLoaded(
241 AppCacheGroup* group, const GURL& manifest_url) {
242 if (group) {
243 group->set_being_deleted(true);
244 group->CancelUpdate();
245 service_->storage()->MakeGroupObsolete(group, this, 0);
246 } else {
247 CacheCompleted(false);
248 }
249 }
250
OnGroupMadeObsolete(AppCacheGroup * group,bool success,int response_code)251 void AppCacheServiceImpl::DeleteOriginHelper::OnGroupMadeObsolete(
252 AppCacheGroup* group,
253 bool success,
254 int response_code) {
255 CacheCompleted(success);
256 }
257
CacheCompleted(bool success)258 void AppCacheServiceImpl::DeleteOriginHelper::CacheCompleted(bool success) {
259 if (success)
260 ++successes_;
261 else
262 ++failures_;
263 if ((successes_ + failures_) < num_caches_to_delete_)
264 return;
265
266 CallCallback(!failures_ ? net::OK : net::ERR_FAILED);
267 delete this;
268 }
269
270
271 // GetInfoHelper -------
272
273 class AppCacheServiceImpl::GetInfoHelper : AsyncHelper {
274 public:
GetInfoHelper(AppCacheServiceImpl * service,AppCacheInfoCollection * collection,const net::CompletionCallback & callback)275 GetInfoHelper(
276 AppCacheServiceImpl* service, AppCacheInfoCollection* collection,
277 const net::CompletionCallback& callback)
278 : AsyncHelper(service, callback), collection_(collection) {
279 }
280
Start()281 virtual void Start() OVERRIDE {
282 service_->storage()->GetAllInfo(this);
283 }
284
285 private:
286 // AppCacheStorage::Delegate implementation.
287 virtual void OnAllInfo(AppCacheInfoCollection* collection) OVERRIDE;
288
289 scoped_refptr<AppCacheInfoCollection> collection_;
290
291 DISALLOW_COPY_AND_ASSIGN(GetInfoHelper);
292 };
293
OnAllInfo(AppCacheInfoCollection * collection)294 void AppCacheServiceImpl::GetInfoHelper::OnAllInfo(
295 AppCacheInfoCollection* collection) {
296 if (collection)
297 collection->infos_by_origin.swap(collection_->infos_by_origin);
298 CallCallback(collection ? net::OK : net::ERR_FAILED);
299 delete this;
300 }
301
302 // CheckResponseHelper -------
303
304 class AppCacheServiceImpl::CheckResponseHelper : AsyncHelper {
305 public:
CheckResponseHelper(AppCacheServiceImpl * service,const GURL & manifest_url,int64 cache_id,int64 response_id)306 CheckResponseHelper(
307 AppCacheServiceImpl* service, const GURL& manifest_url, int64 cache_id,
308 int64 response_id)
309 : AsyncHelper(service, net::CompletionCallback()),
310 manifest_url_(manifest_url),
311 cache_id_(cache_id),
312 response_id_(response_id),
313 kIOBufferSize(32 * 1024),
314 expected_total_size_(0),
315 amount_headers_read_(0),
316 amount_data_read_(0) {
317 }
318
Start()319 virtual void Start() OVERRIDE {
320 service_->storage()->LoadOrCreateGroup(manifest_url_, this);
321 }
322
Cancel()323 virtual void Cancel() OVERRIDE {
324 AppCacheHistograms::CountCheckResponseResult(
325 AppCacheHistograms::CHECK_CANCELED);
326 response_reader_.reset();
327 AsyncHelper::Cancel();
328 }
329
330 private:
331 virtual void OnGroupLoaded(AppCacheGroup* group,
332 const GURL& manifest_url) OVERRIDE;
333 void OnReadInfoComplete(int result);
334 void OnReadDataComplete(int result);
335
336 // Inputs describing what to check.
337 GURL manifest_url_;
338 int64 cache_id_;
339 int64 response_id_;
340
341 // Internals used to perform the checks.
342 const int kIOBufferSize;
343 scoped_refptr<AppCache> cache_;
344 scoped_ptr<AppCacheResponseReader> response_reader_;
345 scoped_refptr<HttpResponseInfoIOBuffer> info_buffer_;
346 scoped_refptr<net::IOBuffer> data_buffer_;
347 int64 expected_total_size_;
348 int amount_headers_read_;
349 int amount_data_read_;
350 DISALLOW_COPY_AND_ASSIGN(CheckResponseHelper);
351 };
352
OnGroupLoaded(AppCacheGroup * group,const GURL & manifest_url)353 void AppCacheServiceImpl::CheckResponseHelper::OnGroupLoaded(
354 AppCacheGroup* group, const GURL& manifest_url) {
355 DCHECK_EQ(manifest_url_, manifest_url);
356 if (!group || !group->newest_complete_cache() || group->is_being_deleted() ||
357 group->is_obsolete()) {
358 AppCacheHistograms::CountCheckResponseResult(
359 AppCacheHistograms::MANIFEST_OUT_OF_DATE);
360 delete this;
361 return;
362 }
363
364 cache_ = group->newest_complete_cache();
365 const AppCacheEntry* entry = cache_->GetEntryWithResponseId(response_id_);
366 if (!entry) {
367 if (cache_->cache_id() == cache_id_) {
368 AppCacheHistograms::CountCheckResponseResult(
369 AppCacheHistograms::ENTRY_NOT_FOUND);
370 service_->DeleteAppCacheGroup(manifest_url_, net::CompletionCallback());
371 } else {
372 AppCacheHistograms::CountCheckResponseResult(
373 AppCacheHistograms::RESPONSE_OUT_OF_DATE);
374 }
375 delete this;
376 return;
377 }
378
379 // Verify that we can read the response info and data.
380 expected_total_size_ = entry->response_size();
381 response_reader_.reset(service_->storage()->CreateResponseReader(
382 manifest_url_, group->group_id(), response_id_));
383 info_buffer_ = new HttpResponseInfoIOBuffer();
384 response_reader_->ReadInfo(
385 info_buffer_.get(),
386 base::Bind(&CheckResponseHelper::OnReadInfoComplete,
387 base::Unretained(this)));
388 }
389
OnReadInfoComplete(int result)390 void AppCacheServiceImpl::CheckResponseHelper::OnReadInfoComplete(int result) {
391 if (result < 0) {
392 AppCacheHistograms::CountCheckResponseResult(
393 AppCacheHistograms::READ_HEADERS_ERROR);
394 service_->DeleteAppCacheGroup(manifest_url_, net::CompletionCallback());
395 delete this;
396 return;
397 }
398 amount_headers_read_ = result;
399
400 // Start reading the data.
401 data_buffer_ = new net::IOBuffer(kIOBufferSize);
402 response_reader_->ReadData(
403 data_buffer_.get(),
404 kIOBufferSize,
405 base::Bind(&CheckResponseHelper::OnReadDataComplete,
406 base::Unretained(this)));
407 }
408
OnReadDataComplete(int result)409 void AppCacheServiceImpl::CheckResponseHelper::OnReadDataComplete(int result) {
410 if (result > 0) {
411 // Keep reading until we've read thru everything or failed to read.
412 amount_data_read_ += result;
413 response_reader_->ReadData(
414 data_buffer_.get(),
415 kIOBufferSize,
416 base::Bind(&CheckResponseHelper::OnReadDataComplete,
417 base::Unretained(this)));
418 return;
419 }
420
421 AppCacheHistograms::CheckResponseResultType check_result;
422 if (result < 0)
423 check_result = AppCacheHistograms::READ_DATA_ERROR;
424 else if (info_buffer_->response_data_size != amount_data_read_ ||
425 expected_total_size_ != amount_data_read_ + amount_headers_read_)
426 check_result = AppCacheHistograms::UNEXPECTED_DATA_SIZE;
427 else
428 check_result = AppCacheHistograms::RESPONSE_OK;
429 AppCacheHistograms::CountCheckResponseResult(check_result);
430
431 if (check_result != AppCacheHistograms::RESPONSE_OK)
432 service_->DeleteAppCacheGroup(manifest_url_, net::CompletionCallback());
433 delete this;
434 }
435
436 // AppCacheStorageReference ------
437
AppCacheStorageReference(scoped_ptr<AppCacheStorage> storage)438 AppCacheStorageReference::AppCacheStorageReference(
439 scoped_ptr<AppCacheStorage> storage)
440 : storage_(storage.Pass()) {}
~AppCacheStorageReference()441 AppCacheStorageReference::~AppCacheStorageReference() {}
442
443 // AppCacheServiceImpl -------
444
AppCacheServiceImpl(storage::QuotaManagerProxy * quota_manager_proxy)445 AppCacheServiceImpl::AppCacheServiceImpl(
446 storage::QuotaManagerProxy* quota_manager_proxy)
447 : appcache_policy_(NULL),
448 quota_client_(NULL),
449 handler_factory_(NULL),
450 quota_manager_proxy_(quota_manager_proxy),
451 request_context_(NULL),
452 force_keep_session_state_(false) {
453 if (quota_manager_proxy_.get()) {
454 quota_client_ = new AppCacheQuotaClient(this);
455 quota_manager_proxy_->RegisterClient(quota_client_);
456 }
457 }
458
~AppCacheServiceImpl()459 AppCacheServiceImpl::~AppCacheServiceImpl() {
460 DCHECK(backends_.empty());
461 std::for_each(pending_helpers_.begin(),
462 pending_helpers_.end(),
463 std::mem_fun(&AsyncHelper::Cancel));
464 STLDeleteElements(&pending_helpers_);
465 if (quota_client_)
466 quota_client_->NotifyAppCacheDestroyed();
467
468 // Destroy storage_ first; ~AppCacheStorageImpl accesses other data members
469 // (special_storage_policy_).
470 storage_.reset();
471 }
472
Initialize(const base::FilePath & cache_directory,const scoped_refptr<base::SingleThreadTaskRunner> & db_thread,const scoped_refptr<base::SingleThreadTaskRunner> & cache_thread)473 void AppCacheServiceImpl::Initialize(
474 const base::FilePath& cache_directory,
475 const scoped_refptr<base::SingleThreadTaskRunner>& db_thread,
476 const scoped_refptr<base::SingleThreadTaskRunner>& cache_thread) {
477 DCHECK(!storage_.get());
478 cache_directory_ = cache_directory;
479 db_thread_ = db_thread;
480 cache_thread_ = cache_thread;
481 AppCacheStorageImpl* storage = new AppCacheStorageImpl(this);
482 storage->Initialize(cache_directory, db_thread, cache_thread);
483 storage_.reset(storage);
484 }
485
ScheduleReinitialize()486 void AppCacheServiceImpl::ScheduleReinitialize() {
487 if (reinit_timer_.IsRunning())
488 return;
489
490 // Reinitialization only happens when corruption has been noticed.
491 // We don't want to thrash the disk but we also don't want to
492 // leave the appcache disabled for an indefinite period of time. Some
493 // users never shutdown the browser.
494
495 const base::TimeDelta kZeroDelta;
496 const base::TimeDelta kOneHour(base::TimeDelta::FromHours(1));
497 const base::TimeDelta k30Seconds(base::TimeDelta::FromSeconds(30));
498
499 // If the system managed to stay up for long enough, reset the
500 // delay so a new failure won't incur a long wait to get going again.
501 base::TimeDelta up_time = base::Time::Now() - last_reinit_time_;
502 if (next_reinit_delay_ != kZeroDelta && up_time > kOneHour)
503 next_reinit_delay_ = kZeroDelta;
504
505 reinit_timer_.Start(FROM_HERE, next_reinit_delay_,
506 this, &AppCacheServiceImpl::Reinitialize);
507
508 // Adjust the delay for next time.
509 base::TimeDelta increment = std::max(k30Seconds, next_reinit_delay_);
510 next_reinit_delay_ = std::min(next_reinit_delay_ + increment, kOneHour);
511 }
512
Reinitialize()513 void AppCacheServiceImpl::Reinitialize() {
514 AppCacheHistograms::CountReinitAttempt(!last_reinit_time_.is_null());
515 last_reinit_time_ = base::Time::Now();
516
517 // Inform observers of about this and give them a chance to
518 // defer deletion of the old storage object.
519 scoped_refptr<AppCacheStorageReference>
520 old_storage_ref(new AppCacheStorageReference(storage_.Pass()));
521 FOR_EACH_OBSERVER(Observer, observers_,
522 OnServiceReinitialized(old_storage_ref.get()));
523
524 Initialize(cache_directory_, db_thread_, cache_thread_);
525 }
526
CanHandleMainResourceOffline(const GURL & url,const GURL & first_party,const net::CompletionCallback & callback)527 void AppCacheServiceImpl::CanHandleMainResourceOffline(
528 const GURL& url,
529 const GURL& first_party,
530 const net::CompletionCallback& callback) {
531 CanHandleOfflineHelper* helper =
532 new CanHandleOfflineHelper(this, url, first_party, callback);
533 helper->Start();
534 }
535
GetAllAppCacheInfo(AppCacheInfoCollection * collection,const net::CompletionCallback & callback)536 void AppCacheServiceImpl::GetAllAppCacheInfo(
537 AppCacheInfoCollection* collection,
538 const net::CompletionCallback& callback) {
539 DCHECK(collection);
540 GetInfoHelper* helper = new GetInfoHelper(this, collection, callback);
541 helper->Start();
542 }
543
DeleteAppCacheGroup(const GURL & manifest_url,const net::CompletionCallback & callback)544 void AppCacheServiceImpl::DeleteAppCacheGroup(
545 const GURL& manifest_url,
546 const net::CompletionCallback& callback) {
547 DeleteHelper* helper = new DeleteHelper(this, manifest_url, callback);
548 helper->Start();
549 }
550
DeleteAppCachesForOrigin(const GURL & origin,const net::CompletionCallback & callback)551 void AppCacheServiceImpl::DeleteAppCachesForOrigin(
552 const GURL& origin, const net::CompletionCallback& callback) {
553 DeleteOriginHelper* helper = new DeleteOriginHelper(this, origin, callback);
554 helper->Start();
555 }
556
CheckAppCacheResponse(const GURL & manifest_url,int64 cache_id,int64 response_id)557 void AppCacheServiceImpl::CheckAppCacheResponse(const GURL& manifest_url,
558 int64 cache_id,
559 int64 response_id) {
560 CheckResponseHelper* helper = new CheckResponseHelper(
561 this, manifest_url, cache_id, response_id);
562 helper->Start();
563 }
564
set_special_storage_policy(storage::SpecialStoragePolicy * policy)565 void AppCacheServiceImpl::set_special_storage_policy(
566 storage::SpecialStoragePolicy* policy) {
567 special_storage_policy_ = policy;
568 }
569
RegisterBackend(AppCacheBackendImpl * backend_impl)570 void AppCacheServiceImpl::RegisterBackend(
571 AppCacheBackendImpl* backend_impl) {
572 DCHECK(backends_.find(backend_impl->process_id()) == backends_.end());
573 backends_.insert(
574 BackendMap::value_type(backend_impl->process_id(), backend_impl));
575 }
576
UnregisterBackend(AppCacheBackendImpl * backend_impl)577 void AppCacheServiceImpl::UnregisterBackend(
578 AppCacheBackendImpl* backend_impl) {
579 backends_.erase(backend_impl->process_id());
580 }
581
582 } // namespace content
583