From 975fa3e4dd9afb3a690aa7332b6dac1eb66cb9fe Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 May 2018 12:34:37 +0200 Subject: [PATCH] Test small mediatag --- www/mediatag/index.html | 22 ++ www/mediatag/main.js | 58 +++++ www/mediatag/media-tag.js | 442 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 www/mediatag/index.html create mode 100644 www/mediatag/main.js create mode 100644 www/mediatag/media-tag.js diff --git a/www/mediatag/index.html b/www/mediatag/index.html new file mode 100644 index 000000000..45e88aba5 --- /dev/null +++ b/www/mediatag/index.html @@ -0,0 +1,22 @@ + + + Test media-tag + + + + + Media-tag: +
+
+
+ diff --git a/www/mediatag/main.js b/www/mediatag/main.js new file mode 100644 index 000000000..d2f1691ae --- /dev/null +++ b/www/mediatag/main.js @@ -0,0 +1,58 @@ +require([ + 'jquery', + '/mediatag/media-tag.js', + '/bower_components/tweetnacl/nacl-fast.min.js' +], function ($, MediaTag) { + console.log(MediaTag); + console.log($('media-tag')); + if (typeof MediaTag === "function") { + MediaTag.PdfPlugin.viewer = '/common/pdfjs/web/viewer.html'; + + var config = { + allowed: ['download'], + download: { + text: 'Download' + } + }; + MediaTag($('media-tag'), config) + .on('progress', function (data) { + console.log(data.progress); + }) + .on('complete', function (data) { + console.log(data); + }) + .on('error', function (data) { + console.error(data); + }); + MediaTag($('media-tag')[1]) + .on('progress', function (data) { + console.log(data.progress); + }) + .on('complete', function (data) { + console.log(data); + }) + .on('error', function (data) { + console.error(data); + }); + MediaTag($('media-tag')[2]) + .on('progress', function (data) { + console.log(data.progress); + }) + .on('complete', function (data) { + console.log(data); + }) + .on('error', function (data) { + console.error(data); + }); + MediaTag($('media-tag')[3]) + .on('progress', function (data) { + console.log(data.progress); + }) + .on('complete', function (data) { + console.log(data); + }) + .on('error', function (data) { + console.error(data); + }); + } +}); diff --git a/www/mediatag/media-tag.js b/www/mediatag/media-tag.js new file mode 100644 index 000000000..f240e0941 --- /dev/null +++ b/www/mediatag/media-tag.js @@ -0,0 +1,442 @@ +(function(name, definition) { + if (typeof module !== 'undefined') { module.exports = definition(); } + else if (typeof define === 'function' && typeof define.amd === 'object') { define(definition); } + else { this[name] = definition(); } +}('MediaTag', function() { + var cache; + var PARANOIA = true; + 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); + } + }; + + // Default config, can be overriden per media-tag call + var config = { + allowed: [ + '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: "Download" + }, + Plugins: { + image: function (metadata, url, content, cfg, cb) { + var img = document.createElement('img'); + img.setAttribute('src', url); + 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.innerHTML = cfg.download.text; + btn.addEventListener('click', function () { + saveFile(content, url, metadata.name); + }); + cb(void 0, btn); + } + } + }; + + + // Download a blob from href + var download = function (src, cb) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function () { + // Error? + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + + var arrayBuffer = xhr.response; + if (arrayBuffer) { cb(null, new Uint8Array(arrayBuffer)); } + }; + + xhr.send(null); + }; + + // Decryption tools + var Decrypt = { + // Create a nonce + createNonce: function () { + if (!Array.prototype.fill) { + // IE support + var arr = []; + for (var i = 0; i < 24; i++) { arr[i] = 0; } + return new Uint8Array(arr); + } + return new Uint8Array(new Array(24).fill(0)); + }, + + // Increment a nonce + // FIXME: remove throw? + increment: function (N) { + var l = N.length; + while (l-- > 1) { + if (PARANOIA) { + if (typeof(N[l]) !== 'number') { + throw new Error('E_UNSAFE_TYPE'); + } + if (N[l] > 255) { + throw new Error('E_OUT_OF_BOUNDS'); + } + } + /* .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 + N[l] = 0; + + // 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'); + } + } + }, + + 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 random key string. + getRandomKeyStr: function () { + var Nacl = window.nacl; + var rdm = Nacl.randomBytes(18); + return Nacl.util.encodeBase64(rdm); + }, + + // 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 < 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 (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) { + 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); + + // Add support for old mediatag library + if (!cfg.pdf.viewer && init.PdfPlugin && init.PdfPlugin.viewer) { + cfg.pdf.viewer = init.PdfPlugin.viewer; + } + + // Handle jQuery elements + if (typeof(el) === "object" && el.jQuery) { el = el[0]; } + + // Abort smoothly if the element is not a media-tag + if (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) { + process(mediaObject, decrypted, cfg, function (err) { + if (err) { return void emit('error', err); } + emit('complete', decrypted); + }); + }; + + // If we have the blob in our cache, don't download & decrypt it again, just display + if (cache[uid]) { + end(cache[uid]); + return mediaObject; + } + + // Download the encrypted blob + download(src, function (err, u8Encrypted) { + if (err) { + return void emit('error', err); + } + // Decrypt the blob + decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { + if (errDecryption) { + return void emit('error', errDecryption); + } + // Cache and display the decrypted blob + cache[uid] = u8Decrypted; + end(u8Decrypted); + }, function (progress) { + emit('progress', { + progress: progress + }); + }); + }); + + return mediaObject; + }; + + // Add the cache as a property of MediaTag + cache = init.__Cryptpad_Cache = {}; + + init.PdfPlugin = {}; + + return init; +}));