tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Platform.mm (13576B)


      1 /* clang-format off */
      2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      3 /* clang-format on */
      4 /* This Source Code Form is subject to the terms of the Mozilla Public
      5 * License, v. 2.0. If a copy of the MPL was not distributed with this
      6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      7 
      8 #import <Cocoa/Cocoa.h>
      9 
     10 #import "MOXTextMarkerDelegate.h"
     11 
     12 #include "Platform.h"
     13 #include "RemoteAccessible.h"
     14 #include "DocAccessibleParent.h"
     15 #include "mozTableAccessible.h"
     16 #include "mozTextAccessible.h"
     17 #include "MOXOuterDoc.h"
     18 #include "MOXWebAreaAccessible.h"
     19 #include "nsAccUtils.h"
     20 #include "TextRange.h"
     21 
     22 #include "nsAppShell.h"
     23 #include "nsCocoaUtils.h"
     24 #include "mozilla/EnumSet.h"
     25 #include "mozilla/glean/AccessibleMetrics.h"
     26 
     27 // Available from 10.13 onwards; test availability at runtime before using
     28 @interface NSWorkspace (AvailableSinceHighSierra)
     29 @property(readonly) BOOL isVoiceOverEnabled;
     30 @property(readonly) BOOL isSwitchControlEnabled;
     31 @end
     32 
     33 namespace mozilla {
     34 namespace a11y {
     35 
     36 // Mac a11y whitelisting
     37 static bool sA11yShouldBeEnabled = false;
     38 
     39 bool ShouldA11yBeEnabled() {
     40  EPlatformDisabledState disabledState = PlatformDisabledState();
     41  return (disabledState == ePlatformIsForceEnabled) ||
     42         ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled);
     43 }
     44 
     45 void PlatformInit() {}
     46 
     47 void PlatformShutdown() {}
     48 
     49 void ProxyCreated(RemoteAccessible* aProxy) {
     50  if (aProxy->Role() == roles::WHITESPACE) {
     51    // We don't create a native object if we're child of a "flat" accessible;
     52    // for example, on OS X buttons shouldn't have any children, because that
     53    // makes the OS confused. We also don't create accessibles for <br>
     54    // (whitespace) elements.
     55    return;
     56  }
     57 
     58  // Pass in dummy state for now as retrieving proxy state requires IPC.
     59  // Note that we can use RemoteAccessible::IsTable* functions here because they
     60  // do not use IPC calls but that might change after bug 1210477.
     61  Class type;
     62  if (aProxy->IsTable()) {
     63    type = [mozTableAccessible class];
     64  } else if (aProxy->IsTableRow()) {
     65    type = [mozTableRowAccessible class];
     66  } else if (aProxy->IsTableCell()) {
     67    type = [mozTableCellAccessible class];
     68  } else if (aProxy->IsDoc()) {
     69    type = [MOXWebAreaAccessible class];
     70  } else if (aProxy->IsOuterDoc()) {
     71    type = [MOXOuterDoc class];
     72  } else {
     73    type = GetTypeFromRole(aProxy->Role());
     74  }
     75 
     76  mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy];
     77  aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper));
     78 }
     79 
     80 void ProxyDestroyed(RemoteAccessible* aProxy) {
     81  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
     82  [wrapper expire];
     83  [wrapper release];
     84  aProxy->SetWrapper(0);
     85 
     86  if (aProxy->IsDoc()) {
     87    [MOXTextMarkerDelegate destroyForDoc:aProxy];
     88  }
     89 }
     90 
     91 void PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
     92  // Ignore event that we don't escape below, they aren't yet supported.
     93  if (aEventType != nsIAccessibleEvent::EVENT_ALERT &&
     94      aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
     95      aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
     96      aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
     97      aEventType != nsIAccessibleEvent::EVENT_REORDER &&
     98      aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
     99      aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
    100      aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED &&
    101      aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
    102      aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED &&
    103      aEventType != nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED) {
    104    return;
    105  }
    106 
    107  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
    108  if (wrapper) {
    109    [wrapper handleAccessibleEvent:aEventType];
    110  }
    111 }
    112 
    113 void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
    114                              bool aEnabled) {
    115  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
    116  if (wrapper) {
    117    [wrapper stateChanged:aState isEnabled:aEnabled];
    118  }
    119 }
    120 
    121 void PlatformFocusEvent(Accessible* aTarget) {
    122  if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
    123    [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS];
    124  }
    125 }
    126 
    127 void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
    128                            bool aIsSelectionCollapsed, int32_t aGranularity,
    129                            bool aFromUser) {
    130  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
    131  MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
    132      getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
    133  [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity];
    134  if (aIsSelectionCollapsed) {
    135    // If selection is collapsed, invalidate selection.
    136    [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset];
    137  }
    138 
    139  if (wrapper) {
    140    if (mozAccessible* editable = [wrapper moxEditableAncestor]) {
    141      [editable
    142          handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
    143    } else {
    144      [wrapper
    145          handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
    146    }
    147  }
    148 }
    149 
    150 void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
    151                             int32_t aStart, uint32_t aLen, bool aIsInsert,
    152                             bool aFromUser) {
    153  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
    154  if (wrapper) {
    155    if (mozAccessible* editable = [wrapper moxEditableAncestor]) {
    156      [editable handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
    157                                       inserted:aIsInsert
    158                                    inContainer:aTarget
    159                                             at:aStart];
    160    } else {
    161      [wrapper maybePostValidationErrorChanged];
    162    }
    163  }
    164 }
    165 
    166 void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {}
    167 
    168 void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget,
    169                            uint32_t aEventType) {
    170  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
    171  if (wrapper) {
    172    [wrapper handleAccessibleEvent:aEventType];
    173  }
    174 }
    175 
    176 void PlatformTextSelectionChangeEvent(Accessible* aTarget,
    177                                      const nsTArray<TextRange>& aSelection) {
    178  if (aSelection.Length()) {
    179    MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
    180        getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
    181    // Cache the selection.
    182    [delegate setSelectionFrom:aSelection[0].StartContainer()
    183                            at:aSelection[0].StartOffset()
    184                            to:aSelection[0].EndContainer()
    185                            at:aSelection[0].EndOffset()];
    186  }
    187 
    188  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
    189  if (wrapper) {
    190    [wrapper
    191        handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
    192  }
    193 }
    194 
    195 void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole,
    196                              uint8_t aRoleMapEntryIndex) {
    197  if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
    198    [wrapper handleRoleChanged:aRole];
    199  }
    200 }
    201 
    202 // This enum lists possible assistive technology clients. It's intended for use
    203 // in an EnumSet since there can be multiple ATs active at once.
    204 enum class Client : uint64_t {
    205  Unknown,
    206  VoiceOver,
    207  SwitchControl,
    208  FullKeyboardAccess,
    209  VoiceControl,
    210  SpeakSelection,
    211  SpeakItemUnderMouse,
    212  SpeakTypingFeedback,
    213  HoverText
    214 };
    215 
    216 // Get the set of currently-active clients and the client to log.
    217 // XXX: We should log all clients, but default to the first one encountered.
    218 std::pair<EnumSet<Client>, Client> GetClients() {
    219  EnumSet<Client> clients;
    220  std::optional<Client> clientToLog;
    221  auto AddClient = [&clients, &clientToLog](Client client) {
    222    clients += client;
    223    if (!clientToLog.has_value()) {
    224      clientToLog = client;
    225    }
    226  };
    227  if ([[NSWorkspace sharedWorkspace]
    228          respondsToSelector:@selector(isVoiceOverEnabled)] &&
    229      [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) {
    230    AddClient(Client::VoiceOver);
    231  } else if ([[NSWorkspace sharedWorkspace]
    232                 respondsToSelector:@selector(isSwitchControlEnabled)] &&
    233             [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) {
    234    AddClient(Client::SwitchControl);
    235  } else {
    236    Boolean foundSpecificClient = false;
    237 
    238    // This is more complicated than the NSWorkspace queries above
    239    // because (a) there is no "full keyboard access" query for NSWorkspace
    240    // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks
    241    // the pre-Monterey version of full keyboard access, which is not what
    242    // we're looking for here. For more info, see bug 1772375 comment 7.
    243    Boolean exists;
    244    long val = CFPreferencesGetAppIntegerValue(
    245        CFSTR("FullKeyboardAccessEnabled"), CFSTR("com.apple.Accessibility"),
    246        &exists);
    247    if (exists && val == 1) {
    248      foundSpecificClient = true;
    249      AddClient(Client::FullKeyboardAccess);
    250    }
    251 
    252    val = CFPreferencesGetAppIntegerValue(CFSTR("CommandAndControlEnabled"),
    253                                          CFSTR("com.apple.Accessibility"),
    254                                          &exists);
    255    if (exists && val == 1) {
    256      foundSpecificClient = true;
    257      AddClient(Client::VoiceControl);
    258    }
    259 
    260    val = CFPreferencesGetAppIntegerValue(
    261        CFSTR("SpeakThisEnabled"), CFSTR("com.apple.universalaccess"), &exists);
    262    if (exists && val == 1) {
    263      foundSpecificClient = true;
    264      AddClient(Client::SpeakSelection);
    265    }
    266 
    267    val = CFPreferencesGetAppIntegerValue(CFSTR("speakItemUnderMouseEnabled"),
    268                                          CFSTR("com.apple.universalaccess"),
    269                                          &exists);
    270    if (exists && val == 1) {
    271      foundSpecificClient = true;
    272      AddClient(Client::SpeakItemUnderMouse);
    273    }
    274 
    275    val = CFPreferencesGetAppIntegerValue(CFSTR("typingEchoEnabled"),
    276                                          CFSTR("com.apple.universalaccess"),
    277                                          &exists);
    278    if (exists && val == 1) {
    279      foundSpecificClient = true;
    280      AddClient(Client::SpeakTypingFeedback);
    281    }
    282 
    283    val = CFPreferencesGetAppIntegerValue(
    284        CFSTR("hoverTextEnabled"), CFSTR("com.apple.universalaccess"), &exists);
    285    if (exists && val == 1) {
    286      foundSpecificClient = true;
    287      AddClient(Client::HoverText);
    288    }
    289 
    290    if (!foundSpecificClient) {
    291      AddClient(Client::Unknown);
    292    }
    293  }
    294  return std::make_pair(clients, clientToLog.value());
    295 }
    296 
    297 // Expects a single client, returns a string representation of that client.
    298 constexpr const char* GetStringForClient(Client aClient) {
    299  switch (aClient) {
    300    case Client::Unknown:
    301      return "Unknown";
    302    case Client::VoiceOver:
    303      return "VoiceOver";
    304    case Client::SwitchControl:
    305      return "SwitchControl";
    306    case Client::FullKeyboardAccess:
    307      return "FullKeyboardAccess";
    308    case Client::VoiceControl:
    309      return "VoiceControl";
    310    case Client::SpeakSelection:
    311      return "SpeakSelection";
    312    case Client::SpeakItemUnderMouse:
    313      return "SpeakItemUnderMouse";
    314    case Client::SpeakTypingFeedback:
    315      return "SpeakTypingFeedback";
    316    case Client::HoverText:
    317      return "HoverText";
    318    default:
    319      break;
    320  }
    321  MOZ_ASSERT_UNREACHABLE("Unknown Client enum value!");
    322  return "";
    323 }
    324 
    325 uint64_t GetCacheDomainsForKnownClients(uint64_t aCacheDomains) {
    326  auto [clients, _] = GetClients();
    327  // We expect VoiceOver will require all information we have.
    328  if (clients.contains(Client::VoiceOver)) {
    329    return CacheDomain::All;
    330  }
    331  if (clients.contains(Client::FullKeyboardAccess)) {
    332    aCacheDomains |= CacheDomain::Bounds;
    333  }
    334  if (clients.contains(Client::SwitchControl)) {
    335    // XXX: Find minimum set of domains required for SwitchControl.
    336    // SwitchControl can give up if we don't furnish it certain information.
    337    return CacheDomain::All;
    338  }
    339  if (clients.contains(Client::VoiceControl)) {
    340    // XXX: Find minimum set of domains required for VoiceControl.
    341    return CacheDomain::All;
    342  }
    343  return aCacheDomains;
    344 }
    345 
    346 }  // namespace a11y
    347 }  // namespace mozilla
    348 
    349 @interface GeckoNSApplication (a11y)
    350 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
    351 @end
    352 
    353 @implementation GeckoNSApplication (a11y)
    354 
    355 - (NSAccessibilityRole)accessibilityRole {
    356  // For ATs that don't request `AXEnhancedUserInterface` we need to enable
    357  // accessibility when a role is fetched. Not ideal, but this is needed
    358  // for such services as Voice Control.
    359  if (!mozilla::a11y::sA11yShouldBeEnabled) {
    360    [self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"];
    361  }
    362  return [super accessibilityRole];
    363 }
    364 
    365 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
    366  if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) {
    367    mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1);
    368    if (sA11yShouldBeEnabled) {
    369      // If accessibility should be enabled, log the appropriate client
    370      auto [_, clientToLog] = GetClients();
    371      const char* client = GetStringForClient(clientToLog);
    372 
    373 #if defined(MOZ_TELEMETRY_REPORTING)
    374      mozilla::glean::a11y::instantiators.Set(nsDependentCString(client));
    375 #endif  // defined(MOZ_TELEMETRY_REPORTING)
    376      CrashReporter::RecordAnnotationCString(
    377          CrashReporter::Annotation::AccessibilityClient, client);
    378    }
    379  }
    380 
    381  return [super accessibilitySetValue:value forAttribute:attribute];
    382 }
    383 
    384 @end