1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "DocManager.h"
8 
9 #include "ApplicationAccessible.h"
10 #include "ARIAMap.h"
11 #include "DocAccessible-inl.h"
12 #include "DocAccessibleChild.h"
13 #include "DocAccessibleParent.h"
14 #include "nsAccessibilityService.h"
15 #include "Platform.h"
16 #include "RootAccessibleWrap.h"
17 
18 #ifdef A11Y_LOG
19 #  include "Logging.h"
20 #endif
21 
22 #include "mozilla/Components.h"
23 #include "mozilla/EventListenerManager.h"
24 #include "mozilla/PresShell.h"
25 #include "mozilla/dom/Event.h"  // for Event
26 #include "nsContentUtils.h"
27 #include "nsDocShellLoadTypes.h"
28 #include "nsIChannel.h"
29 #include "nsIInterfaceRequestorUtils.h"
30 #include "nsIWebNavigation.h"
31 #include "nsServiceManagerUtils.h"
32 #include "nsIWebProgress.h"
33 #include "nsCoreUtils.h"
34 #include "nsXULAppAPI.h"
35 #include "mozilla/dom/BrowserChild.h"
36 #include "xpcAccessibleDocument.h"
37 
38 using namespace mozilla;
39 using namespace mozilla::a11y;
40 using namespace mozilla::dom;
41 
42 StaticAutoPtr<nsTArray<DocAccessibleParent*>> DocManager::sRemoteDocuments;
43 StaticAutoPtr<nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>,
44                                 xpcAccessibleDocument>>
45     DocManager::sRemoteXPCDocumentCache;
46 
47 ////////////////////////////////////////////////////////////////////////////////
48 // DocManager
49 ////////////////////////////////////////////////////////////////////////////////
50 
DocManager()51 DocManager::DocManager() : mDocAccessibleCache(2), mXPCDocumentCache(0) {}
52 
53 ////////////////////////////////////////////////////////////////////////////////
54 // DocManager public
55 
GetDocAccessible(Document * aDocument)56 DocAccessible* DocManager::GetDocAccessible(Document* aDocument) {
57   if (!aDocument) return nullptr;
58 
59   DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
60   if (docAcc) return docAcc;
61 
62   return CreateDocOrRootAccessible(aDocument);
63 }
64 
GetDocAccessible(const PresShell * aPresShell)65 DocAccessible* DocManager::GetDocAccessible(const PresShell* aPresShell) {
66   if (!aPresShell) {
67     return nullptr;
68   }
69 
70   DocAccessible* doc = aPresShell->GetDocAccessible();
71   if (doc) {
72     return doc;
73   }
74 
75   return GetDocAccessible(aPresShell->GetDocument());
76 }
77 
FindAccessibleInCache(nsINode * aNode) const78 LocalAccessible* DocManager::FindAccessibleInCache(nsINode* aNode) const {
79   for (const auto& docAccessible : mDocAccessibleCache.Values()) {
80     NS_ASSERTION(docAccessible,
81                  "No doc accessible for the object in doc accessible cache!");
82 
83     if (docAccessible) {
84       LocalAccessible* accessible = docAccessible->GetAccessible(aNode);
85       if (accessible) {
86         return accessible;
87       }
88     }
89   }
90   return nullptr;
91 }
92 
RemoveFromXPCDocumentCache(DocAccessible * aDocument,bool aAllowServiceShutdown)93 void DocManager::RemoveFromXPCDocumentCache(DocAccessible* aDocument,
94                                             bool aAllowServiceShutdown) {
95   xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument);
96   if (xpcDoc) {
97     xpcDoc->Shutdown();
98     mXPCDocumentCache.Remove(aDocument);
99 
100     if (aAllowServiceShutdown && !HasXPCDocuments()) {
101       MaybeShutdownAccService(nsAccessibilityService::eXPCOM);
102     }
103   }
104 }
105 
NotifyOfDocumentShutdown(DocAccessible * aDocument,Document * aDOMDocument,bool aAllowServiceShutdown)106 void DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument,
107                                           Document* aDOMDocument,
108                                           bool aAllowServiceShutdown) {
109   // We need to remove listeners in both cases, when document is being shutdown
110   // or when accessibility service is being shut down as well.
111   RemoveListeners(aDOMDocument);
112 
113   // Document will already be removed when accessibility service is shutting
114   // down so we do not need to remove it twice.
115   if (nsAccessibilityService::IsShutdown()) {
116     return;
117   }
118 
119   RemoveFromXPCDocumentCache(aDocument, aAllowServiceShutdown);
120   mDocAccessibleCache.Remove(aDOMDocument);
121 }
122 
RemoveFromRemoteXPCDocumentCache(DocAccessibleParent * aDoc)123 void DocManager::RemoveFromRemoteXPCDocumentCache(DocAccessibleParent* aDoc) {
124   xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc);
125   if (doc) {
126     doc->Shutdown();
127     sRemoteXPCDocumentCache->Remove(aDoc);
128   }
129 
130   if (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() == 0) {
131     MaybeShutdownAccService(nsAccessibilityService::eXPCOM);
132   }
133 }
134 
NotifyOfRemoteDocShutdown(DocAccessibleParent * aDoc)135 void DocManager::NotifyOfRemoteDocShutdown(DocAccessibleParent* aDoc) {
136   RemoveFromRemoteXPCDocumentCache(aDoc);
137 }
138 
GetXPCDocument(DocAccessible * aDocument)139 xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessible* aDocument) {
140   if (!aDocument) return nullptr;
141 
142   return mXPCDocumentCache.GetOrInsertNew(aDocument, aDocument);
143 }
144 
GetXPCDocument(DocAccessibleParent * aDoc)145 xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessibleParent* aDoc) {
146   xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc);
147   if (doc) {
148     return doc;
149   }
150 
151   if (!sRemoteXPCDocumentCache) {
152     sRemoteXPCDocumentCache =
153         new nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>,
154                               xpcAccessibleDocument>;
155     ClearOnShutdown(&sRemoteXPCDocumentCache);
156   }
157 
158   MOZ_ASSERT(!aDoc->IsShutdown(), "Adding a shutdown doc to remote XPC cache");
159   doc = new xpcAccessibleDocument(aDoc);
160   sRemoteXPCDocumentCache->InsertOrUpdate(aDoc, RefPtr{doc});
161 
162   return doc;
163 }
164 
165 #ifdef DEBUG
IsProcessingRefreshDriverNotification() const166 bool DocManager::IsProcessingRefreshDriverNotification() const {
167   for (const auto& entry : mDocAccessibleCache) {
168     DocAccessible* docAccessible = entry.GetWeak();
169     NS_ASSERTION(docAccessible,
170                  "No doc accessible for the object in doc accessible cache!");
171 
172     if (docAccessible && docAccessible->mNotificationController &&
173         docAccessible->mNotificationController->IsUpdating()) {
174       return true;
175     }
176   }
177   return false;
178 }
179 #endif
180 
181 ////////////////////////////////////////////////////////////////////////////////
182 // DocManager protected
183 
Init()184 bool DocManager::Init() {
185   nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
186 
187   if (!progress) return false;
188 
189   progress->AddProgressListener(static_cast<nsIWebProgressListener*>(this),
190                                 nsIWebProgress::NOTIFY_STATE_DOCUMENT);
191 
192   return true;
193 }
194 
Shutdown()195 void DocManager::Shutdown() {
196   nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
197 
198   if (progress) {
199     progress->RemoveProgressListener(
200         static_cast<nsIWebProgressListener*>(this));
201   }
202 
203   ClearDocCache();
204 }
205 
206 ////////////////////////////////////////////////////////////////////////////////
207 // nsISupports
208 
NS_IMPL_ISUPPORTS(DocManager,nsIWebProgressListener,nsIDOMEventListener,nsISupportsWeakReference)209 NS_IMPL_ISUPPORTS(DocManager, nsIWebProgressListener, nsIDOMEventListener,
210                   nsISupportsWeakReference)
211 
212 ////////////////////////////////////////////////////////////////////////////////
213 // nsIWebProgressListener
214 
215 NS_IMETHODIMP
216 DocManager::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
217                           uint32_t aStateFlags, nsresult aStatus) {
218   NS_ASSERTION(aStateFlags & STATE_IS_DOCUMENT, "Other notifications excluded");
219 
220   if (nsAccessibilityService::IsShutdown() || !aWebProgress ||
221       (aStateFlags & (STATE_START | STATE_STOP)) == 0) {
222     return NS_OK;
223   }
224 
225   nsCOMPtr<mozIDOMWindowProxy> DOMWindow;
226   aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow));
227   NS_ENSURE_STATE(DOMWindow);
228 
229   nsPIDOMWindowOuter* piWindow = nsPIDOMWindowOuter::From(DOMWindow);
230   MOZ_ASSERT(piWindow);
231 
232   nsCOMPtr<Document> document = piWindow->GetDoc();
233   NS_ENSURE_STATE(document);
234 
235   // Document was loaded.
236   if (aStateFlags & STATE_STOP) {
237 #ifdef A11Y_LOG
238     if (logging::IsEnabled(logging::eDocLoad)) {
239       logging::DocLoad("document loaded", aWebProgress, aRequest, aStateFlags);
240     }
241 #endif
242 
243     // Figure out an event type to notify the document has been loaded.
244     uint32_t eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED;
245 
246     // Some XUL documents get start state and then stop state with failure
247     // status when everything is ok. Fire document load complete event in this
248     // case.
249     if (NS_SUCCEEDED(aStatus) || !document->IsContentDocument()) {
250       eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE;
251     }
252 
253     // If end consumer has been retargeted for loaded content then do not fire
254     // any event because it means no new document has been loaded, for example,
255     // it happens when user clicks on file link.
256     if (aRequest) {
257       uint32_t loadFlags = 0;
258       aRequest->GetLoadFlags(&loadFlags);
259       if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) eventType = 0;
260     }
261 
262     HandleDOMDocumentLoad(document, eventType);
263     return NS_OK;
264   }
265 
266   // Document loading was started.
267 #ifdef A11Y_LOG
268   if (logging::IsEnabled(logging::eDocLoad)) {
269     logging::DocLoad("start document loading", aWebProgress, aRequest,
270                      aStateFlags);
271   }
272 #endif
273 
274   DocAccessible* docAcc = GetExistingDocAccessible(document);
275   if (!docAcc) return NS_OK;
276 
277   nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(DOMWindow));
278   nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(webNav));
279   NS_ENSURE_STATE(docShell);
280 
281   bool isReloading = false;
282   uint32_t loadType;
283   docShell->GetLoadType(&loadType);
284   if (loadType == LOAD_RELOAD_NORMAL || loadType == LOAD_RELOAD_BYPASS_CACHE ||
285       loadType == LOAD_RELOAD_BYPASS_PROXY ||
286       loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE) {
287     isReloading = true;
288   }
289 
290   docAcc->NotifyOfLoading(isReloading);
291   return NS_OK;
292 }
293 
294 NS_IMETHODIMP
OnProgressChange(nsIWebProgress * aWebProgress,nsIRequest * aRequest,int32_t aCurSelfProgress,int32_t aMaxSelfProgress,int32_t aCurTotalProgress,int32_t aMaxTotalProgress)295 DocManager::OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
296                              int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
297                              int32_t aCurTotalProgress,
298                              int32_t aMaxTotalProgress) {
299   MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
300   return NS_OK;
301 }
302 
303 NS_IMETHODIMP
OnLocationChange(nsIWebProgress * aWebProgress,nsIRequest * aRequest,nsIURI * aLocation,uint32_t aFlags)304 DocManager::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
305                              nsIURI* aLocation, uint32_t aFlags) {
306   MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
307   return NS_OK;
308 }
309 
310 NS_IMETHODIMP
OnStatusChange(nsIWebProgress * aWebProgress,nsIRequest * aRequest,nsresult aStatus,const char16_t * aMessage)311 DocManager::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
312                            nsresult aStatus, const char16_t* aMessage) {
313   MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
314   return NS_OK;
315 }
316 
317 NS_IMETHODIMP
OnSecurityChange(nsIWebProgress * aWebProgress,nsIRequest * aRequest,uint32_t aState)318 DocManager::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
319                              uint32_t aState) {
320   MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
321   return NS_OK;
322 }
323 
324 NS_IMETHODIMP
OnContentBlockingEvent(nsIWebProgress * aWebProgress,nsIRequest * aRequest,uint32_t aEvent)325 DocManager::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
326                                    nsIRequest* aRequest, uint32_t aEvent) {
327   MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
328   return NS_OK;
329 }
330 
331 ////////////////////////////////////////////////////////////////////////////////
332 // nsIDOMEventListener
333 
334 NS_IMETHODIMP
HandleEvent(Event * aEvent)335 DocManager::HandleEvent(Event* aEvent) {
336   nsAutoString type;
337   aEvent->GetType(type);
338 
339   nsCOMPtr<Document> document = do_QueryInterface(aEvent->GetTarget());
340   NS_ASSERTION(document, "pagehide or DOMContentLoaded for non document!");
341   if (!document) return NS_OK;
342 
343   if (type.EqualsLiteral("pagehide")) {
344     // 'pagehide' event is registered on every DOM document we create an
345     // accessible for, process the event for the target. This document
346     // accessible and all its sub document accessible are shutdown as result of
347     // processing.
348 
349 #ifdef A11Y_LOG
350     if (logging::IsEnabled(logging::eDocDestroy)) {
351       logging::DocDestroy("received 'pagehide' event", document);
352     }
353 #endif
354 
355     // Shutdown this one and sub document accessibles.
356 
357     // We're allowed to not remove listeners when accessible document is
358     // shutdown since we don't keep strong reference on chrome event target and
359     // listeners are removed automatically when chrome event target goes away.
360     DocAccessible* docAccessible = GetExistingDocAccessible(document);
361     if (docAccessible) docAccessible->Shutdown();
362 
363     return NS_OK;
364   }
365 
366   // XXX: handle error pages loading separately since they get neither
367   // webprogress notifications nor 'pageshow' event.
368   if (type.EqualsLiteral("DOMContentLoaded") &&
369       nsCoreUtils::IsErrorPage(document)) {
370 #ifdef A11Y_LOG
371     if (logging::IsEnabled(logging::eDocLoad)) {
372       logging::DocLoad("handled 'DOMContentLoaded' event", document);
373     }
374 #endif
375 
376     HandleDOMDocumentLoad(document,
377                           nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE);
378   }
379 
380   return NS_OK;
381 }
382 
383 ////////////////////////////////////////////////////////////////////////////////
384 // DocManager private
385 
HandleDOMDocumentLoad(Document * aDocument,uint32_t aLoadEventType)386 void DocManager::HandleDOMDocumentLoad(Document* aDocument,
387                                        uint32_t aLoadEventType) {
388   // Document accessible can be created before we were notified the DOM document
389   // was loaded completely. However if it's not created yet then create it.
390   DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
391   if (!docAcc) {
392     docAcc = CreateDocOrRootAccessible(aDocument);
393     if (!docAcc) return;
394   }
395 
396   docAcc->NotifyOfLoad(aLoadEventType);
397 }
398 
AddListeners(Document * aDocument,bool aAddDOMContentLoadedListener)399 void DocManager::AddListeners(Document* aDocument,
400                               bool aAddDOMContentLoadedListener) {
401   nsPIDOMWindowOuter* window = aDocument->GetWindow();
402   EventTarget* target = window->GetChromeEventHandler();
403   EventListenerManager* elm = target->GetOrCreateListenerManager();
404   elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture());
405 
406 #ifdef A11Y_LOG
407   if (logging::IsEnabled(logging::eDocCreate)) {
408     logging::Text("added 'pagehide' listener");
409   }
410 #endif
411 
412   if (aAddDOMContentLoadedListener) {
413     elm->AddEventListenerByType(this, u"DOMContentLoaded"_ns,
414                                 TrustedEventsAtCapture());
415 #ifdef A11Y_LOG
416     if (logging::IsEnabled(logging::eDocCreate)) {
417       logging::Text("added 'DOMContentLoaded' listener");
418     }
419 #endif
420   }
421 }
422 
RemoveListeners(Document * aDocument)423 void DocManager::RemoveListeners(Document* aDocument) {
424   nsPIDOMWindowOuter* window = aDocument->GetWindow();
425   if (!window) return;
426 
427   EventTarget* target = window->GetChromeEventHandler();
428   if (!target) return;
429 
430   EventListenerManager* elm = target->GetOrCreateListenerManager();
431   elm->RemoveEventListenerByType(this, u"pagehide"_ns,
432                                  TrustedEventsAtCapture());
433 
434   elm->RemoveEventListenerByType(this, u"DOMContentLoaded"_ns,
435                                  TrustedEventsAtCapture());
436 }
437 
CreateDocOrRootAccessible(Document * aDocument)438 DocAccessible* DocManager::CreateDocOrRootAccessible(Document* aDocument) {
439   // Ignore hidden documents, resource documents, static clone
440   // (printing) documents and documents without a docshell.
441   if (!nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocument) ||
442       aDocument->IsResourceDoc() || aDocument->IsStaticDocument() ||
443       !aDocument->IsActive()) {
444     return nullptr;
445   }
446 
447   nsIDocShell* docShell = aDocument->GetDocShell();
448   if (!docShell || docShell->IsInvisible()) {
449     return nullptr;
450   }
451 
452   nsIWidget* widget = nsContentUtils::WidgetForDocument(aDocument);
453   if (!widget || widget->WindowType() == eWindowType_invisible) {
454     return nullptr;
455   }
456 
457   // Ignore documents without presshell. We must not ignore documents with no
458   // root frame because DOM focus can hit such documents and ignoring them would
459   // prevent a11y focus.
460   PresShell* presShell = aDocument->GetPresShell();
461   if (!presShell || presShell->IsDestroying()) {
462     return nullptr;
463   }
464 
465   bool isRootDoc = nsCoreUtils::IsRootDocument(aDocument);
466 
467   DocAccessible* parentDocAcc = nullptr;
468   if (!isRootDoc) {
469     // XXXaaronl: ideally we would traverse the presshell chain. Since there's
470     // no easy way to do that, we cheat and use the document hierarchy.
471     parentDocAcc = GetDocAccessible(aDocument->GetInProcessParentDocument());
472     NS_ASSERTION(parentDocAcc, "Can't create an accessible for the document!");
473     if (!parentDocAcc) return nullptr;
474   }
475 
476   // We only create root accessibles for the true root, otherwise create a
477   // doc accessible.
478   RefPtr<DocAccessible> docAcc =
479       isRootDoc ? new RootAccessibleWrap(aDocument, presShell)
480                 : new DocAccessibleWrap(aDocument, presShell);
481 
482   // Cache the document accessible into document cache.
483   mDocAccessibleCache.InsertOrUpdate(aDocument, RefPtr{docAcc});
484 
485   // Initialize the document accessible.
486   docAcc->Init();
487 
488   // Bind the document to the tree.
489   if (isRootDoc) {
490     if (!ApplicationAcc()->AppendChild(docAcc)) {
491       docAcc->Shutdown();
492       return nullptr;
493     }
494 
495     // Fire reorder event to notify new accessible document has been attached to
496     // the tree. The reorder event is delivered after the document tree is
497     // constructed because event processing and tree construction are done by
498     // the same document.
499     // Note: don't use AccReorderEvent to avoid coalsecense and special reorder
500     // events processing.
501     docAcc->FireDelayedEvent(nsIAccessibleEvent::EVENT_REORDER,
502                              ApplicationAcc());
503 
504   } else {
505     parentDocAcc->BindChildDocument(docAcc);
506   }
507 
508 #ifdef A11Y_LOG
509   if (logging::IsEnabled(logging::eDocCreate)) {
510     logging::DocCreate("document creation finished", aDocument);
511     logging::Stack();
512   }
513 #endif
514 
515   AddListeners(aDocument, isRootDoc);
516   return docAcc;
517 }
518 
519 ////////////////////////////////////////////////////////////////////////////////
520 // DocManager static
521 
ClearDocCache()522 void DocManager::ClearDocCache() {
523   while (mDocAccessibleCache.Count() > 0) {
524     auto iter = mDocAccessibleCache.Iter();
525     MOZ_ASSERT(!iter.Done());
526     DocAccessible* docAcc = iter.UserData();
527     NS_ASSERTION(docAcc,
528                  "No doc accessible for the object in doc accessible cache!");
529     if (docAcc) {
530       docAcc->Shutdown();
531     }
532 
533     iter.Remove();
534   }
535 
536   // Ensure that all xpcom accessible documents are shut down as well.
537   while (mXPCDocumentCache.Count() > 0) {
538     auto iter = mXPCDocumentCache.Iter();
539     MOZ_ASSERT(!iter.Done());
540     xpcAccessibleDocument* xpcDoc = iter.UserData();
541     NS_ASSERTION(xpcDoc, "No xpc doc for the object in xpc doc cache!");
542 
543     if (xpcDoc) {
544       xpcDoc->Shutdown();
545     }
546 
547     iter.Remove();
548   }
549 }
550 
RemoteDocAdded(DocAccessibleParent * aDoc)551 void DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) {
552   if (!sRemoteDocuments) {
553     sRemoteDocuments = new nsTArray<DocAccessibleParent*>;
554     ClearOnShutdown(&sRemoteDocuments);
555   }
556 
557   MOZ_ASSERT(!sRemoteDocuments->Contains(aDoc),
558              "How did we already have the doc!");
559   sRemoteDocuments->AppendElement(aDoc);
560   ProxyCreated(aDoc);
561 }
562 
GetExistingDocAccessible(const dom::Document * aDocument)563 DocAccessible* mozilla::a11y::GetExistingDocAccessible(
564     const dom::Document* aDocument) {
565   PresShell* presShell = aDocument->GetPresShell();
566   return presShell ? presShell->GetDocAccessible() : nullptr;
567 }
568