/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Accessible2.h" #include "RemoteAccessible.h" #include "ia2AccessibleRelation.h" #include "ia2AccessibleValue.h" #include "IGeckoCustom.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "DocAccessible.h" #include "mozilla/a11y/DocManager.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/BrowserParent.h" #include "mozilla/Unused.h" #include "mozilla/a11y/Platform.h" #include "Relation.h" #include "RelationType.h" #include "mozilla/a11y/Role.h" #include "mozilla/StaticPrefs_accessibility.h" #include static const VARIANT kChildIdSelf = {{{VT_I4}}}; namespace mozilla { namespace a11y { bool RemoteAccessible::GetCOMInterface(void** aOutAccessible) const { if (!aOutAccessible) { return false; } // This should never be called if the cache is enabled. We can't get a COM // proxy from the content process in that case. Instead, the code below would // return an MsaaAccessible from our process which would end up calling // methods here in RemoteAccessible, causing infinite recursion. MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup()); if (!mCOMProxy && mSafeToRecurse) { RemoteAccessible* thisPtr = const_cast(this); // See if we can lazily obtain a COM proxy MsaaAccessible* msaa = MsaaAccessible::GetFrom(thisPtr); bool isDefunct = false; // NB: Don't pass CHILDID_SELF here, use the absolute MSAA ID. Otherwise // GetIAccessibleFor will recurse into this function and we will just // overflow the stack. VARIANT realId = {{{VT_I4}}}; realId.ulVal = msaa->GetExistingID(); MOZ_DIAGNOSTIC_ASSERT(realId.ulVal != CHILDID_SELF); thisPtr->mCOMProxy = msaa->GetIAccessibleFor(realId, &isDefunct); } RefPtr addRefed = mCOMProxy; addRefed.forget(aOutAccessible); return !!mCOMProxy; } /** * Specializations of this template map an IAccessible type to its IID */ template struct InterfaceIID {}; template <> struct InterfaceIID { static REFIID Value() { return IID_IAccessibleValue; } }; template <> struct InterfaceIID { static REFIID Value() { return IID_IAccessibleText; } }; template <> struct InterfaceIID { static REFIID Value() { return IID_IAccessibleHyperlink; } }; template <> struct InterfaceIID { static REFIID Value() { return IID_IGeckoCustom; } }; template <> struct InterfaceIID { static REFIID Value() { return IID_IAccessible2_2; } }; /** * Get the COM proxy for this proxy accessible and QueryInterface it with the * correct IID */ template static already_AddRefed QueryInterface( const RemoteAccessible* aProxy) { RefPtr acc; if (!aProxy->GetCOMInterface((void**)getter_AddRefs(acc))) { return nullptr; } RefPtr acc2; if (FAILED(acc->QueryInterface(InterfaceIID::Value(), (void**)getter_AddRefs(acc2)))) { return nullptr; } return acc2.forget(); } static Maybe GetIdFor(DocAccessibleParent* aDoc, IUnknown* aCOMProxy) { RefPtr custom; if (FAILED(aCOMProxy->QueryInterface(IID_IGeckoCustom, (void**)getter_AddRefs(custom)))) { return Nothing(); } uint64_t id; if (FAILED(custom->get_ID(&id))) { return Nothing(); } return Some(id); } static RemoteAccessible* GetProxyFor(DocAccessibleParent* aDoc, IUnknown* aCOMProxy) { if (auto id = GetIdFor(aDoc, aCOMProxy)) { return aDoc->GetAccessible(*id); } return nullptr; } ENameValueFlag RemoteAccessible::Name(nsString& aName) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::Name(aName); } /* The return values here exist only to match behvaiour required * by the header declaration of this function. On Mac, we'd like * to return the associated ENameValueFlag, but we don't have * access to that here, so we return a dummy eNameOK value instead. */ aName.Truncate(); RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return eNameOK; } BSTR result; HRESULT hr = acc->get_accName(kChildIdSelf, &result); _bstr_t resultWrap(result, false); if (FAILED(hr)) { return eNameOK; } aName = (wchar_t*)resultWrap; if (!resultWrap) { aName.SetIsVoid(true); } return eNameOK; } void RemoteAccessible::Value(nsString& aValue) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { RemoteAccessibleBase::Value(aValue); return; } aValue.Truncate(); RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return; } BSTR result; HRESULT hr = acc->get_accValue(kChildIdSelf, &result); _bstr_t resultWrap(result, false); if (FAILED(hr)) { return; } aValue = (wchar_t*)resultWrap; } double RemoteAccessible::Step() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::Step(); } RefPtr custom = QueryInterface(this); if (!custom) { return 0; } double increment; HRESULT hr = custom->get_minimumIncrement(&increment); if (FAILED(hr)) { return 0; } return increment; } void RemoteAccessible::Description(nsString& aDesc) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::Description(aDesc); } aDesc.Truncate(); RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return; } BSTR result; HRESULT hr = acc->get_accDescription(kChildIdSelf, &result); _bstr_t resultWrap(result, false); if (FAILED(hr)) { return; } aDesc = (wchar_t*)resultWrap; } uint64_t RemoteAccessible::State() { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::State(); } RefPtr custom = QueryInterface(this); if (!custom) { return 0; } uint64_t state; HRESULT hr = custom->get_mozState(&state); if (FAILED(hr)) { return 0; } return state; } LayoutDeviceIntRect RemoteAccessible::Bounds() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::Bounds(); } LayoutDeviceIntRect rect; RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return rect; } long left; long top; long width; long height; HRESULT hr = acc->accLocation(&left, &top, &width, &height, kChildIdSelf); if (FAILED(hr)) { return rect; } rect.SetRect(left, top, width, height); return rect; } nsIntRect RemoteAccessible::BoundsInCSSPixels() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::BoundsInCSSPixels(); } RefPtr custom = QueryInterface(this); if (!custom) { return nsIntRect(); } nsIntRect rect; Unused << custom->get_boundsInCSSPixels(&rect.x, &rect.y, &rect.width, &rect.height); return rect; } void RemoteAccessible::Language(nsString& aLocale) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. aLocale.Truncate(); return; } aLocale.Truncate(); RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return; } RefPtr acc2; if (FAILED(acc->QueryInterface(IID_IAccessible2, (void**)getter_AddRefs(acc2)))) { return; } IA2Locale locale; HRESULT hr = acc2->get_locale(&locale); _bstr_t langWrap(locale.language, false); _bstr_t countryWrap(locale.country, false); _bstr_t variantWrap(locale.variant, false); if (FAILED(hr)) { return; } // The remaining code should essentially be the inverse of the // ia2Accessible::get_locale conversion to IA2Locale. if (!!variantWrap) { aLocale = (wchar_t*)variantWrap; return; } if (!!langWrap) { aLocale = (wchar_t*)langWrap; if (!!countryWrap) { aLocale += L"-"; aLocale += (wchar_t*)countryWrap; } } } static bool IsEscapedChar(const wchar_t c) { return c == L'\\' || c == L':' || c == ',' || c == '=' || c == ';'; } // XXX: This creates an all-strings AccAttributes, this isn't ideal // but an OK temporary stop-gap before IPC goes full IPDL. static bool ConvertBSTRAttributesToAccAttributes( const nsAString& aStr, RefPtr& aAttrs) { enum { eName = 0, eValue = 1, eNumStates } state; nsString tokens[eNumStates]; auto itr = aStr.BeginReading(), end = aStr.EndReading(); state = eName; while (itr != end) { switch (*itr) { case L'\\': // Skip the backslash so that we're looking at the escaped char ++itr; if (itr == end || !IsEscapedChar(*itr)) { // Invalid state return false; } break; case L':': if (state != eName) { // Bad, should be looking at name return false; } state = eValue; ++itr; continue; case L';': { if (state != eValue) { // Bad, should be looking at value return false; } state = eName; RefPtr nameAtom = NS_Atomize(tokens[eName]); aAttrs->SetAttribute(nameAtom, std::move(tokens[eValue])); tokens[eName].Truncate(); ++itr; continue; } default: break; } tokens[state] += *itr; ++itr; } return true; } already_AddRefed RemoteAccessible::Attributes() { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::Attributes(); } RefPtr attrsObj = new AccAttributes(); RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return attrsObj.forget(); } RefPtr acc2; if (FAILED(acc->QueryInterface(IID_IAccessible2, (void**)getter_AddRefs(acc2)))) { return attrsObj.forget(); } BSTR attrs; HRESULT hr = acc2->get_attributes(&attrs); _bstr_t attrsWrap(attrs, false); if (FAILED(hr)) { return attrsObj.forget(); } ConvertBSTRAttributesToAccAttributes( nsDependentString((wchar_t*)attrs, attrsWrap.length()), attrsObj); return attrsObj.forget(); } Relation RemoteAccessible::RelationByType(RelationType aType) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::RelationByType(aType); } RefPtr acc = QueryInterface(this); if (!acc) { return Relation(); } _bstr_t relationType; for (uint32_t idx = 0; idx < ArrayLength(sRelationTypePairs); idx++) { if (aType == sRelationTypePairs[idx].first) { relationType = sRelationTypePairs[idx].second; break; } } if (!relationType) { return Relation(); } IUnknown** targets; long nTargets = 0; HRESULT hr = acc->get_relationTargetsOfType(relationType, 0, &targets, &nTargets); if (FAILED(hr)) { return Relation(); } nsTArray ids; for (long idx = 0; idx < nTargets; idx++) { IUnknown* target = targets[idx]; if (auto id = GetIdFor(Document(), target)) { ids.AppendElement(*id); } target->Release(); } CoTaskMemFree(targets); return Relation(new RemoteAccIterator(std::move(ids), Document())); } double RemoteAccessible::CurValue() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::CurValue(); } RefPtr acc = QueryInterface(this); if (!acc) { return UnspecifiedNaN(); } VARIANT currentValue; HRESULT hr = acc->get_currentValue(¤tValue); if (FAILED(hr) || currentValue.vt != VT_R8) { return UnspecifiedNaN(); } return currentValue.dblVal; } bool RemoteAccessible::SetCurValue(double aValue) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return false; } RefPtr acc = QueryInterface(this); if (!acc) { return false; } VARIANT currentValue; VariantInit(¤tValue); currentValue.vt = VT_R8; currentValue.dblVal = aValue; HRESULT hr = acc->setCurrentValue(currentValue); return SUCCEEDED(hr); } double RemoteAccessible::MinValue() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::MinValue(); } RefPtr acc = QueryInterface(this); if (!acc) { return UnspecifiedNaN(); } VARIANT minimumValue; HRESULT hr = acc->get_minimumValue(&minimumValue); if (FAILED(hr) || minimumValue.vt != VT_R8) { return UnspecifiedNaN(); } return minimumValue.dblVal; } double RemoteAccessible::MaxValue() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::MaxValue(); } RefPtr acc = QueryInterface(this); if (!acc) { return UnspecifiedNaN(); } VARIANT maximumValue; HRESULT hr = acc->get_maximumValue(&maximumValue); if (FAILED(hr) || maximumValue.vt != VT_R8) { return UnspecifiedNaN(); } return maximumValue.dblVal; } static IA2TextBoundaryType GetIA2TextBoundary( AccessibleTextBoundary aGeckoBoundaryType) { switch (aGeckoBoundaryType) { case nsIAccessibleText::BOUNDARY_CHAR: return IA2_TEXT_BOUNDARY_CHAR; case nsIAccessibleText::BOUNDARY_WORD_START: return IA2_TEXT_BOUNDARY_WORD; case nsIAccessibleText::BOUNDARY_LINE_START: return IA2_TEXT_BOUNDARY_LINE; case nsIAccessibleText::BOUNDARY_PARAGRAPH: return IA2_TEXT_BOUNDARY_PARAGRAPH; default: MOZ_CRASH(); } } int32_t RemoteAccessible::OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordinateType) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::OffsetAtPoint( aX, aY, aCoordinateType); } RefPtr acc = QueryInterface(this); if (!acc) { return -1; } IA2CoordinateType coordType; if (aCoordinateType == nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) { coordType = IA2_COORDTYPE_SCREEN_RELATIVE; } else if (aCoordinateType == nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE) { coordType = IA2_COORDTYPE_PARENT_RELATIVE; } else { MOZ_CRASH("unsupported coord type"); } long offset; HRESULT hr = acc->get_offsetAtPoint( static_cast(aX), static_cast(aY), coordType, &offset); if (FAILED(hr)) { return -1; } return static_cast(offset); } void RemoteAccessible::TextSubstring(int32_t aStartOffset, int32_t aEndOffset, nsAString& aText) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::TextSubstring( aStartOffset, aEndOffset, aText); } RefPtr acc = QueryInterface(this); if (!acc) { return; } BSTR result; HRESULT hr = acc->get_text(static_cast(aStartOffset), static_cast(aEndOffset), &result); if (FAILED(hr)) { return; } _bstr_t resultWrap(result, false); aText = (wchar_t*)result; } void RemoteAccessible::TextBeforeOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::TextBeforeOffset( aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); } RefPtr acc = QueryInterface(this); if (!acc) { return; } BSTR result; long start, end; HRESULT hr = acc->get_textBeforeOffset( aOffset, GetIA2TextBoundary(aBoundaryType), &start, &end, &result); if (FAILED(hr)) { return; } _bstr_t resultWrap(result, false); *aStartOffset = start; *aEndOffset = end; aText = (wchar_t*)result; } void RemoteAccessible::TextAfterOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::TextAfterOffset( aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); } RefPtr acc = QueryInterface(this); if (!acc) { return; } BSTR result; long start, end; HRESULT hr = acc->get_textAfterOffset( aOffset, GetIA2TextBoundary(aBoundaryType), &start, &end, &result); if (FAILED(hr)) { return; } _bstr_t resultWrap(result, false); aText = (wchar_t*)result; *aStartOffset = start; *aEndOffset = end; } void RemoteAccessible::TextAtOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::TextAtOffset( aOffset, aBoundaryType, aStartOffset, aEndOffset, aText); } RefPtr acc = QueryInterface(this); if (!acc) { return; } BSTR result; long start, end; HRESULT hr = acc->get_textAtOffset(aOffset, GetIA2TextBoundary(aBoundaryType), &start, &end, &result); if (FAILED(hr)) { return; } _bstr_t resultWrap(result, false); aText = (wchar_t*)result; *aStartOffset = start; *aEndOffset = end; } bool RemoteAccessible::AddToSelection(int32_t aStartOffset, int32_t aEndOffset) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return false; } RefPtr acc = QueryInterface(this); if (!acc) { return false; } return SUCCEEDED(acc->addSelection(static_cast(aStartOffset), static_cast(aEndOffset))); } bool RemoteAccessible::RemoveFromSelection(int32_t aSelectionNum) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return false; } RefPtr acc = QueryInterface(this); if (!acc) { return false; } return SUCCEEDED(acc->removeSelection(static_cast(aSelectionNum))); } int32_t RemoteAccessible::CaretOffset() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::CaretOffset(); } RefPtr acc = QueryInterface(this); if (!acc) { return -1; } long offset; HRESULT hr = acc->get_caretOffset(&offset); if (FAILED(hr)) { return -1; } return static_cast(offset); } void RemoteAccessible::SetCaretOffset(int32_t aOffset) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::SetCaretOffset(aOffset); } RefPtr acc = QueryInterface(this); if (!acc) { return; } acc->setCaretOffset(static_cast(aOffset)); } /** * aScrollType should be one of the nsIAccessiblescrollType constants. */ void RemoteAccessible::ScrollSubstringTo(int32_t aStartOffset, int32_t aEndOffset, uint32_t aScrollType) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return; } RefPtr acc = QueryInterface(this); if (!acc) { return; } acc->scrollSubstringTo(static_cast(aStartOffset), static_cast(aEndOffset), static_cast(aScrollType)); } /** * aCoordinateType is one of the nsIAccessibleCoordinateType constants. */ void RemoteAccessible::ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset, uint32_t aCoordinateType, int32_t aX, int32_t aY) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return; } RefPtr acc = QueryInterface(this); if (!acc) { return; } IA2CoordinateType coordType; if (aCoordinateType == nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) { coordType = IA2_COORDTYPE_SCREEN_RELATIVE; } else if (aCoordinateType == nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE) { coordType = IA2_COORDTYPE_PARENT_RELATIVE; } else { MOZ_CRASH("unsupported coord type"); } acc->scrollSubstringToPoint(static_cast(aStartOffset), static_cast(aEndOffset), coordType, static_cast(aX), static_cast(aY)); } bool RemoteAccessible::IsLinkValid() { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return false; } RefPtr acc = QueryInterface(this); if (!acc) { return false; } boolean valid; if (FAILED(acc->get_valid(&valid))) { return false; } return valid; } uint32_t RemoteAccessible::AnchorCount(bool* aOk) { *aOk = false; if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return 0; } RefPtr custom = QueryInterface(this); if (!custom) { return 0; } long count; if (FAILED(custom->get_anchorCount(&count))) { return 0; } *aOk = true; return count; } RemoteAccessible* RemoteAccessible::AnchorAt(uint32_t aIdx) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { // Not yet supported by the cache. return nullptr; } RefPtr link = QueryInterface(this); if (!link) { return nullptr; } VARIANT anchor; if (FAILED(link->get_anchor(aIdx, &anchor))) { return nullptr; } MOZ_ASSERT(anchor.vt == VT_UNKNOWN); RemoteAccessible* proxyAnchor = GetProxyFor(Document(), anchor.punkVal); anchor.punkVal->Release(); return proxyAnchor; } void RemoteAccessible::DOMNodeID(nsString& aID) const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::DOMNodeID(aID); } aID.Truncate(); RefPtr custom = QueryInterface(this); if (!custom) { return; } BSTR result; HRESULT hr = custom->get_DOMNodeID(&result); _bstr_t resultWrap(result, false); if (FAILED(hr)) { return; } aID = (wchar_t*)resultWrap; } void RemoteAccessible::TakeFocus() const { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::TakeFocus(); } RefPtr acc; if (!GetCOMInterface((void**)getter_AddRefs(acc))) { return; } acc->accSelect(SELFLAG_TAKEFOCUS, kChildIdSelf); } Accessible* RemoteAccessible::ChildAtPoint( int32_t aX, int32_t aY, Accessible::EWhichChildAtPoint aWhichChild) { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::ChildAtPoint(aX, aY, aWhichChild); } RefPtr target = QueryInterface(this); if (!target) { return nullptr; } DocAccessibleParent* doc = Document(); RemoteAccessible* proxy = this; // accHitTest only does direct children, but we might want the deepest child. for (;;) { VARIANT childVar; if (FAILED(target->accHitTest(aX, aY, &childVar)) || childVar.vt == VT_EMPTY) { return nullptr; } if (childVar.vt == VT_I4 && childVar.lVal == CHILDID_SELF) { break; } MOZ_ASSERT(childVar.vt == VT_DISPATCH && childVar.pdispVal); target = nullptr; childVar.pdispVal->QueryInterface(IID_IAccessible2_2, getter_AddRefs(target)); childVar.pdispVal->Release(); if (!target) { return nullptr; } // We can't always use GetProxyFor because it can't cross document // boundaries. if (proxy->ChildCount() == 1) { proxy = proxy->RemoteChildAt(0); if (proxy->IsDoc()) { // We're crossing into a child document. doc = proxy->AsDoc(); } } else { proxy = GetProxyFor(doc, target); } if (aWhichChild == Accessible::EWhichChildAtPoint::DirectChild) { break; } } return proxy; } GroupPos RemoteAccessible::GroupPosition() { if (StaticPrefs::accessibility_cache_enabled_AtStartup()) { return RemoteAccessibleBase::GroupPosition(); } // This is only supported when cache is enabled. return GroupPos(); } } // namespace a11y } // namespace mozilla