ClipboardItem.cpp (12235B)
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 "mozilla/dom/ClipboardItem.h" 8 9 #include "mozilla/dom/Clipboard.h" 10 #include "mozilla/dom/Promise.h" 11 #include "mozilla/dom/Record.h" 12 #include "nsComponentManagerUtils.h" 13 #include "nsIClipboard.h" 14 #include "nsIInputStream.h" 15 #include "nsISupportsPrimitives.h" 16 #include "nsNetUtil.h" 17 #include "nsServiceManagerUtils.h" 18 19 namespace mozilla::dom { 20 21 NS_IMPL_CYCLE_COLLECTION(ClipboardItem::ItemEntry, mGlobal, mData, 22 mPendingGetTypeRequests) 23 24 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ClipboardItem::ItemEntry) 25 NS_INTERFACE_MAP_ENTRY(nsIAsyncClipboardRequestCallback) 26 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, PromiseNativeHandler) 27 NS_INTERFACE_MAP_END 28 29 NS_IMPL_CYCLE_COLLECTING_ADDREF(ClipboardItem::ItemEntry) 30 NS_IMPL_CYCLE_COLLECTING_RELEASE(ClipboardItem::ItemEntry) 31 32 void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx, 33 JS::Handle<JS::Value> aValue, 34 ErrorResult& aRv) { 35 MOZ_ASSERT(!mTransferable); 36 mIsLoadingData = false; 37 OwningStringOrBlob clipboardData; 38 if (!clipboardData.Init(aCx, aValue)) { 39 JS_ClearPendingException(aCx); 40 RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); 41 return; 42 } 43 44 MaybeResolvePendingPromises(std::move(clipboardData)); 45 } 46 47 void ClipboardItem::ItemEntry::RejectedCallback(JSContext* aCx, 48 JS::Handle<JS::Value> aValue, 49 ErrorResult& aRv) { 50 MOZ_ASSERT(!mTransferable); 51 mIsLoadingData = false; 52 RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); 53 } 54 55 RefPtr<ClipboardItem::ItemEntry::GetDataPromise> 56 ClipboardItem::ItemEntry::GetData() { 57 // Data is still being loaded, either from the system clipboard or the data 58 // promise provided to the ClipboardItem constructor. 59 if (mIsLoadingData) { 60 MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), 61 "Data should be uninitialized"); 62 MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result"); 63 64 MozPromiseHolder<GetDataPromise> holder; 65 RefPtr<GetDataPromise> promise = holder.Ensure(__func__); 66 mPendingGetDataRequests.AppendElement(std::move(holder)); 67 return promise.forget(); 68 } 69 70 if (NS_FAILED(mLoadResult.value())) { 71 MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), 72 "Data should be uninitialized"); 73 // We are not able to load data, so reject the promise directly. 74 return GetDataPromise::CreateAndReject(mLoadResult.value(), __func__); 75 } 76 77 MOZ_ASSERT(mData.IsString() || mData.IsBlob(), "Data should be initialized"); 78 OwningStringOrBlob data(mData); 79 return GetDataPromise::CreateAndResolve(std::move(data), __func__); 80 } 81 82 NS_IMETHODIMP ClipboardItem::ItemEntry::OnComplete(nsresult aResult) { 83 MOZ_ASSERT(mIsLoadingData); 84 85 mIsLoadingData = false; 86 nsCOMPtr<nsITransferable> trans = std::move(mTransferable); 87 88 if (NS_FAILED(aResult)) { 89 RejectPendingPromises(aResult); 90 return NS_OK; 91 } 92 93 MOZ_ASSERT(trans); 94 nsCOMPtr<nsISupports> data; 95 nsresult rv = trans->GetTransferData(NS_ConvertUTF16toUTF8(mType).get(), 96 getter_AddRefs(data)); 97 if (NS_WARN_IF(NS_FAILED(rv))) { 98 RejectPendingPromises(rv); 99 return NS_OK; 100 } 101 102 RefPtr<Blob> blob; 103 if (nsCOMPtr<nsISupportsString> supportsstr = do_QueryInterface(data)) { 104 nsAutoString str; 105 supportsstr->GetData(str); 106 107 blob = Blob::CreateStringBlob(mGlobal, NS_ConvertUTF16toUTF8(str), mType); 108 } else if (nsCOMPtr<nsIInputStream> istream = do_QueryInterface(data)) { 109 uint64_t available; 110 void* data = nullptr; 111 rv = NS_ReadInputStreamToBuffer(istream, &data, -1, &available); 112 if (NS_WARN_IF(NS_FAILED(rv))) { 113 RejectPendingPromises(rv); 114 return NS_OK; 115 } 116 117 blob = Blob::CreateMemoryBlob(mGlobal, data, available, mType); 118 } else if (nsCOMPtr<nsISupportsCString> supportscstr = 119 do_QueryInterface(data)) { 120 nsAutoCString str; 121 supportscstr->GetData(str); 122 123 blob = Blob::CreateStringBlob(mGlobal, str, mType); 124 } 125 126 if (!blob) { 127 RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); 128 return NS_OK; 129 } 130 131 OwningStringOrBlob clipboardData; 132 clipboardData.SetAsBlob() = std::move(blob); 133 MaybeResolvePendingPromises(std::move(clipboardData)); 134 return NS_OK; 135 } 136 137 void ClipboardItem::ItemEntry::LoadDataFromSystemClipboard( 138 nsIClipboardDataSnapshot* aDataGetter) { 139 MOZ_ASSERT(aDataGetter); 140 // XXX maybe we could consider adding a method to check whether the union 141 // object is uninitialized or initialized. 142 MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), 143 "Data should be uninitialized."); 144 MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); 145 MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, 146 "Should not be in the process of loading data"); 147 148 mIsLoadingData = true; 149 150 mTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); 151 if (NS_WARN_IF(!mTransferable)) { 152 OnComplete(NS_ERROR_FAILURE); 153 return; 154 } 155 156 mTransferable->Init(nullptr); 157 mTransferable->AddDataFlavor(NS_ConvertUTF16toUTF8(mType).get()); 158 159 nsresult rv = aDataGetter->GetData(mTransferable, this); 160 if (NS_FAILED(rv)) { 161 OnComplete(rv); 162 return; 163 } 164 } 165 166 void ClipboardItem::ItemEntry::LoadDataFromDataPromise(Promise& aDataPromise) { 167 MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), 168 "Data should be uninitialized"); 169 MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); 170 MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, 171 "Should not be in the process of loading data"); 172 173 mIsLoadingData = true; 174 aDataPromise.AppendNativeHandler(this); 175 } 176 177 void ClipboardItem::ItemEntry::ReactGetTypePromise(Promise& aPromise) { 178 // Data is still being loaded, either from the system clipboard or the data 179 // promise provided to the ClipboardItem constructor. 180 if (mIsLoadingData) { 181 MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), 182 "Data should be uninitialized"); 183 MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result."); 184 mPendingGetTypeRequests.AppendElement(&aPromise); 185 return; 186 } 187 188 if (NS_FAILED(mLoadResult.value())) { 189 MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), 190 "Data should be uninitialized"); 191 aPromise.MaybeRejectWithDataError("The data for type '"_ns + 192 NS_ConvertUTF16toUTF8(mType) + 193 "' was not found"_ns); 194 return; 195 } 196 197 MaybeResolveGetTypePromise(mData, aPromise); 198 } 199 200 void ClipboardItem::ItemEntry::MaybeResolveGetTypePromise( 201 const OwningStringOrBlob& aData, Promise& aPromise) { 202 if (aData.IsBlob()) { 203 aPromise.MaybeResolve(aData); 204 return; 205 } 206 207 // XXX This is for the case that data is from ClipboardItem constructor, 208 // maybe we should also load that into a Blob earlier. But Safari returns 209 // different `Blob` instances for each `getTypes` call if the string is from 210 // ClipboardItem constructor, which is more like our current setup. 211 if (RefPtr<Blob> blob = Blob::CreateStringBlob( 212 mGlobal, NS_ConvertUTF16toUTF8(aData.GetAsString()), mType)) { 213 aPromise.MaybeResolve(blob); 214 return; 215 } 216 217 aPromise.MaybeRejectWithDataError("The data for type '"_ns + 218 NS_ConvertUTF16toUTF8(mType) + 219 "' was not found"_ns); 220 } 221 222 void ClipboardItem::ItemEntry::RejectPendingPromises(nsresult aRv) { 223 MOZ_ASSERT(NS_FAILED(aRv), "Should have a failure code here"); 224 MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), 225 "Data should be uninitialized"); 226 MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); 227 MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, 228 "Should not be in the process of loading data"); 229 mLoadResult.emplace(aRv); 230 auto promiseHolders = std::move(mPendingGetDataRequests); 231 for (auto& promiseHolder : promiseHolders) { 232 promiseHolder.Reject(aRv, __func__); 233 } 234 auto getTypePromises = std::move(mPendingGetTypeRequests); 235 for (auto& promise : getTypePromises) { 236 promise->MaybeReject(aRv); 237 } 238 } 239 240 void ClipboardItem::ItemEntry::MaybeResolvePendingPromises( 241 OwningStringOrBlob&& aData) { 242 MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), 243 "Data should be uninitialized"); 244 MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); 245 MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, 246 "Should not be in the process of loading data"); 247 mLoadResult.emplace(NS_OK); 248 mData = std::move(aData); 249 auto getDataPromiseHolders = std::move(mPendingGetDataRequests); 250 for (auto& promiseHolder : getDataPromiseHolders) { 251 OwningStringOrBlob data(mData); 252 promiseHolder.Resolve(std::move(data), __func__); 253 } 254 auto promises = std::move(mPendingGetTypeRequests); 255 for (auto& promise : promises) { 256 MaybeResolveGetTypePromise(mData, *promise); 257 } 258 } 259 260 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ClipboardItem, mOwner, mItems) 261 262 ClipboardItem::ClipboardItem(nsISupports* aOwner, 263 const dom::PresentationStyle aPresentationStyle, 264 nsTArray<RefPtr<ItemEntry>>&& aItems) 265 : mOwner(aOwner), 266 mPresentationStyle(aPresentationStyle), 267 mItems(std::move(aItems)) {} 268 269 // static 270 already_AddRefed<ClipboardItem> ClipboardItem::Constructor( 271 const GlobalObject& aGlobal, 272 const Record<nsString, OwningNonNull<Promise>>& aItems, 273 const ClipboardItemOptions& aOptions, ErrorResult& aRv) { 274 if (aItems.Entries().IsEmpty()) { 275 aRv.ThrowTypeError("At least one entry required"); 276 return nullptr; 277 } 278 279 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 280 MOZ_ASSERT(global); 281 282 nsTArray<RefPtr<ItemEntry>> items; 283 for (const auto& entry : aItems.Entries()) { 284 RefPtr<ItemEntry> item = MakeRefPtr<ItemEntry>(global, entry.mKey); 285 item->LoadDataFromDataPromise(*entry.mValue); 286 items.AppendElement(std::move(item)); 287 } 288 289 RefPtr<ClipboardItem> item = MakeRefPtr<ClipboardItem>( 290 global, aOptions.mPresentationStyle, std::move(items)); 291 return item.forget(); 292 } 293 294 // static 295 bool ClipboardItem::Supports(const GlobalObject& aGlobal, 296 const nsAString& aType) { 297 for (const auto& mandatoryType : Clipboard::MandatoryDataTypes()) { 298 if (CompareUTF8toUTF16(mandatoryType, aType) == 0) { 299 return true; 300 } 301 } 302 return false; 303 } 304 305 void ClipboardItem::GetTypes(nsTArray<nsString>& aTypes) const { 306 for (const auto& item : mItems) { 307 aTypes.AppendElement(item->Type()); 308 } 309 } 310 311 already_AddRefed<Promise> ClipboardItem::GetType(const nsAString& aType, 312 ErrorResult& aRv) { 313 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); 314 RefPtr<Promise> p = Promise::Create(global, aRv); 315 if (aRv.Failed()) { 316 return nullptr; 317 } 318 319 for (auto& item : mItems) { 320 MOZ_ASSERT(item); 321 322 const nsAString& type = item->Type(); 323 if (type == aType) { 324 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); 325 if (NS_WARN_IF(!global)) { 326 p->MaybeReject(NS_ERROR_UNEXPECTED); 327 return p.forget(); 328 } 329 330 item->ReactGetTypePromise(*p); 331 return p.forget(); 332 } 333 } 334 335 p->MaybeRejectWithNotFoundError( 336 "The type '"_ns + NS_ConvertUTF16toUTF8(aType) + "' was not found"_ns); 337 return p.forget(); 338 } 339 340 JSObject* ClipboardItem::WrapObject(JSContext* aCx, 341 JS::Handle<JSObject*> aGivenProto) { 342 return mozilla::dom::ClipboardItem_Binding::Wrap(aCx, this, aGivenProto); 343 } 344 345 } // namespace mozilla::dom