You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/media-tag.js

644 lines
22 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

(function (window) {
var factory = function (Cache) {
var Promise = window.Promise;
var cache;
var cypherChunkLength = 131088;
// Save a blob on the file system
var saveFile = function (blob, url, fileName) {
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, fileName);
} else {
// We want to be able to download the file with a name, so we need an "a" tag with
// a download attribute
var a = document.createElement("a");
a.href = url;
a.download = fileName;
// It's not in the DOM, so we can't use a.click();
var event = new MouseEvent("click");
a.dispatchEvent(event);
}
};
var fixHTML = function (str) {
if (!str) { return ''; }
return str.replace(/[<>&"']/g, function (x) {
return ({ "<": "&lt;", ">": "&gt", "&": "&amp;", '"': "&#34;", "'": "&#39;" })[x];
});
};
var isplainTextFile = function (metadata) {
// does its type begins with "text/"
if (metadata.type.indexOf("text/") === 0) { return true; }
// no type and no file extension -> let's guess it's plain text
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(metadata.name) || [];
if (!metadata.type && !parsedName[2]) { return true; }
// other exceptions
if (metadata.type === 'application/x-javascript') { return true; }
if (metadata.type === 'application/xml') { return true; }
return false;
};
// Default config, can be overriden per media-tag call
var config = {
allowed: [
'text/plain',
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'audio/mp3',
'audio/ogg',
'audio/wav',
'audio/webm',
'video/mp4',
'video/ogg',
'video/webm',
'application/pdf',
//'application/dash+xml', // FIXME?
'download'
],
pdf: {},
download: {
text: "Save",
textDl: "Load attachment"
},
Plugins: {
/**
* @param {object} metadataObject {name, metadatatype, owners} containing metadata of the file
* @param {strint} url Url of the blob object
* @param {Blob} content Blob object containing the data of the file
* @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins
* @param {function} cb Callback function: (err, pluginElement) => {}
*/
text: function (metadata, url, content, cfg, cb) {
var plainText = document.createElement('div');
plainText.className = "plain-text-reader";
plainText.setAttribute('style', 'white-space: pre-wrap;');
var reader = new FileReader();
reader.addEventListener('loadend', function (e) {
plainText.innerText = e.srcElement.result;
cb(void 0, plainText);
});
reader.readAsText(content);
},
image: function (metadata, url, content, cfg, cb) {
var img = document.createElement('img');
img.setAttribute('src', url);
img.blob = content;
cb(void 0, img);
},
video: function (metadata, url, content, cfg, cb) {
var video = document.createElement('video');
video.setAttribute('src', url);
video.setAttribute('controls', true);
cb(void 0, video);
},
audio: function (metadata, url, content, cfg, cb) {
var audio = document.createElement('audio');
audio.setAttribute('src', url);
audio.setAttribute('controls', true);
cb(void 0, audio);
},
pdf: function (metadata, url, content, cfg, cb) {
var iframe = document.createElement('iframe');
if (cfg.pdf.viewer) { // PDFJS
var viewerUrl = cfg.pdf.viewer + '?file=' + url;
iframe.src = viewerUrl + '#' + window.encodeURIComponent(metadata.name);
return void cb (void 0, iframe);
}
iframe.src = url + '#' + window.encodeURIComponent(metadata.name);
return void cb (void 0, iframe);
},
download: function (metadata, url, content, cfg, cb) {
var btn = document.createElement('button');
btn.setAttribute('class', 'btn btn-default');
btn.innerHTML = '<i class="fa fa-save"></i>' + cfg.download.text + '<br>' +
(metadata.name ? '<b>' + fixHTML(metadata.name) + '</b>' : '');
btn.addEventListener('click', function () {
saveFile(content, url, metadata.name);
});
cb(void 0, btn);
}
}
};
var makeProgressBar = function (cfg, mediaObject) {
// XXX CSP: we'll need to add style in cryptpad's less
var style = (function(){/*
.mediatag-progress-container {
position: relative;
border: 1px solid #0087FF;
background: white;
height: 25px;
display: inline-flex;
width: 200px;
align-items: center;
justify-content: center;
box-sizing: border-box;
vertical-align: top;
}
.mediatag-progress-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background: #0087FF;
width: 0%;
}
.mediatag-progress-text {
height: 25px;
margin-left: 5px;
line-height: 25px;
vertical-align: top;
width: auto;
display: inline-block;
color: #3F4141;
font-weight: bold;
}
*/}).toString().slice(14, -3);
var container = document.createElement('div');
container.classList.add('mediatag-progress-container');
var bar = document.createElement('div');
bar.classList.add('mediatag-progress-bar');
container.appendChild(bar);
var text = document.createElement('span');
text.classList.add('mediatag-progress-text');
text.innerText = '0%';
mediaObject.on('progress', function (obj) {
var percent = obj.progress;
text.innerText = (Math.round(percent*10))/10+'%';
bar.setAttribute('style', 'width:'+percent+'%;');
});
mediaObject.tag.innerHTML = '<style>'+style+'</style>';
mediaObject.tag.appendChild(container);
mediaObject.tag.appendChild(text);
};
var makeDownloadButton = function (cfg, mediaObject, size, cb) {
var btn = document.createElement('button');
btn.setAttribute('class', 'btn btn-default');
btn.innerHTML = '<i class="fa fa-paperclip"></i>' +
cfg.download.textDl + ' <b>(' + size + 'MB)</b>';
btn.addEventListener('click', function () {
makeProgressBar(cfg, mediaObject);
cb();
});
mediaObject.tag.innerHTML = '';
mediaObject.tag.appendChild(btn);
};
var getCacheKey = function (src) {
var _src = src.replace(/(\/)*$/, ''); // Remove trailing slashes
var idx = _src.lastIndexOf('/');
var cacheKey = _src.slice(idx+1);
if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; }
return cacheKey;
};
var getFileSize = function (src, _cb) {
var cb = function (e, res) {
_cb(e, res);
cb = function () {};
};
var cacheKey = getCacheKey(src);
var check = function () {
var xhr = new XMLHttpRequest();
xhr.open("HEAD", src);
xhr.onerror = function () { return void cb("XHR_ERROR"); };
xhr.onreadystatechange = function() {
if (this.readyState === this.DONE) {
cb(null, Number(xhr.getResponseHeader("Content-Length")));
}
};
xhr.onload = function () {
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
};
xhr.send();
};
if (!cacheKey) { return void check(); }
Cache.getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void check(); }
cb(null, 0);
});
};
// Download a blob from href
var download = function (src, _cb, progressCb) {
var cb = function (e, res) {
_cb(e, res);
cb = function () {};
};
var cacheKey = getCacheKey(src);
var fetch = function () {
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
var progress = function (offset) {
progressCb(offset * 100);
};
xhr.addEventListener("progress", function (evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
progress(percentComplete);
}
}, false);
xhr.onerror = function () { return void cb("XHR_ERROR"); };
xhr.onload = function () {
// Error?
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
if (cacheKey) {
return void Cache.setBlobCache(cacheKey, u8, function () {
cb(null, u8);
});
}
cb(null, u8);
}
};
xhr.send(null);
};
if (!cacheKey) { return void fetch(); }
Cache.getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void fetch(); }
cb(null, u8);
});
};
// Decryption tools
var Decrypt = {
// Create a nonce
createNonce: function () {
var n = new Uint8Array(24);
for (var i = 0; i < 24; i++) { n[i] = 0; }
return n;
},
// Increment a nonce
increment: function (N) {
var l = N.length;
while (l-- > 1) {
/* .jshint probably suspects this is unsafe because we lack types
but as long as this is only used on nonces, it should be safe */
if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
// you don't need to worry about this running out.
// you'd need a REAAAALLY big file
if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); }
N[l] = 0;
}
},
decodePrefix: function (A) {
return (A[0] << 8) | A[1];
},
joinChunks: function (chunks) {
return new Blob(chunks);
},
// Convert a Uint8Array into Array.
slice: function (u8) {
return Array.prototype.slice.call(u8);
},
// Gets the key from the key string.
getKeyFromStr: function (str) {
return window.nacl.util.decodeBase64(str);
}
};
// Decrypts a Uint8Array with the given key.
var decrypt = function (u8, strKey, done, progressCb) {
var Nacl = window.nacl;
var progress = function (offset) {
progressCb((offset / u8.length) * 100);
};
var key = Decrypt.getKeyFromStr(strKey);
var nonce = Decrypt.createNonce();
var i = 0;
var prefix = u8.subarray(0, 2);
var metadataLength = Decrypt.decodePrefix(prefix);
var res = { metadata: undefined };
// Get metadata
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
Decrypt.increment(nonce);
try { res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk)); }
catch (e) { return void done('E_METADATA_DECRYPTION'); }
if (!res.metadata) { return void done('NO_METADATA'); }
var takeChunk = function (cb) {
setTimeout(function () {
var start = i * cypherChunkLength + 2 + metadataLength;
var end = start + cypherChunkLength;
i++;
// Get the chunk
var box = new Uint8Array(u8.subarray(start, end));
// Decrypt the chunk
var plaintext = Nacl.secretbox.open(box, nonce, key);
Decrypt.increment(nonce);
if (!plaintext) { return void cb('DECRYPTION_FAILURE'); }
progress(Math.min(end, u8.length));
cb(void 0, plaintext);
});
};
var chunks = [];
// decrypt file contents
var again = function () {
takeChunk(function (e, plaintext) {
if (e) { return setTimeout(function () { done(e); }); }
if (plaintext) {
if ((i * cypherChunkLength + 2 + metadataLength) < u8.length) { // not done
chunks.push(plaintext);
return again();
}
chunks.push(plaintext);
res.content = Decrypt.joinChunks(chunks);
return void done(void 0, res);
}
done('UNEXPECTED_ENDING');
});
};
again();
};
// Get type
var getType = function (mediaObject, metadata, cfg) {
var mime = metadata.type;
var s = metadata.type.split('/');
var type = s[0];
var extension = s[1];
mediaObject.name = metadata.name;
if (mime && cfg.allowed.indexOf(mime) !== -1) {
mediaObject.type = type;
mediaObject.extension = extension;
mediaObject.mime = mime;
return type;
} else if (cfg.allowed.indexOf('download') !== -1) {
mediaObject.type = type;
mediaObject.extension = extension;
mediaObject.mime = mime;
return 'download';
} else {
return;
}
};
// Copy attributes
var copyAttributes = function (origin, dest) {
Object.keys(origin.attributes).forEach(function (i) {
if (!/^data-attr/.test(origin.attributes[i].name)) { return; }
var name = origin.attributes[i].name.slice(10);
var value = origin.attributes[i].value;
dest.setAttribute(name, value);
});
};
// Process
var process = function (mediaObject, decrypted, cfg, cb) {
var metadata = decrypted.metadata;
var blob = decrypted.content;
var mediaType = getType(mediaObject, metadata, cfg);
if (isplainTextFile(metadata)) {
mediaType = "text";
}
if (mediaType === 'application') {
mediaType = mediaObject.extension;
}
if (!mediaType || !cfg.Plugins[mediaType]) {
return void cb('NO_PLUGIN_FOUND');
}
// Get blob URL
var url = decrypted.url;
if (!url && window.URL) {
url = decrypted.url = window.URL.createObjectURL(new Blob([blob], {
type: metadata.type
}));
}
cfg.Plugins[mediaType](metadata, url, blob, cfg, function (err, el) {
if (err || !el) { return void cb(err || 'ERR_MEDIATAG_DISPLAY'); }
copyAttributes(mediaObject.tag, el);
mediaObject.tag.innerHTML = '';
mediaObject.tag.appendChild(el);
cb();
});
};
var addMissingConfig = function (base, target) {
Object.keys(target).forEach(function (k) {
if (!target[k]) { return; }
// Target is an object, fix it recursively
if (typeof target[k] === "object" && !Array.isArray(target[k])) {
// Sub-object
if (base[k] && (typeof base[k] !== "object" || Array.isArray(base[k]))) { return; }
else if (base[k]) { addMissingConfig(base[k], target[k]); }
else {
base[k] = {};
addMissingConfig(base[k], target[k]);
}
}
// Target is array or immutable, copy the value if it's missing
if (!base[k]) {
base[k] = Array.isArray(target[k]) ? JSON.parse(JSON.stringify(target[k]))
: target[k];
}
});
};
// Initialize a media-tag
var init = function (el, cfg) {
cfg = cfg || {};
addMissingConfig(cfg, config);
// Handle jQuery elements
if (typeof(el) === "object" && el.jQuery) { el = el[0]; }
// Abort smoothly if the element is not a media-tag
if (!el || el.nodeName !== "MEDIA-TAG") {
console.error("Not a media-tag!");
return {
on: function () { return this; }
};
}
var handlers = cfg.handlers || {
'progress': [],
'complete': [],
'error': []
};
var mediaObject = el._mediaObject = {
handlers: handlers,
tag: el
};
var emit = function (ev, data) {
// Check if the event name is valid
if (Object.keys(handlers).indexOf(ev) === -1) {
return void console.error("Invalid mediatag event");
}
// Call the handlers
handlers[ev].forEach(function (h) {
// Make sure a bad handler won't break the media-tag script
try {
h(data);
} catch (err) {
console.error(err);
}
});
};
mediaObject.on = function (ev, handler) {
// Check if the event name is valid
if (Object.keys(handlers).indexOf(ev) === -1) {
console.error("Invalid mediatag event");
return mediaObject;
}
// Check if the handler is valid
if (typeof (handler) !== "function") {
console.error("Handler is not a function!");
return mediaObject;
}
// Add the handler
handlers[ev].push(handler);
return mediaObject;
};
var src = el.getAttribute('src');
var strKey = el.getAttribute('data-crypto-key');
if (/^cryptpad:/.test(strKey)) {
strKey = strKey.slice(9);
}
var uid = [src, strKey].join('');
// End media-tag rendering: display the tag and emit the event
var end = function (decrypted) {
mediaObject.complete = true;
process(mediaObject, decrypted, cfg, function (err) {
if (err) { return void emit('error', err); }
mediaObject._blob = decrypted;
emit('complete', decrypted);
});
};
var error = function (err) {
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;" src="/images/broken.png">';
emit('error', err);
};
var dl = function () {
// Download the encrypted blob
cache[uid] = cache[uid] || new Promise(function (resolve, reject) {
download(src, function (err, u8Encrypted) {
if (err) {
return void reject(err);
}
// Decrypt the blob
decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) {
if (errDecryption) {
return void reject(errDecryption);
}
// Cache and display the decrypted blob
resolve(u8Decrypted);
}, function (progress) {
emit('progress', {
progress: 50+0.5*progress
});
});
}, function (progress) {
emit('progress', {
progress: 0.5*progress
});
});
});
cache[uid].then(function (u8) {
end(u8);
}, function (err) {
error(err);
});
};
if (cfg.force) { dl(); return mediaObject; }
var maxSize = 5 * 1024 * 1024;
getFileSize(src, function (err, size) {
if (err) {
return void error(err);
}
if (!size || size < maxSize) { return void dl(); }
var sizeMb = Math.round(10 * size / 1024 / 1024) / 10;
makeDownloadButton(cfg, mediaObject, sizeMb, dl);
});
return mediaObject;
};
// Add the cache as a property of MediaTag
cache = init.__Cryptpad_Cache = {};
init.setDefaultConfig = function (key, value) {
config[key] = value;
};
return init;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("./outer/cache-store.js")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/outer/cache-store.js',
'/bower_components/es6-promise/es6-promise.min.js'
], function (Cache) {
return factory(Cache);
});
} else {
// unsupported initialization
}
}(typeof(window) !== 'undefined'? window : {}));