2 Copyright (C) 2017 Kai Uwe Broulik <kde@privat.broulik.de>
3 Copyright (C) 2018 David Edmundson <davidedmundson@kde.org>
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License as
7 published by the Free Software Foundation; either version 3 of
8 the License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
21 // from https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
22 function generateGuid() {
23 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
24 const r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
25 return v.toString(16);
29 function addCallback(subsystem, action, callback)
31 if (!callbacks[subsystem]) {
32 callbacks[subsystem] = {};
34 callbacks[subsystem][action] = callback;
37 function executeScript(script) {
38 var element = document.createElement('script');
39 element.innerHTML = '('+ script +')();';
40 (document.body || document.head || document.documentElement).appendChild(element);
41 // We need to remove the script tag after inserting or else websites relying on the order of items in
42 // document.getElementsByTagName("script") will break (looking at you, Google Hangouts)
43 element.parentNode.removeChild(element);
46 chrome.runtime.onMessage.addListener(function (message, sender) {
47 // TODO do something with sender (check privilige or whatever)
49 var subsystem = message.subsystem;
50 var action = message.action;
52 if (!subsystem || !action) {
56 if (callbacks[subsystem] && callbacks[subsystem][action]) {
57 callbacks[subsystem][action](message.payload);
61 SettingsUtils.get().then((items) => {
62 if (items.breezeScrollBars.enabled) {
63 loadBreezeScrollBars();
66 const mpris = items.mpris;
68 const origin = window.location.origin;
70 const websiteSettings = mpris.websiteSettings || {};
72 let mprisAllowed = true;
73 if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") {
74 mprisAllowed = MPRIS_WEBSITE_SETTINGS[origin];
76 if (typeof websiteSettings[origin] === "boolean") {
77 mprisAllowed = websiteSettings[origin];
82 if (items.mprisMediaSessions.enabled) {
83 loadMediaSessionsShim();
88 if (items.purpose.enabled) {
89 sendMessage("settings", "getSubsystemStatus").then((status) => {
90 if (status && status.purpose) {
94 // No warning, can also happen when port isn't connected for unsupported OS
95 console.log("Failed to get subsystem status for purpose", err);
100 // BREEZE SCROLL BARS
101 // ------------------------------------------------------------------------
103 function loadBreezeScrollBars() {
108 if (!document.head) {
112 // You cannot access cssRules for <link rel="stylesheet" ..> on a different domain.
113 // Since our chrome-extension:// URL for a stylesheet would be, this can
114 // lead to problems in e.g modernizr, so include the <style> inline instead.
115 // "Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules"
116 var styleTag = document.createElement("style");
117 styleTag.appendChild(document.createTextNode(`
118 html::-webkit-scrollbar {
119 /* we'll add padding as "border" in the thumb*/
125 html::-webkit-scrollbar-track {
127 border: 7px solid white; /* FIXME why doesn't "transparent" work here?! */
128 background-color: white;
129 width: 6px !important; /* 20px scrollbar - 2 * 7px border */
130 box-sizing: content-box;
132 html::-webkit-scrollbar-track:hover {
133 background-color: #BFC0C2;
136 html::-webkit-scrollbar-thumb {
137 background-color: #3DAEE9; /* default blue breeze color */
138 border: 7px solid transparent;
140 background-clip: content-box;
141 width: 6px !important; /* 20px scrollbar - 2 * 7px border */
142 box-sizing: content-box;
145 html::-webkit-scrollbar-thumb:window-inactive {
146 background-color: #949699; /* when window is inactive it's gray */
148 html::-webkit-scrollbar-thumb:hover {
149 background-color: #93CEE9; /* hovered is a lighter blue */
152 html::-webkit-scrollbar-corner {
153 background-color: white; /* FIXME why doesn't "transparent" work here?! */
157 document.head.appendChild(styleTag);
162 // ------------------------------------------------------------------------
165 // also give the function a "random" name as we have to have it in global scope to be able
166 // to invoke callbacks from outside, UUID might start with a number, so prepend something
167 const mediaSessionsClassName = "f" + generateGuid().replace(/-/g, "");
170 // When a player has no duration yet, we'll wait for it becoming known
171 // to determine whether to ignore it (short sound) or make it active
172 var pendingActivePlayer;
173 var playerMetadata = {};
174 var playerCallbacks = [];
176 // Playback state communicated via media sessions api
177 var playerPlaybackState = "";
181 var pendingSeekingUpdate = 0;
183 var titleTagObserver = null;
184 var oldPageTitle = "";
186 addCallback("mpris", "play", function () {
190 addCallback("mpris", "pause", function () {
194 addCallback("mpris", "playPause", function () {
196 if (activePlayer.paused) { // TODO take into account media sessions playback state
204 addCallback("mpris", "stop", function () {
205 // When available, use the "stop" media sessions action
206 if (playerCallbacks.indexOf("stop") > -1) {
210 ${mediaSessionsClassName}.executeCallback("stop");
212 console.warn("Exception executing 'stop' media sessions callback", e);
219 // otherwise since there's no "stop" on the player, simulate it be rewinding and reloading
221 activePlayer.pause();
222 activePlayer.currentTime = 0;
223 // calling load() now as is suggested in some "how to fake video Stop" code snippets
224 // utterly breaks stremaing sites
225 //activePlayer.load();
227 // needs to be delayed slightly otherwise we pause(), then send "stopped", and only after that
228 // the "paused" signal is handled and we end up in Paused instead of Stopped state
229 setTimeout(function() {
230 sendMessage("mpris", "stopped");
236 addCallback("mpris", "next", function () {
237 if (playerCallbacks.indexOf("nexttrack") > -1) {
241 ${mediaSessionsClassName}.executeCallback("nexttrack");
243 console.warn("Exception executing 'nexttrack' media sessions callback", e);
250 addCallback("mpris", "previous", function () {
251 if (playerCallbacks.indexOf("previoustrack") > -1) {
255 ${mediaSessionsClassName}.executeCallback("previoustrack");
257 console.warn("Exception executing 'previoustrack' media sessions callback", e);
264 addCallback("mpris", "setFullscreen", (message) => {
266 if (message.fullscreen) {
267 activePlayer.requestFullscreen();
269 document.exitFullscreen();
274 addCallback("mpris", "setPosition", function (message) {
276 activePlayer.currentTime = message.position;
280 addCallback("mpris", "setPlaybackRate", function (message) {
282 activePlayer.playbackRate = message.playbackRate;
286 addCallback("mpris", "setVolume", function (message) {
288 activePlayer.volume = message.volume;
289 activePlayer.muted = (message.volume == 0.0);
293 addCallback("mpris", "setLoop", function (message) {
295 activePlayer.loop = message.loop;
299 addCallback("mpris", "identify", function (message) {
301 // We don't have a dedicated "send player info" callback, so we instead send a "playing"
302 // and if we're paused, we'll send a "paused" event right after
303 // TODO figure out a way how to add this to the host without breaking compat
305 var paused = activePlayer.paused;
306 playerPlaying(activePlayer);
308 playerPaused(activePlayer);
313 function playerPlaying(player) {
314 setPlayerActive(player);
317 function playerPaused(player) {
318 sendPlayerInfo(player, "paused");
321 function setPlayerActive(player) {
322 pendingActivePlayer = player;
324 if (isNaN(player.duration)) {
325 // Ignore this player for now until we know a duration
326 // In durationchange event handler we'll check for this and end up here again
330 // Ignore short sounds, they are most likely a chat notification sound
331 // A stream has a duration of Infinity
332 // Note that "NaN" is also not finite but we already returned earlier for that
333 if (isFinite(player.duration) && player.duration > 0 && player.duration < 8) {
337 pendingActivePlayer = undefined;
338 activePlayer = player;
340 // when playback starts, send along metadata
341 // a website might have set Media Sessions metadata prior to playing
342 // and then we would have ignored the metadata signal because there was no player
343 sendMessage("mpris", "playing", {
344 mediaSrc: player.currentSrc || player.src,
345 pageTitle: document.title,
346 poster: player.poster,
347 duration: player.duration,
348 currentTime: player.currentTime,
349 playbackRate: player.playbackRate,
350 volume: player.volume,
353 metadata: playerMetadata,
354 callbacks: playerCallbacks,
355 fullscreen: document.fullscreenElement !== null,
356 canSetFullscreen: player.tagName.toLowerCase() === "video"
359 if (!titleTagObserver) {
361 // Observe changes to the <title> tag in case it is updated after the player has started playing
362 let titleTag = document.querySelector("head > title");
364 oldPageTitle = titleTag.innerText;
366 titleTagObserver = new MutationObserver((mutations) => {
367 mutations.forEach((mutation) => {
368 const pageTitle = mutation.target.textContent;
369 if (pageTitle && oldPageTitle !== pageTitle) {
370 sendMessage("mpris", "titlechange", {
374 oldPageTitle = pageTitle;
378 titleTagObserver.observe(titleTag, {
379 childList: true, // text content is technically a child node
387 function sendPlayerGone() {
388 var playerIdx = players.indexOf(activePlayer);
389 if (playerIdx > -1) {
390 players.splice(playerIdx, 1);
393 activePlayer = undefined;
394 pendingActivePlayer = undefined;
396 playerCallbacks = [];
397 sendMessage("mpris", "gone");
399 if (titleTagObserver) {
400 titleTagObserver.disconnect();
401 titleTagObserver = null;
405 function sendPlayerInfo(player, event, payload) {
406 if (player != activePlayer) {
410 sendMessage("mpris", event, payload);
413 function registerPlayer(player) {
414 if (players.indexOf(player) > -1) {
415 //console.log("Already know", player);
419 // auto-playing player, become active right away
420 if (!player.paused) {
421 playerPlaying(player);
423 player.addEventListener("play", function () {
424 playerPlaying(player);
427 player.addEventListener("pause", function () {
428 playerPaused(player);
431 // what about "stalled" event?
432 player.addEventListener("waiting", function () {
433 sendPlayerInfo(player, "waiting");
436 // playlist is now empty or being reloaded, stop player
437 // e.g. when using Ajax page navigation and the user nagivated away
438 player.addEventListener("emptied", function () {
439 // When the player is emptied but the website tells us it's just "paused"
440 // keep it around (Bug 402324: Soundcloud does this)
441 if (player === activePlayer && playerPlaybackState === "paused") {
445 // could have its own signal but for compat it's easier just to pretend to have stopped
446 sendPlayerInfo(player, "stopped");
449 // opposite of "waiting", we finished buffering enough
450 // only if we are playing, though, should we set playback state back to playing
451 player.addEventListener("canplay", function () {
452 if (!player.paused) {
453 sendPlayerInfo(player, "canplay");
457 player.addEventListener("timeupdate", function () {
458 sendPlayerInfo(player, "timeupdate", {
459 currentTime: player.currentTime
463 player.addEventListener("ratechange", function () {
464 sendPlayerInfo(player, "ratechange", {
465 playbackRate: player.playbackRate
469 // TODO use player.seekable for determining whether we can seek?
470 player.addEventListener("durationchange", function () {
471 // Deferred active due to unknown duration
472 if (pendingActivePlayer == player) {
473 setPlayerActive(pendingActivePlayer);
477 sendPlayerInfo(player, "duration", {
478 duration: player.duration
482 player.addEventListener("seeking", function () {
483 if (pendingSeekingUpdate) {
487 // Compress "seeking" signals, this is invoked continuously as the user drags the slider
488 pendingSeekingUpdate = setTimeout(function() {
489 pendingSeekingUpdate = 0;
492 sendPlayerInfo(player, "seeking", {
493 currentTime: player.currentTime
497 player.addEventListener("seeked", function () {
498 sendPlayerInfo(player, "seeked", {
499 currentTime: player.currentTime
503 player.addEventListener("volumechange", function () {
504 sendPlayerInfo(player, "volumechange", {
505 volume: player.volume,
510 players.push(player);
513 function findAllPlayersFromNode(node) {
514 if (typeof node.getElementsByTagName !== "function") {
518 return [...node.getElementsByTagName("video"), ...node.getElementsByTagName("audio")];
522 function registerAllPlayers() {
523 var players = findAllPlayersFromNode(document);
524 players.forEach(registerPlayer);
527 function playerPlay() {
528 // if a media sessions callback is registered, it takes precedence over us manually messing with the player
529 if (playerCallbacks.indexOf("play") > -1) {
533 ${mediaSessionsClassName}.executeCallback("play");
535 console.warn("Exception executing 'play' media sessions callback", e);
539 } else if (activePlayer) {
544 function playerPause() {
545 if (playerCallbacks.indexOf("pause") > -1) {
549 ${mediaSessionsClassName}.executeCallback("pause");
551 console.warn("Exception executing 'pause' media sessions callback", e);
555 } else if (activePlayer) {
556 activePlayer.pause();
560 function loadMpris() {
561 // TODO figure out somehow when a <video> tag is added dynamically and autoplays
562 // as can happen on Ajax-heavy pages like YouTube
563 // could also be done if we just look for the "audio playing in this tab" and only then check for player?
564 // cf. "checkPlayer" event above
566 var observer = new MutationObserver(function (mutations) {
567 mutations.forEach(function (mutation) {
568 mutation.addedNodes.forEach(function (node) {
569 if (typeof node.matches !== "function") {
573 // Check whether the node itself or any of its children is a player
574 var players = findAllPlayersFromNode(node);
575 if (node.matches("video,audio")) {
576 players.unshift(node);
579 players.forEach(function (player) {
580 registerPlayer(player);
584 mutation.removedNodes.forEach(function (node) {
585 if (typeof node.matches !== "function") {
589 // Check whether the node itself or any of its children is the current player
590 var players = findAllPlayersFromNode(node);
591 if (node.matches("video,audio")) {
592 players.unshift(node);
595 players.forEach(function (player) {
596 if (player == activePlayer) {
597 // If the player is still in the visible DOM, don't consider it gone
598 if (document.body.contains(player)) {
602 // If the player got temporarily added by us, don't consider it gone
603 if (player.dataset.pbiPausedForDomRemoval === "true") {
615 window.addEventListener("pagehide", function () {
616 // about to navigate to a different page, tell our extension that the player will be gone shortly
617 // we listen for tab closed in the extension but we don't for navigating away as URL change doesn't
618 // neccesarily mean a navigation.
619 // NOTE beforeunload is not emitted for iframes!
623 // In some cases DOMContentLoaded won't fire, e.g. when watching a video file directly in the browser
624 // it generates a "video player" page for you but won't fire the event
625 registerAllPlayers();
627 document.addEventListener("DOMContentLoaded", function() {
628 registerAllPlayers();
630 observer.observe(document, {
636 document.addEventListener("fullscreenchange", () => {
638 sendPlayerInfo(activePlayer, "fullscreenchange", {
639 fullscreen: document.fullscreenElement !== null
645 // This adds a shim for the Chrome media sessions API which is currently only supported on Android
646 // Documentation: https://developers.google.com/web/updates/2017/02/media-session
647 // Try it here: https://googlechrome.github.io/samples/media-session/video.html
649 // Bug 379087: Only inject this stuff if we're a proper HTML page
650 // otherwise we might end up messing up XML stuff
651 // only if our documentElement is a "html" tag we'll do it
652 // the rest is only set up in DOMContentLoaded which is only executed for proper pages anyway
654 // tagName always returned "HTML" for me but I wouldn't trust it always being uppercase
655 function loadMediaSessionsShim() {
656 if (document.documentElement.tagName.toLowerCase() === "html") {
658 window.addEventListener("pbiMprisMessage", (e) => {
659 let data = e.detail || {};
661 let action = data.action;
662 let payload = data.payload;
668 if (typeof payload !== "object") {
672 playerMetadata = payload;
673 sendMessage("mpris", "metadata", payload);
677 case "playbackState":
678 if (!["none", "paused", "playing"].includes(payload)) {
682 playerPlaybackState = payload;
688 if (playerPlaybackState === "playing") {
689 playerPlaying(activePlayer);
690 } else if (playerPlaybackState === "paused") {
691 playerPaused(activePlayer);
697 if (Array.isArray(payload)) {
698 playerCallbacks = payload;
700 playerCallbacks = [];
702 sendMessage("mpris", "callbacks", playerCallbacks);
710 ${mediaSessionsClassName}_constructor = function() {
712 this.pendingCallbacksUpdate = 0;
713 this.metadata = null;
714 this.playbackState = "none";
716 this.sendMessage = function (action, payload) {
717 let event = new CustomEvent("pbiMprisMessage", {
723 window.dispatchEvent(event);
726 this.executeCallback = function (action) {
729 // for seekforward, seekbackward, seekto there's additional information one would need to add
731 this.callbacks[action](details);
734 this.setCallback = function (name, cb) {
735 const oldCallbacks = Object.keys(this.callbacks).sort();
738 this.callbacks[name] = cb;
740 delete this.callbacks[name];
743 const newCallbacks = Object.keys(this.callbacks).sort();
745 if (oldCallbacks.toString() === newCallbacks.toString()) {
749 if (this.pendingCallbacksUpdate) {
753 this.pendingCallbacksUpdate = setTimeout(() => {
754 this.pendingCallbacksUpdate = 0;
756 // Make sure to send the current callbacks, not "newCallbacks" at the time of starting the timeout
757 const callbacks = Object.keys(this.callbacks);
758 this.sendMessage("callbacks", callbacks);
762 this.setMetadata = function (metadata) {
763 // MediaMetadata is not a regular Object so we cannot just JSON.stringify it
764 let newMetadata = {};
766 let dirty = (!metadata != !this.metadata);
768 const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(metadata));
770 const oldMetadata = this.metadata || {};
772 keys.forEach((key) => {
773 const value = metadata[key];
774 if (!value || typeof value === "function") {
778 // We only have Strings or the "artwork" Array, so a toString() comparison should suffice...
779 dirty |= (value.toString() !== (oldMetadata[key] || "").toString());
781 newMetadata[key] = value;
785 this.metadata = metadata;
788 this.sendMessage("metadata", newMetadata);
792 this.setPlaybackState = function (playbackState) {
793 if (this.playbackState === playbackState) {
797 this.playbackState = playbackState;
798 this.sendMessage("playbackState", playbackState);
802 ${mediaSessionsClassName} = new ${mediaSessionsClassName}_constructor();
804 if (!navigator.mediaSession) {
805 navigator.mediaSession = {};
808 var noop = function() { };
810 var oldSetActionHandler = navigator.mediaSession.setActionHandler || noop;
811 navigator.mediaSession.setActionHandler = function (name, cb) {
812 ${mediaSessionsClassName}.setCallback(name, cb);
814 // Call the original native implementation
815 // "call()" is needed as the real setActionHandler is a class member
816 // and calling it directly is illegal as it lacks the context
817 // This may throw for unsupported actions but we registered the callback
819 return oldSetActionHandler.call(navigator.mediaSession, name, cb);
822 Object.defineProperty(navigator.mediaSession, "metadata", {
823 get: () => ${mediaSessionsClassName}.metadata,
825 ${mediaSessionsClassName}.setMetadata(newValue);
828 Object.defineProperty(navigator.mediaSession, "playbackState", {
829 get: () => ${mediaSessionsClassName}.playbackState,
831 ${mediaSessionsClassName}.setPlaybackState(newValue);
835 if (!window.MediaMetadata) {
836 window.MediaMetadata = function (data) {
837 Object.assign(this, data);
839 window.MediaMetadata.prototype.title = "";
840 window.MediaMetadata.prototype.artist = "";
841 window.MediaMetadata.prototype.album = "";
842 window.MediaMetadata.prototype.artwork = [];
847 // here we replace the document.createElement function with our own so we can detect
848 // when an <audio> tag is created that is not added to the DOM which most pages do
849 // while a <video> tag typically ends up being displayed to the user, audio is not.
850 // HACK We cannot really pass variables from the page's scope to our content-script's scope
851 // so we just blatantly insert the <audio> tag in the DOM and pick it up through our regular
852 // mechanism. Let's see how this goes :D
854 // HACK When removing a media object from DOM it is paused, so what we do here is once the
855 // player loaded some data we add it (doesn't work earlier since it cannot pause when
856 // there's nothing loaded to pause) to the DOM and before we remove it, we note down that
857 // we will now get a paused event because of that. When we get it, we just play() the player
858 // so it continues playing :-)
859 const addPlayerToDomEvadingAutoPlayBlocking = `
860 player.registerInDom = () => {
861 // Needs to be dataset so it's accessible from mutation observer on webpage
862 player.dataset.pbiPausedForDomRemoval = "true";
863 player.removeEventListener("play", player.registerInDom);
865 // If it is already in DOM by the time it starts playing, we don't need to do anything
866 if (document.body && document.body.contains(player)) {
867 delete player.dataset.pbiPausedForDomRemoval;
868 player.removeEventListener("pause", player.replayAfterRemoval);
870 (document.head || document.documentElement).appendChild(player);
871 player.parentNode.removeChild(player);
875 player.replayAfterRemoval = () => {
876 if (player.dataset.pbiPausedForDomRemoval === "true") {
877 delete player.dataset.pbiPausedForDomRemoval;
878 player.removeEventListener("pause", player.replyAfterRemoval);
884 player.addEventListener("play", player.registerInDom);
885 player.addEventListener("pause", player.replayAfterRemoval);
888 const handleCreateElement = `
889 const tagName = arguments[0];
891 if (typeof tagName === "string") {
892 if (tagName.toLowerCase() === "audio") {
893 const player = createdTag;
894 ${addPlayerToDomEvadingAutoPlayBlocking}
895 } else if (tagName.toLowerCase() === "video") {
896 (document.head || document.documentElement).appendChild(createdTag);
897 createdTag.parentNode.removeChild(createdTag);
903 const oldCreateElement = Document.prototype.createElement;
904 exportFunction(function() {
905 const createdTag = oldCreateElement.apply(this, arguments);
906 eval(handleCreateElement);
908 }, Document.prototype, {defineAs: "createElement"});
912 const oldCreateElement = Document.prototype.createElement;
913 Document.prototype.createElement = function() {
914 const createdTag = oldCreateElement.apply(this, arguments);
915 ${handleCreateElement}
922 // We also briefly add items created as new Audio() to the DOM so we can control it
923 // similar to the document.createElement hack above since we cannot share variables
924 // between the actual website and the background script despite them sharing the same DOM
927 // Firefox enforces Content-Security-Policy also for scripts injected by the content-script
928 // This causes our executeScript calls to fail for pages like Nextcloud
929 // It also doesn't seem to have the aggressive autoplay prevention Chrome has,
930 // so the horrible replyAfterRemoval hack from above isn't copied into this
931 // See Bug 411148: Music playing from the ownCloud Music app does not show up
933 // A function exported with exportFunction loses its prototype, leading to Bug 414512
935 const oldAudio = window.Audio;
936 // It is important to use the prototype on wrappedJSObject as the prototype
937 // of the content script is restricted and not accessible by the website
938 const oldAudioPrototype = window.wrappedJSObject.Audio.prototype;
940 const audioConstructor = function(...args) {
941 const player = new oldAudio(...args);
942 eval(addPlayerToDomEvadingAutoPlayBlocking);
945 exportFunction(audioConstructor, window.wrappedJSObject, {defineAs: "Audio"});
947 window.wrappedJSObject.Audio.prototype = oldAudioPrototype;
949 executeScript(`function() {
950 var oldAudio = window.Audio;
951 window.Audio = function (...args) {
952 const player = new oldAudio(...args);
953 ${addPlayerToDomEvadingAutoPlayBlocking}
961 // PURPOSE / WEB SHARE API
962 // ------------------------------------------------------------------------
964 const purposeTransferClassName = "p" + generateGuid().replace(/-/g, "");
966 var purposeLoaded = false;
967 function loadPurpose() {
972 purposeLoaded = true;
974 // navigator.share must only be defined in secure (https) context
975 if (!window.isSecureContext) {
979 window.addEventListener("pbiPurposeMessage", (e) => {
980 const data = e.detail || {};
982 const action = data.action;
983 const payload = data.payload;
985 if (action !== "share") {
989 sendMessage("purpose", "share", payload).then((response) => {
992 ${purposeTransferClassName}.pendingResolve();
996 // Deliberately not giving any more details about why it got rejected
999 ${purposeTransferClassName}.pendingReject(new DOMException("Share request aborted", "AbortError"));
1005 ${purposeTransferClassName}.reset();
1013 ${purposeTransferClassName} = function() {};
1014 let transfer = ${purposeTransferClassName};
1015 transfer.reset = () => {
1016 transfer.pendingResolve = null;
1017 transfer.pendingReject = null;
1021 if (!navigator.canShare) {
1022 navigator.canShare = (data) => {
1027 if (data.title === undefined && data.text === undefined && data.url === undefined) {
1032 // check if URL is valid
1034 new URL(data.url, document.location.href);
1044 if (!navigator.share) {
1045 navigator.share = (data) => {
1046 return new Promise((resolve, reject) => {
1047 if (!navigator.canShare(data)) {
1048 return reject(new TypeError());
1052 // validity already checked in canShare, hence no catch
1053 data.url = new URL(data.url, document.location.href).toString();
1056 if (!window.event || !window.event.isTrusted) {
1057 return reject(new DOMException("navigator.share can only be called in response to user interaction", "NotAllowedError"));
1060 if (transfer.pendingResolve || transfer.pendingReject) {
1061 return reject(new DOMException("A share is already in progress", "AbortError"));
1064 transfer.pendingResolve = resolve;
1065 transfer.pendingReject = reject;
1067 const event = new CustomEvent("pbiPurposeMessage", {
1073 window.dispatchEvent(event);