1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "SelectionState.h"
7 
8 #include "mozilla/Assertions.h"   // for MOZ_ASSERT, etc.
9 #include "mozilla/EditorUtils.h"  // for EditorUtils
10 #include "mozilla/dom/RangeBinding.h"
11 #include "mozilla/dom/Selection.h"  // for Selection
12 #include "nsAString.h"              // for nsAString::Length
13 #include "nsCycleCollectionParticipant.h"
14 #include "nsDebug.h"          // for NS_WARNING, etc.
15 #include "nsError.h"          // for NS_OK, etc.
16 #include "nsIContent.h"       // for nsIContent
17 #include "nsISupportsImpl.h"  // for nsRange::Release
18 #include "nsRange.h"          // for nsRange
19 
20 namespace mozilla {
21 
22 using namespace dom;
23 
24 /******************************************************************************
25  * mozilla::SelectionState
26  *
27  * Class for recording selection info.  Stores selection as collection of
28  * { {startnode, startoffset} , {endnode, endoffset} } tuples.  Can't store
29  * ranges since dom gravity will possibly change the ranges.
30  ******************************************************************************/
31 
32 template nsresult RangeUpdater::SelAdjCreateNode(const EditorDOMPoint& aPoint);
33 template nsresult RangeUpdater::SelAdjCreateNode(
34     const EditorRawDOMPoint& aPoint);
35 template nsresult RangeUpdater::SelAdjInsertNode(const EditorDOMPoint& aPoint);
36 template nsresult RangeUpdater::SelAdjInsertNode(
37     const EditorRawDOMPoint& aPoint);
38 
SelectionState()39 SelectionState::SelectionState() : mDirection(eDirNext) {}
40 
SaveSelection(Selection & aSelection)41 void SelectionState::SaveSelection(Selection& aSelection) {
42   // if we need more items in the array, new them
43   if (mArray.Length() < aSelection.RangeCount()) {
44     for (uint32_t i = mArray.Length(); i < aSelection.RangeCount(); i++) {
45       mArray.AppendElement();
46       mArray[i] = new RangeItem();
47     }
48   } else if (mArray.Length() > aSelection.RangeCount()) {
49     // else if we have too many, delete them
50     mArray.TruncateLength(aSelection.RangeCount());
51   }
52 
53   // now store the selection ranges
54   for (uint32_t i = 0; i < aSelection.RangeCount(); i++) {
55     const nsRange* range = aSelection.GetRangeAt(i);
56     if (NS_WARN_IF(!range)) {
57       continue;
58     }
59     mArray[i]->StoreRange(*range);
60   }
61 
62   mDirection = aSelection.GetDirection();
63 }
64 
RestoreSelection(Selection & aSelection)65 nsresult SelectionState::RestoreSelection(Selection& aSelection) {
66   // clear out selection
67   IgnoredErrorResult ignoredError;
68   aSelection.RemoveAllRanges(ignoredError);
69   NS_WARNING_ASSERTION(!ignoredError.Failed(),
70                        "Selection::RemoveAllRanges() failed, but ignored");
71 
72   aSelection.SetDirection(mDirection);
73 
74   ErrorResult error;
75   const CopyableAutoTArray<RefPtr<RangeItem>, 10> rangeItems(mArray);
76   for (const RefPtr<RangeItem>& rangeItem : rangeItems) {
77     RefPtr<nsRange> range = rangeItem->GetRange();
78     if (!range) {
79       NS_WARNING("RangeItem::GetRange() failed");
80       return NS_ERROR_FAILURE;
81     }
82     aSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, error);
83     if (error.Failed()) {
84       NS_WARNING(
85           "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
86       return error.StealNSResult();
87     }
88   }
89   return NS_OK;
90 }
91 
IsCollapsed() const92 bool SelectionState::IsCollapsed() const {
93   if (mArray.Length() != 1) {
94     return false;
95   }
96   RefPtr<nsRange> range = mArray[0]->GetRange();
97   if (!range) {
98     NS_WARNING("RangeItem::GetRange() failed");
99     return false;
100   }
101   return range->Collapsed();
102 }
103 
Equals(SelectionState & aOther) const104 bool SelectionState::Equals(SelectionState& aOther) const {
105   if (mArray.Length() != aOther.mArray.Length()) {
106     return false;
107   }
108   if (mArray.IsEmpty()) {
109     return false;  // XXX Why?
110   }
111   if (mDirection != aOther.mDirection) {
112     return false;
113   }
114 
115   // XXX Creating nsRanges are really expensive.  Why cannot we just check
116   //     the container and offsets??
117   IgnoredErrorResult ignoredError;
118   for (size_t i = 0; i < mArray.Length(); i++) {
119     RefPtr<nsRange> range = mArray[i]->GetRange();
120     if (!range) {
121       NS_WARNING("Failed to create a range from the range item");
122       return false;
123     }
124     RefPtr<nsRange> otherRange = aOther.mArray[i]->GetRange();
125     if (!otherRange) {
126       NS_WARNING("Failed to create a range from the other's range item");
127       return false;
128     }
129 
130     int16_t compResult = range->CompareBoundaryPoints(
131         Range_Binding::START_TO_START, *otherRange, ignoredError);
132     if (ignoredError.Failed()) {
133       NS_WARNING(
134           "nsRange::CompareBoundaryPoints(Range_Binding::START_TO_START) "
135           "failed");
136       return false;
137     }
138     if (compResult) {
139       return false;
140     }
141     compResult = range->CompareBoundaryPoints(Range_Binding::END_TO_END,
142                                               *otherRange, ignoredError);
143     if (ignoredError.Failed()) {
144       NS_WARNING(
145           "nsRange::CompareBoundaryPoints(Range_Binding::END_TO_END) failed");
146       return false;
147     }
148     if (compResult) {
149       return false;
150     }
151   }
152   // if we got here, they are equal
153   return true;
154 }
155 
Clear()156 void SelectionState::Clear() {
157   // free any items in the array
158   mArray.Clear();
159   mDirection = eDirNext;
160 }
161 
IsEmpty() const162 bool SelectionState::IsEmpty() const { return mArray.IsEmpty(); }
163 
164 /******************************************************************************
165  * mozilla::RangeUpdater
166  *
167  * Class for updating nsRanges in response to editor actions.
168  ******************************************************************************/
169 
RangeUpdater()170 RangeUpdater::RangeUpdater() : mLocked(false) {}
171 
RegisterRangeItem(RangeItem & aRangeItem)172 void RangeUpdater::RegisterRangeItem(RangeItem& aRangeItem) {
173   if (mArray.Contains(&aRangeItem)) {
174     NS_ERROR("tried to register an already registered range");
175     return;  // don't register it again.  It would get doubly adjusted.
176   }
177   mArray.AppendElement(&aRangeItem);
178 }
179 
DropRangeItem(RangeItem & aRangeItem)180 void RangeUpdater::DropRangeItem(RangeItem& aRangeItem) {
181   NS_WARNING_ASSERTION(
182       mArray.Contains(&aRangeItem),
183       "aRangeItem is not in the range, but tried to removed from it");
184   mArray.RemoveElement(&aRangeItem);
185 }
186 
RegisterSelectionState(SelectionState & aSelectionState)187 void RangeUpdater::RegisterSelectionState(SelectionState& aSelectionState) {
188   for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
189     if (NS_WARN_IF(!rangeItem)) {
190       continue;
191     }
192     RegisterRangeItem(*rangeItem);
193   }
194 }
195 
DropSelectionState(SelectionState & aSelectionState)196 void RangeUpdater::DropSelectionState(SelectionState& aSelectionState) {
197   for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
198     if (NS_WARN_IF(!rangeItem)) {
199       continue;
200     }
201     DropRangeItem(*rangeItem);
202   }
203 }
204 
205 // gravity methods:
206 
207 template <typename PT, typename CT>
SelAdjCreateNode(const EditorDOMPointBase<PT,CT> & aPoint)208 nsresult RangeUpdater::SelAdjCreateNode(
209     const EditorDOMPointBase<PT, CT>& aPoint) {
210   if (mLocked) {
211     // lock set by Will/DidReplaceParent, etc...
212     return NS_OK;
213   }
214   if (mArray.IsEmpty()) {
215     return NS_OK;
216   }
217 
218   if (NS_WARN_IF(!aPoint.IsSetAndValid())) {
219     return NS_ERROR_INVALID_ARG;
220   }
221 
222   for (RefPtr<RangeItem>& rangeItem : mArray) {
223     if (NS_WARN_IF(!rangeItem)) {
224       return NS_ERROR_FAILURE;
225     }
226     if (rangeItem->mStartContainer == aPoint.GetContainer() &&
227         rangeItem->mStartOffset > aPoint.Offset()) {
228       rangeItem->mStartOffset++;
229     }
230     if (rangeItem->mEndContainer == aPoint.GetContainer() &&
231         rangeItem->mEndOffset > aPoint.Offset()) {
232       rangeItem->mEndOffset++;
233     }
234   }
235   return NS_OK;
236 }
237 
238 template <typename PT, typename CT>
SelAdjInsertNode(const EditorDOMPointBase<PT,CT> & aPoint)239 nsresult RangeUpdater::SelAdjInsertNode(
240     const EditorDOMPointBase<PT, CT>& aPoint) {
241   nsresult rv = SelAdjCreateNode(aPoint);
242   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
243                        "RangeUpdater::SelAdjCreateNode() failed");
244   return rv;
245 }
246 
SelAdjDeleteNode(nsINode & aNodeToDelete)247 void RangeUpdater::SelAdjDeleteNode(nsINode& aNodeToDelete) {
248   if (mLocked) {
249     // lock set by Will/DidReplaceParent, etc...
250     return;
251   }
252 
253   if (mArray.IsEmpty()) {
254     return;
255   }
256 
257   EditorRawDOMPoint atNodeToDelete(&aNodeToDelete);
258   NS_ASSERTION(atNodeToDelete.IsSetAndValid(),
259                "aNodeToDelete must be an orphan node or this is called "
260                "during mutation");
261   // check for range endpoints that are after aNodeToDelete and in the same
262   // parent
263   for (RefPtr<RangeItem>& rangeItem : mArray) {
264     MOZ_ASSERT(rangeItem);
265 
266     if (rangeItem->mStartContainer == atNodeToDelete.GetContainer() &&
267         rangeItem->mStartOffset > atNodeToDelete.Offset()) {
268       rangeItem->mStartOffset--;
269     }
270     if (rangeItem->mEndContainer == atNodeToDelete.GetContainer() &&
271         rangeItem->mEndOffset > atNodeToDelete.Offset()) {
272       rangeItem->mEndOffset--;
273     }
274 
275     // check for range endpoints that are in aNodeToDelete
276     if (rangeItem->mStartContainer == &aNodeToDelete) {
277       rangeItem->mStartContainer = atNodeToDelete.GetContainer();
278       rangeItem->mStartOffset = atNodeToDelete.Offset();
279     }
280     if (rangeItem->mEndContainer == &aNodeToDelete) {
281       rangeItem->mEndContainer = atNodeToDelete.GetContainer();
282       rangeItem->mEndOffset = atNodeToDelete.Offset();
283     }
284 
285     // check for range endpoints that are in descendants of aNodeToDelete
286     bool updateEndBoundaryToo = false;
287     if (EditorUtils::IsDescendantOf(*rangeItem->mStartContainer,
288                                     aNodeToDelete)) {
289       updateEndBoundaryToo =
290           rangeItem->mStartContainer == rangeItem->mEndContainer;
291       rangeItem->mStartContainer = atNodeToDelete.GetContainer();
292       rangeItem->mStartOffset = atNodeToDelete.Offset();
293     }
294 
295     // avoid having to call IsDescendantOf() for common case of range startnode
296     // == range endnode.
297     if (updateEndBoundaryToo ||
298         EditorUtils::IsDescendantOf(*rangeItem->mEndContainer, aNodeToDelete)) {
299       rangeItem->mEndContainer = atNodeToDelete.GetContainer();
300       rangeItem->mEndOffset = atNodeToDelete.Offset();
301     }
302   }
303 }
304 
SelAdjSplitNode(nsIContent & aRightNode,nsIContent & aNewLeftNode)305 nsresult RangeUpdater::SelAdjSplitNode(nsIContent& aRightNode,
306                                        nsIContent& aNewLeftNode) {
307   if (mLocked) {
308     // lock set by Will/DidReplaceParent, etc...
309     return NS_OK;
310   }
311 
312   if (mArray.IsEmpty()) {
313     return NS_OK;
314   }
315 
316   EditorRawDOMPoint atLeftNode(&aNewLeftNode);
317   nsresult rv = SelAdjInsertNode(atLeftNode);
318   if (NS_FAILED(rv)) {
319     NS_WARNING("RangeUpdater::SelAdjInsertNode() failed");
320     return rv;
321   }
322 
323   // If point in the ranges is in left node, change its container to the left
324   // node.  If point in the ranges is in right node, subtract numbers of
325   // children moved to left node from the offset.
326   uint32_t lengthOfLeftNode = aNewLeftNode.Length();
327   for (RefPtr<RangeItem>& rangeItem : mArray) {
328     if (NS_WARN_IF(!rangeItem)) {
329       return NS_ERROR_FAILURE;
330     }
331 
332     if (rangeItem->mStartContainer == &aRightNode) {
333       if (rangeItem->mStartOffset > lengthOfLeftNode) {
334         rangeItem->mStartOffset -= lengthOfLeftNode;
335       } else {
336         rangeItem->mStartContainer = &aNewLeftNode;
337       }
338     }
339     if (rangeItem->mEndContainer == &aRightNode) {
340       if (rangeItem->mEndOffset > lengthOfLeftNode) {
341         rangeItem->mEndOffset -= lengthOfLeftNode;
342       } else {
343         rangeItem->mEndContainer = &aNewLeftNode;
344       }
345     }
346   }
347   return NS_OK;
348 }
349 
SelAdjJoinNodes(nsINode & aLeftNode,nsINode & aRightNode,nsINode & aParent,uint32_t aOffset,uint32_t aOldLeftNodeLength)350 nsresult RangeUpdater::SelAdjJoinNodes(nsINode& aLeftNode, nsINode& aRightNode,
351                                        nsINode& aParent, uint32_t aOffset,
352                                        uint32_t aOldLeftNodeLength) {
353   if (mLocked) {
354     // lock set by Will/DidReplaceParent, etc...
355     return NS_OK;
356   }
357 
358   if (mArray.IsEmpty()) {
359     return NS_OK;
360   }
361 
362   for (RefPtr<RangeItem>& rangeItem : mArray) {
363     if (NS_WARN_IF(!rangeItem)) {
364       return NS_ERROR_FAILURE;
365     }
366 
367     if (rangeItem->mStartContainer == &aParent) {
368       // adjust start point in aParent
369       if (rangeItem->mStartOffset > aOffset) {
370         rangeItem->mStartOffset--;
371       } else if (rangeItem->mStartOffset == aOffset) {
372         // join keeps right hand node
373         rangeItem->mStartContainer = &aRightNode;
374         rangeItem->mStartOffset = aOldLeftNodeLength;
375       }
376     } else if (rangeItem->mStartContainer == &aRightNode) {
377       // adjust start point in aRightNode
378       rangeItem->mStartOffset += aOldLeftNodeLength;
379     } else if (rangeItem->mStartContainer == &aLeftNode) {
380       // adjust start point in aLeftNode
381       rangeItem->mStartContainer = &aRightNode;
382     }
383 
384     if (rangeItem->mEndContainer == &aParent) {
385       // adjust end point in aParent
386       if (rangeItem->mEndOffset > aOffset) {
387         rangeItem->mEndOffset--;
388       } else if (rangeItem->mEndOffset == aOffset) {
389         // join keeps right hand node
390         rangeItem->mEndContainer = &aRightNode;
391         rangeItem->mEndOffset = aOldLeftNodeLength;
392       }
393     } else if (rangeItem->mEndContainer == &aRightNode) {
394       // adjust end point in aRightNode
395       rangeItem->mEndOffset += aOldLeftNodeLength;
396     } else if (rangeItem->mEndContainer == &aLeftNode) {
397       // adjust end point in aLeftNode
398       rangeItem->mEndContainer = &aRightNode;
399     }
400   }
401 
402   return NS_OK;
403 }
404 
SelAdjReplaceText(const Text & aTextNode,uint32_t aOffset,uint32_t aReplacedLength,uint32_t aInsertedLength)405 void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, uint32_t aOffset,
406                                      uint32_t aReplacedLength,
407                                      uint32_t aInsertedLength) {
408   if (mLocked) {
409     // lock set by Will/DidReplaceParent, etc...
410     return;
411   }
412 
413   // First, adjust selection for insertion because when offset is in the
414   // replaced range, it's adjusted to aOffset and never modified by the
415   // insertion if we adjust selection for deletion first.
416   SelAdjInsertText(aTextNode, aOffset, aInsertedLength);
417 
418   // Then, adjust selection for deletion.
419   SelAdjDeleteText(aTextNode, aOffset, aReplacedLength);
420 }
421 
SelAdjInsertText(const Text & aTextNode,uint32_t aOffset,uint32_t aInsertedLength)422 void RangeUpdater::SelAdjInsertText(const Text& aTextNode, uint32_t aOffset,
423                                     uint32_t aInsertedLength) {
424   if (mLocked) {
425     // lock set by Will/DidReplaceParent, etc...
426     return;
427   }
428 
429   for (RefPtr<RangeItem>& rangeItem : mArray) {
430     MOZ_ASSERT(rangeItem);
431 
432     if (rangeItem->mStartContainer == &aTextNode &&
433         rangeItem->mStartOffset > aOffset) {
434       rangeItem->mStartOffset += aInsertedLength;
435     }
436     if (rangeItem->mEndContainer == &aTextNode &&
437         rangeItem->mEndOffset > aOffset) {
438       rangeItem->mEndOffset += aInsertedLength;
439     }
440   }
441 }
442 
SelAdjDeleteText(const Text & aTextNode,uint32_t aOffset,uint32_t aDeletedLength)443 void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, uint32_t aOffset,
444                                     uint32_t aDeletedLength) {
445   if (mLocked) {
446     // lock set by Will/DidReplaceParent, etc...
447     return;
448   }
449 
450   for (RefPtr<RangeItem>& rangeItem : mArray) {
451     MOZ_ASSERT(rangeItem);
452 
453     if (rangeItem->mStartContainer == &aTextNode &&
454         rangeItem->mStartOffset > aOffset) {
455       if (rangeItem->mStartOffset >= aDeletedLength) {
456         rangeItem->mStartOffset -= aDeletedLength;
457       } else {
458         rangeItem->mStartOffset = 0;
459       }
460     }
461     if (rangeItem->mEndContainer == &aTextNode &&
462         rangeItem->mEndOffset > aOffset) {
463       if (rangeItem->mEndOffset >= aDeletedLength) {
464         rangeItem->mEndOffset -= aDeletedLength;
465       } else {
466         rangeItem->mEndOffset = 0;
467       }
468     }
469   }
470 }
471 
DidReplaceContainer(const Element & aRemovedElement,Element & aInsertedElement)472 void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement,
473                                        Element& aInsertedElement) {
474   if (NS_WARN_IF(!mLocked)) {
475     return;
476   }
477   mLocked = false;
478 
479   for (RefPtr<RangeItem>& rangeItem : mArray) {
480     if (NS_WARN_IF(!rangeItem)) {
481       return;
482     }
483 
484     if (rangeItem->mStartContainer == &aRemovedElement) {
485       rangeItem->mStartContainer = &aInsertedElement;
486     }
487     if (rangeItem->mEndContainer == &aRemovedElement) {
488       rangeItem->mEndContainer = &aInsertedElement;
489     }
490   }
491 }
492 
DidRemoveContainer(const Element & aRemovedElement,nsINode & aRemovedElementContainerNode,uint32_t aOldOffsetOfRemovedElement,uint32_t aOldChildCountOfRemovedElement)493 void RangeUpdater::DidRemoveContainer(const Element& aRemovedElement,
494                                       nsINode& aRemovedElementContainerNode,
495                                       uint32_t aOldOffsetOfRemovedElement,
496                                       uint32_t aOldChildCountOfRemovedElement) {
497   if (NS_WARN_IF(!mLocked)) {
498     return;
499   }
500   mLocked = false;
501 
502   for (RefPtr<RangeItem>& rangeItem : mArray) {
503     if (NS_WARN_IF(!rangeItem)) {
504       return;
505     }
506 
507     if (rangeItem->mStartContainer == &aRemovedElement) {
508       rangeItem->mStartContainer = &aRemovedElementContainerNode;
509       rangeItem->mStartOffset += aOldOffsetOfRemovedElement;
510     } else if (rangeItem->mStartContainer == &aRemovedElementContainerNode &&
511                rangeItem->mStartOffset > aOldOffsetOfRemovedElement) {
512       rangeItem->mStartOffset += aOldChildCountOfRemovedElement - 1;
513     }
514 
515     if (rangeItem->mEndContainer == &aRemovedElement) {
516       rangeItem->mEndContainer = &aRemovedElementContainerNode;
517       rangeItem->mEndOffset += aOldOffsetOfRemovedElement;
518     } else if (rangeItem->mEndContainer == &aRemovedElementContainerNode &&
519                rangeItem->mEndOffset > aOldOffsetOfRemovedElement) {
520       rangeItem->mEndOffset += aOldChildCountOfRemovedElement - 1;
521     }
522   }
523 }
524 
DidMoveNode(const nsINode & aOldParent,uint32_t aOldOffset,const nsINode & aNewParent,uint32_t aNewOffset)525 void RangeUpdater::DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
526                                const nsINode& aNewParent, uint32_t aNewOffset) {
527   if (NS_WARN_IF(!mLocked)) {
528     return;
529   }
530   mLocked = false;
531 
532   for (RefPtr<RangeItem>& rangeItem : mArray) {
533     if (NS_WARN_IF(!rangeItem)) {
534       return;
535     }
536 
537     // like a delete in aOldParent
538     if (rangeItem->mStartContainer == &aOldParent &&
539         rangeItem->mStartOffset > aOldOffset) {
540       rangeItem->mStartOffset--;
541     }
542     if (rangeItem->mEndContainer == &aOldParent &&
543         rangeItem->mEndOffset > aOldOffset) {
544       rangeItem->mEndOffset--;
545     }
546 
547     // and like an insert in aNewParent
548     if (rangeItem->mStartContainer == &aNewParent &&
549         rangeItem->mStartOffset > aNewOffset) {
550       rangeItem->mStartOffset++;
551     }
552     if (rangeItem->mEndContainer == &aNewParent &&
553         rangeItem->mEndOffset > aNewOffset) {
554       rangeItem->mEndOffset++;
555     }
556   }
557 }
558 
559 /******************************************************************************
560  * mozilla::RangeItem
561  *
562  * Helper struct for SelectionState.  This stores range endpoints.
563  ******************************************************************************/
564 
NS_IMPL_CYCLE_COLLECTION(RangeItem,mStartContainer,mEndContainer)565 NS_IMPL_CYCLE_COLLECTION(RangeItem, mStartContainer, mEndContainer)
566 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(RangeItem, AddRef)
567 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(RangeItem, Release)
568 
569 void RangeItem::StoreRange(const nsRange& aRange) {
570   mStartContainer = aRange.GetStartContainer();
571   mStartOffset = aRange.StartOffset();
572   mEndContainer = aRange.GetEndContainer();
573   mEndOffset = aRange.EndOffset();
574 }
575 
GetRange()576 already_AddRefed<nsRange> RangeItem::GetRange() {
577   RefPtr<nsRange> range = nsRange::Create(
578       mStartContainer, mStartOffset, mEndContainer, mEndOffset, IgnoreErrors());
579   NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
580   return range.forget();
581 }
582 
583 }  // namespace mozilla
584