RFC-0179:基本剪貼簿服務 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 提出基本剪貼簿服務的提案,讓使用者在不同元件之間安全地複製及貼上文字內容,不受執行元件影響。 |
問題 | |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2022-05-16 |
審查日期 (年-月-日) | 2020-07-18 |
摘要
本 RFC 介紹兩個新的架構提供者通訊協定 (fuchsia.ui.clipboard.Writer
和 fuchsia.ui.clipboard.Reader
),以及實作這些通訊協定的服務,讓使用者能夠在文字內容上執行複製和貼上作業。
提振精神
許多現代的使用者介面作業系統 (含圖形殼層) 提供剪貼簿功能 (請參閱先前技術),可讓使用者以互動方式將資料複製到系統提供的記憶體緩衝區或其他管道,並稍後將該資料貼到其他位置。
過去,Fuchsia 有一個簡單的剪貼簿通訊協定,實作方式為模組化代理程式,但這段程式碼已在 2019 年移除。
在這份 RFC 中,我們建議推出新的剪貼簿通訊協定和實作方式,讓 Fuchsia 產品可以選擇整合。最迫切的需求是複製和貼上萬國碼文字的功能,因此這將是第一個版本的焦點。
許多現有作業系統的剪貼簿設施最初設計時並未提供安全性防護措施,因此任何程序都可以在任何時間觀察和/或修改剪貼簿,而無須使用者察覺或意圖。在 Fuchsia 上,我們希望設計時能考量到安全性,因此採取以下做法:
- 依據最低權限原則,透過精細的功能保護剪貼簿存取權
- 嘗試限制剪貼簿存取權,取決於前景視窗中的輸入焦點 (在 Fuchsia 的 Scenic 術語中為「View」)
- 僅在萬不得已時提供背景剪貼簿存取權
相關人員
講師
davemoore@google.com
審查人員
Fuchsia HCI:neelsa@google.com、quiche@google.com
安全性:palmer@google.com
隱私權:enoharemaien@google.com
Chromium:wez@google.com
Flutter:jmccandless@google.com
諮詢對象
azaslavsky, carolineliu@google.com, chaopeng@google.com, cpu@google.com, ddorwin@google.com, fmil@google.com, jsankey@google.com, tjdetwiler@google.com
社交
- 在 Fuchsia Input Team 文件審查中討論
- 在 Fuchsia Security Office Hours 中討論
設計
權限等級
在圖形殼層環境中,剪貼簿的存取範圍可分為三個層級:
- 殼層介入,取決於焦點
元件只有在回應圖形殼層所判斷的明確使用者動作時,才會取得剪貼簿的存取權,且只有在元件目前有輸入焦點時才會取得。 - 焦點依附
只要元件擁有輸入焦點,就能隨時存取剪貼簿。 - 無限制
元件可隨時存取剪貼簿。
在本 RFC 中,我們只會介紹 (2) 焦點相關範圍。
目前尚未規劃範圍 (1) 和 (3) 的設計和實作方式,因此需要另行提出 RFC。
用途
針對初始 RFC,我們考慮了幾個簡單但常見的用途:
- 在網路瀏覽器中,將網頁內容中的網址複製到網址列
- 將殼層指令從網路瀏覽器複製到終端機
- 將資訊從網路瀏覽器複製到工作站產品的意見回饋對話方塊 (在 Flutter 中實作)
通訊協定和服務
我們在合作夥伴 SDK 中推出了兩種可探索的 FIDL 通訊協定:fuchsia.ui.clipboard.FocusedReaderRegistry
和 fuchsia.ui.clipboard.FocusedWriterRegistry
。這些通訊協定會由在工作階段領域中執行的新元件 clipboard.cm
實作及公開。這個元件會納入工作站產品,並可用於任何需要的 Fuchsia 產品。
獲授 FocusedWriterRegistry
和 FocusedReaderRegistry
功能的用戶端元件,將可分別要求 fuchsia.ui.clipboard.Writer
和 fuchsia.ui.clipboard.Reader
的例項。這些方法可隨時要求這些連線 (假設擁有有效的 ViewRef
),但如果用戶端的檢視畫面沒有輸入焦點,Writer
和 Reader
的方法就會傳回錯誤。
library fuchsia.ui.clipboard;
/// A protocol that allows graphical clients that own
/// [`ViewRef`s](https://cs.opensource.google/fuchsia/fuchsia/+/main:{file}/src/development/graphics/scenic/concepts/view_ref) to request read ("paste")
/// access to the clipboard. Clients can register for access at any time, but `GetItem` calls will
/// only succeed while the view has input focus.
@discoverable
protocol FocusedReaderRegistry {
/// If the `ViewRef` is valid, the clipboard server will allow the client to send commands using
/// the given `Reader`. If the `ViewRef` later becomes invalid, the `Reader`'s channel will be
/// closed.
RequestReader(resource table {
1: view_ref fuchsia.ui.views.ViewRef;
2: reader_request server_end:Reader;
}) -> (table {}) error ClipboardError;
};
/// A protocol that allows graphical clients that own `ViewRef`s to request write ("copy") access to
/// the clipboard. Clients can register for access at any time, but `SetItem` calls will only
/// succeed while the view has input focus.
@discoverable
protocol FocusedWriterRegistry {
/// If the `ViewRef` is valid, the clipboard server will allow the client to send commands using
/// the given `Writer`. If the `ViewRef` later becomes invalid, the `Writer`'s channel will be
/// closed.
RequestWriter(resource table {
1: view_ref fuchsia.ui.views.ViewRef;
2: writer_request server_end:Writer;
}) -> (table {}) error ClipboardError;
};
/// Allows data to be read from the clipboard, i.e. pasted.
protocol Reader {
/// Reads a single item from the clipboard. If the client's `View` does not have input focus, an
/// error will be returned. If there is no item on the clipboard, `ClipboardError.EMPTY` will
/// be returned.
GetItem(table {}) -> (ClipboardItem) error ClipboardError;
};
/// Allows data to be written to the clipboard, i.e. copied.
protocol Writer {
/// Writes a single item to the clipboard. If the client's `View` does not have input focus, an
/// error will be returned.
SetItem(ClipboardItem) -> (table {}) error ClipboardError;
/// Clears the contents of the clipboard. If the client's `View` does not have input focus, an
/// error will be returned.
Clear(table {}) -> (table {}) error ClipboardError;
};
/// Set of errors that can be returned by the clipboard server.
type ClipboardError = flexible enum {
/// An internal error occurred. All the client can do is try again later.
INTERNAL = 1;
/// The clipboard was empty, or the requested item(s) were not present on the clipboard.
EMPTY = 2;
/// The client sent an invalid request, e.g. missing requiring fields.
INVALID_REQUEST = 3;
/// The client sent the server an invalid `ViewRef` or a `ViewRef` that is already associated
/// with another client.
INVALID_VIEW_REF = 4;
/// The client attempted to perform an operation that requires input focus, at a moment when
/// it did not have input focus. The client should wait until it has focus again before
/// retrying.
UNAUTHORIZED = 5;
};
在初始版本中,剪貼簿只支援複製及貼上大小不超過 32 KB 的 UTF-8 字串。用戶端可以為資料指定MIME 類型;預設值為 "text/plain;charset=UTF-8"
。
後續修訂版本將新增對 VMOs 的支援,可複製及貼上任意資料。
/// The maximum length of a plain-text clipboard item in bytes. Although FIDL messages support
/// larger messages, this limit allows space to be reserved for potential other fields in the
/// message. Larger payloads will be supported by VMOs in `ClipboardItemData` in future revisions.
const MAX_TEXT_LENGTH uint32 = 32768;
/// The maximum length of a MIME Type identifier. Per
/// [IETF RFC 4288](https://datatracker.ietf.org/doc/html/rfc4288#section-4.2), a MIME type may have
/// up to 127 characters before and 127 characters after the slash, for a total of 255.
const MAX_MIME_TYPE_LENGTH uint32 = 255;
/// A single item on the clipboard, consisting of a MIME type hint and a payload.
type ClipboardItem = resource table {
/// MIME type of the data, according to the client that placed the data on the clipboard.
/// *Note:* The clipboard service does not validate clipboard items and does not guarantee that
/// they conform to the given MIME type's specifications.
1: mime_type_hint string:MAX_MIME_TYPE_LENGTH;
/// The payload of the clipboard item.
2: payload ClipboardItemData;
};
/// The payload of a `ClipboardItem`. Future expansions will support additional transport formats.
type ClipboardItemData = flexible resource union {
/// A UTF-8 string.
1: text string:MAX_TEXT_LENGTH;
};
實作
這會分成以下幾個階段:
- 提交新的
fuchsia.ui.clipboard
FIDL 程式庫 (如上方預覽畫面所示),以便進行 API 審查。 - 在工作階段領域中執行新的剪貼簿伺服器元件,以便公開
fuchsia.ui.clipboard.FocusedWriterRegistry
和fuchsia.ui.clipboard.FocusedReaderRegistry
通訊協定。 - 透過管理 Scenic 檢視畫面的簡單元件,示範如何整合新通訊協定。
- 將新通訊協定的支援功能整合至 Chromium 和 Flutter 執行器。
成效
新增服務會使用額外的二進位檔儲存空間,以及二進位檔和剪貼簿內容的記憶體。每個註冊剪貼簿存取權的用戶端都會保留開放的 Zircon 管道,因此會消耗資源。
安全性考量
必須進行安全性審查。
跨元件通訊
剪貼簿服務的推出,構成了新的跨元件通訊管道。這會讓元件有意或無意地利用彼此的安全漏洞。
不受信任的內容
剪貼簿服務不保證 ClipboardItem
資料或 MIME 類型提示的可信度。因此,用戶端不應信任收到的資料,而應驗證資料是否適合其用途。
特別是,用戶端應在權限較低的「沙箱」程序中,以比 C/C++ 更安全的程式設計語言,執行任何複雜格式的剖析、解讀或轉換作業。詳情請參閱「2 的規則」。
針對 ClipboardItemData.text
變化版本,UTF-8 驗證會由每個用戶端 (以及剪貼簿服務) 使用的 FIDL 程式庫自動執行。
不過,即使使用有效的 UTF-8 純文字,如果一個元件可透過剪貼簿將任意文字傳送給另一個元件,就可能導致各種攻擊途徑,包括:
- 文字小工具的溢位錯誤
- 文字轉譯堆疊中的錯誤
- 同形字攻擊 (以視覺上相似但實際上不同的字元欺騙使用者,例如將使用者偷偷導向到網路釣魚網域)
- 應用程式專屬文字剖析錯誤
- 非預期的程式碼貼到指令提示中
對於處理或顯示任何第三方或使用者提供內容的應用程式而言,這些問題大多已是個問題,但剪貼簿則會帶來額外的挑戰。惡意應用程式可在回應有效的使用者複製指令時,將意外資料 (未明確選取) 放入剪貼簿,欺騙使用者在貼上時,以混淆代理人的身份行事。
未經授權存取
如上文所述,元件可能會在未經授權或使用者不知情的情況下,讀取或寫入剪貼簿。
您可以採取下列做法來緩解這項問題:
- 可用於精細授予或拒絕複製和貼上功能的通訊協定
- 在
fuchsia.ui.clipboard.Focused*
通訊協定中,要求僅向具有輸入焦點的前景檢視畫面授予剪貼簿存取權
在剪貼簿 API 的未來擴充功能中,我們可能會提供觀察剪貼簿 API 事件的通訊協定,系統殼層可使用這項通訊協定,在每次存取剪貼簿時顯示視覺通知。
日後,隨著不受信任的元件和新的剪貼簿用途的加入,我們必須重新考慮未經授權的讀取嘗試是否應以靜默方式傳回空白剪貼簿項目,而非 ClipboardError.UNAUTHORIZED
,以減少公開的剪貼簿存取資訊。
ViewRef
和焦點驗證
剪貼簿服務會使用 Scenic 的焦點鏈系統,判斷目前聚焦的檢視畫面,並因此擁有存取剪貼簿的權限。因此,剪貼簿對剪貼簿焦點的判斷只與 Scenic 對輸入焦點的判斷一樣可靠,但後者有以下缺陷:
ViewRef
可組成焦點鏈結,可輕鬆複製並從一個元件傳送至另一個元件。透過這個機制,惡意元件可以合作,為了取得輸入焦點而互相冒用。(不過,這項功能至少需要ViewRef
的原始擁有者信任複製的ViewRef
收件者)。- 焦點變更會受到競爭條件的影響。
安全性審查結果
- 這個 MVP API 受到限制,因此攻擊管道也受到限制。
- 我們會在剪貼簿服務收到 UTF-8 字串時,依賴基礎的 FIDL 反序列化功能來正確驗證字串。這是一個攻擊途徑,但我們認為這項服務的字串反序列化功能仰賴 Rust 的
std::str::String
實作,而這項功能具有記憶體安全性,且在 Fuchsia 和更廣泛的 Rust 生態系統中都經過大量測試。 - 我們認為
ViewRef
解決方案是追蹤檢視焦點和使用者意圖的良好基礎,但請注意上述限制。
隱私權注意事項
需要進行隱私權審查。
未經授權的貼上
未經授權存取剪貼簿內容會造成隱私權風險,因為惡意元件可能會取得使用者在鍵盤上放置的任何私人資料。如上文所述,fuchsia.ui.clipboard.Focused*
通訊協定會要求檢視畫面至少具備輸入焦點 (因此會在前景中顯示),以便存取剪貼簿,藉此降低這類風險。不過,這也表示應用程式只要獲得短暫的焦點,就能立即擷取剪貼簿的內容,即使這不是使用者的意圖也一樣。在未來,系統殼層通知會在元件讀取剪貼簿內容時通知使用者。
旁路攻擊
在日後的剪貼簿服務版本中,在新增任意資料類型和長度後,記憶體分析可能會揭露可能暗示內容的資訊 (例如剪貼簿緩衝區大小)。
剪貼簿內容的持久性
在這個階段,系統僅支援複製短字串,因此剪貼簿內容會儲存在記憶體中,而非磁碟上。
系統不會記錄剪貼簿內容,也不會透過檢查功能公開這些內容。
跨安全性情境存取
Fuchsia 產品的 UI 元素可能會在不同的安全性環境中執行,例如代表不同的使用者,或在驗證前環境中執行。Fuchsia 必須防止剪貼簿內容跨安全性內容區隔共用。舉例來說,如果登入的使用者複製密碼,然後鎖定螢幕,就無法將密碼貼到鎖定畫面對話方塊中。
您可以在每個安全性情境中執行剪貼簿服務的不同例項,藉此實現這種分隔。
測試
這項功能將透過單元測試和整合測試進行測試:
- 剪貼簿服務中的單元測試
- 剪貼簿服務的整體整合測試
- 針對剪貼簿服務、Scenic、輸入管道和 Flutter 或 Chromium 執行程式之間的互動進行整合測試。
說明文件
fuchsia.ui.clipboard
API 將使用 fidldoc 編寫說明文件。
我們會透過附有詳細註解的簡單元件,說明如何使用此通訊協定,該元件可用於管理 Scenic 檢視畫面 (請參閱「實作」一節)。
缺點、替代方案和未知事項
在面向使用者的作業系統上,沒有可行的替代方案可提供系統級剪貼簿服務。這項功能無法單獨由執行工具提供,因為執行工具無法在不同執行階段之間複製及貼上資料。
在上述設計選項中,另一種做法可能是從更嚴格的「殼層中介、焦點依附」剪貼簿通訊協定開始,或從完全不受限制的通訊協定開始 (請參閱「存取層級」)。
雖然殼層介接方法更安全,但可能過於嚴格,不適合許多用途。舉例來說,
- 禁止應用程式在內容功能表中提供複製和貼上指令
- 會干擾以 Chromium 為基礎的執行器中網路剪貼簿 API 的功能
雖然不受限制的做法對某些特殊應用程式很有幫助,但會造成過多隱私權風險,因此不建議將其設為預設值。
我們會從根據輸入焦點設定的中間存取層級著手,這樣就能:
- 一開始就優先保障剪貼簿服務的部分安全性和隱私權保證
- 鼓勵在 Fuchsia 剪貼簿的執行程式整合中採用最低權限原則
- 避免過於嚴格的限制,以便實際整合現有的執行工具
日後的作業
提供觀察剪貼簿 API 事件 (讀取和寫入) 的通訊協定,系統殼層可在存取剪貼簿時使用此通訊協定,顯示視覺通知。
擴充支援的資料格式和酬載大小集合,特別是透過使用傳送用途用戶端擁有的 VMOs。
既有技術與參考資料
紫紅色
Fuchsia 先前有一個最小剪貼簿 API,以模組化架構代理程式實作,可讓任何具有 fuchsia.modular.Clipboard
能力的元件儲存或擷取 UTF-8 字串。(這項功能已在 2019 年 11 月刪除)。
Linux:X11
X11 提供多個稱為「選取項目」的內容儲存區,其中最常見的是 CLIPBOARD
和 PRIMARY
(隱含文字選取項目的剪貼簿)。
傳送應用程式會向 X 伺服器宣告,在特定視窗 (XSetSelectionOwner()
) 中,它「擁有」其中一個選項,並以特定資料格式呈現。然後,它會等待後續事件。
接收應用程式會在其中一個視窗中要求將所選內容轉換為支援的特定格式 (XConvertSelection()
)。
X 伺服器會將要求轉送至傳送應用程式,如果該應用程式支援要求的格式,就會透過 X 伺服器將資料傳送至接收應用程式,以便回應。如果內容很大,則必須切割成最多 256 KB 的片段。
如果來源視窗遭到銷毀,選取項目就會遺失,因此在實際操作中,(1) 大多數應用程式會在使用者不會關閉的隱藏視窗中保留選取項目,(2) 常見的 Linux 發行版本會納入剪貼簿管理工具,該工具會取得選取項目的擁有權,即使原始擁有應用程式已離開,也能保留選取項目。
詳情請參閱 https://www.uninformativ.de/blog/postings/2017-04-02/0/POSTING-zh-TW.html。
Linux:Wayland
傳送應用程式 (必須處於聚焦狀態) 會通知合成器,表示該應用程式有 wl_data_source
、指出資料來源支援哪些 MIME 類型,以及註冊事件監聽器。然後等待 send
事件。
接收應用程式必須在嘗試貼上時將焦點轉移至該應用程式,並監聽資料 offer
事件,以判斷是否已填入剪貼簿。當它想要貼上時,會呼叫 wl_data_offer_receive
,並傳入要求的 MIME 類型和檔案描述符 (通常是管道的寫入端)。
發送應用程式會收到 send
事件,並將事件寫入指定的檔案描述元;接收應用程式會讀取另一端。
詳情請參閱 https://emersion.fr/blog/2020/wayland-clipboard-drag-and-drop/。
Windows (win32)
您可以呼叫 OpenClipboard()
並傳入目前視窗的控制代碼,取得系統剪貼簿。發送應用程式會呼叫 EmptyClipboard()
來清除任何現有資料,然後呼叫 SetClipboardData()
,並傳入整數資料類型 ID 和資料本身。傳送資料的記憶體必須使用 GlobalAlloc()
分配。
有幾種標準的剪貼簿資料類型;此外,您也可以為自訂全域格式呼叫 RegisterClipboardFormat()
(似乎會持續存在,直到重新啟動為止),或是使用特定範圍內的 ID 來表示私人剪貼簿格式。對於非私密格式,作業系統會取得傳入的物件擁有權,並負責最終銷毀該物件;對於私密格式,來源視窗仍會在剪貼簿銷毀時負責清理。針對延遲格式轉換,來源視窗可以將 NULL
資料值傳遞至 SetClipboardData()
,稍後再根據 WM_RENDERFORMAT
的回應,算繪所要求的格式,並將預留位置替換為另一個對 SetClipboardData()
的呼叫。建議開發人員盡可能以多種格式設定剪貼簿資料。
接收應用程式也會擷取全域剪貼簿的句柄,用於其視窗,檢查可用格式清單 (包括傳送應用程式明確放置的格式,以及作業系統提供的自動轉換格式),呼叫 GetClipboardData()
以取得特定格式的剪貼簿物件句柄,然後 GlobalLock()
鎖定該全域資源,並取得其內容的存取權。
系統也提供方法,讓視窗註冊監控剪貼簿內容的變更。
詳情請參閱 https://docs.microsoft.com/zh-TW/windows/win32/dataxchg/using-the-clipboard 和 https://docs.microsoft.com/zh-TW/windows/win32/dataxchg/clipboard-operations。
Android
傳送應用程式會建立 ClipData
物件,其中包含支援的 MIME 類型清單,並在 ClipData
中填入一或多個項目,這些項目可以是字串、指向任何資料的內容 URI,或意圖 (適用於應用程式捷徑)。接著,傳送應用程式會取得全域 ClipboardManager
物件的參照,並將 ClipData
物件傳遞至 setPrimaryClip()
。
如果複製內容 URI,傳送應用程式必須匯出可為該 URI 提供資料的 ContentProvider
。
接收應用程式會取得全域 ClipboardManager
的參照,檢查是否有主要剪輯片段,然後檢查是否支援任何 ClipData.Item
的資料類型。如果貼上純文字字串,接收應用程式可以直接呼叫 getText()
。如果是從內容 URI 貼上資料,接收應用程式必須建立 ContentResolver
例項,並使用指定的 URI 進行 query()
作業,然後從傳回的 Cursor
擷取資料。
自 Android 12 起,當一個應用程式存取另一個應用程式傳送的 ClipData
時,作業系統會顯示浮動式訊息。
詳情請參閱 https://developer.android.com/guide/topics/text/copy-paste。
MacOS
系統層級的「剪貼簿」是透過 NSPasteboard.general
欄位存取。
傳送應用程式會將實作 NSPasteboardWriting
通訊協定的物件陣列,傳遞至 writeObjects()
方法,藉此複製項目。實作者包括字串和其他常見資料類型,以及 NSPasteboardItem
,可做為自訂資料類型的包裝函式。NSPasteboardWriting
會提供支援的統一類型 ID (UTI,Apple 的 MIME 類型等同物) 清單,以及資料是否可立即使用或「承諾」。相應地,NSPasteboardItem
可以直接包裝資料或資料提供者。
在接收端,應用程式可以查詢一般 NSPasteboard
,以取得可讀取的類型,包括可由篩選器服務自動轉換的類型。接著,它可以選擇讀取剪貼板上儲存的所有或部分項目。
詳情請參閱 https://developer.apple.com/documentation/appkit/nspasteboard。
iOS
iOS 剪貼簿 API 與 macOS 的剪貼簿 API 類似。系統層級剪貼簿可透過 UIPasteboard.general
存取。
如要傳送,您可以使用多種方法將一或多個標示為 UTI 類型的項目新增至剪貼簿。您也可以插入 NSItemProviders
,讓系統以延遲方式提供值。為方便起見,在 UIPasteboard
例項中,幾種標準資料類型會獲得可讀/可寫的陣列屬性:strings
、images
、urls
和 colors
,以及這些屬性的單一版本,用於存取每種類型的首個項目。
在接收端,您可以依索引或類型擷取任何選取的項目。
自 iOS 14 起,擷取其他應用程式放置在剪貼簿的內容會觸發系統通知。為減少在實際貼上內容前出現的錯誤通知,iOS 讓用戶端能夠在不存取資料的情況下,查詢剪貼簿 (hasStrings
、hasImages
) 是否含有特定資料類型。
詳情請參閱 https://developer.apple.com/documentation/uikit/uipasteboard。
Web API
雖然網頁上的剪貼簿互動主要是由網頁瀏覽器本身處理 (視各作業系統的特性而定),但也有 JavaScript API 可供使用,讓網頁能與剪貼簿互動,而不必仰賴直接的使用者指令。
舊版 ClipboardEvent
API 允許指令碼監聽 DOM Element
上的 "cut"
、"copy"
或 "paste"
事件,然後存取事件的 clipboardData
欄位,以便根據 MIME 類型呼叫 setData
或 getData
。您也可以透過程式輔助功能,對目前聚焦的元素叫用 "cut"
、"copy"
或 "paste"
。基於隱私權考量,在 "cut"
和 "copy"
事件中,系統無法讀取剪貼簿內容,因此無法以程式輔助方式貼上內容。
我們現在提供新的非同步 Clipboard
API,並受到個別網站使用者權限的保護。如果使用者授予權限,指令碼就能存取 navigator.clipboard
,然後存取 writeText()
或 readText()
,或是 write()
ClipboardItem
,其中包含以 MIME 類型做為索引的一或多個 blob。(非圖片 MIME 類型在部分瀏覽器中仍屬於實驗性質)。
詳情請參閱 https://whatwebcando.today/clipboard.html 和 https://developer.mozilla.org/en-US/docs/Web/API/Clipboard。
ChromeOS
Chrome 擴充功能可以使用上述的剪貼簿 API,但須取得權限。