/*! * Webogram v0.4.7 - messaging web application for MTProto * https://github.com/zhukov/webogram * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ angular.module('izhukov.utils', []) .provider('Storage', function () { this.setPrefix = function (newPrefix) { ConfigStorage.prefix(newPrefix); }; this.$get = ['$q', function ($q) { var methods = {}; angular.forEach(['get', 'set', 'remove'], function (methodName) { methods[methodName] = function () { var deferred = $q.defer(), args = Array.prototype.slice.call(arguments); args.push(function (result) { deferred.resolve(result); }); ConfigStorage[methodName].apply(ConfigStorage, args); return deferred.promise; }; }); return methods; }]; }) .service('qSync', function () { return { when: function (result) { return {then: function (cb) { return cb(result); }}; }, reject: function (result) { return {then: function (cb, badcb) { if (badcb) { return badcb(result); } }}; } } }) .service('FileManager', function ($window, $q, $timeout, qSync) { $window.URL = $window.URL || $window.webkitURL; $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder; var buggyUnknownBlob = navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1; var blobSupported = true; try { blobConstruct([], ''); } catch (e) { blobSupported = false; } function isBlobAvailable () { return blobSupported; } function fileCopyTo (fromFileEntry, toFileEntry) { return getFileWriter(toFileEntry).then(function (fileWriter) { return fileWriteData(fileWriter, fromFileEntry).then(function () { return fileWriter; }, function (error) { return $q.reject(error); fileWriter.truncate(0); }); }); } function fileWriteData(fileWriter, bytes) { var deferred = $q.defer(); fileWriter.onwriteend = function(e) { deferred.resolve(); }; fileWriter.onerror = function (e) { deferred.reject(e); }; if (bytes.file) { bytes.file(function (file) { fileWriter.write(file); }, function (error) { deferred.reject(error); }) } else if (bytes instanceof Blob) { // is file bytes fileWriter.write(bytes); } else { try { var blob = blobConstruct([bytesToArrayBuffer(bytes)]); fileWriter.write(blob); } catch (e) { deferred.reject(e); } } return deferred.promise; } function chooseSaveFile (fileName, ext, mimeType) { if (!$window.chrome || !chrome.fileSystem || !chrome.fileSystem.chooseEntry) { return qSync.reject(); }; var deferred = $q.defer(); chrome.fileSystem.chooseEntry({ type: 'saveFile', suggestedName: fileName, accepts: [{ mimeTypes: [mimeType], extensions: [ext] }] }, function (writableFileEntry) { deferred.resolve(writableFileEntry); }); return deferred.promise; } function getFileWriter (fileEntry) { var deferred = $q.defer(); fileEntry.createWriter(function (fileWriter) { deferred.resolve(fileWriter); }, function (error) { deferred.reject(error); }); return deferred.promise; } function getFakeFileWriter (mimeType, saveFileCallback) { var blobParts = [], fakeFileWriter = { write: function (blob) { if (!blobSupported) { if (fakeFileWriter.onerror) { fakeFileWriter.onerror(new Error('Blob not supported by browser')); } return false; } blobParts.push(blob); setZeroTimeout(function () { if (fakeFileWriter.onwriteend) { fakeFileWriter.onwriteend(); } }); }, truncate: function () { blobParts = []; }, finalize: function () { var blob = blobConstruct(blobParts, mimeType); if (saveFileCallback) { saveFileCallback(blob); } return blob; } }; return fakeFileWriter; }; function getUrl (fileData, mimeType) { // console.log(dT(), 'get url', fileData, mimeType, fileData.toURL !== undefined, fileData instanceof Blob); if (fileData.toURL !== undefined) { return fileData.toURL(mimeType); } if (fileData instanceof Blob) { return URL.createObjectURL(fileData); } return 'data:' + mimeType + ';base64,' + bytesToBase64(fileData); } function getByteArray(fileData) { if (fileData instanceof Blob) { var deferred = $q.defer(); try { var reader = new FileReader(); reader.onloadend = function (e) { deferred.resolve(new Uint8Array(e.target.result)); }; reader.onerror = function (e) { deferred.reject(e); }; reader.readAsArrayBuffer(fileData); return deferred.promise; } catch (e) { return $q.reject(e); } } else if (fileData.file) { var deferred = $q.defer(); fileData.file(function (blob) { getByteArray(blob).then(function (result) { deferred.resolve(result); }, function (error) { deferred.reject(error); }) }, function (error) { deferred.reject(error); }); return deferred.promise; } return $q.when(fileData); } function getDataUrl(blob) { var deferred; try { var reader = new FileReader(); reader.onloadend = function() { deferred.resolve(reader.result); } reader.readAsDataURL(blob); } catch (e) { return $q.reject(e); } deferred = $q.defer(); return deferred.promise; } function getFileCorrectUrl(blob, mimeType) { if (buggyUnknownBlob && blob instanceof Blob) { var mimeType = blob.type || blob.mimeType || mimeType || ''; if (!mimeType.match(/image\/(jpeg|gif|png|bmp)|video\/quicktime/)) { return getDataUrl(blob); } } return qSync.when(getUrl(blob, mimeType)); } function downloadFile (blob, mimeType, fileName) { if (window.navigator && navigator.msSaveBlob !== undefined) { window.navigator.msSaveBlob(blob, fileName); return false; } if (window.navigator && navigator.getDeviceStorage) { var storageName = 'sdcard'; switch (mimeType.split('/')[0]) { case 'video': storageName = 'videos'; break; case 'audio': storageName = 'music'; break; case 'image': storageName = 'pictures'; break; } var deviceStorage = navigator.getDeviceStorage(storageName); var request = deviceStorage.addNamed(blob, fileName); request.onsuccess = function () { console.log('Device storage save result', this.result); }; request.onerror = function () { }; return; } var popup = false; if (window.safari) { popup = window.open(); } getFileCorrectUrl(blob, mimeType).then(function (url) { if (popup) { try { popup.location.href = url; return; } catch (e) {} } var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); anchor.href = url; anchor.target = '_blank'; anchor.download = fileName; if (anchor.dataset) { anchor.dataset.downloadurl = ["video/quicktime", fileName, url].join(':'); } $(anchor).css({position: 'absolute', top: 1, left: 1}).appendTo('body'); try { var clickEvent = document.createEvent('MouseEvents'); clickEvent.initMouseEvent( 'click', true, false, window, 0, 0, 0, 0, 0 , false, false, false, false, 0, null ); anchor.dispatchEvent(clickEvent); } catch (e) { console.error('Download click error', e); try { anchor[0].click(); } catch (e) { window.open(url, '_blank'); } } $timeout(function () { $(anchor).remove(); }, 100); }); } return { isAvailable: isBlobAvailable, copy: fileCopyTo, write: fileWriteData, getFileWriter: getFileWriter, getFakeFileWriter: getFakeFileWriter, chooseSave: chooseSaveFile, getUrl: getUrl, getDataUrl: getDataUrl, getByteArray: getByteArray, getFileCorrectUrl: getFileCorrectUrl, download: downloadFile }; }) .service('IdbFileStorage', function ($q, $window, FileManager) { $window.indexedDB = $window.indexedDB || $window.webkitIndexedDB || $window.mozIndexedDB || $window.OIndexedDB || $window.msIndexedDB; $window.IDBTransaction = $window.IDBTransaction || $window.webkitIDBTransaction || $window.OIDBTransaction || $window.msIDBTransaction; var dbName = 'cachedFiles'; var dbStoreName = 'files'; var dbVersion = 2; var openDbPromise; var storageIsAvailable = $window.indexedDB !== undefined && $window.IDBTransaction !== undefined; // IndexedDB is REALLY slow without blob support in Safari 8, no point in it if (storageIsAvailable && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 // && navigator.userAgent.match(/Version\/([67]|8.0.[012])/) ) { storageIsAvailable = false; } var storeBlobsAvailable = storageIsAvailable || false; function isAvailable () { return storageIsAvailable; } function openDatabase() { if (openDbPromise) { return openDbPromise; } try { var request = indexedDB.open(dbName, dbVersion), deferred = $q.defer(), createObjectStore = function (db) { db.createObjectStore(dbStoreName); }; if (!request) { throw new Exception(); } } catch (error) { storageIsAvailable = false; return $q.reject(error); } request.onsuccess = function (event) { db = request.result; db.onerror = function (error) { storageIsAvailable = false; console.error('Error creating/accessing IndexedDB database', error); deferred.reject(error); }; deferred.resolve(db); }; request.onerror = function (event) { storageIsAvailable = false; console.error('Error creating/accessing IndexedDB database', event); deferred.reject(event); } request.onupgradeneeded = function (event) { console.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion); var db = event.target.result; if (event.oldVersion == 1) { db.deleteObjectStore(dbStoreName); } createObjectStore(db); }; return openDbPromise = deferred.promise; }; function saveFile (fileName, blob) { return openDatabase().then(function (db) { if (!storeBlobsAvailable) { return saveFileBase64(db, fileName, blob); } if (!(blob instanceof Blob)) { blob = blobConstruct([blob]); } try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), request = objectStore.put(blob, fileName); } catch (error) { if (storeBlobsAvailable) { storeBlobsAvailable = false; return saveFileBase64(db, fileName, blob); } storageIsAvailable = false; return $q.reject(error); } var deferred = $q.defer(); request.onsuccess = function (event) { deferred.resolve(blob); }; request.onerror = function (error) { deferred.reject(error); }; return deferred.promise; }); }; function saveFileBase64(db, fileName, blob) { try { var reader = new FileReader(); reader.readAsDataURL(blob); } catch (e) { storageIsAvailable = false; return $q.reject(); } var deferred = $q.defer(); reader.onloadend = function() { try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), request = objectStore.put(reader.result, fileName); } catch (error) { storageIsAvailable = false; deferred.reject(error); return; }; request.onsuccess = function (event) { deferred.resolve(blob); }; request.onerror = function (error) { deferred.reject(error); }; } return deferred.promise; } function getFile (fileName) { return openDatabase().then(function (db) { var deferred = $q.defer(), objectStore = db.transaction([dbStoreName], IDBTransaction.READ || 'readonly').objectStore(dbStoreName), request = objectStore.get(fileName); request.onsuccess = function (event) { var result = event.target.result; if (result === undefined) { deferred.reject(); } else if (typeof result === 'string' && result.substr(0, 5) === 'data:') { deferred.resolve(dataUrlToBlob(result)); } else { deferred.resolve(result); } }; request.onerror = function (error) { deferred.reject(error); }; return deferred.promise; }); } function getFileWriter (fileName, mimeType) { var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { saveFile(fileName, blob); }); return $q.when(fakeWriter); } openDatabase(); return { isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter }; }) .service('TmpfsFileStorage', function ($q, $window, FileManager) { $window.requestFileSystem = $window.requestFileSystem || $window.webkitRequestFileSystem; var reqFsPromise, fileSystem, storageIsAvailable = $window.requestFileSystem !== undefined; function requestFS () { if (reqFsPromise) { return reqFsPromise; } if (!$window.requestFileSystem) { return reqFsPromise = $q.reject({type: 'FS_BROWSER_UNSUPPORTED', description: 'requestFileSystem not present'}); } var deferred = $q.defer(); $window.requestFileSystem($window.TEMPORARY, 500 * 1024 * 1024, function (fs) { cachedFs = fs; deferred.resolve(); }, function (e) { storageIsAvailable = false; deferred.reject(e); }); return reqFsPromise = deferred.promise; }; function isAvailable () { return storageIsAvailable; } function getFile (fileName, size) { size = size || 1; return requestFS().then(function () { // console.log(dT(), 'get file', fileName); var deferred = $q.defer(); cachedFs.root.getFile(fileName, {create: false}, function(fileEntry) { fileEntry.file(function(file) { // console.log(dT(), 'aa', file); if (file.size >= size) { deferred.resolve(fileEntry); } else { deferred.reject(new Error('FILE_NOT_FOUND')); } }, function (error) { console.log(dT(), 'error', error); deferred.reject(error); }); }, function () { deferred.reject(new Error('FILE_NOT_FOUND')); }); return deferred.promise; }); } function saveFile (fileName, blob) { return getFileWriter(fileName).then(function (fileWriter) { return FileManager.write(fileWriter, blob).then(function () { return fileWriter.finalize(); }) }); } function getFileWriter (fileName) { // console.log(dT(), 'get file writer', fileName); return requestFS().then(function () { var deferred = $q.defer(); cachedFs.root.getFile(fileName, {create: true}, function (fileEntry) { FileManager.getFileWriter(fileEntry).then(function (fileWriter) { fileWriter.finalize = function () { return fileEntry; } deferred.resolve(fileWriter); }, function (error) { storageIsAvailable = false; deferred.reject(error); }); }, function (error) { storageIsAvailable = false; deferred.reject(error); }); return deferred.promise; }) } requestFS(); return { isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter }; }) .service('MemoryFileStorage', function ($q, FileManager) { var storage = {}; function isAvailable () { return true; } function getFile (fileName, size) { if (storage[fileName]) { return $q.when(storage[fileName]); } return $q.reject(new Error('FILE_NOT_FOUND')); } function saveFile (fileName, blob) { return $q.when(storage[fileName] = blob); } function getFileWriter (fileName, mimeType) { var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { saveFile(fileName, blob); }); return $q.when(fakeWriter); } return { isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter }; }) .service('WebpManager', function (qSync, $q) { var nativeWebpSupport = false; var image = new Image(); image.onload = function () { nativeWebpSupport = this.width === 2 && this.height === 1; }; image.onerror = function () { nativeWebpSupport = false; }; image.src = 'data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA=='; var canvas, context; function getCanvasFromWebp(data) { var start = tsNow(); var decoder = new WebPDecoder(); var config = decoder.WebPDecoderConfig; var buffer = config.j || config.output; var bitstream = config.input; if (!decoder.WebPInitDecoderConfig(config)) { console.error('[webpjs] Library version mismatch!'); return false; } // console.log('[webpjs] status code', decoder.VP8StatusCode); var StatusCode = decoder.VP8StatusCode; status = decoder.WebPGetFeatures(data, data.length, bitstream); if (status != (StatusCode.VP8_STATUS_OK || 0)) { console.error('[webpjs] status error', status, StatusCode); } var mode = decoder.WEBP_CSP_MODE; buffer.colorspace = mode.MODE_RGBA; buffer.J = 4; try { status = decoder.WebPDecode(data, data.length, config); } catch (e) { status = e; } ok = (status == 0); if (!ok) { console.error('[webpjs] decoding failed', status, StatusCode); return false; } // console.log('[webpjs] decoded: ', buffer.width, buffer.height, bitstream.has_alpha, 'Now saving...'); var bitmap = buffer.c.RGBA.ma; // console.log('[webpjs] done in ', tsNow() - start); if (!bitmap) { return false; } var biHeight = buffer.height; var biWidth = buffer.width; if (!canvas || !context) { canvas = document.createElement('canvas'); context = canvas.getContext('2d'); } else { context.clearRect(0, 0, canvas.width, canvas.height); } canvas.height = biHeight; canvas.width = biWidth; var output = context.createImageData(canvas.width, canvas.height); var outputData = output.data; for (var h = 0; h < biHeight; h++) { for (var w = 0; w < biWidth; w++) { outputData[0+w*4+(biWidth*4)*h] = bitmap[1+w*4+(biWidth*4)*h]; outputData[1+w*4+(biWidth*4)*h] = bitmap[2+w*4+(biWidth*4)*h]; outputData[2+w*4+(biWidth*4)*h] = bitmap[3+w*4+(biWidth*4)*h]; outputData[3+w*4+(biWidth*4)*h] = bitmap[0+w*4+(biWidth*4)*h]; }; } context.putImageData(output, 0, 0); return true; } function getPngBlobFromWebp (data) { if (!getCanvasFromWebp(data)) { return $q.reject({type: 'WEBP_PROCESS_FAILED'}); } if (canvas.toBlob === undefined) { return qSync.when(dataUrlToBlob(canvas.toDataURL('image/png'))); } var deferred = $q.defer(); canvas.toBlob(function (blob) { deferred.resolve(blob); }, 'image/png'); return deferred.promise; } return { isWebpSupported: function () { return nativeWebpSupport; }, getPngBlobFromWebp: getPngBlobFromWebp } }) .service('CryptoWorker', function ($timeout, $q) { var webWorker = false, naClEmbed = false, taskID = 0, awaiting = {}, webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle)/* || window.msCrypto && window.msCrypto.subtle*/, useSha1Crypto = webCrypto && webCrypto.digest !== undefined, useSha256Crypto = webCrypto && webCrypto.digest !== undefined, finalizeTask = function (taskID, result) { var deferred = awaiting[taskID]; if (deferred !== undefined) { // console.log(dT(), 'CW done'); deferred.resolve(result); delete awaiting[taskID]; } }; if (Config.Modes.nacl && navigator.mimeTypes && navigator.mimeTypes['application/x-pnacl'] !== undefined) { var listener = $('
').appendTo($('body'))[0]; listener.addEventListener('load', function (e) { naClEmbed = listener.firstChild; console.log(dT(), 'NaCl ready'); }, true); listener.addEventListener('message', function (e) { finalizeTask(e.data.taskID, e.data.result); }, true); listener.addEventListener('error', function (e) { console.error('NaCl error', e); }, true); } if (window.Worker) { var tmpWorker = new Worker('js/lib/crypto_worker.js'); tmpWorker.onmessage = function (e) { if (!webWorker) { webWorker = tmpWorker; } else { finalizeTask(e.data.taskID, e.data.result); } }; tmpWorker.onerror = function(error) { console.error('CW error', error, error.stack); webWorker = false; }; } function performTaskWorker (task, params, embed) { // console.log(dT(), 'CW start', task); var deferred = $q.defer(); awaiting[taskID] = deferred; params.task = task; params.taskID = taskID; (embed || webWorker).postMessage(params); taskID++; return deferred.promise; } return { sha1Hash: function (bytes) { if (useSha1Crypto) { // We don't use buffer since typedArray.subarray(...).buffer gives the whole buffer and not sliced one. webCrypto.digest supports typed array var deferred = $q.defer(), bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; // console.log(dT(), 'Native sha1 start'); webCrypto.digest({name: 'SHA-1'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done'); deferred.resolve(digest); }, function (e) { console.error('Crypto digest error', e); useSha1Crypto = false; deferred.resolve(sha1HashSync(bytes)); }); return deferred.promise; } return $timeout(function () { return sha1HashSync(bytes); }); }, sha256Hash: function (bytes) { if (useSha256Crypto) { var deferred = $q.defer(), bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; // console.log(dT(), 'Native sha1 start'); webCrypto.digest({name: 'SHA-256'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done'); deferred.resolve(digest); }, function (e) { console.error('Crypto digest error', e); useSha256Crypto = false; deferred.resolve(sha256HashSync(bytes)); }); return deferred.promise; } return $timeout(function () { return sha256HashSync(bytes); }); }, aesEncrypt: function (bytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-encrypt', { bytes: addPadding(convertToArrayBuffer(bytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed); } return $timeout(function () { return convertToArrayBuffer(aesEncryptSync(bytes, keyBytes, ivBytes)); }); }, aesDecrypt: function (encryptedBytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-decrypt', { encryptedBytes: addPadding(convertToArrayBuffer(encryptedBytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed); } return $timeout(function () { return convertToArrayBuffer(aesDecryptSync(encryptedBytes, keyBytes, ivBytes)); }); }, factorize: function (bytes) { bytes = convertToByteArray(bytes); if (naClEmbed && bytes.length <= 8) { return performTaskWorker('factorize', {bytes: bytes}, naClEmbed); } if (webWorker) { return performTaskWorker('factorize', {bytes: bytes}); } return $timeout(function () { return pqPrimeFactorization(bytes); }); }, modPow: function (x, y, m) { if (webWorker) { return performTaskWorker('mod-pow', { x: x, y: y, m: m }); } return $timeout(function () { return bytesModPow(x, y, m); }); }, }; }) .service('ExternalResourcesManager', function ($q, $http) { var urlPromises = {}; function downloadImage (url) { if (urlPromises[url] !== undefined) { return urlPromises[url]; } return urlPromises[url] = $http.get(url, {responseType: 'blob', transformRequest: null}) .then(function (response) { window.URL = window.URL || window.webkitURL; return window.URL.createObjectURL(response.data); }); } return { downloadImage: downloadImage } }) .service('IdleManager', function ($rootScope, $window, $timeout) { $rootScope.idle = {isIDLE: false}; var toPromise, started = false; var hidden = 'hidden'; var visibilityChange = 'visibilitychange'; if (typeof document.hidden !== 'undefined') { // default } else if (typeof document.mozHidden !== 'undefined') { hidden = 'mozHidden'; visibilityChange = 'mozvisibilitychange'; } else if (typeof document.msHidden !== 'undefined') { hidden = 'msHidden'; visibilityChange = 'msvisibilitychange'; } else if (typeof document.webkitHidden !== 'undefined') { hidden = 'webkitHidden'; visibilityChange = 'webkitvisibilitychange'; } return { start: start }; function start () { if (!started) { started = true; $($window).on(visibilityChange + ' blur focus keydown mousedown touchstart', onEvent); setTimeout(function () { onEvent({type: 'blur'}); }, 0); } } function onEvent (e) { // console.log('event', e.type); if (e.type == 'mousemove') { var e = e.originalEvent || e; if (e && e.movementX === 0 && e.movementY === 0) { return; } $($window).off('mousemove', onEvent); } var isIDLE = e.type == 'blur' || e.type == 'timeout' ? true : false; if (hidden && document[hidden]) { isIDLE = true; } $timeout.cancel(toPromise); if (!isIDLE) { // console.log('update timeout'); toPromise = $timeout(function () { onEvent({type: 'timeout'}); }, 30000); } if ($rootScope.idle.isIDLE == isIDLE) { return; } // console.log('IDLE changed', isIDLE); $rootScope.$apply(function () { $rootScope.idle.isIDLE = isIDLE; }); if (isIDLE && e.type == 'timeout') { $($window).on('mousemove', onEvent); } } }) .service('AppRuntimeManager', function ($window) { return { reload: function () { try { location.reload(); } catch (e) {}; if ($window.chrome && chrome.runtime && chrome.runtime.reload) { chrome.runtime.reload(); }; }, close: function () { try { $window.close(); } catch (e) {} }, focus: function () { if (window.navigator.mozApps && document.hidden) { // Get app instance and launch it to bring app to foreground window.navigator.mozApps.getSelf().onsuccess = function() { this.result.launch(); }; } else { if (window.chrome && chrome.app && chrome.app.window) { chrome.app.window.current().focus(); } window.focus(); } } } })