Line data Source code
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 "DataTransferItemList.h"
7 :
8 : #include "nsContentUtils.h"
9 : #include "nsIGlobalObject.h"
10 : #include "nsIClipboard.h"
11 : #include "nsIScriptObjectPrincipal.h"
12 : #include "nsIScriptGlobalObject.h"
13 : #include "nsIScriptContext.h"
14 : #include "nsISupportsPrimitives.h"
15 : #include "nsQueryObject.h"
16 : #include "nsVariant.h"
17 : #include "mozilla/ContentEvents.h"
18 : #include "mozilla/EventForwards.h"
19 : #include "mozilla/storage/Variant.h"
20 : #include "mozilla/dom/DataTransferItemListBinding.h"
21 :
22 : namespace mozilla {
23 : namespace dom {
24 :
25 0 : NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DataTransferItemList, mDataTransfer, mItems,
26 : mIndexedItems, mFiles)
27 0 : NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransferItemList)
28 0 : NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransferItemList)
29 :
30 0 : NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransferItemList)
31 0 : NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
32 0 : NS_INTERFACE_MAP_ENTRY(nsISupports)
33 0 : NS_INTERFACE_MAP_END
34 :
35 : JSObject*
36 0 : DataTransferItemList::WrapObject(JSContext* aCx,
37 : JS::Handle<JSObject*> aGivenProto)
38 : {
39 0 : return DataTransferItemListBinding::Wrap(aCx, this, aGivenProto);
40 : }
41 :
42 : already_AddRefed<DataTransferItemList>
43 0 : DataTransferItemList::Clone(DataTransfer* aDataTransfer) const
44 : {
45 : RefPtr<DataTransferItemList> list =
46 0 : new DataTransferItemList(aDataTransfer, mIsExternal);
47 :
48 : // We need to clone the mItems and mIndexedItems lists while keeping the same
49 : // correspondences between the mIndexedItems and mItems lists (namely, if an
50 : // item is in mIndexedItems, and mItems it must have the same new identity)
51 :
52 : // First, we copy over indexedItems, and clone every entry. Then, we go over
53 : // mItems. For every entry, we use its mIndex property to locate it in
54 : // mIndexedItems on the original DataTransferItemList, and then copy over the
55 : // reference from the same index pair on the new DataTransferItemList
56 :
57 0 : list->mIndexedItems.SetLength(mIndexedItems.Length());
58 0 : list->mItems.SetLength(mItems.Length());
59 :
60 : // Copy over mIndexedItems, cloning every entry
61 0 : for (uint32_t i = 0; i < mIndexedItems.Length(); i++) {
62 0 : const nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
63 0 : nsTArray<RefPtr<DataTransferItem>>& newItems = list->mIndexedItems[i];
64 0 : newItems.SetLength(items.Length());
65 0 : for (uint32_t j = 0; j < items.Length(); j++) {
66 0 : newItems[j] = items[j]->Clone(aDataTransfer);
67 : }
68 : }
69 :
70 : // Copy over mItems, getting the actual entries from mIndexedItems
71 0 : for (uint32_t i = 0; i < mItems.Length(); i++) {
72 0 : uint32_t index = mItems[i]->Index();
73 0 : MOZ_ASSERT(index < mIndexedItems.Length());
74 0 : uint32_t subIndex = mIndexedItems[index].IndexOf(mItems[i]);
75 :
76 : // Copy over the reference
77 0 : list->mItems[i] = list->mIndexedItems[index][subIndex];
78 : }
79 :
80 0 : return list.forget();
81 : }
82 :
83 : void
84 0 : DataTransferItemList::Remove(uint32_t aIndex,
85 : nsIPrincipal& aSubjectPrincipal,
86 : ErrorResult& aRv)
87 : {
88 0 : if (mDataTransfer->IsReadOnly()) {
89 0 : aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
90 0 : return;
91 : }
92 :
93 0 : if (aIndex >= Length()) {
94 0 : aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
95 0 : return;
96 : }
97 :
98 0 : ClearDataHelper(mItems[aIndex], aIndex, -1, aSubjectPrincipal, aRv);
99 : }
100 :
101 : DataTransferItem*
102 0 : DataTransferItemList::IndexedGetter(uint32_t aIndex, bool& aFound) const
103 : {
104 0 : if (aIndex >= mItems.Length()) {
105 0 : aFound = false;
106 0 : return nullptr;
107 : }
108 :
109 0 : MOZ_ASSERT(mItems[aIndex]);
110 0 : aFound = true;
111 0 : return mItems[aIndex];
112 : }
113 :
114 : uint32_t
115 0 : DataTransferItemList::MozItemCount() const
116 : {
117 0 : uint32_t length = mIndexedItems.Length();
118 : // XXX: Compat hack - Index 0 always exists due to changes in internals, but
119 : // if it is empty, scripts using the moz* APIs should see it as not existing.
120 0 : if (length == 1 && mIndexedItems[0].IsEmpty()) {
121 0 : return 0;
122 : }
123 0 : return length;
124 : }
125 :
126 : void
127 0 : DataTransferItemList::Clear(nsIPrincipal& aSubjectPrincipal,
128 : ErrorResult& aRv)
129 : {
130 0 : if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
131 0 : return;
132 : }
133 :
134 0 : uint32_t count = Length();
135 0 : for (uint32_t i = 0; i < count; i++) {
136 : // We always remove the last item first, to avoid moving items around in
137 : // memory as much
138 0 : Remove(Length() - 1, aSubjectPrincipal, aRv);
139 0 : ENSURE_SUCCESS_VOID(aRv);
140 : }
141 :
142 0 : MOZ_ASSERT(Length() == 0);
143 : }
144 :
145 : DataTransferItem*
146 0 : DataTransferItemList::Add(const nsAString& aData,
147 : const nsAString& aType,
148 : nsIPrincipal& aSubjectPrincipal,
149 : ErrorResult& aRv)
150 : {
151 0 : if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
152 0 : return nullptr;
153 : }
154 :
155 0 : nsCOMPtr<nsIVariant> data(new storage::TextVariant(aData));
156 :
157 0 : nsAutoString format;
158 0 : mDataTransfer->GetRealFormat(aType, format);
159 :
160 0 : if (!DataTransfer::PrincipalMaySetData(format, data, &aSubjectPrincipal)) {
161 0 : aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
162 0 : return nullptr;
163 : }
164 :
165 : // We add the textual data to index 0. We set aInsertOnly to true, as we don't
166 : // want to update an existing entry if it is already present, as per the spec.
167 : RefPtr<DataTransferItem> item =
168 0 : SetDataWithPrincipal(format, data, 0, &aSubjectPrincipal,
169 : /* aInsertOnly = */ true,
170 : /* aHidden = */ false,
171 0 : aRv);
172 0 : if (NS_WARN_IF(aRv.Failed())) {
173 0 : return nullptr;
174 : }
175 0 : MOZ_ASSERT(item->Kind() != DataTransferItem::KIND_FILE);
176 :
177 0 : return item;
178 : }
179 :
180 : DataTransferItem*
181 0 : DataTransferItemList::Add(File& aData,
182 : nsIPrincipal& aSubjectPrincipal,
183 : ErrorResult& aRv)
184 : {
185 0 : if (mDataTransfer->IsReadOnly()) {
186 0 : return nullptr;
187 : }
188 :
189 0 : nsCOMPtr<nsISupports> supports = do_QueryObject(&aData);
190 0 : nsCOMPtr<nsIWritableVariant> data = new nsVariant();
191 0 : data->SetAsISupports(supports);
192 :
193 0 : nsAutoString type;
194 0 : aData.GetType(type);
195 :
196 0 : if (!DataTransfer::PrincipalMaySetData(type, data, &aSubjectPrincipal)) {
197 0 : aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
198 0 : return nullptr;
199 : }
200 :
201 : // We need to add this as a new item, as multiple files can't exist in the
202 : // same item in the Moz DataTransfer layout. It will be appended at the end of
203 : // the internal specced layout.
204 0 : uint32_t index = mIndexedItems.Length();
205 : RefPtr<DataTransferItem> item =
206 0 : SetDataWithPrincipal(type, data, index, &aSubjectPrincipal,
207 : /* aInsertOnly = */ true,
208 : /* aHidden = */ false,
209 0 : aRv);
210 0 : if (NS_WARN_IF(aRv.Failed())) {
211 0 : return nullptr;
212 : }
213 0 : MOZ_ASSERT(item->Kind() == DataTransferItem::KIND_FILE);
214 :
215 0 : return item;
216 : }
217 :
218 : already_AddRefed<FileList>
219 0 : DataTransferItemList::Files(nsIPrincipal* aPrincipal)
220 : {
221 : // The DataTransfer can hold data with varying principals, coming from
222 : // different windows. This means that permissions checks need to be made when
223 : // accessing data from the DataTransfer. With the accessor methods, this is
224 : // checked by DataTransferItem::Data(), however with files, we keep a cached
225 : // live copy of the files list for spec compliance.
226 : //
227 : // A DataTransfer is only exposed to one webpage, and chrome code. The chrome
228 : // code should be able to see all files on the DataTransfer, while the webpage
229 : // should only be able to see the files it can see. As chrome code doesn't
230 : // need as strict spec compliance as web visible code, we generate a new
231 : // FileList object every time you access the Files list from chrome code, but
232 : // re-use the cached one when accessing from non-chrome code.
233 : //
234 : // It is not legal to expose an identical DataTransfer object is to multiple
235 : // different principals without using the `Clone` method or similar to copy it
236 : // first. If that happens, this method will assert, and return nullptr in
237 : // release builds. If this functionality is required in the future, a more
238 : // advanced caching mechanism for the FileList objects will be required.
239 0 : RefPtr<FileList> files;
240 0 : if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
241 0 : files = new FileList(static_cast<nsIDOMDataTransfer*>(mDataTransfer));
242 0 : GenerateFiles(files, aPrincipal);
243 0 : return files.forget();
244 : }
245 :
246 0 : if (!mFiles) {
247 0 : mFiles = new FileList(static_cast<nsIDOMDataTransfer*>(mDataTransfer));
248 0 : mFilesPrincipal = aPrincipal;
249 0 : RegenerateFiles();
250 : }
251 :
252 0 : if (!aPrincipal->Subsumes(mFilesPrincipal)) {
253 0 : MOZ_ASSERT(false, "This DataTransfer should only be accessed by the system "
254 : "and a single principal");
255 : return nullptr;
256 : }
257 :
258 0 : files = mFiles;
259 0 : return files.forget();
260 : }
261 :
262 : void
263 0 : DataTransferItemList::MozRemoveByTypeAt(const nsAString& aType,
264 : uint32_t aIndex,
265 : nsIPrincipal& aSubjectPrincipal,
266 : ErrorResult& aRv)
267 : {
268 0 : if (NS_WARN_IF(mDataTransfer->IsReadOnly() ||
269 : aIndex >= mIndexedItems.Length())) {
270 0 : return;
271 : }
272 :
273 0 : bool removeAll = aType.IsEmpty();
274 :
275 0 : nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex];
276 0 : uint32_t count = items.Length();
277 : // We remove the last item of the list repeatedly - that way we don't
278 : // have to worry about modifying the loop iterator
279 0 : if (removeAll) {
280 0 : for (uint32_t i = 0; i < count; ++i) {
281 0 : uint32_t index = items.Length() - 1;
282 0 : MOZ_ASSERT(index == count - i - 1);
283 :
284 0 : ClearDataHelper(items[index], -1, index, aSubjectPrincipal, aRv);
285 0 : if (NS_WARN_IF(aRv.Failed())) {
286 0 : return;
287 : }
288 : }
289 :
290 : // items is no longer a valid reference, as removing the last element from
291 : // it via ClearDataHelper invalidated it. so we can't MOZ_ASSERT that the
292 : // length is now 0.
293 0 : return;
294 : }
295 :
296 0 : for (uint32_t i = 0; i < count; ++i) {
297 : // NOTE: As this is a moz-prefixed API, it works based on internal types.
298 0 : nsAutoString type;
299 0 : items[i]->GetInternalType(type);
300 0 : if (type == aType) {
301 0 : ClearDataHelper(items[i], -1, i, aSubjectPrincipal, aRv);
302 0 : return;
303 : }
304 : }
305 : }
306 :
307 : DataTransferItem*
308 0 : DataTransferItemList::MozItemByTypeAt(const nsAString& aType, uint32_t aIndex)
309 : {
310 0 : if (NS_WARN_IF(aIndex >= mIndexedItems.Length())) {
311 0 : return nullptr;
312 : }
313 :
314 0 : uint32_t count = mIndexedItems[aIndex].Length();
315 0 : for (uint32_t i = 0; i < count; i++) {
316 0 : RefPtr<DataTransferItem> item = mIndexedItems[aIndex][i];
317 : // NOTE: As this is a moz-prefixed API it works on internal types
318 0 : nsString type;
319 0 : item->GetInternalType(type);
320 0 : if (type.Equals(aType)) {
321 0 : return item;
322 : }
323 : }
324 :
325 0 : return nullptr;
326 : }
327 :
328 : already_AddRefed<DataTransferItem>
329 0 : DataTransferItemList::SetDataWithPrincipal(const nsAString& aType,
330 : nsIVariant* aData,
331 : uint32_t aIndex,
332 : nsIPrincipal* aPrincipal,
333 : bool aInsertOnly,
334 : bool aHidden,
335 : ErrorResult& aRv)
336 : {
337 0 : if (aIndex < mIndexedItems.Length()) {
338 0 : nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex];
339 0 : uint32_t count = items.Length();
340 0 : for (uint32_t i = 0; i < count; i++) {
341 0 : RefPtr<DataTransferItem> item = items[i];
342 0 : nsString type;
343 0 : item->GetInternalType(type);
344 0 : if (type.Equals(aType)) {
345 0 : if (NS_WARN_IF(aInsertOnly)) {
346 0 : aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
347 0 : return nullptr;
348 : }
349 :
350 : // don't allow replacing data that has a stronger principal
351 : bool subsumes;
352 0 : if (NS_WARN_IF(item->Principal() && aPrincipal &&
353 : (NS_FAILED(aPrincipal->Subsumes(item->Principal(),
354 : &subsumes))
355 : || !subsumes))) {
356 0 : aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
357 0 : return nullptr;
358 : }
359 0 : item->SetPrincipal(aPrincipal);
360 :
361 0 : DataTransferItem::eKind oldKind = item->Kind();
362 0 : item->SetData(aData);
363 0 : if (oldKind != item->Kind()) {
364 : // Types list may have changed, even if aIndex == 0.
365 0 : mDataTransfer->TypesListMayHaveChanged();
366 : }
367 :
368 0 : if (aIndex != 0) {
369 : // If the item changes from being a file to not a file or vice-versa,
370 : // its presence in the mItems array may need to change.
371 0 : if (item->Kind() == DataTransferItem::KIND_FILE &&
372 : oldKind != DataTransferItem::KIND_FILE) {
373 : // not file => file
374 0 : mItems.AppendElement(item);
375 0 : } else if (item->Kind() != DataTransferItem::KIND_FILE &&
376 : oldKind == DataTransferItem::KIND_FILE) {
377 : // file => not file
378 0 : mItems.RemoveElement(item);
379 : }
380 : }
381 :
382 : // Regenerate the Files array if we have modified a file's status
383 0 : if (item->Kind() == DataTransferItem::KIND_FILE ||
384 : oldKind == DataTransferItem::KIND_FILE) {
385 0 : RegenerateFiles();
386 : }
387 :
388 0 : return item.forget();
389 : }
390 : }
391 : } else {
392 : // Make sure that we aren't adding past the end of the mIndexedItems array.
393 : // XXX Should this be a MOZ_ASSERT instead?
394 0 : aIndex = mIndexedItems.Length();
395 : }
396 :
397 : // Add the new item
398 0 : RefPtr<DataTransferItem> item = AppendNewItem(aIndex, aType, aData, aPrincipal, aHidden);
399 :
400 0 : if (item->Kind() == DataTransferItem::KIND_FILE) {
401 0 : RegenerateFiles();
402 : }
403 :
404 0 : return item.forget();
405 : }
406 :
407 : DataTransferItem*
408 0 : DataTransferItemList::AppendNewItem(uint32_t aIndex,
409 : const nsAString& aType,
410 : nsIVariant* aData,
411 : nsIPrincipal* aPrincipal,
412 : bool aHidden)
413 : {
414 0 : if (mIndexedItems.Length() <= aIndex) {
415 0 : MOZ_ASSERT(mIndexedItems.Length() == aIndex);
416 0 : mIndexedItems.AppendElement();
417 : }
418 0 : RefPtr<DataTransferItem> item = new DataTransferItem(mDataTransfer, aType);
419 0 : item->SetIndex(aIndex);
420 0 : item->SetPrincipal(aPrincipal);
421 0 : item->SetData(aData);
422 0 : item->SetChromeOnly(aHidden);
423 :
424 0 : mIndexedItems[aIndex].AppendElement(item);
425 :
426 : // We only want to add the item to the main mItems list if the index we are
427 : // adding to is 0, or the item we are adding is a file. If we add an item
428 : // which is not a file to a non-zero index, invariants could be broken.
429 : // (namely the invariant that there are not 2 non-file entries in the items
430 : // array with the same type).
431 : //
432 : // We also want to update our DataTransfer's type list any time we're adding a
433 : // KIND_FILE item, or an item at index 0.
434 0 : if (item->Kind() == DataTransferItem::KIND_FILE || aIndex == 0) {
435 0 : if (!aHidden) {
436 0 : mItems.AppendElement(item);
437 : }
438 0 : mDataTransfer->TypesListMayHaveChanged();
439 : }
440 :
441 0 : return item;
442 : }
443 :
444 : const nsTArray<RefPtr<DataTransferItem>>*
445 0 : DataTransferItemList::MozItemsAt(uint32_t aIndex) // -- INDEXED
446 : {
447 0 : if (aIndex >= mIndexedItems.Length()) {
448 0 : return nullptr;
449 : }
450 :
451 0 : return &mIndexedItems[aIndex];
452 : }
453 :
454 : void
455 0 : DataTransferItemList::PopIndexZero()
456 : {
457 0 : MOZ_ASSERT(mIndexedItems.Length() > 1);
458 0 : MOZ_ASSERT(mIndexedItems[0].IsEmpty());
459 :
460 0 : mIndexedItems.RemoveElementAt(0);
461 :
462 : // Update the index of every element which has now been shifted
463 0 : for (uint32_t i = 0; i < mIndexedItems.Length(); i++) {
464 0 : nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
465 0 : for (uint32_t j = 0; j < items.Length(); j++) {
466 0 : items[j]->SetIndex(i);
467 : }
468 : }
469 0 : }
470 :
471 : void
472 0 : DataTransferItemList::ClearAllItems()
473 : {
474 : // We always need to have index 0, so don't delete that one
475 0 : mItems.Clear();
476 0 : mIndexedItems.Clear();
477 0 : mIndexedItems.SetLength(1);
478 0 : mDataTransfer->TypesListMayHaveChanged();
479 :
480 : // Re-generate files (into an empty list)
481 0 : RegenerateFiles();
482 0 : }
483 :
484 : void
485 0 : DataTransferItemList::ClearDataHelper(DataTransferItem* aItem,
486 : uint32_t aIndexHint,
487 : uint32_t aMozOffsetHint,
488 : nsIPrincipal& aSubjectPrincipal,
489 : ErrorResult& aRv)
490 : {
491 0 : MOZ_ASSERT(aItem);
492 0 : if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
493 0 : return;
494 : }
495 :
496 0 : if (aItem->Principal() && !aSubjectPrincipal.Subsumes(aItem->Principal())) {
497 0 : aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
498 0 : return;
499 : }
500 :
501 : // Check if the aIndexHint is actually the index, and then remove the item
502 : // from aItems
503 : bool found;
504 0 : if (IndexedGetter(aIndexHint, found) == aItem) {
505 0 : mItems.RemoveElementAt(aIndexHint);
506 : } else {
507 0 : mItems.RemoveElement(aItem);
508 : }
509 :
510 : // Check if the aMozIndexHint and aMozOffsetHint are actually the index and
511 : // offset, and then remove them from mIndexedItems
512 0 : MOZ_ASSERT(aItem->Index() < mIndexedItems.Length());
513 0 : nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aItem->Index()];
514 0 : if (aMozOffsetHint < items.Length() && aItem == items[aMozOffsetHint]) {
515 0 : items.RemoveElementAt(aMozOffsetHint);
516 : } else {
517 0 : items.RemoveElement(aItem);
518 : }
519 :
520 0 : mDataTransfer->TypesListMayHaveChanged();
521 :
522 : // Check if we should remove the index. We never remove index 0.
523 0 : if (items.Length() == 0 && aItem->Index() != 0) {
524 0 : mIndexedItems.RemoveElementAt(aItem->Index());
525 :
526 : // Update the index of every element which has now been shifted
527 0 : for (uint32_t i = aItem->Index(); i < mIndexedItems.Length(); i++) {
528 0 : nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i];
529 0 : for (uint32_t j = 0; j < items.Length(); j++) {
530 0 : items[j]->SetIndex(i);
531 : }
532 : }
533 : }
534 :
535 : // Give the removed item the invalid index
536 0 : aItem->SetIndex(-1);
537 :
538 0 : if (aItem->Kind() == DataTransferItem::KIND_FILE) {
539 0 : RegenerateFiles();
540 : }
541 : }
542 :
543 : void
544 0 : DataTransferItemList::RegenerateFiles()
545 : {
546 : // We don't want to regenerate the files list unless we already have a files
547 : // list. That way we can avoid the unnecessary work if the user never touches
548 : // the files list.
549 0 : if (mFiles) {
550 : // We clear the list rather than performing smaller updates, because it
551 : // simplifies the logic greatly on this code path, which should be very
552 : // infrequently used.
553 0 : mFiles->Clear();
554 :
555 0 : DataTransferItemList::GenerateFiles(mFiles, mFilesPrincipal);
556 : }
557 0 : }
558 :
559 : void
560 0 : DataTransferItemList::GenerateFiles(FileList* aFiles,
561 : nsIPrincipal* aFilesPrincipal)
562 : {
563 0 : MOZ_ASSERT(aFiles);
564 0 : MOZ_ASSERT(aFilesPrincipal);
565 0 : uint32_t count = Length();
566 0 : for (uint32_t i = 0; i < count; i++) {
567 : bool found;
568 0 : RefPtr<DataTransferItem> item = IndexedGetter(i, found);
569 0 : MOZ_ASSERT(found);
570 :
571 0 : if (item->Kind() == DataTransferItem::KIND_FILE) {
572 0 : IgnoredErrorResult rv;
573 0 : RefPtr<File> file = item->GetAsFile(*aFilesPrincipal, rv);
574 0 : if (NS_WARN_IF(rv.Failed() || !file)) {
575 0 : continue;
576 : }
577 0 : aFiles->Append(file);
578 : }
579 : }
580 0 : }
581 :
582 : } // namespace mozilla
583 : } // namespace dom
|