PointerEvent.cpp (19179B)
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 * Portions Copyright 2013 Microsoft Open Technologies, Inc. */ 8 9 #include "PointerEvent.h" 10 11 #include "jsfriendapi.h" 12 #include "mozilla/MouseEvents.h" 13 #include "mozilla/StaticPrefs_dom.h" 14 #include "mozilla/dom/MouseEventBinding.h" 15 #include "mozilla/dom/PointerEventBinding.h" 16 #include "mozilla/dom/PointerEventHandler.h" 17 #include "nsContentUtils.h" 18 #include "prtime.h" 19 20 namespace mozilla::dom { 21 22 PointerEvent::PointerEvent(EventTarget* aOwner, nsPresContext* aPresContext, 23 WidgetPointerEvent* aEvent) 24 : MouseEvent(aOwner, aPresContext, 25 aEvent ? aEvent 26 : new WidgetPointerEvent(false, eVoidEvent, nullptr)) { 27 NS_ASSERTION(mEvent->mClass == ePointerEventClass, 28 "event type mismatch ePointerEventClass"); 29 30 WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent(); 31 if (aEvent) { 32 mEventIsInternal = false; 33 mTiltX.emplace(aEvent->tiltX); 34 mTiltY.emplace(aEvent->tiltY); 35 // mAltitudeAngle and mAzimuthAngle should be computed when they are 36 // requested by JS. 37 } else { 38 mEventIsInternal = true; 39 mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); 40 mouseEvent->mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; 41 } 42 // 5.2 Pointer Event types, for all pointer events, |detail| attribute SHOULD 43 // be 0. However, UI Events defines that it should be click count if the 44 // event type is "click". 45 mDetail = 46 IsPointerEventMessageOriginallyMouseEventMessage(mouseEvent->mMessage) 47 ? mouseEvent->mClickCount 48 : 0; 49 } 50 51 JSObject* PointerEvent::WrapObjectInternal(JSContext* aCx, 52 JS::Handle<JSObject*> aGivenProto) { 53 return PointerEvent_Binding::Wrap(aCx, this, aGivenProto); 54 } 55 56 static uint16_t ConvertStringToPointerType(const nsAString& aPointerTypeArg, 57 bool aForTrustedEvent) { 58 if (aPointerTypeArg.EqualsLiteral("mouse")) { 59 return MouseEvent_Binding::MOZ_SOURCE_MOUSE; 60 } 61 if (aPointerTypeArg.EqualsLiteral("pen")) { 62 return MouseEvent_Binding::MOZ_SOURCE_PEN; 63 } 64 if (aPointerTypeArg.EqualsLiteral("touch")) { 65 return MouseEvent_Binding::MOZ_SOURCE_TOUCH; 66 } 67 68 // Some chrome script need to copy the input source of a source event to 69 // dispatching new event. Therefore, we need to allow chrome script to 70 // set it to any input sources which we are supporting. However, these 71 // types are not standardized by the specs. Therefore, we should do this 72 // only when the event is a trusted one. 73 if (aForTrustedEvent) { 74 if (aPointerTypeArg.EqualsLiteral("eraser")) { 75 return MouseEvent_Binding::MOZ_SOURCE_ERASER; 76 } 77 if (aPointerTypeArg.EqualsLiteral("cursor")) { 78 return MouseEvent_Binding::MOZ_SOURCE_CURSOR; 79 } 80 if (aPointerTypeArg.EqualsLiteral("keyboard")) { 81 return MouseEvent_Binding::MOZ_SOURCE_KEYBOARD; 82 } 83 } 84 85 return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; 86 } 87 88 void ConvertPointerTypeToString(uint16_t aPointerTypeSrc, 89 nsAString& aPointerTypeDest) { 90 switch (aPointerTypeSrc) { 91 case MouseEvent_Binding::MOZ_SOURCE_MOUSE: 92 aPointerTypeDest.AssignLiteral("mouse"); 93 break; 94 case MouseEvent_Binding::MOZ_SOURCE_PEN: 95 aPointerTypeDest.AssignLiteral("pen"); 96 break; 97 case MouseEvent_Binding::MOZ_SOURCE_TOUCH: 98 aPointerTypeDest.AssignLiteral("touch"); 99 break; 100 // In ConvertStringToPointerType(), we allow chrome script to set the 101 // input source from Gecko specific pointerType value. However, we won't 102 // expose them to the web because they are not standardized. 103 case MouseEvent_Binding::MOZ_SOURCE_ERASER: 104 case MouseEvent_Binding::MOZ_SOURCE_CURSOR: 105 case MouseEvent_Binding::MOZ_SOURCE_KEYBOARD: 106 aPointerTypeDest.Truncate(); 107 break; 108 default: 109 aPointerTypeDest.Truncate(); 110 break; 111 } 112 } 113 114 // static 115 already_AddRefed<PointerEvent> PointerEvent::Constructor( 116 EventTarget* aOwner, const nsAString& aType, 117 const PointerEventInit& aParam) { 118 RefPtr<PointerEvent> e = new PointerEvent(aOwner, nullptr, nullptr); 119 bool trusted = e->Init(aOwner); 120 121 e->InitMouseEventInternal( 122 aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, aParam.mDetail, 123 aParam.mScreenX, aParam.mScreenY, aParam.mClientX, aParam.mClientY, false, 124 false, false, false, aParam.mButton, aParam.mRelatedTarget); 125 e->InitializeExtraMouseEventDictionaryMembers(aParam); 126 e->mPointerType = Some(aParam.mPointerType); 127 128 WidgetPointerEvent* widgetEvent = e->mEvent->AsPointerEvent(); 129 widgetEvent->pointerId = aParam.mPointerId; 130 widgetEvent->mWidth = aParam.mWidth; 131 widgetEvent->mHeight = aParam.mHeight; 132 widgetEvent->mPressure = aParam.mPressure; 133 widgetEvent->tangentialPressure = aParam.mTangentialPressure; 134 widgetEvent->twist = aParam.mTwist; 135 widgetEvent->mInputSource = 136 ConvertStringToPointerType(aParam.mPointerType, trusted); 137 widgetEvent->mIsPrimary = aParam.mIsPrimary; 138 widgetEvent->mButtons = aParam.mButtons; 139 140 if (aParam.mTiltX.WasPassed()) { 141 e->mTiltX.emplace(aParam.mTiltX.Value()); 142 } 143 if (aParam.mTiltY.WasPassed()) { 144 e->mTiltY.emplace(aParam.mTiltY.Value()); 145 } 146 if (aParam.mAltitudeAngle.WasPassed()) { 147 e->mAltitudeAngle.emplace(aParam.mAltitudeAngle.Value()); 148 } 149 if (aParam.mAzimuthAngle.WasPassed()) { 150 e->mAzimuthAngle.emplace(aParam.mAzimuthAngle.Value()); 151 } 152 153 e->mPersistentDeviceId.emplace(aParam.mPersistentDeviceId); 154 155 if (!aParam.mCoalescedEvents.IsEmpty()) { 156 e->mCoalescedEvents.AppendElements(aParam.mCoalescedEvents); 157 } 158 if (!aParam.mPredictedEvents.IsEmpty()) { 159 e->mPredictedEvents.AppendElements(aParam.mPredictedEvents); 160 } 161 162 // If only tiltX and/or tiltY is set, altitudeAngle and azimuthAngle should 163 // be computed from them when they are requested by JS. 164 if ((e->mTiltX || e->mTiltY) && (!e->mAltitudeAngle && !e->mAzimuthAngle)) { 165 if (!e->mTiltX) { 166 e->mTiltX.emplace(0); 167 } 168 if (!e->mTiltY) { 169 e->mTiltY.emplace(0); 170 } 171 } 172 // If only altitudeAngle and/or azimuthAngle is set, tiltX and tiltY should be 173 // computed from them when they are requested by JS. 174 else if ((e->mAltitudeAngle || e->mAzimuthAngle) && 175 (!e->mTiltX && !e->mTiltY)) { 176 if (!e->mAltitudeAngle) { 177 e->mAltitudeAngle.emplace(WidgetPointerHelper::GetDefaultAltitudeAngle()); 178 } 179 if (!e->mAzimuthAngle) { 180 e->mAzimuthAngle.emplace(WidgetPointerHelper::GetDefaultAzimuthAngle()); 181 } 182 } 183 // Otherwise, initialize the uninitialized values with their default values 184 else { 185 if (!e->mTiltX) { 186 e->mTiltX.emplace(0); 187 } 188 if (!e->mTiltY) { 189 e->mTiltY.emplace(0); 190 } 191 if (!e->mAltitudeAngle) { 192 e->mAltitudeAngle.emplace(WidgetPointerHelper::GetDefaultAltitudeAngle()); 193 } 194 if (!e->mAzimuthAngle) { 195 e->mAzimuthAngle.emplace(WidgetPointerHelper::GetDefaultAzimuthAngle()); 196 } 197 } 198 199 e->SetTrusted(trusted); 200 e->SetComposed(aParam.mComposed); 201 return e.forget(); 202 } 203 204 // static 205 already_AddRefed<PointerEvent> PointerEvent::Constructor( 206 const GlobalObject& aGlobal, const nsAString& aType, 207 const PointerEventInit& aParam) { 208 nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); 209 return Constructor(owner, aType, aParam); 210 } 211 212 NS_IMPL_CYCLE_COLLECTION_CLASS(PointerEvent) 213 214 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PointerEvent, MouseEvent) 215 NS_IMPL_CYCLE_COLLECTION_UNLINK(mCoalescedEvents) 216 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPredictedEvents) 217 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 218 219 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PointerEvent, MouseEvent) 220 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCoalescedEvents) 221 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPredictedEvents) 222 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 223 224 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PointerEvent) 225 NS_INTERFACE_MAP_END_INHERITING(MouseEvent) 226 227 NS_IMPL_ADDREF_INHERITED(PointerEvent, MouseEvent) 228 NS_IMPL_RELEASE_INHERITED(PointerEvent, MouseEvent) 229 230 uint16_t PointerEvent::ResistantInputSource(CallerType aCallerType) const { 231 const uint16_t inputSource = mEvent->AsPointerEvent()->mInputSource; 232 if (!ShouldResistFingerprinting(aCallerType)) { 233 return inputSource; 234 } 235 236 MOZ_ASSERT(IsTrusted()); 237 238 // Bug 1953665: Pen events are inconsistent between platforms. 239 // They might emit touch events on Windows and Android, but only mouse events 240 // in other platforms. In particular, touch is always disabled on macOS. 241 #if defined(XP_WIN) 242 if (inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH || 243 inputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) { 244 return inputSource; 245 } 246 // Similar to nsWindow::DispatchTouchEventFromWMPointer. 247 switch (mEvent->mMessage) { 248 case ePointerMove: 249 return mEvent->AsPointerEvent()->mPressure == 0 250 ? MouseEvent_Binding::MOZ_SOURCE_MOUSE // hover 251 : MouseEvent_Binding::MOZ_SOURCE_TOUCH; 252 case ePointerUp: 253 case ePointerDown: 254 case ePointerCancel: 255 return MouseEvent_Binding::MOZ_SOURCE_TOUCH; 256 default: 257 return MouseEvent_Binding::MOZ_SOURCE_MOUSE; 258 } 259 #elif defined(MOZ_WIDGET_ANDROID) 260 return inputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE 261 ? MouseEvent_Binding::MOZ_SOURCE_MOUSE 262 : MouseEvent_Binding::MOZ_SOURCE_TOUCH; 263 #elif defined(MOZ_WIDGET_GTK) 264 return inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH 265 ? MouseEvent_Binding::MOZ_SOURCE_TOUCH 266 : MouseEvent_Binding::MOZ_SOURCE_MOUSE; 267 #elif defined(MOZ_WIDGET_COCOA) 268 return MouseEvent_Binding::MOZ_SOURCE_MOUSE; 269 #else 270 return inputSource; 271 #endif 272 } 273 274 void PointerEvent::GetPointerType(nsAString& aPointerType, 275 CallerType aCallerType) const { 276 if (mPointerType.isSome()) { 277 aPointerType = mPointerType.value(); 278 return; 279 } 280 ConvertPointerTypeToString(ResistantInputSource(aCallerType), aPointerType); 281 } 282 283 int32_t PointerEvent::PointerId(CallerType aCallerType) const { 284 #ifdef MOZ_WIDGET_COCOA 285 if (ShouldResistFingerprinting(aCallerType)) { 286 return PointerEventHandler::GetSpoofedPointerIdForRFP(); 287 } 288 #endif 289 return mEvent->AsPointerEvent()->pointerId; 290 } 291 292 double PointerEvent::Width(CallerType aCallerType) const { 293 return ShouldResistFingerprinting(aCallerType) 294 ? 1.0 295 : mEvent->AsPointerEvent()->mWidth; 296 } 297 298 double PointerEvent::Height(CallerType aCallerType) const { 299 return ShouldResistFingerprinting(aCallerType) 300 ? 1.0 301 : mEvent->AsPointerEvent()->mHeight; 302 } 303 304 float PointerEvent::Pressure(CallerType aCallerType) const { 305 if (mEvent->mMessage == ePointerUp || 306 !ShouldResistFingerprinting(aCallerType)) { 307 return mEvent->AsPointerEvent()->mPressure; 308 } 309 310 // According to [1], we should use 0.5 when it is in active buttons state and 311 // 0 otherwise for devices that don't support pressure. And a pointerup event 312 // always reports 0, so we don't need to spoof that. 313 // 314 // [1] https://www.w3.org/TR/pointerevents/#dom-pointerevent-pressure 315 float spoofedPressure = 0.0; 316 if (mEvent->AsPointerEvent()->mButtons) { 317 spoofedPressure = 0.5; 318 } 319 320 return spoofedPressure; 321 } 322 323 float PointerEvent::TangentialPressure(CallerType aCallerType) const { 324 return ShouldResistFingerprinting(aCallerType) 325 ? 0 326 : mEvent->AsPointerEvent()->tangentialPressure; 327 } 328 329 int32_t PointerEvent::TiltX(CallerType aCallerType) { 330 if (ShouldResistFingerprinting(aCallerType)) { 331 return 0; 332 } 333 if (mTiltX.isSome()) { 334 return *mTiltX; 335 } 336 mTiltX.emplace( 337 WidgetPointerHelper::ComputeTiltX(*mAltitudeAngle, *mAzimuthAngle)); 338 return *mTiltX; 339 } 340 341 int32_t PointerEvent::TiltY(CallerType aCallerType) { 342 if (ShouldResistFingerprinting(aCallerType)) { 343 return 0; 344 } 345 if (mTiltY.isSome()) { 346 return *mTiltY; 347 } 348 mTiltY.emplace( 349 WidgetPointerHelper::ComputeTiltY(*mAltitudeAngle, *mAzimuthAngle)); 350 return *mTiltY; 351 } 352 353 int32_t PointerEvent::Twist(CallerType aCallerType) const { 354 return ShouldResistFingerprinting(aCallerType) 355 ? 0 356 : mEvent->AsPointerEvent()->twist; 357 } 358 359 double PointerEvent::AltitudeAngle(CallerType aCallerType) { 360 if (ShouldResistFingerprinting(aCallerType)) { 361 return WidgetPointerHelper::GetDefaultAltitudeAngle(); 362 } 363 if (mAltitudeAngle.isSome()) { 364 return *mAltitudeAngle; 365 } 366 mAltitudeAngle.emplace( 367 WidgetPointerHelper::ComputeAltitudeAngle(*mTiltX, *mTiltY)); 368 return *mAltitudeAngle; 369 } 370 371 double PointerEvent::AzimuthAngle(CallerType aCallerType) { 372 if (ShouldResistFingerprinting(aCallerType)) { 373 return WidgetPointerHelper::GetDefaultAzimuthAngle(); 374 } 375 if (mAzimuthAngle.isSome()) { 376 return *mAzimuthAngle; 377 } 378 mAzimuthAngle.emplace( 379 WidgetPointerHelper::ComputeAzimuthAngle(*mTiltX, *mTiltY)); 380 return *mAzimuthAngle; 381 } 382 383 bool PointerEvent::IsPrimary() const { 384 return mEvent->AsPointerEvent()->mIsPrimary; 385 } 386 387 int32_t PointerEvent::PersistentDeviceId(CallerType aCallerType) { 388 const auto MaybeNonZero = [&]() { 389 return mEvent->IsTrusted() && IsPointerEventMessage(mEvent->mMessage) && 390 !IsPointerEventMessageOriginallyMouseEventMessage(mEvent->mMessage); 391 }; 392 393 if (ShouldResistFingerprinting(aCallerType)) { 394 return MaybeNonZero() && ResistantInputSource(aCallerType) == 395 MouseEvent_Binding::MOZ_SOURCE_MOUSE 396 ? 1 397 : 0; 398 } 399 400 if (mPersistentDeviceId.isNothing()) { 401 if (MaybeNonZero() && mEvent->AsPointerEvent()->mInputSource == 402 MouseEvent_Binding::MOZ_SOURCE_MOUSE) { 403 // Follow the behavior which Chrome has for mouse. 404 mPersistentDeviceId.emplace(1); 405 } else { 406 // For now the default value is reported for non-mouse based events. 407 mPersistentDeviceId.emplace(0); 408 } 409 } 410 411 return mPersistentDeviceId.value(); 412 } 413 414 bool PointerEvent::EnableGetCoalescedEvents(JSContext* aCx, JSObject* aGlobal) { 415 return !StaticPrefs:: 416 dom_w3c_pointer_events_getcoalescedevents_only_in_securecontext() || 417 nsContentUtils::IsSecureContextOrWebExtension(aCx, aGlobal); 418 } 419 420 void PointerEvent::GetCoalescedEvents( 421 nsTArray<RefPtr<PointerEvent>>& aPointerEvents) { 422 WidgetPointerEvent* widgetEvent = mEvent->AsPointerEvent(); 423 MOZ_ASSERT(widgetEvent); 424 EnsureFillingCoalescedEvents(*widgetEvent); 425 if (mCoalescedEvents.IsEmpty() && widgetEvent && 426 widgetEvent->mCoalescedWidgetEvents && 427 !widgetEvent->mCoalescedWidgetEvents->mEvents.IsEmpty()) { 428 nsCOMPtr<EventTarget> owner = do_QueryInterface(mOwner); 429 for (WidgetPointerEvent& event : 430 widgetEvent->mCoalescedWidgetEvents->mEvents) { 431 RefPtr<PointerEvent> domEvent = 432 NS_NewDOMPointerEvent(owner, nullptr, &event); 433 domEvent->mCoalescedOrPredictedEvent = true; 434 435 // The dom event is derived from an OS generated widget event. Setup 436 // mWidget and mPresContext since they are necessary to calculate 437 // offsetX / offsetY. 438 domEvent->mEvent->AsGUIEvent()->mWidget = widgetEvent->mWidget; 439 domEvent->mPresContext = mPresContext; 440 441 // The coalesced widget mouse events shouldn't have been dispatched. 442 MOZ_ASSERT(!domEvent->mEvent->mTarget); 443 // The event target should be the same as the dispatched event's target. 444 domEvent->mEvent->mTarget = mEvent->mTarget; 445 446 // JS could hold reference to dom events. We have to ask dom event to 447 // duplicate its private data to avoid the widget event is destroyed. 448 domEvent->DuplicatePrivateData(); 449 450 mCoalescedEvents.AppendElement(domEvent); 451 } 452 } 453 if (mEvent->IsTrusted() && mEvent->mTarget) { 454 for (RefPtr<PointerEvent>& pointerEvent : mCoalescedEvents) { 455 // Only set event target when it's null. 456 if (!pointerEvent->mEvent->mTarget) { 457 pointerEvent->mEvent->mTarget = mEvent->mTarget; 458 } 459 } 460 } 461 aPointerEvents.AppendElements(mCoalescedEvents); 462 } 463 464 void PointerEvent::EnsureFillingCoalescedEvents( 465 WidgetPointerEvent& aWidgetEvent) { 466 if (!aWidgetEvent.IsTrusted() || 467 (aWidgetEvent.mMessage != ePointerMove && 468 aWidgetEvent.mMessage != ePointerRawUpdate) || 469 !mCoalescedEvents.IsEmpty() || 470 (aWidgetEvent.mCoalescedWidgetEvents && 471 !aWidgetEvent.mCoalescedWidgetEvents->mEvents.IsEmpty()) || 472 mCoalescedOrPredictedEvent) { 473 return; 474 } 475 if (!aWidgetEvent.mCoalescedWidgetEvents) { 476 aWidgetEvent.mCoalescedWidgetEvents = new WidgetPointerEventHolder(); 477 } 478 WidgetPointerEvent* const coalescedEvent = 479 aWidgetEvent.mCoalescedWidgetEvents->mEvents.AppendElement( 480 WidgetPointerEvent(true, aWidgetEvent.mMessage, 481 aWidgetEvent.mWidget)); 482 MOZ_ASSERT(coalescedEvent); 483 PointerEventHandler::InitCoalescedEventFromPointerEvent(*coalescedEvent, 484 aWidgetEvent); 485 } 486 487 void PointerEvent::GetPredictedEvents( 488 nsTArray<RefPtr<PointerEvent>>& aPointerEvents) { 489 // XXX Add support for native predicted events, bug 1550461 490 // And when doing so, update mCoalescedOrPredictedEvent here. 491 if (mEvent->IsTrusted() && mEvent->mTarget) { 492 for (RefPtr<PointerEvent>& pointerEvent : mPredictedEvents) { 493 // Only set event target when it's null. 494 if (!pointerEvent->mEvent->mTarget) { 495 pointerEvent->mEvent->mTarget = mEvent->mTarget; 496 } 497 } 498 } 499 aPointerEvents.AppendElements(mPredictedEvents); 500 } 501 502 bool PointerEvent::ShouldResistFingerprinting(CallerType aCallerType) const { 503 // There are a few simple situations we don't need to spoof this pointer 504 // event. 505 // * We are being called by a System caller 506 // * The pref privcy.resistFingerprinting' is false, we fast return here 507 // since we don't need to do any QI of following codes. 508 // * This event is generated by scripts. 509 // * This event is a mouse pointer event. 510 // We don't need to check for the system group since pointer events won't be 511 // dispatched to the system group. 512 RFPTarget target = RFPTarget::PointerEvents; 513 if (aCallerType == CallerType::System || 514 !nsContentUtils::ShouldResistFingerprinting("Efficiency Check", target) || 515 !mEvent->IsTrusted() || 516 mEvent->AsPointerEvent()->mInputSource == 517 MouseEvent_Binding::MOZ_SOURCE_MOUSE) { 518 return false; 519 } 520 521 // Pref is checked above, so use true as fallback. 522 nsCOMPtr<Document> doc = GetDocument(); 523 return doc ? doc->ShouldResistFingerprinting(target) : true; 524 } 525 526 } // namespace mozilla::dom 527 528 using namespace mozilla; 529 using namespace mozilla::dom; 530 531 already_AddRefed<PointerEvent> NS_NewDOMPointerEvent( 532 EventTarget* aOwner, nsPresContext* aPresContext, 533 WidgetPointerEvent* aEvent) { 534 RefPtr<PointerEvent> it = new PointerEvent(aOwner, aPresContext, aEvent); 535 return it.forget(); 536 }