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
// @connect docs.google.com
// @run-at document-end
// @version 1.0.0
// @version 1.1.0
// ==/UserScript==
(function () {
try {
function debug(message) {
if (!unsafeWindow.enableCyTubeGoogleDriveUserscriptDebug) {
return;
}
unsafeWindow.console.log.apply(unsafeWindow.console, arguments);
try {
unsafeWindow.console.log(message);
} catch (error) {
unsafeWindow.console.error(error);
}
}
var ITAG_QMAP = {
@ -53,36 +57,42 @@
method: 'GET',
url: url,
onload: function (res) {
var data = {};
var error;
res.responseText.split('&').forEach(function (kv) {
var pair = kv.split('=');
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
try {
debug('Got response ' + res.responseText);
var data = {};
var error;
res.responseText.split('&').forEach(function (kv) {
var pair = kv.split('=');
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
if (data.status === 'fail') {
error = new Error('Google Docs request failed: ' +
'metadata indicated status=fail');
error.response = res.responseText;
error.reason = 'RESPONSE_STATUS_FAIL';
return cb(error);
if (data.status === 'fail') {
error = new Error('Google Docs request failed: ' +
'metadata indicated status=fail');
error.response = res.responseText;
error.reason = 'RESPONSE_STATUS_FAIL';
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 () {
@ -118,36 +128,83 @@
return videos;
}
function GoogleDrivePlayer(data) {
if (!(this instanceof GoogleDrivePlayer)) {
return new GoogleDrivePlayer(data);
}
/*
* Greasemonkey 2.0 has this wonderful sandbox that attempts
* 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);
GoogleDrivePlayer.prototype.load = function (data) {
var self = this;
getVideoInfo(data.id, function (err, videoData) {
if (err) {
debug(err);
var alertBox = unsafeWindow.document.createElement('div');
alertBox.className = 'alert alert-danger';
alertBox.textContent = err.message;
document.getElementById('ytapiplayer').appendChild(alertBox);
return;
/*
* Sandbox side function -- polls gdUserscript.pollID for
* the ID of a Drive video to be queried, looks up the
* metadata, and stores it in gdUserscript.pollResult
*/
function setupGDPoll() {
unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow);
var pollInterval = setInterval(function () {
if (unsafeWindow.gdUserscript.pollID) {
var id = unsafeWindow.gdUserscript.pollID;
unsafeWindow.gdUserscript.pollID = null;
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));
data.meta.direct = mapLinks(videoData.links);
unsafeWindow.VideoJSPlayer.prototype.loadPlayer.call(self, data);
});
};
function isRunningTampermonkey() {
try {
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.hasDriveUserscript = true;
})();
} catch (error) {
unsafeWindow.console.error(error);
}

View file

@ -4,3 +4,19 @@ window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
return new GoogleDrivePlayer(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'
try
if data.meta.html5hack or window.hasDriveUserscript
window.PLAYER = new window.GoogleDrivePlayer(data)
window.PLAYER = new GoogleDrivePlayer(data)
else
window.PLAYER = new GoogleDriveYouTubePlayer(data)
catch e

View file

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

View file

@ -503,8 +503,7 @@
if (!(this instanceof VideoJSPlayer)) {
return new VideoJSPlayer(data);
}
this.setMediaProperties(data);
this.loadPlayer(data);
this.load(data);
}
VideoJSPlayer.prototype.loadPlayer = function(data) {
@ -676,6 +675,28 @@
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;
})(VideoJSPlayer);
@ -1359,7 +1380,7 @@
} else if (data.type === 'gd') {
try {
if (data.meta.html5hack || window.hasDriveUserscript) {
return window.PLAYER = new window.GoogleDrivePlayer(data);
return window.PLAYER = new GoogleDrivePlayer(data);
} else {
return window.PLAYER = new GoogleDriveYouTubePlayer(data);
}