Add workaround for GM sandbox and refactor userscript a bit

This commit is contained in:
Calvin Montgomery 2016-08-20 10:59:20 -07:00
parent 8d3b2e59df
commit 578d3fbb23
5 changed files with 155 additions and 62 deletions

View file

@ -7,16 +7,20 @@
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @connect docs.google.com // @connect docs.google.com
// @run-at document-end // @run-at document-end
// @version 1.0.0 // @version 1.1.0
// ==/UserScript== // ==/UserScript==
(function () { try {
function debug(message) { function debug(message) {
if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) { if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) {
return; return;
} }
unsafeWindow.console.log.apply(unsafeWindow.console, arguments); try {
unsafeWindow.console.log(message);
} catch (error) {
unsafeWindow.console.error(error);
}
} }
var ITAG_QMAP = { var ITAG_QMAP = {
@ -53,36 +57,42 @@
method: 'GET', method: 'GET',
url: url, url: url,
onload: function (res) { onload: function (res) {
var data = {}; try {
var error; debug('Got response ' + res.responseText);
res.responseText.split('&').forEach(function (kv) { var data = {};
var pair = kv.split('='); var error;
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); res.responseText.split('&').forEach(function (kv) {
}); var pair = kv.split('=');
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
if (data.status === 'fail') { if (data.status === 'fail') {
error = new Error('Google Docs request failed: ' + error = new Error('Google Docs request failed: ' +
'metadata indicated status=fail'); 'metadata indicated status=fail');
error.response = res.responseText; error.response = res.responseText;
error.reason = 'RESPONSE_STATUS_FAIL'; error.reason = 'RESPONSE_STATUS_FAIL';
return cb(error); return cb(error);
}
if (!data.fmt_stream_map) {
error = new Error('Google Docs request failed: ' +
'metadata lookup returned no valid links');
error.response = res.responseText;
error.reason = 'MISSING_LINKS';
return cb(error);
}
data.links = {};
data.fmt_stream_map.split(',').forEach(function (item) {
var pair = item.split('|');
data.links[pair[0]] = pair[1];
});
data.videoMap = mapLinks(data.links);
cb(null, data);
} catch (error) {
unsafeWindow.console.error(error);
} }
if (!data.fmt_stream_map) {
error = new Error('Google Docs request failed: ' +
'metadata lookup returned no valid links');
error.response = res.responseText;
error.reason = 'MISSING_LINKS';
return cb(error);
}
data.links = {};
data.fmt_stream_map.split(',').forEach(function (item) {
var pair = item.split('|');
data.links[pair[0]] = pair[1];
});
cb(null, data);
}, },
onerror: function () { onerror: function () {
@ -118,36 +128,83 @@
return videos; return videos;
} }
function GoogleDrivePlayer(data) { /*
if (!(this instanceof GoogleDrivePlayer)) { * Greasemonkey 2.0 has this wonderful sandbox that attempts
return new GoogleDrivePlayer(data); * to prevent script developers from shooting themselves in
} * the foot by removing the trigger from the gun, i.e. it's
* impossible to cross the boundary between the browser JS VM
* and the privileged sandbox that can run GM_xmlhttpRequest().
*
* So in this case, we have to resort to polling a special
* variable to see if getGoogleDriveMetadata needs to be called
* and deliver the result into another special variable that is
* being polled on the browser side.
*/
this.setMediaProperties(data); /*
this.load(data); * Browser side function -- sets gdUserscript.pollID to the
* ID of the Drive video to be queried and polls
* gdUserscript.pollResult for the result.
*/
function getGoogleDriveMetadata_GM(id, callback) {
debug('Setting GD poll ID to ' + id);
unsafeWindow.gdUserscript.pollID = id;
var tries = 0;
var i = setInterval(function () {
if (unsafeWindow.gdUserscript.pollResult) {
debug('Got result');
clearInterval(i);
var result = unsafeWindow.gdUserscript.pollResult;
unsafeWindow.gdUserscript.pollResult = null;
callback(result.error, result.result);
} else if (++tries > 100) {
// Took longer than 10 seconds, give up
clearInterval(i);
}
}, 100);
} }
GoogleDrivePlayer.prototype = Object.create(unsafeWindow.VideoJSPlayer.prototype); /*
* Sandbox side function -- polls gdUserscript.pollID for
GoogleDrivePlayer.prototype.load = function (data) { * the ID of a Drive video to be queried, looks up the
var self = this; * metadata, and stores it in gdUserscript.pollResult
getVideoInfo(data.id, function (err, videoData) { */
if (err) { function setupGDPoll() {
debug(err); unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow);
var alertBox = unsafeWindow.document.createElement('div'); var pollInterval = setInterval(function () {
alertBox.className = 'alert alert-danger'; if (unsafeWindow.gdUserscript.pollID) {
alertBox.textContent = err.message; var id = unsafeWindow.gdUserscript.pollID;
document.getElementById('ytapiplayer').appendChild(alertBox); unsafeWindow.gdUserscript.pollID = null;
return; debug('Polled and got ' + id);
getVideoInfo(id, function (error, data) {
unsafeWindow.gdUserscript.pollResult = cloneInto({
error: error,
result: data
}, unsafeWindow);
});
} }
}, 1000);
}
debug('Retrieved links: ' + JSON.stringify(videoData.links)); function isRunningTampermonkey() {
data.meta.direct = mapLinks(videoData.links); try {
unsafeWindow.VideoJSPlayer.prototype.loadPlayer.call(self, data); return GM_info.scriptHandler === 'Tampermonkey';
}); } catch (error) {
}; return false;
}
}
if (isRunningTampermonkey()) {
unsafeWindow.getGoogleDriveMetadata = getVideoInfo;
} else {
debug('Using non-TM polling workaround');
unsafeWindow.getGoogleDriveMetadata = exportFunction(
getGoogleDriveMetadata_GM, unsafeWindow);
setupGDPoll();
}
unsafeWindow.GoogleDrivePlayer = GoogleDrivePlayer;
unsafeWindow.console.log('Initialized userscript Google Drive player'); unsafeWindow.console.log('Initialized userscript Google Drive player');
unsafeWindow.hasDriveUserscript = true; unsafeWindow.hasDriveUserscript = true;
})(); } catch (error) {
unsafeWindow.console.error(error);
}

View file

@ -4,3 +4,19 @@ window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
return new GoogleDrivePlayer(data) return new GoogleDrivePlayer(data)
super(data) super(data)
load: (data) ->
if typeof window.getGoogleDriveMetadata is 'function'
window.getGoogleDriveMetadata(data.id, (error, metadata) =>
if error
console.error(error)
alertBox = window.document.createElement('div')
alertBox.className = 'alert alert-danger'
alertBox.textContent = error.message
document.getElementById('ytapiplayer').appendChild(alertBox)
else
data.meta.direct = metadata.videoMap
super(data)
)
else
super(data)

View file

@ -33,7 +33,7 @@ window.loadMediaPlayer = (data) ->
else if data.type is 'gd' else if data.type is 'gd'
try try
if data.meta.html5hack or window.hasDriveUserscript if data.meta.html5hack or window.hasDriveUserscript
window.PLAYER = new window.GoogleDrivePlayer(data) window.PLAYER = new GoogleDrivePlayer(data)
else else
window.PLAYER = new GoogleDriveYouTubePlayer(data) window.PLAYER = new GoogleDriveYouTubePlayer(data)
catch e catch e

View file

@ -43,8 +43,7 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player
if not (this instanceof VideoJSPlayer) if not (this instanceof VideoJSPlayer)
return new VideoJSPlayer(data) return new VideoJSPlayer(data)
@setMediaProperties(data) @load(data)
@loadPlayer(data)
loadPlayer: (data) -> loadPlayer: (data) ->
waitUntilDefined(window, 'videojs', => waitUntilDefined(window, 'videojs', =>

View file

@ -503,8 +503,7 @@
if (!(this instanceof VideoJSPlayer)) { if (!(this instanceof VideoJSPlayer)) {
return new VideoJSPlayer(data); return new VideoJSPlayer(data);
} }
this.setMediaProperties(data); this.load(data);
this.loadPlayer(data);
} }
VideoJSPlayer.prototype.loadPlayer = function(data) { VideoJSPlayer.prototype.loadPlayer = function(data) {
@ -676,6 +675,28 @@
GoogleDrivePlayer.__super__.constructor.call(this, data); GoogleDrivePlayer.__super__.constructor.call(this, data);
} }
GoogleDrivePlayer.prototype.load = function(data) {
if (typeof window.getGoogleDriveMetadata === 'function') {
return window.getGoogleDriveMetadata(data.id, (function(_this) {
return function(error, metadata) {
var alertBox;
if (error) {
console.error(error);
alertBox = window.document.createElement('div');
alertBox.className = 'alert alert-danger';
alertBox.textContent = error.message;
return document.getElementById('ytapiplayer').appendChild(alertBox);
} else {
data.meta.direct = metadata.videoMap;
return GoogleDrivePlayer.__super__.load.call(_this, data);
}
};
})(this));
} else {
return GoogleDrivePlayer.__super__.load.call(this, data);
}
};
return GoogleDrivePlayer; return GoogleDrivePlayer;
})(VideoJSPlayer); })(VideoJSPlayer);
@ -1359,7 +1380,7 @@
} else if (data.type === 'gd') { } else if (data.type === 'gd') {
try { try {
if (data.meta.html5hack || window.hasDriveUserscript) { if (data.meta.html5hack || window.hasDriveUserscript) {
return window.PLAYER = new window.GoogleDrivePlayer(data); return window.PLAYER = new GoogleDrivePlayer(data);
} else { } else {
return window.PLAYER = new GoogleDriveYouTubePlayer(data); return window.PLAYER = new GoogleDriveYouTubePlayer(data);
} }